From ea2afcd6e91a1d5b7a1f72b9ed7200ae29ccdda0 Mon Sep 17 00:00:00 2001 From: stefano Date: Sun, 25 May 2025 21:18:35 +0200 Subject: [PATCH 01/21] Aggiorna README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8da7443..e0a4c90 100644 --- a/README.md +++ b/README.md @@ -884,7 +884,7 @@ BSSG includes a standalone post editor (`bssg-editor.html`) that provides a mode - **Theme Support**: Dark/light mode toggle - **Focus Mode**: Distraction-free writing environment - **Export Options**: Export to .md files, copy to clipboard, or import existing files -- **Responsive Design**: Works on desktop, tablet, and mobile devices +- **Responsive Design**: Works on desktop, tablet, and mobile devices (not perfect on mobile, yet) - **Offline Capable**: No server required - runs entirely in your browser ### Getting Started From 47d0ea02b0b3a5742e113db166a08b3484e35deb Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Mon, 26 May 2025 09:58:50 +0200 Subject: [PATCH 02/21] Fixed ampersand and feed generation --- scripts/build/generate_feeds.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build/generate_feeds.sh b/scripts/build/generate_feeds.sh index 4f868e4..3385bd2 100755 --- a/scripts/build/generate_feeds.sh +++ b/scripts/build/generate_feeds.sh @@ -85,9 +85,9 @@ _generate_rss_feed() { - ${feed_title} + $(html_escape "$feed_title") $(fix_url "$feed_link_rel") - ${feed_description} + $(html_escape "$feed_description") ${SITE_LANG:-en} $(format_date "now" "$rss_date_fmt") @@ -182,7 +182,7 @@ EOF cat >> "$output_file" << EOF - ${title} + $(html_escape "$title") ${full_url} ${full_url} ${pub_date} From 6582872d703380ca6255bf18b318f2eec752e77e Mon Sep 17 00:00:00 2001 From: stefano Date: Tue, 27 May 2025 08:34:00 +0200 Subject: [PATCH 03/21] Fixes --- generate_theme_previews.sh | 472 +++++++++++++++---------------------- 1 file changed, 191 insertions(+), 281 deletions(-) diff --git a/generate_theme_previews.sh b/generate_theme_previews.sh index e14246d..056956e 100755 --- a/generate_theme_previews.sh +++ b/generate_theme_previews.sh @@ -12,7 +12,6 @@ set -euo pipefail # Ensure this script (generate_theme_previews.sh) is run from the project root. readonly BSSG_MAIN_SCRIPT="./bssg.sh" readonly THEMES_DIR="./themes" -# EXAMPLE_ROOT_DIR is now dynamic, see determine_example_root_dir function CONFIG_FILE="config.sh" # For reading default SITE_URL if not overridden LOCAL_CONFIG_FILE="config.sh.local" # For reading default SITE_URL if not overridden @@ -27,7 +26,6 @@ BLUE='\033[0;34m' NC='\033[0m' # No Color # Default SITE_URL from config.sh if no other is specified by script's --site-url -# This will be the BASE for theme preview URLs. SITE_URL_BASE="http://localhost" # --- Helper Functions --- @@ -50,13 +48,9 @@ error() { # --- Cleanup Functions --- cleanup_directories() { - # This function is called on EXIT by the trap. - # Currently, EXAMPLE_ROOT_DIR_DYNAMIC is cleaned at the start of build_previews. - # If any other script-specific temporary files were created, they would be cleaned here. info "Cleanup function called on exit. No specific preview-script files to clean in this version." } -# Trap EXIT signal to ensure cleanup (if any specific cleanup actions are needed later) trap cleanup_directories EXIT # --- Print Help --- @@ -88,10 +82,8 @@ EOF # --- Parse Command Line Arguments (for this script) --- parse_args() { - # Initialize variable - site_url_from_cli="" + site_url_from_cli="" # Made global for load_config - # Parse command line arguments while [[ $# -gt 0 ]]; do case "$1" in -h|--help) @@ -117,12 +109,10 @@ parse_args() { load_config() { info "Loading base SITE_URL configuration for previews..." - # Load main config if it exists to get a default SITE_URL if [ -f "$CONFIG_FILE" ]; then - # Source with subshell to avoid polluting global namespace - # but extract SITE_URL if defined. + # Portable way to extract SITE_URL="value" local main_conf_site_url - main_conf_site_url=$(grep -m 1 "^SITE_URL=" "$CONFIG_FILE" | cut -d'"' -f2 || echo "") + main_conf_site_url=$(awk -F'"' '/^SITE_URL=/ {print $2; exit}' "$CONFIG_FILE") if [ -n "$main_conf_site_url" ]; then SITE_URL_BASE="$main_conf_site_url" info "Using SITE_URL_BASE='$SITE_URL_BASE' from $CONFIG_FILE as default" @@ -131,11 +121,11 @@ load_config() { warn "Main configuration file '$CONFIG_FILE' not found, using default SITE_URL_BASE='$SITE_URL_BASE'." fi - # Load local config if it exists (overrides main config for SITE_URL_BASE) if [ -f "$LOCAL_CONFIG_FILE" ]; then local local_conf_site_url + # Check if SITE_URL is actually defined in the local config if grep -q "^SITE_URL=" "$LOCAL_CONFIG_FILE" 2>/dev/null; then - local_conf_site_url=$(grep -m 1 "^SITE_URL=" "$LOCAL_CONFIG_FILE" | cut -d'"' -f2 || echo "") + local_conf_site_url=$(awk -F'"' '/^SITE_URL=/ {print $2; exit}' "$LOCAL_CONFIG_FILE") if [ -n "$local_conf_site_url" ]; then SITE_URL_BASE="$local_conf_site_url" info "Overridden SITE_URL_BASE='$SITE_URL_BASE' from $LOCAL_CONFIG_FILE" @@ -145,7 +135,6 @@ load_config() { fi fi - # Command line argument for this script overrides all config files for SITE_URL_BASE if [ -n "$site_url_from_cli" ]; then SITE_URL_BASE="$site_url_from_cli" info "Using SITE_URL_BASE='$SITE_URL_BASE' from command line argument for previews" @@ -154,8 +143,6 @@ load_config() { success "Configuration loaded. Using SITE_URL_BASE='$SITE_URL_BASE' for theme previews." } - - # --- Sanity Checks --- check_dependencies() { info "Checking requirements..." @@ -168,69 +155,139 @@ check_dependencies() { if [ ! -d "$THEMES_DIR" ]; then error "Themes directory not found at '$THEMES_DIR'. This script must be run from the BSSG project root." fi - # Check for essential commands - for cmd in find basename mkdir mv cat date rm ls grep cut git realpath; do # Added realpath + for cmd in find basename mkdir mv cat date rm ls grep awk sed dirname sort printf bash; do # Added awk, sed, printf, bash if ! command -v "$cmd" >/dev/null 2>&1; then error "Required command '$cmd' not found in PATH." fi done + # Check for pwd -P behavior (standard in POSIX sh, but good to be aware) + if ! (pwd -P >/dev/null 2>&1); then + warn "pwd -P might not be supported or behave as expected on this system. Path resolution might be affected." + fi success "Requirements met." } +# --- Path Normalization Helper (replaces realpath -m) --- +_normalize_path_string() { + local path_to_normalize="$1" + local current_dir + current_dir=$(pwd -P) # Get current physical working directory + + local temp_path + # Make path absolute if it's relative, using the script's CWD as base + if [[ "$path_to_normalize" != /* ]]; then + temp_path="$current_dir/$path_to_normalize" + else + temp_path="$path_to_normalize" + fi + + # Add a sentinel component to handle leading '..' correctly by making the path e.g., /sentinel/actual/path + # This simplifies logic for popping '..' at the "root" + temp_path="/sentinel${temp_path}" + + local OIFS="$IFS" + IFS='/' + # shellcheck disable=SC2206 # Word splitting is desired here for path components + local components=($temp_path) + IFS="$OIFS" + + local result_components=() + for comp in "${components[@]}"; do + if [[ -z "$comp" || "$comp" == "." ]]; then + continue # Skip empty or current dir components + fi + if [[ "$comp" == ".." ]]; then + # Only pop if result_components is not empty and last component is not 'sentinel' + if [[ ${#result_components[@]} -gt 0 && "${result_components[${#result_components[@]}-1]}" != "sentinel" ]]; then + unset 'result_components[${#result_components[@]}-1]' + fi + else + result_components+=("$comp") + fi + done + + # Reconstruct the path + local final_path + # Remove 'sentinel' if it's the first component + if [[ ${#result_components[@]} -gt 0 && "${result_components[0]}" == "sentinel" ]]; then + # Handle case where only sentinel remains (e.g. /sentinel/../..) -> / + if [[ ${#result_components[@]} -eq 1 ]]; then + final_path="/" + else + # Join remaining components. ${array[*]:1} gives elements from index 1. + final_path="/$(IFS=/; echo "${result_components[*]:1}")" + fi + else + # This case implies original path was something like /../../.. that resolved above sentinel + # or the sentinel was incorrectly processed. Should resolve to root. + final_path="/" + fi + + # Post-process: remove multiple slashes, trailing slash (unless it's just "/") + final_path=$(echo "$final_path" | sed 's#//*#/#g') + if [[ "$final_path" != "/" && "${final_path: -1}" == "/" ]]; then + final_path="${final_path%/}" + fi + # If final_path is empty after all this (e.g. input was just "/"), ensure it's "/" + if [[ -z "$final_path" ]]; then + echo "/" + else + echo "$final_path" + fi +} + + # --- Main Logic --- -# 1. Find all themes (directories inside the themes directory) find_themes() { info "Searching for themes in '$THEMES_DIR'..." - # Check if directory exists first if [ ! -d "$THEMES_DIR" ]; then error "Themes directory '$THEMES_DIR' does not exist!" fi - # Debug the find command output echo "Debug: listing themes directory content with ls" - ls -la "$THEMES_DIR" + ls -la "$THEMES_DIR" # Keep for debugging if needed - echo "Debug: attempting BSD/FreeBSD compatible find" - - # Use a more compatible approach for FreeBSD and other systems - themes=() + local theme_names=() for d in "$THEMES_DIR"/*; do if [ -d "$d" ]; then - # Extract just the basename - theme_name=$(basename "$d") - themes+=("$theme_name") + theme_names+=("$(basename "$d")") fi done - if [ ${#themes[@]} -eq 0 ]; then + if [ ${#theme_names[@]} -eq 0 ]; then error "No valid theme directories found in '$THEMES_DIR'." fi - # Sort the themes array (not needed with find command previously) - # Simple bubble sort - for ((i=0; i<${#themes[@]}; i++)); do - for ((j=0; j<${#themes[@]}-i-1; j++)); do - if [[ "${themes[j]}" > "${themes[j+1]}" ]]; then - # swap - temp="${themes[j]}" - themes[j]="${themes[j+1]}" - themes[j+1]="$temp" - fi - done - done + # Sort themes using standard sort command + # Store sorted names back into the global 'themes' array + local sorted_theme_names_nl + sorted_theme_names_nl=$(printf "%s\n" "${theme_names[@]}" | sort) + + themes=() # Clear global themes array before repopulating + while IFS= read -r line; do + if [ -n "$line" ]; then # Ensure no empty lines become theme names + themes+=("$line") + fi + done <<< "$sorted_theme_names_nl" info "Found ${#themes[@]} themes: ${themes[*]}" } -# 2. Build preview for each theme build_previews() { info "Clearing existing example directory: '$EXAMPLE_ROOT_DIR_DYNAMIC'" - # Ensure the EXAMPLE_ROOT_DIR_DYNAMIC itself exists, then clear its contents mkdir -p "$EXAMPLE_ROOT_DIR_DYNAMIC" - # Remove contents including hidden files, suppress errors for non-existent hidden files - rm -rf "${EXAMPLE_ROOT_DIR_DYNAMIC:?}"/* "${EXAMPLE_ROOT_DIR_DYNAMIC:?}"/.??* 2>/dev/null || true + # More robustly clear contents. Using find is safer for unusual filenames. + # However, rm -rf with :? guard is common. + # Ensure EXAMPLE_ROOT_DIR_DYNAMIC is not empty and not root, for safety. + if [ -z "$EXAMPLE_ROOT_DIR_DYNAMIC" ] || [ "$EXAMPLE_ROOT_DIR_DYNAMIC" = "/" ] || [ "$EXAMPLE_ROOT_DIR_DYNAMIC" = "." ] || [ "$EXAMPLE_ROOT_DIR_DYNAMIC" = ".." ]; then + error "Safety check failed: EXAMPLE_ROOT_DIR_DYNAMIC is '$EXAMPLE_ROOT_DIR_DYNAMIC'. Aborting clear." + fi + rm -rf "${EXAMPLE_ROOT_DIR_DYNAMIC:?}"/* "${EXAMPLE_ROOT_DIR_DYNAMIC:?}"/.* 2>/dev/null || true + # Note: .??* misses files like .a but covers most common dotfiles. /.* is more thorough but needs care. + # A safer alternative if `find` is available: + # find "$EXAMPLE_ROOT_DIR_DYNAMIC" -mindepth 1 -delete success "Example directory cleared and ready." info "Starting theme preview builds..." @@ -239,18 +296,14 @@ build_previews() { for theme in "${themes[@]}"; do info "Building preview for theme: '$theme'" - local theme_site_url="${SITE_URL_BASE%/}/${theme}" # Ensure no double slashes if SITE_URL_BASE ends with / + local theme_site_url="${SITE_URL_BASE%/}/${theme}" local theme_output_path="${EXAMPLE_ROOT_DIR_DYNAMIC}/${theme}" info "Theme Site URL: $theme_site_url" info "Theme Output Path: $theme_output_path" - # Ensure the specific theme's output directory exists mkdir -p "$theme_output_path" - # Run the main bssg.sh build script for this theme. - # It will use the standard BSSG configuration loading (respecting config.sh.local). - # The -f flag forces rebuild, which also clears the active cache for that build. info "Executing: $BSSG_MAIN_SCRIPT build -f --theme \"$theme\" --site-url \"$theme_site_url\" --output \"$theme_output_path\"" if ! "$BSSG_MAIN_SCRIPT" build -f --theme "$theme" --site-url "$theme_site_url" --output "$theme_output_path"; then @@ -262,19 +315,15 @@ build_previews() { success "All theme previews built." } -# 3. Create an index.html in EXAMPLE_ROOT_DIR_DYNAMIC to navigate themes create_index_page() { local index_file="$EXAMPLE_ROOT_DIR_DYNAMIC/index.html" info "Generating index file at '$index_file'..." - # Get current date for the footer local current_date - current_date=$(date) # Use default date format - - # Theme count for display + current_date=$(date) local theme_count=${#themes[@]} - # Use cat heredoc to create the HTML file + # HTML content remains the same, heredoc is portable cat << EOF > "$index_file" @@ -284,222 +333,82 @@ create_index_page() { BSSG Theme Previews @@ -510,15 +419,13 @@ create_index_page() {
${theme_count} Themes Available

Browse these theme previews using the current site content. Click on any theme to explore its design.

-
EOF - # Add grid items for each theme for theme in "${themes[@]}"; do - local safe_theme_name="${theme//&/&}" - safe_theme_name="${safe_theme_name///>}" + local safe_theme_name="${theme//&/&}" + safe_theme_name="${safe_theme_name///>}" cat << EOF >> "$index_file"
@@ -529,10 +436,8 @@ EOF EOF done - # Close the HTML structure cat << EOF >> "$index_file"
-

Generated on ${current_date}

Base SITE_URL: ${SITE_URL_BASE}

@@ -545,67 +450,72 @@ EOF success "Index file generated successfully with ${theme_count} themes." } -# --- Determine Dynamic EXAMPLE_ROOT_DIR --- determine_example_root_dir() { info "Determining effective site root for EXAMPLE_ROOT_DIR_DYNAMIC..." local project_root_abs - project_root_abs=$(realpath "$(pwd)") # Assuming generate_theme_previews.sh is in BSSG root + # Portable way to get absolute path of current directory + project_root_abs=$( (cd . && pwd -P) || { error "Could not determine project root."; exit 1; } ) + local effective_output_dir - # Use a subshell to source config_loader.sh and get OUTPUT_DIR value - # config_loader.sh can output messages, so redirect its stdout/stderr to /dev/null - # The final echo "$OUTPUT_DIR" will be captured by the command substitution. - # Ensure BSSG_SCRIPT_DIR is exported to the subshell environment. effective_output_dir=$(export BSSG_SCRIPT_DIR="$project_root_abs"; \ bash -c 'source "$BSSG_SCRIPT_DIR/scripts/build/config_loader.sh" "" &>/dev/null; echo "$OUTPUT_DIR"') if [ -z "$effective_output_dir" ]; then - warn "Could not determine effective OUTPUT_DIR from BSSG configuration. Defaulting EXAMPLE_ROOT_DIR_DYNAMIC to \'$EXAMPLE_ROOT_DIR_DYNAMIC\'." - # EXAMPLE_ROOT_DIR_DYNAMIC remains ./example (its default) + warn "Could not determine effective OUTPUT_DIR from BSSG configuration. Defaulting EXAMPLE_ROOT_DIR_DYNAMIC to '$EXAMPLE_ROOT_DIR_DYNAMIC'." return fi info "Effective OUTPUT_DIR from BSSG configuration: '$effective_output_dir'" - local effective_output_dir_abs - if [[ "$effective_output_dir" == /* ]]; then # Already absolute - effective_output_dir_abs="$effective_output_dir" - else # Relative, resolve it from project_root_abs - effective_output_dir_abs="$project_root_abs/$effective_output_dir" + local effective_output_dir_abs_unnormalized + if [[ "$effective_output_dir" == /* ]]; then + effective_output_dir_abs_unnormalized="$effective_output_dir" + else + effective_output_dir_abs_unnormalized="$project_root_abs/$effective_output_dir" fi - # Normalize the path (remove ., .. if any) - effective_output_dir_abs=$(realpath -m "$effective_output_dir_abs") + + # Normalize the path using our helper (handles ., .., and non-existent paths) + local effective_output_dir_abs + effective_output_dir_abs=$(_normalize_path_string "$effective_output_dir_abs_unnormalized") + info "Normalized effective_output_dir_abs: '$effective_output_dir_abs'" + - # Derive site root from output_dir. Typically output_dir is a direct child of site_root. local site_root_candidate site_root_candidate=$(dirname "$effective_output_dir_abs") + # dirname /foo is / ; dirname / is / + # Ensure site_root_candidate is cleaned up if it's just "//" or similar from dirname + if [[ "$site_root_candidate" != "/" ]]; then + site_root_candidate=$(echo "$site_root_candidate" | sed 's#//*#/#g') + fi + - # Check if the site_root_candidate is different from the BSSG project root AND - # if the original effective_output_dir was specified as an absolute path. - # This suggests an external site configuration. if [[ "$site_root_candidate" != "$project_root_abs" && "$effective_output_dir" == /* ]]; then - info "Detected external site configuration. Previews will be generated in \'$site_root_candidate/example\'." + info "Detected external site configuration. Previews will be generated in '$site_root_candidate/example'." EXAMPLE_ROOT_DIR_DYNAMIC="$site_root_candidate/example" else - info "Using BSSG project directory for previews. Previews will be generated in \'$project_root_abs/example\'." - EXAMPLE_ROOT_DIR_DYNAMIC="$project_root_abs/example" # Ensures absolute path for clarity + info "Using BSSG project directory for previews. Previews will be generated in '$project_root_abs/example'." + EXAMPLE_ROOT_DIR_DYNAMIC="$project_root_abs/example" fi - success "EXAMPLE_ROOT_DIR_DYNAMIC set to \'$EXAMPLE_ROOT_DIR_DYNAMIC\'." + # Normalize the final EXAMPLE_ROOT_DIR_DYNAMIC as well + EXAMPLE_ROOT_DIR_DYNAMIC=$(_normalize_path_string "$EXAMPLE_ROOT_DIR_DYNAMIC") + success "EXAMPLE_ROOT_DIR_DYNAMIC set to '$EXAMPLE_ROOT_DIR_DYNAMIC'." } -# --- Script Execution --- main() { - parse_args "$@" - load_config # Load SITE_URL_BASE for previews - check_dependencies - determine_example_root_dir # Determine the correct EXAMPLE_ROOT_DIR_DYNAMIC + # Ensure global 'themes' array is declared if not implicitly through find_themes + declare -a themes - find_themes + parse_args "$@" + load_config + check_dependencies + determine_example_root_dir + + find_themes # Populates global 'themes' array build_previews create_index_page - success "Theme previews generated successfully in \'$EXAMPLE_ROOT_DIR_DYNAMIC\'" - info "Open \'$EXAMPLE_ROOT_DIR_DYNAMIC/index.html\' in your browser to view them." + success "Theme previews generated successfully in '$EXAMPLE_ROOT_DIR_DYNAMIC'" + info "Open '$EXAMPLE_ROOT_DIR_DYNAMIC/index.html' in your browser to view them." } -# Call main function with all script arguments -main "$@" +main "$@" \ No newline at end of file From 3c88fc7e69337f28c7357b7ead6622b9ad936626 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Mon, 2 Jun 2025 08:46:08 +0200 Subject: [PATCH 04/21] The headline feature of this release is **complete multi-author support** throughout BSSG! This feature transforms BSSG from a single-author platform into a collaborative publishing system. **New Author Frontmatter Fields:** - `author_name`: Override the default site author on a per-post basis - `author_email`: Specify custom author email information **Intelligent Fallback System:** - **Custom Author**: Both name and email override defaults - **Name Only**: Just specify author name, email remains empty - **Default Fallback**: Empty fields automatically use site configuration **Author Index Pages:** - **Main Authors Index**: Located at `/authors/` with post counts for each author - **Individual Author Pages**: Dedicated pages at `/authors/author-slug/` for each author - **Conditional Navigation**: "Authors" menu appears only when multiple authors exist (configurable threshold) - **Visual Consistency**: Reuses existing tag page styling for familiar user experience **Complete Integration:** - **Schema.org JSON-LD**: Proper structured data for search engines - **RSS Feeds**: Dublin Core `dc:creator` elements with full author attribution - **Footer Copyright**: Dynamic author information in copyright notices - **Index Listings**: "by Author Name" attribution throughout the site - **Enhanced Sitemap**: Author pages automatically included for better SEO **Configuration Options:** ENABLE_AUTHOR_PAGES=false # Enable/disable author pages (default: false) SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2) ENABLE_AUTHOR_RSS=false # Author-specific RSS feeds (default: false) This release includes **BSSG Themes 0.30**, a comprehensive improvement of all 50 included themes focusing on performance, accessibility, and cross-platform compatibility. **Performance Optimizations:** - **External Font Dependencies Removed**: Eliminated Google Fonts and base64 encoded fonts across 35+ themes - **System Font Stacks**: Comprehensive fallback fonts for better performance and reliability - **Animation Optimization**: Added `@media (prefers-reduced-motion)` support to all themes - **Mobile Performance**: 40-60% improvement in rendering speed on mobile devices - **Backdrop Filter Optimization**: Reduced blur amounts and added mobile fallbacks **Accessibility Enhancements:** - **WCAG AA Compliance**: Tried to achieve 100% compliance across all themes - **Keyboard Navigation**: Complete focus management with visible outlines - **Screen Reader Support**: Enhanced semantic HTML structure and ARIA attributes - **Reduced Motion**: Full support for users who prefer reduced motion **Cross-Platform Compatibility:** - **Text Browser Support**: Full functionality in lynx, w3m, and links browsers - **Progressive Enhancement**: Graceful degradation for older browsers - **Icon Fallbacks**: ASCII alternatives for Unicode symbols and decorative elements **Critical Theme Fixes:** - **Glassmorphism**: Complete redesign for maximum contrast and readability - **Vaporwave**: Optimized neon effects and improved mobile performance - **Flat**: Fixed critical invisible post title bug - **Retro Computing Themes**: Enhanced authenticity while maintaining modern accessibility **Improved Edit Command:** - Fixed filename generation in edit mode (`-n` flag) that was causing build failures - Clean filename formatting prevents awk errors with spaces in filenames - Consistent approach across all BSSG operations **Better Build Options:** - Fixed `--force-rebuild` option that wasn't working as documented - Both `--force-rebuild` and `-f` now work correctly as aliases - Improved help text and command consistency **Enhanced Post Editor:** - Fixed datetime-local input showing GMT instead of local time - Better date/time handling for content creation - Improved user experience in the standalone editor **RSS Feed Enhancements:** - **Fixed XML Escaping**: Proper escaping of ampersands and special characters in RSS titles and descriptions - **Image Caption Fix**: Eliminated duplicate `
` elements in RSS feeds - **Valid XML Output**: RSS feeds now validate properly with all RSS parsers **Unicode and Internationalization:** - **German Umlaut Handling**: Consistent Unicode character handling in URL slugs across all interfaces - **Comprehensive Transliteration**: Support for German, French, Spanish, Polish, and other European languages - **Cross-Interface Consistency**: Uniform slug generation in command-line tools, web editor, and admin interface **Better Incremental Builds:** - **New Author Detection**: Fixed caching issue where new authors weren't detected during incremental builds - **Author Page URLs**: Fixed incorrect URL generation that didn't honor configured `URL_SLUG_FORMAT` - **Dependency Tracking**: Improved rebuild logic for author-related content **Enhanced Post Metadata:** - **Improved Visual Presentation**: Better typography and spacing for post metadata banners - **Semantic HTML**: Proper `
- + @@ -947,18 +1326,22 @@ description: '', image: '', imageCaption: '', + authorName: '', + authorEmail: '', content: '' }, - isDirty: false, - lastSaved: null, - theme: localStorage.getItem('bssg-editor-theme') || 'light', - focusMode: false, - previewMode: false, - currentArticleId: null, - selectedArticleId: null, - lastWordCount: 0, - autoSaveTimeout: null, - lastActivity: null + isDirty: false, + lastSaved: null, + theme: localStorage.getItem('bssg-editor-theme') || 'light', + focusMode: false, + previewMode: false, + currentArticleId: null, + selectedArticleId: null, + lastWordCount: 0, + autoSaveTimeout: null, + lastActivity: null, + isMobile: window.innerWidth <= 768, + sidebarOpen: false }; // DOM elements @@ -972,69 +1355,104 @@ description: document.getElementById('description'), image: document.getElementById('image'), imageCaption: document.getElementById('imageCaption'), + authorName: document.getElementById('authorName'), + authorEmail: document.getElementById('authorEmail'), markdownEditor: document.getElementById('markdownEditor'), previewContent: document.getElementById('previewContent'), wordCount: document.getElementById('wordCount'), charCount: document.getElementById('charCount'), saveStatus: document.getElementById('saveStatus'), lastSaved: document.getElementById('lastSaved'), - themeToggle: document.getElementById('themeToggle'), - newBtn: document.getElementById('newBtn'), - saveBtn: document.getElementById('saveBtn'), - loadBtn: document.getElementById('loadBtn'), - importBtn: document.getElementById('importBtn'), - copyBtn: document.getElementById('copyBtn'), - exportBtn: document.getElementById('exportBtn'), + themeToggle: document.getElementById('themeToggle'), + newBtn: document.getElementById('newBtn'), + saveBtn: document.getElementById('saveBtn'), + loadBtn: document.getElementById('loadBtn'), + importBtn: document.getElementById('importBtn'), + copyBtn: document.getElementById('copyBtn'), + exportBtn: document.getElementById('exportBtn'), fileInput: document.getElementById('fileInput'), unsplashBtn: document.getElementById('unsplashBtn'), - unsplashModal: document.getElementById('unsplashModal'), - unsplashSearch: document.getElementById('unsplashSearch'), - unsplashResults: document.getElementById('unsplashResults'), - unsplashStatus: document.getElementById('unsplashStatus'), - closeUnsplashModal: document.getElementById('closeUnsplashModal'), - cancelUnsplash: document.getElementById('cancelUnsplash'), - boldBtn: document.getElementById('boldBtn'), - italicBtn: document.getElementById('italicBtn'), - linkBtn: document.getElementById('linkBtn'), - imageBtn: document.getElementById('imageBtn'), - codeBtn: document.getElementById('codeBtn'), - codeBlockBtn: document.getElementById('codeBlockBtn'), - quoteBtn: document.getElementById('quoteBtn'), - listBtn: document.getElementById('listBtn'), - orderedListBtn: document.getElementById('orderedListBtn'), - h1Btn: document.getElementById('h1Btn'), - h2Btn: document.getElementById('h2Btn'), - h3Btn: document.getElementById('h3Btn'), - hrBtn: document.getElementById('hrBtn'), - previewBtn: document.getElementById('previewBtn'), - focusBtn: document.getElementById('focusBtn'), - focusExit: document.getElementById('focusExit'), - unsplashKey: document.getElementById('unsplashKey'), - saveModal: document.getElementById('saveModal'), - saveTitle: document.getElementById('saveTitle'), - saveDescription: document.getElementById('saveDescription'), - closeSaveModal: document.getElementById('closeSaveModal'), - cancelSave: document.getElementById('cancelSave'), - confirmSave: document.getElementById('confirmSave'), - loadModal: document.getElementById('loadModal'), - searchArticles: document.getElementById('searchArticles'), - articlesList: document.getElementById('articlesList'), - closeLoadModal: document.getElementById('closeLoadModal'), - cancelLoad: document.getElementById('cancelLoad'), - deleteSelected: document.getElementById('deleteSelected') + unsplashModal: document.getElementById('unsplashModal'), + unsplashSearch: document.getElementById('unsplashSearch'), + unsplashResults: document.getElementById('unsplashResults'), + unsplashStatus: document.getElementById('unsplashStatus'), + closeUnsplashModal: document.getElementById('closeUnsplashModal'), + cancelUnsplash: document.getElementById('cancelUnsplash'), + boldBtn: document.getElementById('boldBtn'), + italicBtn: document.getElementById('italicBtn'), + linkBtn: document.getElementById('linkBtn'), + imageBtn: document.getElementById('imageBtn'), + codeBtn: document.getElementById('codeBtn'), + codeBlockBtn: document.getElementById('codeBlockBtn'), + quoteBtn: document.getElementById('quoteBtn'), + listBtn: document.getElementById('listBtn'), + orderedListBtn: document.getElementById('orderedListBtn'), + h1Btn: document.getElementById('h1Btn'), + h2Btn: document.getElementById('h2Btn'), + h3Btn: document.getElementById('h3Btn'), + hrBtn: document.getElementById('hrBtn'), + previewBtn: document.getElementById('previewBtn'), + focusBtn: document.getElementById('focusBtn'), + focusExit: document.getElementById('focusExit'), + unsplashKey: document.getElementById('unsplashKey'), + saveModal: document.getElementById('saveModal'), + saveTitle: document.getElementById('saveTitle'), + saveDescription: document.getElementById('saveDescription'), + closeSaveModal: document.getElementById('closeSaveModal'), + cancelSave: document.getElementById('cancelSave'), + confirmSave: document.getElementById('confirmSave'), + loadModal: document.getElementById('loadModal'), + searchArticles: document.getElementById('searchArticles'), + articlesList: document.getElementById('articlesList'), + closeLoadModal: document.getElementById('closeLoadModal'), + cancelLoad: document.getElementById('cancelLoad'), + deleteSelected: document.getElementById('deleteSelected'), + mobileMenuToggle: document.getElementById('mobileMenuToggle'), + headerActions: document.getElementById('headerActions'), + sidebar: document.getElementById('sidebar'), + sidebarToggle: document.getElementById('sidebarToggle'), + sidebarClose: document.getElementById('sidebarClose'), + editorContent: document.getElementById('editorContent'), + previewMobileToggle: document.getElementById('previewMobileToggle') }; - // Utility functions - function generateSlug(title) { - if (!title) return ''; - return title - .toLowerCase() - .trim() - .replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/--+/g, '-') // Replace multiple hyphens with single hyphen - .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens - } + // Utility functions + function generateSlug(title) { + if (!title) return ''; + + let slug = title.toLowerCase().trim(); + + // Transliterate common Unicode characters to ASCII equivalents + const transliterationMap = { + 'à': 'a', 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', + 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', + 'î': 'i', 'ï': 'i', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', + 'ö': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u', 'ý': 'y', + 'ÿ': 'y', 'ß': 'ss', 'đ': 'd', 'ł': 'l', 'ń': 'n', 'ś': 's', 'ź': 'z', + 'ż': 'z', 'č': 'c', 'ř': 'r', 'š': 's', 'ž': 'z', 'ą': 'a', 'ę': 'e', + 'ć': 'c', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's', 'ź': 'z', 'ż': 'z' + }; + + // Apply transliteration + for (const [unicode, ascii] of Object.entries(transliterationMap)) { + slug = slug.replace(new RegExp(unicode, 'g'), ascii); + } + + // Remove all non-alphanumeric characters except spaces and hyphens + slug = slug.replace(/[^a-z0-9\s-]/g, ''); + + // Replace spaces with hyphens + slug = slug.replace(/\s+/g, '-'); + + // Replace multiple hyphens with single hyphen + slug = slug.replace(/--+/g, '-'); + + // Remove leading/trailing hyphens + slug = slug.replace(/^-+|-+$/g, ''); + + // If slug is empty, return 'untitled' + return slug || 'untitled'; + } function formatDate(date) { const d = new Date(date); @@ -1094,137 +1512,148 @@ frontmatter += `image_caption: ${post.imageCaption}\n`; } + if (post.authorName) { + frontmatter += `author_name: ${post.authorName}\n`; + } + + if (post.authorEmail) { + frontmatter += `author_email: ${post.authorEmail}\n`; + } + frontmatter += '---\n\n'; return frontmatter; } function generateMarkdown() { - return generateFrontmatter() + (state.currentPost.content || ''); + const content = state.currentPost.content || ''; + // Ensure content ends with a newline for proper BSSG processing + const contentWithNewline = content.endsWith('\n') ? content : content + '\n'; + return generateFrontmatter() + contentWithNewline; } - // Enhanced markdown to HTML converter - function markdownToHtml(markdown) { - if (!markdown.trim()) return ''; - - let html = markdown; - - // Code blocks (must be processed before inline code) - html = html.replace(/```([\s\S]*?)```/g, '
$1
'); - - // Headers - html = html.replace(/^### (.*$)/gim, '

$1

'); - html = html.replace(/^## (.*$)/gim, '

$1

'); - html = html.replace(/^# (.*$)/gim, '

$1

'); - - // Horizontal rules - html = html.replace(/^---$/gm, '
'); - html = html.replace(/^\*\*\*$/gm, '
'); - - // Bold (must be before italic) - html = html.replace(/\*\*(.*?)\*\*/g, '$1'); - html = html.replace(/__(.*?)__/g, '$1'); - - // Italic - html = html.replace(/\*(.*?)\*/g, '$1'); - html = html.replace(/_(.*?)_/g, '$1'); - - // Inline code - html = html.replace(/`(.*?)`/g, '$1'); - - // Images - html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); - - // Links - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); - - // Lists - process line by line to maintain context - const listLines = html.split('\n'); - let inList = false; - let listType = ''; - - for (let i = 0; i < listLines.length; i++) { - const line = listLines[i]; - const isUnorderedItem = /^[\*\-] (.+)$/.test(line); - const isOrderedItem = /^(\d+)\. (.+)$/.test(line); - - if (isUnorderedItem || isOrderedItem) { - const content = line.replace(/^[\*\-\d\.] /, ''); - const currentListType = isOrderedItem ? 'ol' : 'ul'; - - if (!inList) { - listLines[i] = `<${currentListType}>
  • ${content}
  • `; - inList = true; - listType = currentListType; - } else if (listType === currentListType) { - listLines[i] = `
  • ${content}
  • `; - } else { - listLines[i] = `<${currentListType}>
  • ${content}
  • `; - listType = currentListType; - } - } else if (inList && line.trim() === '') { - // Empty line continues the list - continue; - } else if (inList) { - // Close the list - listLines[i-1] += ``; - inList = false; - listType = ''; - } - } - - // Close list if we end with one - if (inList) { - listLines[listLines.length - 1] += ``; - } - - html = listLines.join('\n'); - - // Blockquotes - html = html.replace(/^> (.+)$/gm, '
    $1
    '); - - // Line breaks and paragraphs - html = html.replace(/\n\n/g, '

    '); - html = html.replace(/\n/g, '
    '); - - // Wrap in paragraphs (but not headers, lists, blockquotes, etc.) - const paragraphLines = html.split('
    '); - const processedLines = paragraphLines.map(line => { - const trimmed = line.trim(); - if (!trimmed || - trimmed.startsWith('

    ')) { - return line; - } - return `

    ${line}

    `; - }); - html = processedLines.join('
    '); - - // Clean up - html = html.replace(/

    <\/p>/g, ''); - html = html.replace(/


    <\/p>/g, ''); - html = html.replace(/

    /g, '

    '); - html = html.replace(/<\/p>
    /g, '

    '); - html = html.replace(/(<\/(?:ul|ol|blockquote|pre|h[1-6]|hr)>)
    /g, '$1'); - html = html.replace(/
    (<(?:ul|ol|blockquote|pre|h[1-6]|hr))/g, '$1'); - - return html; - } + // Enhanced markdown to HTML converter + function markdownToHtml(markdown) { + if (!markdown.trim()) return ''; + + let html = markdown; + + // Code blocks (must be processed before inline code) + html = html.replace(/```([\s\S]*?)```/g, '
    $1
    '); + + // Headers + html = html.replace(/^### (.*$)/gim, '

    $1

    '); + html = html.replace(/^## (.*$)/gim, '

    $1

    '); + html = html.replace(/^# (.*$)/gim, '

    $1

    '); + + // Horizontal rules + html = html.replace(/^---$/gm, '
    '); + html = html.replace(/^\*\*\*$/gm, '
    '); + + // Bold (must be before italic) + html = html.replace(/\*\*(.*?)\*\*/g, '$1'); + html = html.replace(/__(.*?)__/g, '$1'); + + // Italic + html = html.replace(/\*(.*?)\*/g, '$1'); + html = html.replace(/_(.*?)_/g, '$1'); + + // Inline code + html = html.replace(/`(.*?)`/g, '$1'); + + // Images + html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); + + // Links + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Lists - process line by line to maintain context + const listLines = html.split('\n'); + let inList = false; + let listType = ''; + + for (let i = 0; i < listLines.length; i++) { + const line = listLines[i]; + const isUnorderedItem = /^[\*\-] (.+)$/.test(line); + const isOrderedItem = /^(\d+)\. (.+)$/.test(line); + + if (isUnorderedItem || isOrderedItem) { + const content = line.replace(/^[\*\-\d\.] /, ''); + const currentListType = isOrderedItem ? 'ol' : 'ul'; + + if (!inList) { + listLines[i] = `<${currentListType}>
  • ${content}
  • `; + inList = true; + listType = currentListType; + } else if (listType === currentListType) { + listLines[i] = `
  • ${content}
  • `; + } else { + listLines[i] = `<${currentListType}>
  • ${content}
  • `; + listType = currentListType; + } + } else if (inList && line.trim() === '') { + // Empty line continues the list + continue; + } else if (inList) { + // Close the list + listLines[i-1] += ``; + inList = false; + listType = ''; + } + } + + // Close list if we end with one + if (inList) { + listLines[listLines.length - 1] += ``; + } + + html = listLines.join('\n'); + + // Blockquotes + html = html.replace(/^> (.+)$/gm, '
    $1
    '); + + // Line breaks and paragraphs + html = html.replace(/\n\n/g, '

    '); + html = html.replace(/\n/g, '
    '); + + // Wrap in paragraphs (but not headers, lists, blockquotes, etc.) + const paragraphLines = html.split('
    '); + const processedLines = paragraphLines.map(line => { + const trimmed = line.trim(); + if (!trimmed || + trimmed.startsWith('

    ')) { + return line; + } + return `

    ${line}

    `; + }); + html = processedLines.join('
    '); + + // Clean up + html = html.replace(/

    <\/p>/g, ''); + html = html.replace(/


    <\/p>/g, ''); + html = html.replace(/

    /g, '

    '); + html = html.replace(/<\/p>
    /g, '

    '); + html = html.replace(/(<\/(?:ul|ol|blockquote|pre|h[1-6]|hr)>)
    /g, '$1'); + html = html.replace(/
    (<(?:ul|ol|blockquote|pre|h[1-6]|hr))/g, '$1'); + + return html; + } - function updatePreview() { - // Only update preview if it's visible - if (state.previewMode) { - const content = elements.markdownEditor.value; - const html = markdownToHtml(content); - elements.previewContent.innerHTML = html || '

    Preview will appear here as you type...

    '; - } - } + function updatePreview() { + // Only update preview if it's visible + if (state.previewMode) { + const content = elements.markdownEditor.value; + const html = markdownToHtml(content); + elements.previewContent.innerHTML = html || '

    Preview will appear here as you type...

    '; + } + } function updateWordCount() { const content = elements.markdownEditor.value; @@ -1313,258 +1742,265 @@ } } - function saveToLocalStorage() { - try { - localStorage.setItem('bssg-editor-draft', JSON.stringify(state.currentPost)); - markClean(); - } catch (error) { - console.error('Failed to save to localStorage:', error); - elements.saveStatus.textContent = 'Save failed'; - elements.saveStatus.style.color = 'var(--error)'; - } - } + function saveToLocalStorage() { + try { + localStorage.setItem('bssg-editor-draft', JSON.stringify(state.currentPost)); + markClean(); + } catch (error) { + console.error('Failed to save to localStorage:', error); + elements.saveStatus.textContent = 'Save failed'; + elements.saveStatus.style.color = 'var(--error)'; + } + } - // Article management functions - function generateArticleId() { - return 'article_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); - } + // Article management functions + function generateArticleId() { + return 'article_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } - function getStoredArticles() { - try { - const articles = localStorage.getItem('bssg-editor-articles'); - return articles ? JSON.parse(articles) : {}; - } catch (error) { - console.error('Failed to load articles:', error); - return {}; - } - } + function getStoredArticles() { + try { + const articles = localStorage.getItem('bssg-editor-articles'); + return articles ? JSON.parse(articles) : {}; + } catch (error) { + console.error('Failed to load articles:', error); + return {}; + } + } - function saveArticle(id, articleData) { - try { - const articles = getStoredArticles(); - articles[id] = { - ...articleData, - id: id, - lastModified: new Date().toISOString(), - created: articles[id]?.created || new Date().toISOString() - }; - localStorage.setItem('bssg-editor-articles', JSON.stringify(articles)); - return true; - } catch (error) { - console.error('Failed to save article:', error); - return false; - } - } + function saveArticle(id, articleData) { + try { + const articles = getStoredArticles(); + articles[id] = { + ...articleData, + id: id, + lastModified: new Date().toISOString(), + created: articles[id]?.created || new Date().toISOString() + }; + localStorage.setItem('bssg-editor-articles', JSON.stringify(articles)); + return true; + } catch (error) { + console.error('Failed to save article:', error); + return false; + } + } - function deleteArticle(id) { - try { - const articles = getStoredArticles(); - delete articles[id]; - localStorage.setItem('bssg-editor-articles', JSON.stringify(articles)); - return true; - } catch (error) { - console.error('Failed to delete article:', error); - return false; - } - } + function deleteArticle(id) { + try { + const articles = getStoredArticles(); + delete articles[id]; + localStorage.setItem('bssg-editor-articles', JSON.stringify(articles)); + return true; + } catch (error) { + console.error('Failed to delete article:', error); + return false; + } + } - function newArticle() { - if (state.isDirty) { - const currentTitle = state.currentPost.title || 'Untitled'; - const message = `You have unsaved changes to "${currentTitle}". Creating a new article will discard these changes.\n\nDo you want to continue?`; - if (!confirm(message)) { - return; - } - } + function newArticle() { + if (state.isDirty) { + const currentTitle = state.currentPost.title || 'Untitled'; + const message = `You have unsaved changes to "${currentTitle}". Creating a new article will discard these changes.\n\nDo you want to continue?`; + if (!confirm(message)) { + return; + } + } - // Reset to default state - state.currentPost = { - title: '', - slug: '', - date: new Date().toISOString().slice(0, 16), - lastmod: '', - tags: [], - description: '', - image: '', - imageCaption: '', - content: '' - }; - state.currentArticleId = null; - state.isDirty = false; - state.lastWordCount = 0; - - // Clear any pending auto-save - if (state.autoSaveTimeout) { - clearTimeout(state.autoSaveTimeout); - state.autoSaveTimeout = null; - } + // Reset to default state + state.currentPost = { + title: '', + slug: '', + date: new Date().toISOString().slice(0, 16), + lastmod: '', + tags: [], + description: '', + image: '', + imageCaption: '', + authorName: '', + authorEmail: '', + content: '' + }; + state.currentArticleId = null; + state.isDirty = false; + state.lastWordCount = 0; + + // Clear any pending auto-save + if (state.autoSaveTimeout) { + clearTimeout(state.autoSaveTimeout); + state.autoSaveTimeout = null; + } - updateFormFromState(); - elements.markdownEditor.focus(); - - elements.saveStatus.textContent = 'New article'; - elements.saveStatus.style.color = 'var(--text-muted)'; - elements.lastSaved.textContent = 'Never saved'; - } + updateFormFromState(); + elements.markdownEditor.focus(); + + elements.saveStatus.textContent = 'New article'; + elements.saveStatus.style.color = 'var(--text-muted)'; + elements.lastSaved.textContent = 'Never saved'; - function openSaveModal() { - updateStateFromForm(); - elements.saveTitle.value = state.currentPost.title || ''; - elements.saveDescription.value = state.currentPost.description || ''; - elements.saveModal.classList.add('active'); - elements.saveTitle.focus(); - } + // Close mobile menu if open + if (state.isMobile) { + closeMobileMenu(); + } + } - function saveCurrentArticle() { - const title = elements.saveTitle.value.trim(); - if (!title) { - alert('Please enter a title for the article.'); - return; - } + function openSaveModal() { + updateStateFromForm(); + elements.saveTitle.value = state.currentPost.title || ''; + elements.saveDescription.value = state.currentPost.description || ''; + elements.saveModal.classList.add('active'); + elements.saveTitle.focus(); + } - updateStateFromForm(); - - const articleData = { - title: title, - description: elements.saveDescription.value.trim(), - post: { ...state.currentPost } - }; + function saveCurrentArticle() { + const title = elements.saveTitle.value.trim(); + if (!title) { + alert('Please enter a title for the article.'); + return; + } - const id = state.currentArticleId || generateArticleId(); - - if (saveArticle(id, articleData)) { - state.currentArticleId = id; - elements.saveModal.classList.remove('active'); - markClean(); - elements.saveStatus.textContent = 'Article saved'; - elements.saveStatus.style.color = 'var(--success)'; - elements.lastSaved.textContent = `Saved at ${new Date().toLocaleTimeString()}`; - } else { - alert('Failed to save article. Please try again.'); - } - } + updateStateFromForm(); + + const articleData = { + title: title, + description: elements.saveDescription.value.trim(), + post: { ...state.currentPost } + }; - function openLoadModal() { - elements.loadModal.classList.add('active'); - displayArticles(); - elements.searchArticles.focus(); - } + const id = state.currentArticleId || generateArticleId(); + + if (saveArticle(id, articleData)) { + state.currentArticleId = id; + elements.saveModal.classList.remove('active'); + markClean(); + elements.saveStatus.textContent = 'Article saved'; + elements.saveStatus.style.color = 'var(--success)'; + elements.lastSaved.textContent = `Saved at ${new Date().toLocaleTimeString()}`; + } else { + alert('Failed to save article. Please try again.'); + } + } - function displayArticles(searchTerm = '') { - const articles = getStoredArticles(); - const articleIds = Object.keys(articles); - - if (articleIds.length === 0) { - elements.articlesList.innerHTML = '
    No saved articles found.
    '; - return; - } + function openLoadModal() { + elements.loadModal.classList.add('active'); + displayArticles(); + elements.searchArticles.focus(); + } - const filteredArticles = articleIds - .map(id => articles[id]) - .filter(article => { - if (!searchTerm) return true; - const term = searchTerm.toLowerCase(); - return article.title.toLowerCase().includes(term) || - (article.description && article.description.toLowerCase().includes(term)); - }) - .sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + function displayArticles(searchTerm = '') { + const articles = getStoredArticles(); + const articleIds = Object.keys(articles); + + if (articleIds.length === 0) { + elements.articlesList.innerHTML = '
    No saved articles found.
    '; + return; + } - if (filteredArticles.length === 0) { - elements.articlesList.innerHTML = '
    No articles match your search.
    '; - return; - } + const filteredArticles = articleIds + .map(id => articles[id]) + .filter(article => { + if (!searchTerm) return true; + const term = searchTerm.toLowerCase(); + return article.title.toLowerCase().includes(term) || + (article.description && article.description.toLowerCase().includes(term)); + }) + .sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); - elements.articlesList.innerHTML = filteredArticles.map(article => { - const lastModified = new Date(article.lastModified).toLocaleDateString(); - const wordCount = article.post.content ? article.post.content.trim().split(/\s+/).length : 0; - - return ` -
    -
    ${article.title}
    - - ${article.description ? `
    ${article.description}
    ` : ''} -
    - `; - }).join(''); + if (filteredArticles.length === 0) { + elements.articlesList.innerHTML = '
    No articles match your search.
    '; + return; + } - // Add click handlers - elements.articlesList.querySelectorAll('.article-item').forEach(item => { - item.addEventListener('click', () => { - // Remove previous selection - elements.articlesList.querySelectorAll('.article-item').forEach(i => i.classList.remove('selected')); - // Select current item - item.classList.add('selected'); - state.selectedArticleId = item.dataset.id; - elements.deleteSelected.disabled = false; - }); + elements.articlesList.innerHTML = filteredArticles.map(article => { + const lastModified = new Date(article.lastModified).toLocaleDateString(); + const wordCount = article.post.content ? article.post.content.trim().split(/\s+/).length : 0; + + return ` +
    +
    ${article.title}
    + + ${article.description ? `
    ${article.description}
    ` : ''} +
    + `; + }).join(''); - item.addEventListener('dblclick', () => { - loadArticle(item.dataset.id); - }); - }); - } + // Add click handlers + elements.articlesList.querySelectorAll('.article-item').forEach(item => { + item.addEventListener('click', () => { + // Remove previous selection + elements.articlesList.querySelectorAll('.article-item').forEach(i => i.classList.remove('selected')); + // Select current item + item.classList.add('selected'); + state.selectedArticleId = item.dataset.id; + elements.deleteSelected.disabled = false; + }); - function loadArticle(id) { - const articles = getStoredArticles(); - const article = articles[id]; - - if (!article) { - alert('Article not found.'); - return; - } + item.addEventListener('dblclick', () => { + loadArticle(item.dataset.id); + }); + }); + } - if (state.isDirty) { - if (!confirm('You have unsaved changes. Are you sure you want to load this article?')) { - return; - } - } + function loadArticle(id) { + const articles = getStoredArticles(); + const article = articles[id]; + + if (!article) { + alert('Article not found.'); + return; + } - state.currentPost = { ...article.post }; - state.currentArticleId = id; - state.isDirty = false; - state.lastWordCount = article.post.content ? article.post.content.trim().split(/\s+/).length : 0; - - // Clear any pending auto-save - if (state.autoSaveTimeout) { - clearTimeout(state.autoSaveTimeout); - state.autoSaveTimeout = null; - } + if (state.isDirty) { + if (!confirm('You have unsaved changes. Are you sure you want to load this article?')) { + return; + } + } - updateFormFromState(); - elements.loadModal.classList.remove('active'); - - elements.saveStatus.textContent = 'Article loaded'; - elements.saveStatus.style.color = 'var(--success)'; - elements.lastSaved.textContent = `Loaded: ${article.title}`; - } + state.currentPost = { ...article.post }; + state.currentArticleId = id; + state.isDirty = false; + state.lastWordCount = article.post.content ? article.post.content.trim().split(/\s+/).length : 0; + + // Clear any pending auto-save + if (state.autoSaveTimeout) { + clearTimeout(state.autoSaveTimeout); + state.autoSaveTimeout = null; + } - function deleteSelectedArticle() { - if (!state.selectedArticleId) return; + updateFormFromState(); + elements.loadModal.classList.remove('active'); + + elements.saveStatus.textContent = 'Article loaded'; + elements.saveStatus.style.color = 'var(--success)'; + elements.lastSaved.textContent = `Loaded: ${article.title}`; + } - const articles = getStoredArticles(); - const article = articles[state.selectedArticleId]; - - if (!article) return; + function deleteSelectedArticle() { + if (!state.selectedArticleId) return; - if (confirm(`Are you sure you want to delete "${article.title}"? This action cannot be undone.`)) { - if (deleteArticle(state.selectedArticleId)) { - // If we're deleting the currently loaded article, reset - if (state.currentArticleId === state.selectedArticleId) { - state.currentArticleId = null; - elements.saveStatus.textContent = 'Article deleted'; - elements.saveStatus.style.color = 'var(--warning)'; - } - - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - displayArticles(elements.searchArticles.value); - } else { - alert('Failed to delete article. Please try again.'); - } - } - } + const articles = getStoredArticles(); + const article = articles[state.selectedArticleId]; + + if (!article) return; + + if (confirm(`Are you sure you want to delete "${article.title}"? This action cannot be undone.`)) { + if (deleteArticle(state.selectedArticleId)) { + // If we're deleting the currently loaded article, reset + if (state.currentArticleId === state.selectedArticleId) { + state.currentArticleId = null; + elements.saveStatus.textContent = 'Article deleted'; + elements.saveStatus.style.color = 'var(--warning)'; + } + + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + displayArticles(elements.searchArticles.value); + } else { + alert('Failed to delete article. Please try again.'); + } + } + } function loadFromLocalStorage() { try { @@ -1587,6 +2023,8 @@ state.currentPost.description = elements.description.value; state.currentPost.image = elements.image.value; state.currentPost.imageCaption = elements.imageCaption.value; + state.currentPost.authorName = elements.authorName.value; + state.currentPost.authorEmail = elements.authorEmail.value; state.currentPost.content = elements.markdownEditor.value; } @@ -1598,14 +2036,16 @@ elements.description.value = state.currentPost.description || ''; elements.image.value = state.currentPost.image || ''; elements.imageCaption.value = state.currentPost.imageCaption || ''; + elements.authorName.value = state.currentPost.authorName || ''; + elements.authorEmail.value = state.currentPost.authorEmail || ''; elements.markdownEditor.value = state.currentPost.content || ''; - // Update tags - updateTagDisplay(); - - // Update preview and word count - updatePreview(); - updateWordCount(); + // Update tags + updateTagDisplay(); + + // Update preview and word count + updatePreview(); + updateWordCount(); } function updateTagDisplay() { @@ -1643,38 +2083,103 @@ } } - function toggleTheme() { - state.theme = state.theme === 'light' ? 'dark' : 'light'; - document.documentElement.setAttribute('data-theme', state.theme); - localStorage.setItem('bssg-editor-theme', state.theme); - elements.themeToggle.textContent = state.theme === 'light' ? '🌙' : '☀️'; - } + function toggleTheme() { + state.theme = state.theme === 'light' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', state.theme); + localStorage.setItem('bssg-editor-theme', state.theme); + elements.themeToggle.textContent = state.theme === 'light' ? '🌙' : '☀'; + } - function toggleFocusMode() { - state.focusMode = !state.focusMode; - if (state.focusMode) { - document.body.classList.add('focus-mode'); - elements.markdownEditor.focus(); - } else { - document.body.classList.remove('focus-mode'); - } - } + function toggleFocusMode() { + state.focusMode = !state.focusMode; + if (state.focusMode) { + document.body.classList.add('focus-mode'); + elements.markdownEditor.focus(); + // Close mobile menu and sidebar + closeMobileMenu(); + closeSidebar(); + } else { + document.body.classList.remove('focus-mode'); + } + } - function togglePreview() { - state.previewMode = !state.previewMode; - const editorContent = document.querySelector('.editor-content'); - - if (state.previewMode) { - editorContent.classList.add('split-view'); - elements.previewBtn.textContent = '📝'; - elements.previewBtn.title = 'Hide Preview (Ctrl+P)'; - updatePreview(); // Ensure preview is up to date - } else { - editorContent.classList.remove('split-view'); - elements.previewBtn.textContent = '👁️'; - elements.previewBtn.title = 'Toggle Preview (Ctrl+P)'; - } - } + function togglePreview() { + state.previewMode = !state.previewMode; + const editorContent = elements.editorContent; + + if (state.previewMode) { + editorContent.classList.add('split-view'); + elements.previewBtn.textContent = '📝'; + elements.previewBtn.title = 'Hide Preview (Ctrl+P)'; + updatePreview(); // Ensure preview is up to date + } else { + editorContent.classList.remove('split-view'); + elements.previewBtn.textContent = '👁'; + elements.previewBtn.title = 'Toggle Preview (Ctrl+P)'; + } + } + + // Mobile-specific functions + function toggleMobileMenu() { + const isOpen = elements.headerActions.classList.contains('active'); + if (isOpen) { + closeMobileMenu(); + } else { + openMobileMenu(); + } + } + + function openMobileMenu() { + elements.headerActions.classList.add('active'); + elements.mobileMenuToggle.textContent = '✕'; + } + + function closeMobileMenu() { + elements.headerActions.classList.remove('active'); + elements.mobileMenuToggle.textContent = '☰'; + } + + function toggleSidebar() { + state.sidebarOpen = !state.sidebarOpen; + if (state.sidebarOpen) { + openSidebar(); + } else { + closeSidebar(); + } + } + + function openSidebar() { + if (state.isMobile) { + elements.sidebar.classList.add('mobile-open'); + state.sidebarOpen = true; + } + } + + function closeSidebar() { + if (state.isMobile) { + elements.sidebar.classList.remove('mobile-open'); + state.sidebarOpen = false; + } + } + + function handleResize() { + const wasMobile = state.isMobile; + state.isMobile = window.innerWidth <= 768; + + // If switching from mobile to desktop, reset mobile states + if (wasMobile && !state.isMobile) { + closeMobileMenu(); + closeSidebar(); + elements.sidebar.classList.remove('mobile-open'); + state.sidebarOpen = false; // Reset mobile sidebar state + } + + // If switching from desktop to mobile, ensure sidebar is properly hidden + if (!wasMobile && state.isMobile) { + elements.sidebar.classList.remove('mobile-open'); + state.sidebarOpen = false; + } + } function exportMarkdown() { updateStateFromForm(); @@ -1691,6 +2196,11 @@ a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); + + // Close mobile menu + if (state.isMobile) { + closeMobileMenu(); + } } function copyToClipboard() { @@ -1717,6 +2227,11 @@ elements.copyBtn.innerHTML = '📋 Copy'; }, 2000); }); + + // Close mobile menu + if (state.isMobile) { + closeMobileMenu(); + } } function importMarkdown(file) { @@ -1759,6 +2274,8 @@ description: parsed.description || '', image: parsed.image || '', imageCaption: parsed.image_caption || '', + authorName: parsed.author_name || '', + authorEmail: parsed.author_email || '', content: markdownContent.trim() }; @@ -1774,72 +2291,72 @@ } } - // Unsplash integration - async function searchUnsplash(query) { - // Try to use real Unsplash API first, fallback to demo service - const UNSPLASH_ACCESS_KEY = localStorage.getItem('unsplash-access-key'); - - if (UNSPLASH_ACCESS_KEY) { - try { - const response = await fetch(`https://api.unsplash.com/search/photos?query=${encodeURIComponent(query)}&per_page=12&orientation=landscape`, { - headers: { - 'Authorization': `Client-ID ${UNSPLASH_ACCESS_KEY}` - } - }); - - if (response.ok) { - const data = await response.json(); - return data.results.map(photo => ({ - id: photo.id, - urls: { - small: photo.urls.small, - regular: photo.urls.regular, - full: photo.urls.full - }, - user: { - name: photo.user.name, - username: photo.user.username - }, - description: photo.description || photo.alt_description || 'Unsplash photo', - links: { - html: photo.links.html - } - })); - } - } catch (error) { - console.warn('Unsplash API failed, using demo service:', error); - } - } - - // Fallback to demo service (Lorem Picsum with better variety) - const categories = ['nature', 'city', 'technology', 'people', 'abstract', 'architecture']; - const results = []; - - for (let i = 0; i < 12; i++) { - const category = categories[i % categories.length]; - const randomId = Math.floor(Math.random() * 1000) + 1; - results.push({ - id: `demo-${i}`, - urls: { - small: `https://picsum.photos/300/200?random=${randomId}`, - regular: `https://picsum.photos/800/600?random=${randomId}`, - full: `https://picsum.photos/1200/800?random=${randomId}` - }, - user: { - name: `Demo User ${i + 1}`, - username: `demo_user_${i + 1}` - }, - description: `${category} photo - demo image`, - links: { - html: 'https://unsplash.com' - } - }); - } - - return new Promise(resolve => { - setTimeout(() => resolve(results), 300); - }); - } + // Unsplash integration + async function searchUnsplash(query) { + // Try to use real Unsplash API first, fallback to demo service + const UNSPLASH_ACCESS_KEY = localStorage.getItem('unsplash-access-key'); + + if (UNSPLASH_ACCESS_KEY) { + try { + const response = await fetch(`https://api.unsplash.com/search/photos?query=${encodeURIComponent(query)}&per_page=12&orientation=landscape`, { + headers: { + 'Authorization': `Client-ID ${UNSPLASH_ACCESS_KEY}` + } + }); + + if (response.ok) { + const data = await response.json(); + return data.results.map(photo => ({ + id: photo.id, + urls: { + small: photo.urls.small, + regular: photo.urls.regular, + full: photo.urls.full + }, + user: { + name: photo.user.name, + username: photo.user.username + }, + description: photo.description || photo.alt_description || 'Unsplash photo', + links: { + html: photo.links.html + } + })); + } + } catch (error) { + console.warn('Unsplash API failed, using demo service:', error); + } + } + + // Fallback to demo service (Lorem Picsum with better variety) + const categories = ['nature', 'city', 'technology', 'people', 'abstract', 'architecture']; + const results = []; + + for (let i = 0; i < 12; i++) { + const category = categories[i % categories.length]; + const randomId = Math.floor(Math.random() * 1000) + 1; + results.push({ + id: `demo-${i}`, + urls: { + small: `https://picsum.photos/300/200?random=${randomId}`, + regular: `https://picsum.photos/800/600?random=${randomId}`, + full: `https://picsum.photos/1200/800?random=${randomId}` + }, + user: { + name: `Demo User ${i + 1}`, + username: `demo_user_${i + 1}` + }, + description: `${category} photo - demo image`, + links: { + html: 'https://unsplash.com' + } + }); + } + + return new Promise(resolve => { + setTimeout(() => resolve(results), 300); + }); + } function displayUnsplashResults(results) { elements.unsplashResults.innerHTML = ''; @@ -1867,25 +2384,25 @@ }); } - function selectUnsplashImage(image) { - const imageUrl = image.urls.regular || image.urls.small; - let caption; - - if (image.id.startsWith('demo-')) { - caption = `${image.description} (Demo image)`; - } else { - caption = `Photo by ${image.user.name} on Unsplash`; - } - - elements.image.value = imageUrl; - elements.imageCaption.value = caption; - - state.currentPost.image = imageUrl; - state.currentPost.imageCaption = caption; - - elements.unsplashModal.classList.remove('active'); - markDirty(); - } + function selectUnsplashImage(image) { + const imageUrl = image.urls.regular || image.urls.small; + let caption; + + if (image.id.startsWith('demo-')) { + caption = `${image.description} (Demo image)`; + } else { + caption = `Photo by ${image.user.name} on Unsplash`; + } + + elements.image.value = imageUrl; + elements.imageCaption.value = caption; + + state.currentPost.image = imageUrl; + state.currentPost.imageCaption = caption; + + elements.unsplashModal.classList.remove('active'); + markDirty(); + } // Toolbar functions function insertMarkdown(before, after = '', placeholder = '') { @@ -1909,22 +2426,37 @@ // Event listeners function setupEventListeners() { - // Form inputs - elements.title.addEventListener('input', (e) => { - const oldTitle = state.currentPost.title; - state.currentPost.title = e.target.value; - - // Auto-generate slug if it's empty or matches the old title's slug - const oldSlug = generateSlug(oldTitle); - const currentSlug = elements.slug.value; - - if (!currentSlug || currentSlug === oldSlug) { - const newSlug = generateSlug(e.target.value); - elements.slug.value = newSlug; - state.currentPost.slug = newSlug; - } - markDirty(); - }); + // Mobile menu toggle + elements.mobileMenuToggle.addEventListener('click', toggleMobileMenu); + + // Sidebar toggle + elements.sidebarToggle.addEventListener('click', toggleSidebar); + elements.sidebarClose.addEventListener('click', closeSidebar); + + // Preview mobile toggle + elements.previewMobileToggle.addEventListener('click', () => { + togglePreview(); + }); + + // Window resize + window.addEventListener('resize', handleResize); + + // Form inputs + elements.title.addEventListener('input', (e) => { + const oldTitle = state.currentPost.title; + state.currentPost.title = e.target.value; + + // Auto-generate slug if it's empty or matches the old title's slug + const oldSlug = generateSlug(oldTitle); + const currentSlug = elements.slug.value; + + if (!currentSlug || currentSlug === oldSlug) { + const newSlug = generateSlug(e.target.value); + elements.slug.value = newSlug; + state.currentPost.slug = newSlug; + } + markDirty(); + }); elements.slug.addEventListener('input', (e) => { state.currentPost.slug = e.target.value; @@ -1955,20 +2487,30 @@ markDirty(); }); - elements.imageCaption.addEventListener('input', (e) => { - state.currentPost.imageCaption = e.target.value; - markDirty(); - }); + elements.imageCaption.addEventListener('input', (e) => { + state.currentPost.imageCaption = e.target.value; + markDirty(); + }); - // Unsplash API key - elements.unsplashKey.addEventListener('input', (e) => { - const key = e.target.value.trim(); - if (key) { - localStorage.setItem('unsplash-access-key', key); - } else { - localStorage.removeItem('unsplash-access-key'); - } - }); + elements.authorName.addEventListener('input', (e) => { + state.currentPost.authorName = e.target.value; + markDirty(); + }); + + elements.authorEmail.addEventListener('input', (e) => { + state.currentPost.authorEmail = e.target.value; + markDirty(); + }); + + // Unsplash API key + elements.unsplashKey.addEventListener('input', (e) => { + const key = e.target.value.trim(); + if (key) { + localStorage.setItem('unsplash-access-key', key); + } else { + localStorage.removeItem('unsplash-access-key'); + } + }); // Tag input elements.tagInput.addEventListener('keydown', (e) => { @@ -1998,53 +2540,95 @@ markDirty(); }); - // Toolbar buttons - elements.boldBtn.addEventListener('click', () => insertMarkdown('**', '**', 'bold text')); - elements.italicBtn.addEventListener('click', () => insertMarkdown('*', '*', 'italic text')); - elements.linkBtn.addEventListener('click', () => { - const url = prompt('Enter URL:'); - if (url) { - insertMarkdown('[', `](${url})`, 'link text'); - } - }); - elements.imageBtn.addEventListener('click', () => { - const url = prompt('Enter image URL:'); - if (url) { - const alt = prompt('Enter alt text (optional):') || 'image'; - insertMarkdown(`![${alt}](${url})`, '', ''); - } - }); - elements.codeBtn.addEventListener('click', () => insertMarkdown('`', '`', 'code')); - elements.codeBlockBtn.addEventListener('click', () => insertMarkdown('\n```\n', '\n```\n', 'code block')); - elements.quoteBtn.addEventListener('click', () => insertMarkdown('> ', '', 'quote')); - elements.listBtn.addEventListener('click', () => insertMarkdown('- ', '', 'list item')); - elements.orderedListBtn.addEventListener('click', () => insertMarkdown('1. ', '', 'list item')); - elements.h1Btn.addEventListener('click', () => insertMarkdown('# ', '', 'Heading 1')); - elements.h2Btn.addEventListener('click', () => insertMarkdown('## ', '', 'Heading 2')); - elements.h3Btn.addEventListener('click', () => insertMarkdown('### ', '', 'Heading 3')); - elements.hrBtn.addEventListener('click', () => insertMarkdown('\n---\n', '', '')); - elements.previewBtn.addEventListener('click', togglePreview); - elements.focusBtn.addEventListener('click', toggleFocusMode); - elements.focusExit.addEventListener('click', toggleFocusMode); + // Toolbar buttons - ensure all work properly + elements.boldBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('**', '**', 'bold text'); + }); + elements.italicBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('*', '*', 'italic text'); + }); + elements.linkBtn.addEventListener('click', (e) => { + e.preventDefault(); + const url = prompt('Enter URL:'); + if (url) { + insertMarkdown('[', `](${url})`, 'link text'); + } + }); + elements.imageBtn.addEventListener('click', (e) => { + e.preventDefault(); + const url = prompt('Enter image URL:'); + if (url) { + const alt = prompt('Enter alt text (optional):') || 'image'; + insertMarkdown(`![${alt}](${url})`, '', ''); + } + }); + elements.codeBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('`', '`', 'code'); + }); + elements.codeBlockBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('\n```\n', '\n```\n', 'code block'); + }); + elements.quoteBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('> ', '', 'quote'); + }); + elements.listBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('- ', '', 'list item'); + }); + elements.orderedListBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('1. ', '', 'list item'); + }); + elements.h1Btn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('# ', '', 'Heading 1'); + }); + elements.h2Btn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('## ', '', 'Heading 2'); + }); + elements.h3Btn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('### ', '', 'Heading 3'); + }); + elements.hrBtn.addEventListener('click', (e) => { + e.preventDefault(); + insertMarkdown('\n---\n', '', ''); + }); + elements.previewBtn.addEventListener('click', (e) => { + e.preventDefault(); + togglePreview(); + }); + elements.focusBtn.addEventListener('click', (e) => { + e.preventDefault(); + toggleFocusMode(); + }); + elements.focusExit.addEventListener('click', toggleFocusMode); - // Header buttons - elements.themeToggle.addEventListener('click', toggleTheme); - elements.newBtn.addEventListener('click', newArticle); - elements.saveBtn.addEventListener('click', openSaveModal); - elements.loadBtn.addEventListener('click', openLoadModal); - elements.exportBtn.addEventListener('click', exportMarkdown); - elements.copyBtn.addEventListener('click', copyToClipboard); - - elements.importBtn.addEventListener('click', () => { - elements.fileInput.click(); - }); - - elements.fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (file) { - importMarkdown(file); - } - }); + // Header buttons + elements.themeToggle.addEventListener('click', toggleTheme); + elements.newBtn.addEventListener('click', newArticle); + elements.saveBtn.addEventListener('click', openSaveModal); + elements.loadBtn.addEventListener('click', openLoadModal); + elements.exportBtn.addEventListener('click', exportMarkdown); + elements.copyBtn.addEventListener('click', copyToClipboard); + + elements.importBtn.addEventListener('click', () => { + elements.fileInput.click(); + closeMobileMenu(); + }); + + elements.fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + importMarkdown(file); + } + }); // Unsplash modal elements.unsplashBtn.addEventListener('click', () => { @@ -2059,172 +2643,256 @@ elements.unsplashModal.classList.remove('active'); }); - elements.unsplashSearch.addEventListener('input', async (e) => { - const query = e.target.value.trim(); - if (query.length > 2) { - elements.unsplashResults.innerHTML = '
    '; - - // Show status - const hasApiKey = localStorage.getItem('unsplash-access-key'); - elements.unsplashStatus.style.display = 'block'; - if (hasApiKey) { - elements.unsplashStatus.style.background = 'var(--success)'; - elements.unsplashStatus.style.color = 'white'; - elements.unsplashStatus.textContent = '✓ Using Unsplash API - Real photos'; - } else { - elements.unsplashStatus.style.background = 'var(--warning)'; - elements.unsplashStatus.style.color = 'white'; - elements.unsplashStatus.textContent = '⚠ Using demo images - Add Unsplash API key in settings for real photos'; - } - - try { - const results = await searchUnsplash(query); - displayUnsplashResults(results); - } catch (error) { - console.error('Unsplash search failed:', error); - elements.unsplashResults.innerHTML = '

    Search failed. Please try again.

    '; - } - } else { - elements.unsplashStatus.style.display = 'none'; - } - }); + elements.unsplashSearch.addEventListener('input', async (e) => { + const query = e.target.value.trim(); + if (query.length > 2) { + elements.unsplashResults.innerHTML = '
    '; + + // Show status + const hasApiKey = localStorage.getItem('unsplash-access-key'); + elements.unsplashStatus.style.display = 'block'; + if (hasApiKey) { + elements.unsplashStatus.style.background = 'var(--success)'; + elements.unsplashStatus.style.color = 'white'; + elements.unsplashStatus.textContent = '✓ Using Unsplash API - Real photos'; + } else { + elements.unsplashStatus.style.background = 'var(--warning)'; + elements.unsplashStatus.style.color = 'white'; + elements.unsplashStatus.textContent = '⚠ Using demo images - Add Unsplash API key in settings for real photos'; + } + + try { + const results = await searchUnsplash(query); + displayUnsplashResults(results); + } catch (error) { + console.error('Unsplash search failed:', error); + elements.unsplashResults.innerHTML = '

    Search failed. Please try again.

    '; + } + } else { + elements.unsplashStatus.style.display = 'none'; + } + }); - // Keyboard shortcuts - document.addEventListener('keydown', (e) => { - if (e.ctrlKey || e.metaKey) { - switch (e.key) { - case 'b': - e.preventDefault(); - insertMarkdown('**', '**', 'bold text'); - break; - case 'i': - e.preventDefault(); - insertMarkdown('*', '*', 'italic text'); - break; - case 'k': - e.preventDefault(); - insertMarkdown('[', '](url)', 'link text'); - break; - case '`': - e.preventDefault(); - insertMarkdown('`', '`', 'code'); - break; - case 's': - e.preventDefault(); - openSaveModal(); - break; - case 'p': - e.preventDefault(); - togglePreview(); - break; - case 'n': - e.preventDefault(); - newArticle(); - break; - case 'o': - e.preventDefault(); - openLoadModal(); - break; - } - } - - // Escape key to exit focus mode - if (e.key === 'Escape' && state.focusMode) { - e.preventDefault(); - toggleFocusMode(); - } - }); + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case 'b': + e.preventDefault(); + insertMarkdown('**', '**', 'bold text'); + break; + case 'i': + e.preventDefault(); + insertMarkdown('*', '*', 'italic text'); + break; + case 'k': + e.preventDefault(); + insertMarkdown('[', '](url)', 'link text'); + break; + case '`': + e.preventDefault(); + insertMarkdown('`', '`', 'code'); + break; + case 's': + e.preventDefault(); + openSaveModal(); + break; + case 'p': + e.preventDefault(); + togglePreview(); + break; + case 'n': + e.preventDefault(); + newArticle(); + break; + case 'o': + e.preventDefault(); + openLoadModal(); + break; + } + } + + // Escape key to exit focus mode or close mobile elements + if (e.key === 'Escape') { + if (state.focusMode) { + e.preventDefault(); + toggleFocusMode(); + } else if (state.isMobile) { + if (elements.headerActions.classList.contains('active')) { + closeMobileMenu(); + } + if (state.sidebarOpen) { + closeSidebar(); + } + } + } + }); - // Note: Auto-save is now handled by: - // 1. Every 10 words (in updateWordCount function) - // 2. After 5 seconds of inactivity (in scheduleAutoSave function) + // Save modal events + elements.closeSaveModal.addEventListener('click', () => { + elements.saveModal.classList.remove('active'); + }); + + elements.cancelSave.addEventListener('click', () => { + elements.saveModal.classList.remove('active'); + }); + + elements.confirmSave.addEventListener('click', saveCurrentArticle); + + elements.saveTitle.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + saveCurrentArticle(); + } + }); - // Save modal events - elements.closeSaveModal.addEventListener('click', () => { - elements.saveModal.classList.remove('active'); - }); - - elements.cancelSave.addEventListener('click', () => { - elements.saveModal.classList.remove('active'); - }); - - elements.confirmSave.addEventListener('click', saveCurrentArticle); - - elements.saveTitle.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - saveCurrentArticle(); - } - }); + // Load modal events + elements.closeLoadModal.addEventListener('click', () => { + elements.loadModal.classList.remove('active'); + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + }); + + elements.cancelLoad.addEventListener('click', () => { + elements.loadModal.classList.remove('active'); + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + }); + + elements.deleteSelected.addEventListener('click', deleteSelectedArticle); + + elements.searchArticles.addEventListener('input', (e) => { + displayArticles(e.target.value); + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + }); - // Load modal events - elements.closeLoadModal.addEventListener('click', () => { - elements.loadModal.classList.remove('active'); - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - }); - - elements.cancelLoad.addEventListener('click', () => { - elements.loadModal.classList.remove('active'); - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - }); - - elements.deleteSelected.addEventListener('click', deleteSelectedArticle); - - elements.searchArticles.addEventListener('input', (e) => { - displayArticles(e.target.value); - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - }); + // Close modals when clicking outside + elements.unsplashModal.addEventListener('click', (e) => { + if (e.target === elements.unsplashModal) { + elements.unsplashModal.classList.remove('active'); + } + }); - // Close modals when clicking outside - elements.unsplashModal.addEventListener('click', (e) => { - if (e.target === elements.unsplashModal) { - elements.unsplashModal.classList.remove('active'); - } - }); + elements.saveModal.addEventListener('click', (e) => { + if (e.target === elements.saveModal) { + elements.saveModal.classList.remove('active'); + } + }); - elements.saveModal.addEventListener('click', (e) => { - if (e.target === elements.saveModal) { - elements.saveModal.classList.remove('active'); - } - }); + elements.loadModal.addEventListener('click', (e) => { + if (e.target === elements.loadModal) { + elements.loadModal.classList.remove('active'); + state.selectedArticleId = null; + elements.deleteSelected.disabled = true; + } + }); - elements.loadModal.addEventListener('click', (e) => { - if (e.target === elements.loadModal) { - elements.loadModal.classList.remove('active'); - state.selectedArticleId = null; - elements.deleteSelected.disabled = true; - } - }); + // Close mobile menu when clicking outside + document.addEventListener('click', (e) => { + if (state.isMobile && + elements.headerActions.classList.contains('active') && + !elements.headerActions.contains(e.target) && + !elements.mobileMenuToggle.contains(e.target)) { + closeMobileMenu(); + } + }); + + // Close sidebar when clicking backdrop on mobile + document.addEventListener('click', (e) => { + if (state.isMobile && + state.sidebarOpen && + !elements.sidebar.contains(e.target) && + !elements.sidebarToggle.contains(e.target)) { + closeSidebar(); + } + }); + + // Touch gestures for mobile (basic swipe to close sidebar) + let touchStartX = 0; + let touchStartY = 0; + + elements.sidebar.addEventListener('touchstart', (e) => { + touchStartX = e.touches[0].clientX; + touchStartY = e.touches[0].clientY; + }); + + elements.sidebar.addEventListener('touchmove', (e) => { + if (!state.isMobile || !state.sidebarOpen) return; + + const touchX = e.touches[0].clientX; + const touchY = e.touches[0].clientY; + const diffX = touchStartX - touchX; + const diffY = Math.abs(touchStartY - touchY); + + // If horizontal swipe is more significant than vertical + if (Math.abs(diffX) > diffY && diffX > 50) { + closeSidebar(); + } + }); } // Initialize the application function init() { - // Set initial date + // Set initial date in local time const now = new Date(); - elements.date.value = now.toISOString().slice(0, 16); + const localDateTime = new Date(now.getTime() - (now.getTimezoneOffset() * 60000)); + elements.date.value = localDateTime.toISOString().slice(0, 16); state.currentPost.date = elements.date.value; - // Apply saved theme - document.documentElement.setAttribute('data-theme', state.theme); - elements.themeToggle.textContent = state.theme === 'light' ? '🌙' : '☀️'; + // Apply saved theme + document.documentElement.setAttribute('data-theme', state.theme); + elements.themeToggle.textContent = state.theme === 'light' ? '🌙' : '☀'; - // Load saved Unsplash key - const savedKey = localStorage.getItem('unsplash-access-key'); - if (savedKey) { - elements.unsplashKey.value = savedKey; - } + // Load saved Unsplash key + const savedKey = localStorage.getItem('unsplash-access-key'); + if (savedKey) { + elements.unsplashKey.value = savedKey; + } - // Load saved draft - loadFromLocalStorage(); + // Check if mobile and set initial sidebar state + state.isMobile = window.innerWidth <= 768; + if (state.isMobile) { + // On mobile, sidebar starts hidden + state.sidebarOpen = false; + elements.sidebar.classList.remove('mobile-open'); + } else { + // On desktop, sidebar is always visible (default CSS) + state.sidebarOpen = false; // Not using mobile open state + } - // Setup event listeners - setupEventListeners(); + // Load saved draft + loadFromLocalStorage(); - // Initial update - updateWordCount(); + // Setup event listeners + setupEventListeners(); + + // Initial update + updateWordCount(); + + // Set initial viewport height for mobile + if (state.isMobile) { + const setVH = () => { + const vh = window.innerHeight * 0.01; + document.documentElement.style.setProperty('--vh', `${vh}px`); + }; + setVH(); + window.addEventListener('resize', setVH); + window.addEventListener('orientationchange', setVH); + } + + // Prevent zoom on inputs for iOS + if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { + const inputs = document.querySelectorAll('input, textarea, select'); + inputs.forEach(input => { + input.addEventListener('focus', () => { + input.style.fontSize = '16px'; + }); + input.addEventListener('blur', () => { + input.style.fontSize = ''; + }); + }); + } console.log('BSSG Post Editor initialized'); } @@ -2233,4 +2901,4 @@ document.addEventListener('DOMContentLoaded', init); - \ No newline at end of file + diff --git a/config.sh b/config.sh index eaa0895..a08d033 100644 --- a/config.sh +++ b/config.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # BSSG - Configuration File -# Version 0.16 +# Version 0.30 # Contains all configurable parameters for the static site generator # Developed by Stefano Marinelli (stefano@dragas.it) # @@ -47,6 +47,9 @@ RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". Include full post content in RSS feed. RSS_FILENAME="rss.xml" # The filename for the main RSS feed (e.g., feed.xml, rss.xml) ENABLE_ARCHIVES=true # Enable or disable archive pages +ENABLE_AUTHOR_PAGES=false # Enable or disable author pages (default: false) +ENABLE_AUTHOR_RSS=false # Enable or disable author-specific RSS feeds (default: false) +SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2) URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs. Available: Year, Month, Day, slug ENABLE_TAG_RSS=true # Enable or disable tag-specific RSS feed generation (default: true) diff --git a/locales/de.sh b/locales/de.sh index 66d5a00..3ef2cf0 100644 --- a/locales/de.sh +++ b/locales/de.sh @@ -3,14 +3,17 @@ export MSG_HOME="Startseite" export MSG_TAGS="Tags" +export MSG_AUTHORS="Autoren" export MSG_ARCHIVES="Archive" export MSG_RSS="RSS" export MSG_PAGES="Seiten" export MSG_SUBSCRIBE_RSS="Per RSS abonnieren" export MSG_PUBLISHED_ON="Veröffentlicht am" export MSG_BY="von" +export MSG_POSTS_BY="Beiträge von" export MSG_TAG_PAGE_TITLE="Beiträge mit dem Tag" export MSG_ALL_TAGS="Alle Tags" +export MSG_ALL_AUTHORS="Alle Autoren" export MSG_ALL_PAGES="Alle Seiten" export MSG_ARCHIVES_FOR="Archive für" export MSG_BACK_TO="Zurück zu" diff --git a/locales/en.sh b/locales/en.sh index 8652166..606e881 100644 --- a/locales/en.sh +++ b/locales/en.sh @@ -3,14 +3,17 @@ export MSG_HOME="Home" export MSG_TAGS="Tags" +export MSG_AUTHORS="Authors" export MSG_ARCHIVES="Archives" export MSG_RSS="RSS" export MSG_PAGES="Pages" export MSG_SUBSCRIBE_RSS="Subscribe via RSS" export MSG_PUBLISHED_ON="Published on" export MSG_BY="by" +export MSG_POSTS_BY="Posts by" export MSG_TAG_PAGE_TITLE="Posts tagged with" export MSG_ALL_TAGS="All Tags" +export MSG_ALL_AUTHORS="All Authors" export MSG_ALL_PAGES="All Pages" export MSG_ARCHIVES_FOR="Archives for" export MSG_BACK_TO="Back to" diff --git a/locales/es.sh b/locales/es.sh index 9149f23..8351c2f 100644 --- a/locales/es.sh +++ b/locales/es.sh @@ -3,14 +3,17 @@ export MSG_HOME="Inicio" export MSG_TAGS="Etiquetas" +export MSG_AUTHORS="Autores" export MSG_ARCHIVES="Archivos" export MSG_RSS="RSS" export MSG_PAGES="Páginas" export MSG_SUBSCRIBE_RSS="Suscribirse vía RSS" export MSG_PUBLISHED_ON="Publicado el" export MSG_BY="por" +export MSG_POSTS_BY="Entradas de" export MSG_TAG_PAGE_TITLE="Entradas etiquetadas con" export MSG_ALL_TAGS="Todas las etiquetas" +export MSG_ALL_AUTHORS="Todos los autores" export MSG_ALL_PAGES="Todas las páginas" export MSG_ARCHIVES_FOR="Archivos de" export MSG_BACK_TO="Volver a" diff --git a/locales/fr.sh b/locales/fr.sh index 4e71e26..af14657 100644 --- a/locales/fr.sh +++ b/locales/fr.sh @@ -3,14 +3,17 @@ export MSG_HOME="Accueil" export MSG_TAGS="Étiquettes" +export MSG_AUTHORS="Auteurs" export MSG_ARCHIVES="Archives" export MSG_RSS="RSS" export MSG_PAGES="Pages" export MSG_SUBSCRIBE_RSS="S'abonner via RSS" export MSG_PUBLISHED_ON="Publié le" export MSG_BY="par" +export MSG_POSTS_BY="Articles de" export MSG_TAG_PAGE_TITLE="Articles étiquetés avec" export MSG_ALL_TAGS="Toutes les étiquettes" +export MSG_ALL_AUTHORS="Tous les auteurs" export MSG_ALL_PAGES="Toutes les pages" export MSG_ARCHIVES_FOR="Archives pour" export MSG_BACK_TO="Retour à" diff --git a/locales/it.sh b/locales/it.sh index 501b34b..20275cf 100644 --- a/locales/it.sh +++ b/locales/it.sh @@ -3,14 +3,17 @@ export MSG_HOME="Home" export MSG_TAGS="Tag" +export MSG_AUTHORS="Autori" export MSG_ARCHIVES="Archivi" export MSG_RSS="RSS" export MSG_PAGES="Pagine" export MSG_SUBSCRIBE_RSS="Abbonati via RSS" export MSG_PUBLISHED_ON="Pubblicato il" export MSG_BY="da" +export MSG_POSTS_BY="Articoli di" export MSG_TAG_PAGE_TITLE="Articoli taggati con" export MSG_ALL_TAGS="Tutti i tag" +export MSG_ALL_AUTHORS="Tutti gli autori" export MSG_ALL_PAGES="Tutte le pagine" export MSG_ARCHIVES_FOR="Archivi per" export MSG_BACK_TO="Torna a" diff --git a/locales/ja.sh b/locales/ja.sh index bbfcda0..92a9f7d 100644 --- a/locales/ja.sh +++ b/locales/ja.sh @@ -3,14 +3,17 @@ export MSG_HOME="ホーム" export MSG_TAGS="タグ" +export MSG_AUTHORS="著者" export MSG_ARCHIVES="アーカイブ" export MSG_RSS="RSS" export MSG_PAGES="ページ" export MSG_SUBSCRIBE_RSS="RSSで購読する" export MSG_PUBLISHED_ON="公開日" export MSG_BY="作成者" +export MSG_POSTS_BY="の投稿" export MSG_TAG_PAGE_TITLE="タグ付きの投稿" export MSG_ALL_TAGS="すべてのタグ" +export MSG_ALL_AUTHORS="すべての著者" export MSG_ALL_PAGES="すべてのページ" export MSG_ARCHIVES_FOR="のアーカイブ" export MSG_BACK_TO="に戻る" diff --git a/locales/pt.sh b/locales/pt.sh index baa89a3..6313ddf 100644 --- a/locales/pt.sh +++ b/locales/pt.sh @@ -3,14 +3,17 @@ export MSG_HOME="Início" export MSG_TAGS="Etiquetas" +export MSG_AUTHORS="Autores" export MSG_ARCHIVES="Arquivos" export MSG_RSS="RSS" export MSG_PAGES="Páginas" export MSG_SUBSCRIBE_RSS="Subscrever via RSS" export MSG_PUBLISHED_ON="Publicado em" export MSG_BY="por" +export MSG_POSTS_BY="Posts de" export MSG_TAG_PAGE_TITLE="Posts etiquetados com" export MSG_ALL_TAGS="Todas as Etiquetas" +export MSG_ALL_AUTHORS="Todos os Autores" export MSG_ALL_PAGES="Todas as Páginas" export MSG_ARCHIVES_FOR="Arquivos de" export MSG_BACK_TO="Voltar para" diff --git a/locales/zh.sh b/locales/zh.sh index b00fbbc..10cd77b 100644 --- a/locales/zh.sh +++ b/locales/zh.sh @@ -3,14 +3,17 @@ export MSG_HOME="首页" export MSG_TAGS="标签" +export MSG_AUTHORS="作者" export MSG_ARCHIVES="归档" export MSG_RSS="RSS" export MSG_PAGES="页面" export MSG_SUBSCRIBE_RSS="通过 RSS 订阅" export MSG_PUBLISHED_ON="发布于" export MSG_BY="作者" +export MSG_POSTS_BY="的文章" export MSG_TAG_PAGE_TITLE="标签为 的文章" export MSG_ALL_TAGS="所有标签" +export MSG_ALL_AUTHORS="所有作者" export MSG_ALL_PAGES="所有页面" export MSG_ARCHIVES_FOR="的归档" export MSG_BACK_TO="返回" diff --git a/scripts/bssg.sh b/scripts/bssg.sh index c9ea3e9..092510a 100755 --- a/scripts/bssg.sh +++ b/scripts/bssg.sh @@ -109,7 +109,7 @@ chmod +x scripts/*.sh 2>/dev/null || true # Function to display help information show_help() { - echo "BSSG - Bash Static Site Generator (v0.16)" + echo "BSSG - Bash Static Site Generator (v0.30)" echo "=========================================" echo "" echo "Usage: $0 [--config ] command [options]" @@ -294,7 +294,7 @@ main() { shift 1 fi ;; - -f) + -f|--force-rebuild) export FORCE_REBUILD=true shift 1 ;; diff --git a/scripts/build/config_loader.sh b/scripts/build/config_loader.sh index a9e6815..3be4620 100755 --- a/scripts/build/config_loader.sh +++ b/scripts/build/config_loader.sh @@ -42,6 +42,9 @@ ENABLE_ARCHIVES="${ENABLE_ARCHIVES:-true}" URL_SLUG_FORMAT="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" PAGE_URL_FORMAT="${PAGE_URL_FORMAT:-slug}" ENABLE_TAG_RSS="${ENABLE_TAG_RSS:-false}" # Generate RSS feed for each tag +ENABLE_AUTHOR_PAGES="${ENABLE_AUTHOR_PAGES:-true}" # Generate author index pages +ENABLE_AUTHOR_RSS="${ENABLE_AUTHOR_RSS:-false}" # Generate RSS feed for each author +SHOW_AUTHORS_MENU_THRESHOLD="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" # Minimum authors to show menu # --- Backup Directory --- Added --- BACKUP_DIR="${BACKUP_DIR:-backup}" # Default backup location @@ -207,7 +210,7 @@ BSSG_CONFIG_VARS_ARRAY=( MARKDOWN_PL_PATH ENABLE_ARCHIVES URL_SLUG_FORMAT PAGE_URL_FORMAT DRAFTS_DIR REBUILD_AFTER_POST REBUILD_AFTER_EDIT CUSTOM_CSS - ENABLE_TAG_RSS + ENABLE_TAG_RSS ENABLE_AUTHOR_PAGES ENABLE_AUTHOR_RSS SHOW_AUTHORS_MENU_THRESHOLD BACKUP_DIR CACHE_DIR DEPLOY_AFTER_BUILD DEPLOY_SCRIPT ARCHIVES_LIST_ALL_POSTS @@ -255,6 +258,9 @@ export REBUILD_AFTER_POST export REBUILD_AFTER_EDIT export CUSTOM_CSS export ENABLE_TAG_RSS +export ENABLE_AUTHOR_PAGES +export ENABLE_AUTHOR_RSS +export SHOW_AUTHORS_MENU_THRESHOLD export BACKUP_DIR export CACHE_DIR export DEPLOY_AFTER_BUILD diff --git a/scripts/build/content.sh b/scripts/build/content.sh index b3d28ea..b2e0b12 100755 --- a/scripts/build/content.sh +++ b/scripts/build/content.sh @@ -91,7 +91,7 @@ extract_metadata() { fi # If we're here, we need to parse the file - local title="" date="" lastmod="" tags="" slug="" image="" image_caption="" description="" + local title="" date="" lastmod="" tags="" slug="" image="" image_caption="" description="" author_name="" author_email="" # Check file type and parse accordingly if [[ "$file" == *.html ]]; then @@ -106,6 +106,8 @@ extract_metadata() { image=$(grep -m 1 -o 'name="image" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') image_caption=$(grep -m 1 -o 'name="image_caption" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') description=$(grep -m 1 -o 'name="description" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_name=$(grep -m 1 -o 'name="author_name" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_email=$(grep -m 1 -o 'name="author_email" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') # Note: Excerpt generation (fallback for description) might not work well for HTML elif [[ "$file" == *.md ]]; then @@ -122,6 +124,7 @@ extract_metadata() { vars["title"] = ""; vars["date"] = ""; vars["lastmod"] = ""; vars["tags"] = ""; vars["slug"] = ""; vars["image"] = ""; vars["image_caption"] = ""; vars["description"] = ""; + vars["author_name"] = ""; vars["author_email"] = ""; } /^---$/ { if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } @@ -154,12 +157,13 @@ extract_metadata() { # Print values in specific order print vars["title"] "|" vars["date"] "|" vars["lastmod"] "|" \ vars["tags"] "|" vars["slug"] "|" vars["image"] "|" \ - vars["image_caption"] "|" vars["description"]; + vars["image_caption"] "|" vars["description"] "|" \ + vars["author_name"] "|" vars["author_email"]; } EOF ) - IFS='|' read -r title date lastmod tags slug image image_caption description <<< "$parsed_data" + IFS='|' read -r title date lastmod tags slug image image_caption description author_name author_email <<< "$parsed_data" else echo "Warning: Unknown file type '$file' for metadata extraction." >&2 @@ -188,9 +192,19 @@ EOF echo "[DEBUG] Generating excerpt for $file" >&2 description=$(generate_excerpt "$file") fi + + # Apply fallback logic for author fields + if [ -z "$author_name" ]; then + author_name="${AUTHOR_NAME:-Anonymous}" + fi + if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then + # Only use default email if using default name + author_email="${AUTHOR_EMAIL:-anonymous@example.com}" + fi + # If author_name is specified but author_email is empty, leave email empty # Construct the metadata string for comparison and caching - local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description" + local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description|$author_name|$author_email" # Check if there was a previous metadata file and compare if [ -f "$metadata_cache_file" ]; then diff --git a/scripts/build/generate_archives.sh b/scripts/build/generate_archives.sh index 9d6d8a9..701ea41 100755 --- a/scripts/build/generate_archives.sh +++ b/scripts/build/generate_archives.sh @@ -161,7 +161,7 @@ _generate_main_archive_index() { local post_year post_month post_day url_path post_url # Grep posts for this specific year and numeric month, sort REVERSE chronologically - grep "^$year|$month|" "$archive_index_file" 2>/dev/null | sort -t'|' -k5,5r | while IFS='|' read -r _ _ _ title date _ filename slug _; do + grep "^$year|$month|" "$archive_index_file" 2>/dev/null | sort -t'|' -k5,5r | while IFS='|' read -r _ _ _ title date _ filename slug _ _ _ author_name author_email; do # Construct post URL (logic adapted from process_single_month) if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then post_year="${BASH_REMATCH[1]}" @@ -335,7 +335,7 @@ process_single_month() { echo "
    " # Grep for posts from this specific month and year - grep "^$year|$month_num|" "$archive_index_file" 2>/dev/null | while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description; do + grep "^$year|$month_num|" "$archive_index_file" 2>/dev/null | while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description author_name author_email; do # --- Start: Card Generation Logic (copied from generate_tags.sh) --- local post_url post_year post_month post_day url_path @@ -367,11 +367,14 @@ process_single_month() { fi local formatted_date=$(format_date "$date" "$display_date_format") + # Determine author for display (with fallback) + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + # Use cat heredoc for multi-line article structure cat << EOF

    $title

    -
    ${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date
    +
    ${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date ${MSG_BY:-\"by\"} $display_author_name
    EOF if [ -n "$image" ]; then diff --git a/scripts/build/generate_authors.sh b/scripts/build/generate_authors.sh new file mode 100644 index 0000000..9c1b649 --- /dev/null +++ b/scripts/build/generate_authors.sh @@ -0,0 +1,476 @@ +#!/usr/bin/env bash +# +# BSSG - Author Page Generation +# Handles the creation of individual author pages and the main author index. +# + +# Source dependencies +# shellcheck source=utils.sh disable=SC1091 +source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_authors.sh"; exit 1; } +# shellcheck source=cache.sh disable=SC1091 +source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_authors.sh"; exit 1; } +# Source the feed generator script for the reusable RSS function +# shellcheck source=generate_feeds.sh disable=SC1091 +source "$(dirname "$0")/generate_feeds.sh" || { echo >&2 "Error: Failed to source generate_feeds.sh from generate_authors.sh"; exit 1; } + +# Generate author pages +generate_author_pages() { + echo -e "${YELLOW}Processing author pages${NC}${ENABLE_AUTHOR_RSS:+" and RSS feeds"}...${NC}" + + local authors_index_file="$CACHE_DIR/authors_index.txt" + local main_authors_index_output="$OUTPUT_DIR/authors/index.html" + local modified_authors_list_file="${CACHE_DIR:-.bssg_cache}/modified_authors.list" + + # Check if the authors index file exists (needed for listing authors) + if [ ! -f "$authors_index_file" ]; then + echo -e "${YELLOW}Authors index file not found at $authors_index_file. Skipping author page generation.${NC}" + # If the index doesn't exist, no authors were found in posts. + # Ensure the main output directory exists but is empty. + mkdir -p "$(dirname "$main_authors_index_output")" + echo -e "${GREEN}Author pages processed! (No authors found)${NC}" + echo -e "${GREEN}Generated author list pages. (No authors found)${NC}" + return 0 + fi + + # --- Calculate Latest Common Dependency Time --- START --- + # Get mtimes of config hash, templates, and locale file + local latest_common_dep_time=0 + local config_hash_time=$(get_file_mtime "$CONFIG_HASH_FILE") + latest_common_dep_time=$(( config_hash_time > latest_common_dep_time ? config_hash_time : latest_common_dep_time )) + + local template_dir="${TEMPLATES_DIR:-templates}" + if [ -d "$template_dir/${THEME:-default}" ]; then + template_dir="$template_dir/${THEME:-default}" + fi + local header_template="$template_dir/header.html" + local footer_template="$template_dir/footer.html" + local header_time=$(get_file_mtime "$header_template") + local footer_time=$(get_file_mtime "$footer_template") + latest_common_dep_time=$(( header_time > latest_common_dep_time ? header_time : latest_common_dep_time )) + latest_common_dep_time=$(( footer_time > latest_common_dep_time ? footer_time : latest_common_dep_time )) + + local active_locale_file="" + if [ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]; then + active_locale_file="${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" + elif [ -f "${LOCALE_DIR:-locales}/en.sh" ]; then + active_locale_file="${LOCALE_DIR:-locales}/en.sh" + fi + local locale_time=$(get_file_mtime "$active_locale_file") + latest_common_dep_time=$(( locale_time > latest_common_dep_time ? locale_time : latest_common_dep_time )) + # --- Calculate Latest Common Dependency Time --- END --- + + # --- Simplified Global Check --- START --- + # Decide if we need to proceed with any author generation steps at all. + local proceed_with_generation=false + local force_rebuild_status="${FORCE_REBUILD:-false}" + + if [ "$force_rebuild_status" = true ]; then + proceed_with_generation=true + echo "Force rebuild enabled, proceeding with author generation." >&2 # Debug + elif [ "$latest_common_dep_time" -gt 0 ] && { [ ! -f "$main_authors_index_output" ] || (( $(get_file_mtime "$main_authors_index_output") < latest_common_dep_time )); }; then + # Common dependencies are newer than the main output (or main output missing) + proceed_with_generation=true + echo "Common dependencies changed, proceeding with author generation." >&2 # Debug + elif [ -s "$modified_authors_list_file" ]; then + # Modified authors list exists and is not empty + proceed_with_generation=true + echo "Modified authors detected, proceeding with author generation." >&2 # Debug + elif [ ! -f "$main_authors_index_output" ]; then + # Fallback: if main output is missing, we should generate it + proceed_with_generation=true + echo "Main authors index missing, proceeding with author generation." >&2 # Debug + fi + + if [ "$proceed_with_generation" = false ]; then + echo -e "${GREEN}Authors index, author pages${NC}${ENABLE_AUTHOR_RSS:+, and author RSS feeds} appear up to date based on common dependencies and modified posts, skipping.${NC}" + echo -e "${GREEN}Author pages processed!${NC}" # Keep consistent final message + echo -e "${GREEN}Generated author list pages.${NC}" # Keep consistent final message + return 0 + fi + # --- Simplified Global Check --- END --- + + # --- Proceed with Generation --- + + # Get unique authors (Author|Slug pairs) + local unique_authors_lines=$(awk -F'|' '{print $1 "|" $2}' "$authors_index_file" | sort | uniq) + local author_count=$(echo "$unique_authors_lines" | grep -v '^$' | wc -l) + echo -e "Checking ${GREEN}$author_count${NC} author pages${NC}${ENABLE_AUTHOR_RSS:+/feeds} for changes (based on common deps & modified authors)" # Updated message + + # --- Pre-group posts by author slug --- START --- + local author_data_dir="$CACHE_DIR/author_data" + rm -rf "$author_data_dir" # Clean previous data + mkdir -p "$author_data_dir" + echo -e "Pre-grouping posts by author into ${BLUE}$author_data_dir${NC}..." + if awk -F'|' -v author_dir="$author_data_dir" ' + NF >= 2 { # Ensure at least author and slug fields exist + author_slug = $2; + if (author_slug != "") { + # Sanitize slug just in case for filename safety? (basic: remove /) + gsub(/\//, "_", author_slug); + output_file = author_dir "/" author_slug ".tmp"; + print $0 >> output_file; # Append the whole line + close(output_file); # Close file handle to avoid too many open files + } else { + print "Warning: Skipping line with empty author slug in authors_index: " $0 > "/dev/stderr"; + } + } + ' "$authors_index_file"; then + echo -e "${GREEN}Pre-grouping complete.${NC}" + else + echo -e "${RED}Error: Failed to pre-group author data using awk.${NC}" >&2 + return 1 + fi + # --- Pre-group posts by author slug --- END --- + + # Define a modified file_needs_rebuild function for parallel use + parallel_file_needs_rebuild() { + local output_file="$1" + local latest_dep_time="$2" # This should be latest_common_dep_time + + # Rebuild if output file doesn't exist + if [ ! -f "$output_file" ]; then + return 0 # Rebuild needed + fi + + local output_time=$(get_file_mtime "$output_file") + + # Rebuild if output is older than the latest relevant *common* dependency + if (( output_time < latest_dep_time )); then + return 0 # Rebuild needed + fi + + return 1 # No rebuild needed + } + + # Define a function to process a single author + process_author() { + local author_line="$1" + local author_data_dir="$2" + local latest_common_dep_time_for_author="$3" + local modified_authors_file="$4" # Accept filename instead of hash + + # --- Load modified authors from file --- + declare -A modified_authors_hash + if [ -f "$modified_authors_file" ]; then + local mod_author_local + while IFS= read -r mod_author_local || [[ -n "$mod_author_local" ]]; do + if [ -n "$mod_author_local" ]; then # Ensure not empty line + modified_authors_hash["$mod_author_local"]=1 + fi + done < "$modified_authors_file" + fi + + local author author_slug + IFS='|' read -r author author_slug <<< "$author_line" + + if [ -n "$author" ]; then + local author_page_html_file="$OUTPUT_DIR/authors/$author_slug/index.html" + local author_rss_file="$OUTPUT_DIR/authors/$author_slug/${RSS_FILENAME:-rss.xml}" + local author_page_rel_url="authors/${author_slug}/" + local author_rss_rel_url="/authors/${author_slug}/${RSS_FILENAME:-rss.xml}" + local rebuild_html=false + local rebuild_rss=false + + # --- Force rebuild flags if author was modified --- + local author_was_modified=false + if [ -n "${modified_authors_hash[$author]}" ]; then + author_was_modified=true + rebuild_html=true # Force rebuild if author was modified + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + rebuild_rss=true # Force rebuild if author was modified + fi + fi + + # --- Check if HTML rebuild is needed --- + if [ "$rebuild_html" = false ]; then + if parallel_file_needs_rebuild "$author_page_html_file" "$latest_common_dep_time_for_author"; then + rebuild_html=true + fi + fi + + # --- Check if RSS rebuild is needed --- + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ] && [ "$rebuild_rss" = false ]; then + if parallel_file_needs_rebuild "$author_rss_file" "$latest_common_dep_time_for_author"; then + rebuild_rss=true + fi + fi + + # --- Skip if no rebuilds needed --- + if [ "$rebuild_html" = false ] && { [ "${ENABLE_AUTHOR_RSS:-false}" = false ] || [ "$rebuild_rss" = false ]; }; then + echo "Author '$author' pages are up to date, skipping." + return 0 + fi + + # --- Load author posts data --- + local author_data_file="$author_data_dir/${author_slug}.tmp" + if [ ! -f "$author_data_file" ]; then + echo "Warning: No posts found for author '$author' (expected file: $author_data_file)" >&2 + return 0 + fi + + # Count posts for this author + local post_count=$(wc -l < "$author_data_file") + + echo "Processing author '$author' ($post_count posts)..." + + # --- Generate Author HTML Page --- + if [ "$rebuild_html" = true ]; then + mkdir -p "$(dirname "$author_page_html_file")" + + # Generate author page content + local author_page_content="" + author_page_content+="

    ${MSG_POSTS_BY:-Posts by} $author

    "$'\n' + + # Add RSS link if enabled + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + author_page_content+="

    ${MSG_RSS_FEED:-RSS Feed}

    "$'\n' + fi + + # Add posts list + author_page_content+="
    "$'\n' + + # Sort posts by date (newest first) and generate HTML + local posts_html="" + while IFS='|' read -r author_name author_slug_inner author_email post_title post_date post_lastmod post_filename post_slug post_image post_image_caption post_description; do + # Construct post URL using URL_SLUG_FORMAT (same logic as generate_posts.sh) + local post_url="" + if [ -n "$post_date" ]; then + local year month day + if [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + year=$(date +%Y); month=$(date +%m); day=$(date +%d) # Fallback + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; + url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$post_slug}" + # Ensure relative post_url starts with / and ends with / + post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')" + else + # Fallback for posts without date + post_url="/$(echo "$post_slug" | sed 's|^/||; s|/*$|/|')" + fi + # Convert to full URL with BASE_URL + post_url="$BASE_URL$post_url" + local formatted_date=$(format_date "$post_date") + + posts_html+="
    "$'\n' + posts_html+="

    $post_title

    "$'\n' + posts_html+="
    "$'\n' + posts_html+=" "$'\n' + posts_html+="
    "$'\n' + + if [ -n "$post_description" ]; then + posts_html+="

    $post_description

    "$'\n' + fi + + if [ -n "$post_image" ]; then + posts_html+="
    "$'\n' + posts_html+=" \"$post_image_caption\""$'\n' + posts_html+="
    "$'\n' + fi + + posts_html+="
    "$'\n' + done < <(sort -t'|' -k5,5r "$author_data_file") + + author_page_content+="$posts_html" + + author_page_content+="
    "$'\n' + + # Generate full HTML page + local page_title="${MSG_POSTS_BY:-Posts by} $author" + local page_description="${MSG_POSTS_BY:-Posts by} $author - $post_count ${MSG_POSTS:-posts}" + + # Process templates with placeholder replacement + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + + # Replace placeholders in the header (following tags generator pattern) + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"$page_title"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$page_description"} + header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"$author_page_rel_url"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + + # Remove unprocessed image placeholders + header_content=${header_content//\{\{og_image\}\}/} + header_content=${header_content//\{\{twitter_image\}\}/} + + # Remove the placeholder for the tag-specific RSS feed link + header_content=${header_content///} + + # Add author RSS link if enabled + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + local author_rss_link="" + header_content=${header_content///$author_rss_link} + fi + + # Schema.org structured data + local schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"$page_description\",\"url\": \"$SITE_URL$author_page_rel_url\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" + header_content=${header_content//\{\{schema_json_ld\}\}/""} + + # Replace placeholders in the footer + local current_year=$(date +%Y) + footer_content=${footer_content//\{\{current_year\}\}/"$current_year"} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"} + + # Create the full HTML page + { + echo "$header_content" + echo "$author_page_content" + echo "$footer_content" + } > "$author_page_html_file" + + echo "Generated author page: $author_page_html_file" + fi + + # --- Generate Author RSS Feed --- + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ] && [ "$rebuild_rss" = true ]; then + mkdir -p "$(dirname "$author_rss_file")" + + # Generate RSS feed for this author + local rss_title="$SITE_TITLE - ${MSG_POSTS_BY:-Posts by} $author" + local rss_description="${MSG_POSTS_BY:-Posts by} $author" + local feed_link_rel="$author_page_rel_url" + local feed_atom_link_rel="$author_rss_rel_url" + + # Read and format author post data for RSS generation + local author_post_data="" + if [ -f "$author_data_file" ]; then + # Transform author data format to RSS format and sort by date (newest first) + # Author format: Author|Slug|Email|Title|Date|LastMod|Filename|PostSlug|Image|ImageCaption|Description + # RSS format: file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email + author_post_data=$(sort -t'|' -k5,5r "$author_data_file" | awk -F'|' '{ + # Map fields from author format to RSS format + author_name = $1 + author_slug = $2 + author_email = $3 + title = $4 + date = $5 + lastmod = $6 + filename = $7 + post_slug = $8 + image = $9 + image_caption = $10 + description = $11 + + # RSS format: file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email + printf "%s|%s|%s|%s|%s||%s|%s|%s|%s|%s|%s\n", filename, filename, title, date, lastmod, post_slug, image, image_caption, description, author_name, author_email + }') + fi + + # Check if _generate_rss_feed function exists + if ! command -v _generate_rss_feed > /dev/null 2>&1; then + echo -e "${RED}Error: _generate_rss_feed function not found. Ensure generate_feeds.sh is sourced correctly.${NC}" >&2 + else + _generate_rss_feed "$author_rss_file" "$rss_title" "$rss_description" "$feed_link_rel" "$feed_atom_link_rel" "$author_post_data" + echo "Generated author RSS feed: $author_rss_file" + fi + fi + fi + } + + # Export the function for potential parallel use + export -f process_author parallel_file_needs_rebuild + + # Process each unique author + echo "$unique_authors_lines" | while IFS= read -r author_line || [[ -n "$author_line" ]]; do + if [ -n "$author_line" ]; then + process_author "$author_line" "$author_data_dir" "$latest_common_dep_time" "$modified_authors_list_file" + fi + done + + # --- Generate Main Authors Index Page --- + if [ "${AUTHORS_INDEX_NEEDS_REBUILD:-false}" = true ] || [ ! -f "$main_authors_index_output" ] || (( $(get_file_mtime "$main_authors_index_output") < latest_common_dep_time )); then + echo "Generating main authors index page..." + mkdir -p "$(dirname "$main_authors_index_output")" + + # Count posts per author and generate the main index + local authors_with_counts="" + echo "$unique_authors_lines" | while IFS= read -r author_line || [[ -n "$author_line" ]]; do + if [ -n "$author_line" ]; then + local author author_slug + IFS='|' read -r author author_slug <<< "$author_line" + local author_data_file="$author_data_dir/${author_slug}.tmp" + if [ -f "$author_data_file" ]; then + local post_count=$(wc -l < "$author_data_file") + echo "$author|$author_slug|$post_count" + fi + fi + done | sort > "${CACHE_DIR}/authors_with_counts.tmp" + + # Generate main authors index HTML + local main_content="" + main_content+="

    ${MSG_ALL_AUTHORS:-All Authors}

    "$'\n' + main_content+="
    "$'\n' # Reuse tags styling + + while IFS='|' read -r author author_slug post_count; do + if [ -n "$author" ] && [ "$post_count" -gt 0 ]; then + main_content+=" $author ($post_count)"$'\n' + fi + done < "${CACHE_DIR}/authors_with_counts.tmp" + + main_content+="
    "$'\n' + + # Generate full HTML page for main authors index + local page_title="${MSG_ALL_AUTHORS:-All Authors}" + local page_description="${MSG_ALL_AUTHORS:-All Authors} - $SITE_DESCRIPTION" + local authors_index_rel_url="authors/" + + # Process templates with placeholder replacement (following tags generator pattern) + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + + # Replace placeholders in the header + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"$page_title"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$page_description"} + header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"$authors_index_rel_url"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + + # Remove unprocessed image placeholders + header_content=${header_content//\{\{og_image\}\}/} + header_content=${header_content//\{\{twitter_image\}\}/} + + # Remove the placeholder for the tag-specific RSS feed link in the main authors index + header_content=${header_content///} + + # Schema.org structured data + local schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"List of all authors on $SITE_TITLE\",\"url\": \"$SITE_URL/authors/\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" + header_content=${header_content//\{\{schema_json_ld\}\}/""} + + # Replace placeholders in the footer + local current_year=$(date +%Y) + footer_content=${footer_content//\{\{current_year\}\}/"$current_year"} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"} + + { + echo "$header_content" + echo "$main_content" + echo "$footer_content" + } > "$main_authors_index_output" + + echo "Generated main authors index: $main_authors_index_output" + + # Clean up temporary file + rm -f "${CACHE_DIR}/authors_with_counts.tmp" + else + echo "Main authors index is up to date, skipping..." + fi + + # Clean up author data directory + rm -rf "$author_data_dir" + + echo -e "${GREEN}Author pages processed!${NC}" + echo -e "${GREEN}Generated author list pages.${NC}" +} \ No newline at end of file diff --git a/scripts/build/generate_feeds.sh b/scripts/build/generate_feeds.sh index 3385bd2..9d13d7c 100755 --- a/scripts/build/generate_feeds.sh +++ b/scripts/build/generate_feeds.sh @@ -83,7 +83,7 @@ _generate_rss_feed() { # Create the RSS feed header cat > "$output_file" << EOF - + $(html_escape "$feed_title") $(fix_url "$feed_link_rel") @@ -94,7 +94,7 @@ _generate_rss_feed() { EOF # Process the provided post data - echo "$post_data_input" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description; do + echo "$post_data_input" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do # Skip if essential fields are missing (robustness) if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ] || [ -z "$lastmod" ] || [ -z "$slug" ]; then echo "Warning: Skipping RSS item due to missing fields in input line: file=$file, title=$title, date=$date, lastmod=$lastmod, slug=$slug" >&2 @@ -175,11 +175,25 @@ EOF fi # Combine parts safely - item_description_content="${figure_part}${caption_part}${content_part}" + item_description_content="${figure_part}${content_part}" # Wrap final description in CDATA local final_description="" + # Determine author for RSS item (with fallback) + local rss_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local rss_author_email="${author_email}" + + # Build author element if we have author info + local author_element="" + if [ -n "$rss_author_name" ]; then + if [ -n "$rss_author_email" ]; then + author_element=" $(html_escape "$rss_author_name") ($(html_escape "$rss_author_email"))" + else + author_element=" $(html_escape "$rss_author_name")" + fi + fi + cat >> "$output_file" << EOF $(html_escape "$title") @@ -188,6 +202,7 @@ EOF ${pub_date} ${updated_date_iso} ${final_description} +${author_element} EOF done @@ -284,6 +299,7 @@ generate_sitemap() { local sitemap="$OUTPUT_DIR/sitemap.xml" local file_index="$CACHE_DIR/file_index.txt" local tags_index="$CACHE_DIR/tags_index.txt" + local authors_index="$CACHE_DIR/authors_index.txt" local primary_pages_cache="$CACHE_DIR/primary_pages.tmp" local secondary_pages_cache="$CACHE_DIR/secondary_pages.tmp" local config_hash_file="$CONFIG_HASH_FILE" # Use the global var @@ -309,6 +325,7 @@ generate_sitemap() { # Check main content index files if [ -f "$file_index" ] && [ "$(get_file_mtime "$file_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi if ! $rebuild_needed && [ -f "$tags_index" ] && [ "$(get_file_mtime "$tags_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi + if ! $rebuild_needed && [ -f "$authors_index" ] && [ "$(get_file_mtime "$authors_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi if ! $rebuild_needed && [ -f "$primary_pages_cache" ] && [ "$(get_file_mtime "$primary_pages_cache")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi if ! $rebuild_needed && [ -f "$secondary_pages_cache" ] && [ "$(get_file_mtime "$secondary_pages_cache")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi # Removed checks for script, config, locale mtime for simplicity to avoid sourcing errors @@ -320,9 +337,10 @@ generate_sitemap() { return 0 fi - # --- Pre-calculate latest dates (Still needed for Homepage/Tags) --- + # --- Pre-calculate latest dates (Still needed for Homepage/Tags/Authors) --- local latest_post_mod_date=$(get_latest_mod_date "$file_index" 5 "" "$sitemap_date_fmt") local latest_tag_page_mod_date=$(get_latest_mod_date "$tags_index" 5 "" "$sitemap_date_fmt") # Assumes lastmod is relevant field in tags_index + local latest_author_page_mod_date=$(get_latest_mod_date "$authors_index" 6 "" "$sitemap_date_fmt") # Field 6 is lastmod in authors_index # --- Generate Sitemap using AWK --- START --- echo "Generating sitemap content using awk..." @@ -339,10 +357,12 @@ generate_sitemap() { -v url_slug_format="$URL_SLUG_FORMAT" \ -v latest_post_mod_date="$latest_post_mod_date" \ -v latest_tag_page_mod_date="$latest_tag_page_mod_date" \ + -v latest_author_page_mod_date="$latest_author_page_mod_date" \ + -v enable_author_pages="${ENABLE_AUTHOR_PAGES:-true}" \ -v sitemap_date_fmt="$sitemap_date_fmt" \ -F'|' \ -f - \ - "$file_index" "$primary_pages_cache" "$secondary_pages_cache" "$tags_index" <<'AWK_EOF' > "$sitemap" + "$file_index" "$primary_pages_cache" "$secondary_pages_cache" "$tags_index" "$authors_index" <<'AWK_EOF' > "$sitemap" # AWK script for sitemap generation (fed via here-doc) BEGIN { OFS=""; # No output field separator needed for XML @@ -466,6 +486,37 @@ FILENAME == ARGV[4] { } } +# Process authors_index.txt (Author Pages) - only if author pages are enabled +FILENAME == ARGV[5] && enable_author_pages == "true" { + author_name=$1; author_slug=$2; # $6 = lastmod for posts with this author + if (length(author_slug) == 0) next; + # Check if author slug already processed + if ( !(author_slug in processed_authors) ) { + processed_authors[author_slug] = 1; # Mark as processed + + # Add main authors index page (only once) + if (!authors_index_added) { + authors_index_added = 1; + print " "; + print " " fix_url_awk("/authors/", site_url) ""; + print " " latest_author_page_mod_date ""; + print " weekly"; + print " 0.6"; + print " "; + } + + # Add individual author page + item_url = "/authors/" author_slug "/"; + mod_time = latest_author_page_mod_date; + print " "; + print " " fix_url_awk(item_url, site_url) ""; + print " " mod_time ""; + print " weekly"; + print " 0.5"; + print " "; + } +} + END { print ""; } diff --git a/scripts/build/generate_index.sh b/scripts/build/generate_index.sh index c2dc92b..455d1af 100755 --- a/scripts/build/generate_index.sh +++ b/scripts/build/generate_index.sh @@ -190,7 +190,7 @@ EOF local end_index=$(( current_page * POSTS_PER_PAGE )) # Add posts to the index page - awk -v start="$start_index" -v end="$end_index" 'NR >= start && NR <= end { print }' "$file_index" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description; do + awk -v start="$start_index" -v end="$end_index" 'NR >= start && NR <= end { print }' "$file_index" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do # ... (rest of the post item generation logic remains the same) ... if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ]; then continue @@ -216,7 +216,7 @@ EOF cat >> "$output_file" << EOF

    $title

    -
    ${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${AUTHOR_NAME:+" ${MSG_BY:-"by"} $AUTHOR_NAME"}
    +
    ${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${author_name:+" ${MSG_BY:-"by"} ${author_name:-$AUTHOR_NAME}"}
    EOF if [ -n "$image" ]; then local image_url="$image" diff --git a/scripts/build/generate_posts.sh b/scripts/build/generate_posts.sh index 7a82233..22592e3 100755 --- a/scripts/build/generate_posts.sh +++ b/scripts/build/generate_posts.sh @@ -26,6 +26,8 @@ convert_markdown() { local image="$8" local image_caption="$9" local description="${10}" + local author_name="${11}" + local author_email="${12}" local content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$input_file")" local output_html_file="$output_base_path/index.html" @@ -239,13 +241,23 @@ convert_markdown() { image_url=$(fix_url "$image") fi - # Create JSON-LD - schema_json_ld=$(printf '' \ + # Create JSON-LD using post-specific author info + local post_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local post_author_email="${author_email:-${AUTHOR_EMAIL:-anonymous@example.com}}" + + # Build author JSON - only include email if it's provided + local author_json + if [ -n "$author_email" ]; then + author_json=$(printf '{\n "@type": "Person",\n "name": "%s",\n "email": "%s"\n }' "$post_author_name" "$post_author_email") + else + author_json=$(printf '{\n "@type": "Person",\n "name": "%s"\n }' "$post_author_name") + fi + + schema_json_ld=$(printf '' \ "$(echo "$title" | sed 's/"/\"/g')" \ "$iso_date" \ "$iso_lastmod_date" \ - "${AUTHOR_NAME:-Anonymous}" \ - "${AUTHOR_EMAIL:-anonymous@example.com}" \ + "$author_json" \ "$SITE_TITLE" \ "$SITE_URL" \ "$(echo "$meta_desc" | sed 's/"/\"/g')" \ @@ -281,11 +293,19 @@ convert_markdown() { local formatted_lastmod=$(format_date "$lastmod" "$display_date_format") local post_meta_reading_time post_meta_reading_time=$(printf "${MSG_READING_TIME_TEMPLATE:-%d min read}" "$reading_time") - local post_meta="
    ${MSG_PUBLISHED_ON:-Published on}: $formatted_date" + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local post_meta="
    " + post_meta+="

    " + post_meta+="${MSG_PUBLISHED_ON:-Published on}: ${MSG_BY:-by} $display_author_name" + post_meta+="

    " if [ "$formatted_date" != "$formatted_lastmod" ]; then - post_meta+=" • ${MSG_UPDATED_ON:-Updated on}: $formatted_lastmod" + post_meta+="

    " + post_meta+="${MSG_UPDATED_ON:-Updated on}: • $post_meta_reading_time" + post_meta+="

    " + else + post_meta+="

    $post_meta_reading_time

    " fi - post_meta+=" • $post_meta_reading_time
    " + post_meta+="
    " # Construct featured image HTML local image_html="" @@ -300,8 +320,9 @@ convert_markdown() { # Replace placeholders in footer content local current_year=$(date +'%Y') + local post_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" footer_content=${footer_content//\{\{current_year\}\}/$current_year} - footer_content=${footer_content//\{\{author_name\}\}/${AUTHOR_NAME:-Anonymous}} + footer_content=${footer_content//\{\{author_name\}\}/$post_author_name} final_html+="${footer_content}" @@ -325,6 +346,7 @@ process_all_markdown_files() { local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" local modified_tags_list="${CACHE_DIR:-.bssg_cache}/modified_tags.list" # Define path for modified tags + local modified_authors_list="${CACHE_DIR:-.bssg_cache}/modified_authors.list" # Define path for modified authors local file_index_prev="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" # Path to previous index if [ ! -f "$file_index" ]; then @@ -339,10 +361,13 @@ process_all_markdown_files() { fi echo -e "Checking ${GREEN}$total_file_count${NC} potential posts listed in index." - # --- Start Change: Clear previous modified tags list --- + # --- Start Change: Clear previous modified tags and authors lists --- echo "Clearing previous modified tags list: $modified_tags_list" >&2 # Debug message + echo "Clearing previous modified authors list: $modified_authors_list" >&2 # Debug message rm -f "$modified_tags_list" + rm -f "$modified_authors_list" touch "$modified_tags_list" # Ensure file exists even if empty + touch "$modified_authors_list" # Ensure file exists even if empty # --- End Change --- # Pre-filter files that need rebuilding @@ -369,8 +394,8 @@ process_all_markdown_files() { local locale_time=$(get_file_mtime "$active_locale_file") while IFS= read -r line; do - local file filename title date lastmod tags slug image image_caption description - IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line" + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" # Basic check if it looks like a post if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then @@ -413,7 +438,7 @@ process_all_markdown_files() { if $needs_rebuild; then files_to_process_list+=("$line") files_to_process_count=$((files_to_process_count + 1)) - # --- Start Change: Track ALL modified tags (old and new) --- + # --- Start Change: Track ALL modified tags and authors (old and new) --- # 'tags' variable holds the NEW tags from the current file_index line local new_tags="$tags" local old_tags="" @@ -433,6 +458,24 @@ process_all_markdown_files() { # Split by comma, trim, filter empty, sort unique, and add each tag on a new line echo "$combined_tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . | sort -u >> "$modified_tags_list" fi + + # Track modified authors (similar logic to tags) + local new_author="$author_name" + local old_author="" + # Try to get old author from the previous index snapshot + if [ -f "$file_index_prev" ]; then + # Grep for the exact file path ($file), assuming it's the first field + # Extract the 11th field (author_name) + old_author=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f11) + fi + + # Add both old and new authors to the modified list (if they exist) + if [ -n "$old_author" ] && [ "$old_author" != "" ]; then + echo "$old_author" >> "$modified_authors_list" + fi + if [ -n "$new_author" ] && [ "$new_author" != "" ]; then + echo "$new_author" >> "$modified_authors_list" + fi # --- End Change --- else # Only print skip message if not rebuilding @@ -441,7 +484,7 @@ process_all_markdown_files() { fi done < "$file_index" - # --- Start Change: Unique sort the modified tags list (redundant now but safe) --- + # --- Start Change: Unique sort the modified tags and authors lists (redundant now but safe) --- if [ -f "$modified_tags_list" ]; then echo "Sorting and making modified tags list unique: $modified_tags_list" >&2 # Debug message local temp_tags_list=$(mktemp) @@ -449,6 +492,14 @@ process_all_markdown_files() { sort -u "$modified_tags_list" > "$temp_tags_list" mv "$temp_tags_list" "$modified_tags_list" fi + + if [ -f "$modified_authors_list" ]; then + echo "Sorting and making modified authors list unique: $modified_authors_list" >&2 # Debug message + local temp_authors_list=$(mktemp) + # Sort unique to remove duplicates + sort -u "$modified_authors_list" > "$temp_authors_list" + mv "$temp_authors_list" "$modified_authors_list" + fi # --- End Change --- # Check if any files need processing @@ -466,8 +517,8 @@ process_all_markdown_files() { local line="$1" # Read the line from the argument variable - local file filename title date lastmod tags slug image image_caption description - IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line" + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" # No need for the basic check here, already done in pre-filter @@ -489,7 +540,7 @@ process_all_markdown_files() { # Call the main conversion function # We no longer rely on its internal file_needs_rebuild check # TODO: Consider modifying convert_markdown to accept a force flag or skip its check - if ! convert_markdown "$file" "$output_path" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description"; then + if ! convert_markdown "$file" "$output_path" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description" "$author_name" "$author_email"; then local exit_code=$? echo -e "${RED}ERROR:${NC} convert_markdown failed for '$file' with exit code $exit_code. Output HTML may be missing or incomplete." >&2 fi diff --git a/scripts/build/generate_tags.sh b/scripts/build/generate_tags.sh index d738196..01e6012 100755 --- a/scripts/build/generate_tags.sh +++ b/scripts/build/generate_tags.sh @@ -307,8 +307,8 @@ EOF if [ -z "$post_line" ]; then continue; fi # echo "DEBUG (process_tag for '$tag'): Processing post_line: $post_line" >&2 # Removed - local _ _ title date lastmod filename slug image image_caption description - IFS='|' read -r _ _ title date lastmod filename slug image image_caption description <<< "$post_line" + local _ _ title date lastmod filename slug image image_caption description author_name author_email + IFS='|' read -r _ _ title date lastmod filename slug image image_caption description author_name author_email <<< "$post_line" # Create slug-based URL path local post_year post_month post_day @@ -333,6 +333,9 @@ EOF fi local formatted_date=$(format_date "$date" "$display_date_format") + # Determine author for display (with fallback) + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + # --- Start Debug: Check variables before appending article --- #echo "DEBUGAPPEND (tag='$tag', title='$title'): Appending article HTML with link='$post_link', date='$formatted_date'" >&2 # --- End Debug --- @@ -340,7 +343,7 @@ EOF cat >> "$tag_page_html_file" << EOF

    $title

    -
    ${MSG_PUBLISHED_ON:-"Published on"} $formatted_date
    +
    ${MSG_PUBLISHED_ON:-"Published on"} $formatted_date ${MSG_BY:-"by"} $display_author_name
    EOF if [ -n "$image" ]; then @@ -393,9 +396,9 @@ EOF # Get post data for this tag from the tags index # Sort by post date (field 4), then lastmod (field 5) reverse, limit - # IMPORTANT: tags_index.txt has format: Tag|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|Image|ImageCaption|PostDescription|OriginalFilePath + # IMPORTANT: tags_index.txt has format: Tag|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|Image|ImageCaption|PostDescription|AuthorName|AuthorEmail # We need to map this to the format expected by _generate_rss_feed: - # file|filename|title|date|lastmod|tags|slug|image|image_caption|description + # file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email # We lack the original 'file' path and 'tags' string here. We can approximate. local tag_post_data_tmp=$(mktemp) @@ -404,8 +407,8 @@ EOF head -n "$rss_item_limit" | \ awk -F'|' -v tag_val="$tag" 'BEGIN {OFS="|"} { # Reconstruct needed fields. Use filename ($6) as placeholder for first field. - # file (placeholder) | filename | title | date | lastmod | tags | slug | image | image_caption | description - print $6 "|" $6 "|" $3 "|" $4 "|" $5 "|" tag_val "|" $7 "|" $8 "|" $9 "|" $10 + # file (placeholder) | filename | title | date | lastmod | tags | slug | image | image_caption | description | author_name | author_email + print $6 "|" $6 "|" $3 "|" $4 "|" $5 "|" tag_val "|" $7 "|" $8 "|" $9 "|" $10 "|" $11 "|" $12 }' > "$tag_post_data_tmp" local tag_post_data=$(cat "$tag_post_data_tmp") diff --git a/scripts/build/indexing.sh b/scripts/build/indexing.sh index 69064dc..044f6aa 100755 --- a/scripts/build/indexing.sh +++ b/scripts/build/indexing.sh @@ -32,6 +32,7 @@ _build_raw_file_index() { vars["title"] = ""; vars["date"] = ""; vars["lastmod"] = ""; vars["tags"] = ""; vars["slug"] = ""; vars["image"] = ""; vars["image_caption"] = ""; vars["description"] = ""; + vars["author_name"] = ""; vars["author_email"] = ""; in_fm = 0; found_fm = 0; is_html = (FILENAME ~ /\.html$/); is_md = (FILENAME ~ /\.md$/); @@ -40,7 +41,8 @@ _build_raw_file_index() { if (NR > 1) { # Print previous file raw data print current_filename, current_basename, vars["title"], vars["date"], vars["lastmod"], \ - vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"]; + vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"], \ + vars["author_name"], vars["author_email"]; } reset_vars(); current_filename = FILENAME; @@ -101,7 +103,8 @@ _build_raw_file_index() { if (NR > 0) { # Print last file raw data print current_filename, current_basename, vars["title"], vars["date"], vars["lastmod"], \ - vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"]; + vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"], \ + vars["author_name"], vars["author_email"]; } } EOF @@ -119,9 +122,9 @@ _process_raw_file_index() { > "$output_processed_index" # Ensure output file is empty - local file filename title date lastmod tags slug image image_caption description + local file filename title date lastmod tags slug image image_caption description author_name author_email local file_mtime - while IFS='|' read -r file filename title date lastmod tags slug image image_caption description || [[ -n "$file" ]]; do + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email || [[ -n "$file" ]]; do # Fallback for Title (use filename without extension) if [ -z "$title" ]; then title="${filename%.*}" @@ -155,8 +158,18 @@ _process_raw_file_index() { description=$(generate_excerpt "$file") fi + # Apply fallback logic for author fields + if [ -z "$author_name" ]; then + author_name="${AUTHOR_NAME:-Anonymous}" + fi + if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then + # Only use default email if using default name + author_email="${AUTHOR_EMAIL:-}" + fi + # If author_name is specified but author_email is empty, leave email empty + # Output the fully processed line to the final index file - echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description" >> "$output_processed_index" + echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description|$author_name|$author_email" >> "$output_processed_index" done < "$input_raw_index" wait # Ensure background processes from potential subshells (like generate_excerpt) finish } @@ -323,7 +336,7 @@ build_tags_index() { # Use awk for efficient processing and slug generation awk -F'|' -v OFS='|' '{ # $1=file, $2=filename, $3=title, $4=date, $5=lastmod, - # $6=tags, $7=slug, $8=image, $9=image_caption, $10=description + # $6=tags, $7=slug, $8=image, $9=image_caption, $10=description, $11=author_name, $12=author_email if (length($6) > 0) { # Check if tags field is not empty split($6, tags_array, ","); # Split tags by comma for (i in tags_array) { @@ -337,8 +350,8 @@ build_tags_index() { gsub(/^-+|-+$/, "", tag_slug); # Trim leading/trailing hyphens if (length(tag_slug) == 0) tag_slug = "-"; # Handle empty slugs - # Print: TagName|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription - print tag, tag_slug, $3, $4, $5, $2, $7, $8, $9, $10; + # Print: TagName|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription|AuthorName|AuthorEmail + print tag, tag_slug, $3, $4, $5, $2, $7, $8, $9, $10, $11, $12; } } }' "$file_index" > "$tags_index_file" @@ -356,6 +369,149 @@ build_tags_index() { echo -e "${GREEN}Tags index built!${NC}" } +# Build authors index from the file index +build_authors_index() { + echo -e "${YELLOW}Building authors index...${NC}" + + local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" + + # Check if rebuild is needed: missing cache or input/dependencies changed + local rebuild_needed=false + if [ ! -f "$authors_index_file" ]; then + rebuild_needed=true + elif file_needs_rebuild "$file_index" "$authors_index_file"; then + echo -e "${YELLOW}Authors index is outdated or dependencies changed, rebuilding authors...${NC}" + rebuild_needed=true + fi + + if [ "$rebuild_needed" = false ]; then + echo -e "${GREEN}Authors index is up to date, skipping...${NC}" + return 0 + fi + + if [ ! -f "$file_index" ]; then + echo -e "${RED}Error: File index '$file_index' not found. Cannot build authors index.${NC}" + return 1 + fi + + lock_file "$authors_index_file" + + > "$authors_index_file" # Clear the file + + # Read from file index and extract author info + # Use awk for efficient processing and slug generation + awk -F'|' -v OFS='|' '{ + # $1=file, $2=filename, $3=title, $4=date, $5=lastmod, + # $6=tags, $7=slug, $8=image, $9=image_caption, $10=description, $11=author_name, $12=author_email + author_name = $11; + author_email = $12; + + # Skip if author_name is empty + if (length(author_name) == 0) next; + + # Generate slug within awk (replicating generate_slug logic) + author_slug = tolower(author_name); + gsub(/[^a-z0-9]+/, "-", author_slug); # Replace non-alphanumeric with hyphens + gsub(/^-+|-+$/, "", author_slug); # Trim leading/trailing hyphens + if (length(author_slug) == 0) author_slug = "anonymous"; # Handle empty slugs + + # Print: AuthorName|AuthorSlug|AuthorEmail|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription + print author_name, author_slug, author_email, $3, $4, $5, $2, $7, $8, $9, $10; + }' "$file_index" > "$authors_index_file" + + unlock_file "$authors_index_file" + + # Check if the generated index is not empty and create/remove flag file + local authors_flag_file="${CACHE_DIR:-.bssg_cache}/has_authors.flag" + if [ -s "$authors_index_file" ]; then + touch "$authors_flag_file" + else + rm -f "$authors_flag_file" + fi + + echo -e "${GREEN}Authors index built!${NC}" +} + +# Compare current and previous authors index to find affected authors and check if index needs rebuild +# Exports: AFFECTED_AUTHORS (space-separated list of author names) +# AUTHORS_INDEX_NEEDS_REBUILD ("true" or "false") +identify_affected_authors() { + local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" + local authors_index_prev_file="${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" + + export AFFECTED_AUTHORS="" + export AUTHORS_INDEX_NEEDS_REBUILD="false" + + # If previous index doesn't exist, all authors in the current index are affected, + # and the main index needs rebuilding. + if [ ! -f "$authors_index_prev_file" ]; then + if [ -s "$authors_index_file" ]; then # Check if current index has content + echo "Previous authors index not found. Marking all authors as affected." >&2 # Debug + AFFECTED_AUTHORS=$(cut -d'|' -f1 "$authors_index_file" | sort -u | tr '\n' ' ') + AUTHORS_INDEX_NEEDS_REBUILD="true" + else + echo "Both previous and current authors indexes are missing or empty. No authors affected." >&2 # Debug + fi + export AFFECTED_AUTHORS + export AUTHORS_INDEX_NEEDS_REBUILD + return 0 + fi + + # If current index doesn't exist (but previous did), means all posts were deleted? + # Mark authors from previous index as affected, index needs rebuild. + if [ ! -f "$authors_index_file" ] || [ ! -s "$authors_index_file" ]; then + echo "Current authors index not found or empty. Marking all previous authors as affected." >&2 # Debug + AFFECTED_AUTHORS=$(cut -d'|' -f1 "$authors_index_prev_file" | sort -u | tr '\n' ' ') + AUTHORS_INDEX_NEEDS_REBUILD="true" + export AFFECTED_AUTHORS + export AUTHORS_INDEX_NEEDS_REBUILD + return 0 + fi + + # Extract AuthorName|Filename from both files for precise comparison + local current_entries="${CACHE_DIR:-.bssg_cache}/authors_curr_af.$$" + local prev_entries="${CACHE_DIR:-.bssg_cache}/authors_prev_af.$$" + trap 'rm -f "$current_entries" "$prev_entries"' RETURN + + cut -d'|' -f1,7 "$authors_index_file" | sort > "$current_entries" + cut -d'|' -f1,7 "$authors_index_prev_file" | sort > "$prev_entries" + + # Find differences (lines unique to current or previous) + local diff_output + diff_output=$(comm -3 "$current_entries" "$prev_entries") + + # Extract unique author names from the differences + if [ -n "$diff_output" ]; then + AFFECTED_AUTHORS=$(echo "$diff_output" | sed 's/^[[:space:]]*//' | cut -d'|' -f1 | sort -u | tr '\n' ' ') + echo "Affected authors identified: $AFFECTED_AUTHORS" >&2 # Debug + else + echo "No difference in posts per author found." >&2 # Debug + AFFECTED_AUTHORS="" + fi + + # Compare author counts (AuthorName|Count) to see if the main index needs rebuilding + local current_counts="${CACHE_DIR:-.bssg_cache}/authors_curr_counts.$$" + local prev_counts="${CACHE_DIR:-.bssg_cache}/authors_prev_counts.$$" + trap 'rm -f "$current_entries" "$prev_entries" "$current_counts" "$prev_counts"' RETURN + + cut -d'|' -f1 "$authors_index_file" | sort | uniq -c | awk '{print $2"|"$1}' | sort > "$current_counts" + cut -d'|' -f1 "$authors_index_prev_file" | sort | uniq -c | awk '{print $2"|"$1}' | sort > "$prev_counts" + + if ! cmp -s "$current_counts" "$prev_counts"; then + echo "Author counts differ. Main authors index needs rebuild." >&2 # Debug + AUTHORS_INDEX_NEEDS_REBUILD="true" + else + echo "Author counts are the same." >&2 # Debug + AUTHORS_INDEX_NEEDS_REBUILD="false" + fi + + export AFFECTED_AUTHORS + export AUTHORS_INDEX_NEEDS_REBUILD + rm -f "$current_entries" "$prev_entries" "$current_counts" "$prev_counts" + trap - RETURN # Remove trap upon successful completion +} + # Build archive index by year and month from the file index build_archive_index() { echo -e "${YELLOW}Building archive index...${NC}" @@ -387,9 +543,9 @@ build_archive_index() { > "$archive_index_file" # Clear the file # Read from file index and extract date info - local line file filename title date lastmod tags slug image image_caption description + local line file filename title date lastmod tags slug image image_caption description author_name author_email while IFS= read -r line || [[ -n "$line" ]]; do - IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line" + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" if [ -n "$date" ]; then local year month month_name @@ -428,8 +584,8 @@ build_archive_index() { [[ -z "$month_name" ]] && month_name="Unknown" fi - # Output: Year|MonthNum|MonthName|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription - echo "$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description" >> "$archive_index_file" + # Output: Year|MonthNum|MonthName|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription|AuthorName|AuthorEmail + echo "$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description|$author_name|$author_email" >> "$archive_index_file" fi done < "$file_index" diff --git a/scripts/build/main.sh b/scripts/build/main.sh index 8add6f4..7e0de0a 100755 --- a/scripts/build/main.sh +++ b/scripts/build/main.sh @@ -196,6 +196,24 @@ build_tags_index || { echo -e "${RED}Error: Failed to build tags index.${NC}"; e # echo "--- End $tags_index_file DEBUG ---" >&2 # --- End Debug --- +# --- Start Change: Snapshot previous authors index --- +authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" +authors_index_prev_file="${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" +if [ -f "$authors_index_file" ]; then + echo "Snapshotting previous authors index to $authors_index_prev_file" >&2 # Debug + cp "$authors_index_file" "$authors_index_prev_file" +else + # Ensure previous file doesn't exist if current doesn't + rm -f "$authors_index_prev_file" +fi +# --- End Change --- + +build_authors_index || { echo -e "${RED}Error: Failed to build authors index.${NC}"; exit 1; } + +# --- Start Change: Identify affected authors --- +identify_affected_authors || { echo -e "${RED}Error: Failed to identify affected authors.${NC}"; exit 1; } +# --- End Change --- + if [ "${ENABLE_ARCHIVES:-false}" = true ]; then # --- Start Change: Snapshot previous archive index --- archive_index_file="${CACHE_DIR:-.bssg_cache}/archive_index.txt" @@ -307,6 +325,19 @@ generate_tag_pages || { echo -e "${RED}Error: Tag page generation failed.${NC}"; echo "Generated tag list pages." # --- Tag Page Generation --- END --- +# --- Author Page Generation --- START --- +# Source and run Author Page Generator (if enabled) +if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then + # shellcheck source=generate_authors.sh disable=SC1091 + source "$SCRIPT_DIR/generate_authors.sh" || { echo -e "${RED}Error: Failed to source generate_authors.sh${NC}"; exit 1; } + + # Call the main generation function + # It will internally use AFFECTED_AUTHORS and AUTHORS_INDEX_NEEDS_REBUILD + generate_author_pages || { echo -e "${RED}Error: Author page generation failed.${NC}"; exit 1; } + echo "Generated author pages." +fi +# --- Author Page Generation --- END --- + # --- Archive Page Generation --- START --- # Source and run Archive Page Generator (if enabled) if [ "${ENABLE_ARCHIVES:-false}" = true ]; then @@ -390,6 +421,7 @@ create_config_hash echo "Cleaning up previous index files..." rm -f "${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" rm -f "${CACHE_DIR:-.bssg_cache}/tags_index_prev.txt" +rm -f "${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" rm -f "${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt" # Remove the frontmatter changes marker if it exists diff --git a/scripts/build/templates.sh b/scripts/build/templates.sh index d640ab3..d12f184 100755 --- a/scripts/build/templates.sh +++ b/scripts/build/templates.sh @@ -211,6 +211,20 @@ preload_templates() { menu_items+=" ${MSG_TAGS:-"Tags"}" fi + # Add Authors link if enabled and multiple authors exist + local authors_flag_file="${CACHE_DIR:-.bssg_cache}/has_authors.flag" + if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ] && [ -f "$authors_flag_file" ]; then + # Check if we have multiple authors (more than the threshold) + local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" + if [ -f "$authors_index_file" ]; then + local unique_author_count=$(awk -F'|' '{print $1}' "$authors_index_file" | sort -u | wc -l) + local threshold="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" + if [ "$unique_author_count" -ge "$threshold" ]; then + menu_items+=" ${MSG_AUTHORS:-"Authors"}" + fi + fi + fi + # Only add Archives link if enabled if [ "${ENABLE_ARCHIVES:-true}" = true ]; then menu_items+=" ${MSG_ARCHIVES:-"Archives"}" @@ -222,6 +236,18 @@ preload_templates() { if [ -f "$tags_flag_file" ]; then footer_items+=" ${MSG_TAGS:-"Tags"} ·" fi + + # Add Authors link to footer if enabled and multiple authors exist + if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ] && [ -f "$authors_flag_file" ]; then + if [ -f "$authors_index_file" ]; then + local unique_author_count_footer=$(awk -F'|' '{print $1}' "$authors_index_file" | sort -u | wc -l) + local threshold_footer="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" + if [ "$unique_author_count_footer" -ge "$threshold_footer" ]; then + footer_items+=" ${MSG_AUTHORS:-"Authors"} ·" + fi + fi + fi + footer_items+=" ${MSG_SUBSCRIBE_RSS:-"Subscribe via RSS"}" # Replace menu placeholders in templates diff --git a/scripts/edit.sh b/scripts/edit.sh index 871c6af..9fbbd10 100755 --- a/scripts/edit.sh +++ b/scripts/edit.sh @@ -60,10 +60,34 @@ else sed_requires_backup_cleanup=true fi -# Function to generate a slug from a title +# Generate a URL-friendly slug from a title +# This implementation matches the one in scripts/build/utils.sh generate_slug() { local title="$1" - echo "$title" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//' + + # Convert to lowercase + local slug=$(echo "$title" | tr '[:upper:]' '[:lower:]') + + # First use iconv to transliterate if available + if command -v iconv >/dev/null 2>&1; then + slug=$(echo "$slug" | iconv -f utf-8 -t ascii//TRANSLIT 2>/dev/null || echo "$slug") + fi + + # Replace all non-alphanumeric characters with hyphens + slug=$(echo "$slug" | sed -e 's/[^a-z0-9]/-/g') + + # Replace multiple consecutive hyphens with a single one + slug=$(echo "$slug" | sed -e 's/--*/-/g') + + # Remove leading and trailing hyphens + slug=$(echo "$slug" | sed -e 's/^-//' -e 's/-$//') + + # If slug is empty, use 'untitled' as fallback + if [ -z "$slug" ]; then + slug="untitled" + fi + + echo "$slug" } # Function to edit a post @@ -166,7 +190,11 @@ edit_post() { # If no date found, use current date if [ -z "$new_date" ]; then - new_date=$(date +"%Y-%m-%d %H:%M:%S %z") + new_date=$(date +"%Y-%m-%d") + else + # Extract only the date portion (YYYY-MM-DD) from the date field + # This handles cases where the date field contains time and timezone + new_date=$(echo "$new_date" | sed 's/^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/') fi # If title found, rename the file diff --git a/scripts/page.sh b/scripts/page.sh index 7c880a0..ca5c862 100755 --- a/scripts/page.sh +++ b/scripts/page.sh @@ -49,10 +49,34 @@ if [ -z "$EDITOR" ]; then fi fi -# Function to generate a slug from a title +# Generate a URL-friendly slug from a title +# This implementation matches the one in scripts/build/utils.sh generate_slug() { local title="$1" - echo "$title" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//' + + # Convert to lowercase + local slug=$(echo "$title" | tr '[:upper:]' '[:lower:]') + + # First use iconv to transliterate if available + if command -v iconv >/dev/null 2>&1; then + slug=$(echo "$slug" | iconv -f utf-8 -t ascii//TRANSLIT 2>/dev/null || echo "$slug") + fi + + # Replace all non-alphanumeric characters with hyphens + slug=$(echo "$slug" | sed -e 's/[^a-z0-9]/-/g') + + # Replace multiple consecutive hyphens with a single one + slug=$(echo "$slug" | sed -e 's/--*/-/g') + + # Remove leading and trailing hyphens + slug=$(echo "$slug" | sed -e 's/^-//' -e 's/-$//') + + # If slug is empty, use 'untitled' as fallback + if [ -z "$slug" ]; then + slug="untitled" + fi + + echo "$slug" } # Function to create a new page diff --git a/scripts/post.sh b/scripts/post.sh index 14ba030..9533ac5 100755 --- a/scripts/post.sh +++ b/scripts/post.sh @@ -49,11 +49,34 @@ if [ -z "$EDITOR" ]; then fi fi -# Function to generate a slug from a title +# Generate a URL-friendly slug from a title +# This implementation matches the one in scripts/build/utils.sh generate_slug() { local title="$1" - # POSIX compliant slug generation - echo "$title" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]/-/g' -e 's/-\{1,\}/-/g' -e 's/^-//' -e 's/-$//' + + # Convert to lowercase + local slug=$(echo "$title" | tr '[:upper:]' '[:lower:]') + + # First use iconv to transliterate if available + if command -v iconv >/dev/null 2>&1; then + slug=$(echo "$slug" | iconv -f utf-8 -t ascii//TRANSLIT 2>/dev/null || echo "$slug") + fi + + # Replace all non-alphanumeric characters with hyphens + slug=$(echo "$slug" | sed -e 's/[^a-z0-9]/-/g') + + # Replace multiple consecutive hyphens with a single one + slug=$(echo "$slug" | sed -e 's/--*/-/g') + + # Remove leading and trailing hyphens + slug=$(echo "$slug" | sed -e 's/^-//' -e 's/-$//') + + # If slug is empty, use 'untitled' as fallback + if [ -z "$slug" ]; then + slug="untitled" + fi + + echo "$slug" } # Function to display usage information @@ -199,6 +222,8 @@ EOM + +

    $title

    @@ -222,6 +247,8 @@ slug: $slug image: image_caption: description: +author_name: +author_email: --- $initial_content @@ -446,6 +473,8 @@ else + +

    $POST_TITLE

    @@ -465,6 +494,8 @@ slug: $POST_SLUG image: image_caption: description: +author_name: +author_email: --- $POST_CONTENT diff --git a/themes/amiga500/style.css b/themes/amiga500/style.css index bb5ab1c..fe02c34 100644 --- a/themes/amiga500/style.css +++ b/themes/amiga500/style.css @@ -1,3 +1,25 @@ +/* + * Amiga 500 Workbench Theme for BSSG + * Inspired by the classic Amiga Workbench interface + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks + * - Optimized font loading while maintaining authentic look + */ + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Amiga 500 Workbench inspired color scheme */ --bg-color: #0055aa; @@ -21,10 +43,10 @@ --checkered-color1: #0055aa; --checkered-color2: #0066bb; - /* Typography */ - --font-main: 'Topaz', 'Lucida Console', Monaco, monospace; - --font-headings: 'Topaz', 'Lucida Console', Monaco, monospace; - --font-mono: 'Topaz', 'Lucida Console', Monaco, monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Topaz', 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + --font-headings: 'Topaz', 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + --font-mono: 'Topaz', 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; /* Spacing and sizing */ --radius: 0; /* Amiga didn't have rounded corners */ @@ -32,6 +54,7 @@ --transition: 0.2s ease; } +/* OPTIMIZED font loading with better fallbacks */ @font-face { font-family: 'Topaz'; font-style: normal; @@ -41,6 +64,15 @@ font-display: swap; } +/* TEXT BROWSER FALLBACK: Provide fallback when custom fonts aren't supported */ +@supports not (font-display: swap) { + :root { + --font-main: 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + --font-headings: 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + --font-mono: 'Lucida Console', Monaco, 'Courier New', 'Courier', monospace; + } +} + *, *::before, *::after { box-sizing: border-box; } @@ -62,7 +94,7 @@ html, body { font-smooth: never; } -/* Checkered background pattern like Workbench */ +/* Checkered background pattern like Workbench - OPTIMIZED for performance */ body::before { content: ""; position: fixed; @@ -81,6 +113,17 @@ body::before { z-index: -1; } +/* TEXT BROWSER FALLBACK: Disable complex background when not supported */ +@supports not (background-clip: text) { + body::before { + display: none; + } + + body { + background-color: var(--bg-color); + } +} + ::selection { background-color: var(--link-color); color: var(--window-title-text); @@ -146,6 +189,25 @@ header::after { text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); } +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title { + color: var(--window-title-bg); + background: none; + } + + .site-title a { + color: var(--window-title-bg); + background: none; + } + + .site-title a:hover, + .site-title a:focus { + color: var(--link-hover-color); + background: none; + } +} + .site-title a { text-decoration: none; display: inline-block; @@ -168,6 +230,18 @@ header::after { text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); } +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-decoration: none; + transform: translateX(2px); + background: linear-gradient(90deg, var(--link-hover-color) 0%, var(--window-title-bg) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); +} + header h1 { font-family: var(--font-headings); font-weight: 400; @@ -219,6 +293,13 @@ nav a:hover { color: var(--window-title-text); } +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); + color: var(--window-title-text); +} + main { padding: 0 15px; margin-bottom: 1.5rem; @@ -465,34 +546,36 @@ article h3, article .meta, article .summary { h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); - color: var(--header-color); - line-height: 1.3; + color: var(--text-color); margin: 1.5rem 0 1rem; font-weight: 400; } h1 { - font-size: 1.4rem; + font-size: 1.8rem; margin-top: 0; - margin-bottom: 0.75rem; + margin-bottom: 1.5rem; } h2 { - font-size: 1.2rem; + font-size: 1.4rem; } h3 { - font-size: 1.1rem; + font-size: 1.2rem; } h4 { - font-size: 1rem; + font-size: 1.1rem; } article h1 { + font-family: var(--font-headings); + font-weight: 400; color: var(--window-title-text); margin: 0; font-size: 1.1rem; + letter-spacing: 0; position: absolute; top: 0; left: 4px; @@ -506,26 +589,26 @@ article h1 { } .date-header { - color: var(--window-title-text); - background-color: var(--window-title-bg); - padding: 2px 10px; - font-size: 1rem; + font-size: 0.8rem; + color: var(--date-color); + margin-bottom: 0.5rem; + font-family: var(--font-headings); font-weight: 400; - margin: 2rem 0 1.5rem; - display: inline-block; - border: 2px solid var(--window-border); + text-transform: uppercase; + letter-spacing: 1px; } article .meta { - font-size: 0.85rem; + font-size: 0.8rem; color: var(--date-color); margin-bottom: 1.5rem; display: flex; - align-items: center; flex-wrap: wrap; - gap: 0.5rem; + gap: 1rem; + align-items: center; } +/* Reading time - TEXT BROWSER FALLBACK */ .reading-time { font-size: 0.8rem; display: inline-flex; @@ -535,6 +618,18 @@ article .meta { border: 1px solid var(--date-color); } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + article .summary { margin-bottom: 1.2rem; overflow-wrap: break-word; @@ -604,6 +699,13 @@ article .tags a:hover, article .tags .tag:hover, article .tags a.tag:hover { transform: translateY(-1px); } +article .tags a:focus, article .tags .tag:focus, article .tags a.tag:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); + transform: translateY(-1px); +} + a { color: var(--link-color); text-decoration: none; @@ -615,6 +717,12 @@ a:hover { color: var(--link-hover-color); } +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + p { margin: 0 0 1.5rem; } @@ -660,6 +768,12 @@ footer a:hover { color: var(--link-hover-color); } +footer a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + blockquote { border-left: 4px solid var(--link-color); padding: 0.5rem 1rem; @@ -726,6 +840,12 @@ hr { background-color: var(--link-hover-color); } +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -751,6 +871,12 @@ hr { background-color: var(--link-hover-color); } +.tags-list a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); +} + .tag-count { background-color: var(--accent-secondary); color: var(--text-color); @@ -850,6 +976,12 @@ hr { background-color: var(--link-hover-color); } +.archives-nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-hover-color); +} + /* Responsive styles */ @media (max-width: 768px) { html { diff --git a/themes/apple2/style.css b/themes/apple2/style.css index 48c056f..45d4d9e 100644 --- a/themes/apple2/style.css +++ b/themes/apple2/style.css @@ -1,8 +1,26 @@ /* * Apple II Theme for BSSG * Green monochrome text terminal style from the Apple II era + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for gradient text and decorative elements + * - Optimized font loading with better fallbacks + * - Enhanced performance while maintaining authentic Apple II look */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Apple II color scheme */ --bg-color: #000000; @@ -14,13 +32,14 @@ --title-color: #88ff88; --title-highlight: #ccffcc; - /* Typography */ - --font-main: 'Courier New', monospace; - --font-headings: 'Courier New', monospace; - --font-mono: 'Courier New', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-headings: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-mono: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; /* Sizing */ --content-width: 800px; + --transition: 0.2s ease; } /* Apple II screen effect */ @@ -34,6 +53,7 @@ body { text-shadow: 0 0 5px var(--text-color); } +/* OPTIMIZED scanline effect with progressive enhancement */ body::before { content: ""; position: fixed; @@ -47,6 +67,13 @@ body::before { pointer-events: none; } +/* TEXT BROWSER FALLBACK: Disable complex background when not supported */ +@supports not (background-clip: text) { + body::before { + display: none; + } +} + /* Container with simulated terminal screen */ .container { max-width: var(--content-width); @@ -81,6 +108,20 @@ header { text-shadow: 0 0 8px var(--title-color); display: inline-block; position: relative; + transition: all var(--transition); +} + +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title a { + color: var(--title-color); + background: none; + } + + .site-title a:hover { + color: var(--title-highlight); + background: none; + } } .site-title a:hover { @@ -92,6 +133,11 @@ header { text-shadow: 0 0 15px var(--title-highlight); } +.site-title a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; +} + .site-title a::after { content: "_"; display: inline-block; @@ -125,6 +171,7 @@ nav a { border: 1px solid var(--text-color); position: relative; display: inline-block; + transition: all var(--transition); } nav a::before { @@ -143,6 +190,11 @@ nav a:hover, nav a:focus { text-shadow: none; } +nav a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; +} + /* Selected menu item */ nav a.active { background-color: var(--text-color); @@ -200,7 +252,8 @@ p { /* Links */ a { color: var(--link-color); - text-decoration: none; + text-decoration: underline; + transition: color var(--transition); } a:visited { @@ -208,30 +261,40 @@ a:visited { } a:hover { - text-decoration: underline; + color: var(--title-highlight); + text-shadow: 0 0 5px var(--title-highlight); +} + +a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; + color: var(--title-highlight); } -/* Apple II style prompt for articles */ article { margin-bottom: 30px; - padding-bottom: 20px; - border-bottom: 1px dotted var(--text-color); + padding: 15px; + border: 1px dashed var(--text-color); position: relative; } article::before { - content: ">"; - position: absolute; - left: -20px; + content: "***** ARTICLE START *****"; + display: block; + text-align: center; + margin-bottom: 15px; + font-family: var(--font-mono); + text-transform: uppercase; } article:last-child { - border-bottom: none; + margin-bottom: 0; } article .meta { font-size: 0.9rem; - margin-bottom: 15px; + margin-bottom: 10px; + color: var(--text-color); display: flex; flex-wrap: wrap; gap: 15px; @@ -409,10 +472,18 @@ article .meta { .tags a { color: var(--text-color); text-decoration: none; + transition: color var(--transition); } .tags a:hover { text-decoration: underline; + color: var(--title-highlight); +} + +.tags a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; + color: var(--title-highlight); } code { @@ -440,77 +511,92 @@ img { border: 1px solid var(--text-color); } -/* Footer with prompt style */ footer { + margin-top: 30px; + padding: 15px; border-top: 1px solid var(--text-color); - margin-top: 40px; - padding-top: 20px; + text-align: center; font-size: 0.9rem; - display: flex; - justify-content: space-between; } footer::before { - content: "READY."; + content: "***** SYSTEM STATUS: READY *****"; display: block; margin-bottom: 10px; -} - -/* Apple II command prompt pagination */ -.pagination { - display: flex; - justify-content: center; - align-items: center; - margin: 30px 0; - gap: 20px; -} - -.pagination a { - color: var(--link-color); - text-decoration: none; + font-family: var(--font-mono); text-transform: uppercase; } -.pagination a::before { - content: "CMD:"; - margin-right: 5px; +footer a { + color: var(--link-color); + transition: color var(--transition); } -.pagination a:hover { - text-decoration: underline; +footer a:hover { + color: var(--title-highlight); +} + +footer a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; + color: var(--title-highlight); +} + +.pagination { + display: flex; + justify-content: space-between; + margin-top: 30px; + padding: 15px; + border: 1px dashed var(--text-color); +} + +.pagination a { + color: var(--text-color); + text-decoration: none; + padding: 5px 10px; + border: 1px solid var(--text-color); + transition: all var(--transition); +} + +.pagination a::before { + content: "> "; +} + +.pagination a:hover, .pagination a:focus { + background-color: var(--text-color); + color: var(--bg-color); +} + +.pagination a:focus { + outline: 2px solid var(--title-color); + outline-offset: 2px; } .pagination .page-info { - margin: 0 10px; + color: var(--text-color); + font-family: var(--font-mono); } -/* Media query for responsive design */ @media (max-width: 768px) { .container { margin: 10px; - width: auto; padding: 15px; + max-width: none; } nav { flex-direction: column; - align-items: center; } nav a { - margin: 5px 0; - width: auto; - min-width: 60%; + margin-bottom: 5px; + padding: 8px 12px; text-align: center; - box-sizing: border-box; - padding: 5px 20px; } article::before { - left: 0; - position: relative; - display: inline-block; - margin-right: 10px; + font-size: 0.8rem; + margin-bottom: 10px; } .featured-image, @@ -522,26 +608,26 @@ footer::before { } article { - padding-left: 5px; - padding-right: 5px; + padding: 10px; + margin-bottom: 20px; } footer { - flex-direction: column; - gap: 10px; - align-items: center; - text-align: center; + padding: 10px; + font-size: 0.8rem; } .pagination { flex-direction: column; - gap: 15px; + gap: 10px; + text-align: center; } } @media (max-width: 480px) { body { padding: 10px; + font-size: 0.9rem; } .container { @@ -553,11 +639,12 @@ footer::before { } h2 { - font-size: 1.1rem; + font-size: 1.2rem; } pre { - padding: 5px; + padding: 8px; + font-size: 0.8rem; } .featured-image::before, @@ -567,27 +654,22 @@ footer::before { .featured-image::after, .index-image::after, .tag-image::after, - .archive-image::after { - font-size: 0.8rem; + .archive-image::after, + article::before, + footer::before { + font-size: 0.7rem; } } -/* Apple II style hr element - replaces default grey bar with themed separator */ hr { border: none; - border-top: 1px dotted var(--text-color); - height: 1px; + border-top: 1px dashed var(--text-color); margin: 20px 0; - background-color: transparent; + height: 1px; } -/* Date header styling for index page */ .date-header { color: var(--text-color); - font-size: 1.2rem; - margin-top: 1.5rem; - margin-bottom: 1rem; - font-weight: normal; - border-bottom: 1px dotted var(--text-color); - padding-bottom: 0.5rem; + font-family: var(--font-mono); + text-transform: uppercase; } \ No newline at end of file diff --git a/themes/art-deco/style.css b/themes/art-deco/style.css index f1ef1ee..c343729 100644 --- a/themes/art-deco/style.css +++ b/themes/art-deco/style.css @@ -2,8 +2,25 @@ * Art Deco Theme for BSSG * Inspired by 1920s-30s Art Deco style with geometric patterns, * elegant fonts, and gold/black/silver/jewel color palettes + * IMPROVED: Better accessibility, performance, and text browser support */ +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Simplify decorative elements for reduced motion */ + body, header { + background-image: none !important; + background-attachment: scroll !important; + } +} + :root { /* Art Deco color scheme */ --gold: #D4AF37; @@ -34,17 +51,17 @@ --footer-text: var(--silver); --border-color: var(--gold); - /* Typography */ - --font-main: 'Georgia', 'Times New Roman', serif; - --font-headings: 'Copperplate', 'Copperplate Gothic Light', 'Garamond', serif; - --font-mono: 'Courier New', monospace; + /* Typography - IMPROVED fallbacks for text browsers */ + --font-main: Georgia, 'Times New Roman', Times, serif; + --font-headings: 'Copperplate Gothic Light', 'Copperplate', Garamond, Georgia, 'Times New Roman', serif; + --font-mono: 'Courier New', Courier, monospace; /* Sizing */ --content-width: 900px; --content-padding: 2rem; } -/* Base elements */ +/* Base elements - OPTIMIZED performance */ html { font-size: 16px; } @@ -56,15 +73,18 @@ body { margin: 0; padding: 0; line-height: 1.6; + /* OPTIMIZED: Simplified pattern for better performance */ background-image: linear-gradient(45deg, var(--gold) 25%, transparent 25%), linear-gradient(-45deg, var(--gold) 25%, transparent 25%); background-size: 20px 20px; background-position: 0 0, 10px 10px; - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ background-repeat: repeat; background-color: var(--cream); - background-blend-mode: soft-light; + /* OPTIMIZED: Reduced blend mode complexity */ + background-blend-mode: multiply; + opacity: 0.95; } .container { @@ -76,7 +96,7 @@ body { position: relative; } -/* Art Deco border pattern */ +/* Art Deco border pattern - OPTIMIZED */ .container::before { content: ""; position: absolute; @@ -109,25 +129,28 @@ body { ); } -/* Header */ +/* Header - OPTIMIZED performance */ header { background-color: var(--header-bg); color: var(--header-text); padding: 2rem var(--content-padding) 1rem; position: relative; text-align: center; + /* OPTIMIZED: Simplified background pattern */ background-image: linear-gradient(45deg, var(--gold) 25%, transparent 25%), linear-gradient(-45deg, var(--gold) 25%, transparent 25%); background-size: 10px 10px; background-position: 0 0, 5px 5px; - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ background-repeat: repeat; background-color: var(--black); + /* OPTIMIZED: Reduced blend mode complexity */ background-blend-mode: overlay; + opacity: 0.95; } -/* Decorative header element */ +/* Decorative header element - IMPROVED text browser support */ header::before { content: "★ ★ ★"; display: block; @@ -139,6 +162,13 @@ header::before { padding-left: 2rem; } +/* FALLBACK: Text browser support for decorative elements */ +@supports not (content: "★") { + header::before { + content: "* * *"; + } +} + header::after { content: ""; display: block; @@ -148,7 +178,7 @@ header::after { margin: 1rem auto 0; } -/* Site title */ +/* Site title - IMPROVED accessibility */ .site-title { font-family: var(--font-headings); font-weight: normal; @@ -164,6 +194,7 @@ header::after { text-decoration: none; display: inline-block; position: relative; + transition: all 0.2s ease; } .site-title a::before, @@ -176,6 +207,26 @@ header::after { vertical-align: middle; } +/* FALLBACK: Text browser support for decorative elements */ +@supports not (content: "◆") { + .site-title a::before, + .site-title a::after { + content: "*"; + } +} + +.site-title a:hover { + transform: translateY(-2px); + text-shadow: 0 2px 4px rgba(212, 175, 55, 0.3); +} + +.site-title a:focus { + outline: 2px solid var(--gold); + outline-offset: 3px; + transform: translateY(-2px); + text-shadow: 0 2px 4px rgba(212, 175, 55, 0.3); +} + /* Site description */ header p { margin: 0.5rem 0 0; @@ -187,7 +238,7 @@ header p { margin-right: auto; } -/* Navigation */ +/* Navigation - IMPROVED accessibility */ nav { background-color: var(--nav-bg); display: flex; @@ -224,13 +275,22 @@ nav a { letter-spacing: 2px; text-transform: uppercase; position: relative; - transition: all 0.3s ease; + transition: all 0.2s ease; /* Reduced transition time for better performance */ } nav a:hover, nav a.active { background-color: var(--nav-hover-bg); color: var(--nav-hover-text); + transform: translateY(-1px); +} + +nav a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + background-color: var(--nav-hover-bg); + color: var(--nav-hover-text); + transform: translateY(-1px); } /* Content area */ @@ -326,6 +386,13 @@ a:hover { border-bottom-color: var(--link-hover); } +a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + color: var(--link-hover); + border-bottom-color: var(--link-hover); +} + /* Articles */ article { margin-bottom: 4rem; @@ -429,6 +496,14 @@ article .meta { border: none; } +.tags a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + background-color: var(--black); + color: var(--gold); + border: none; +} + /* Tags list page */ .tags-list { list-style-type: none; @@ -464,6 +539,14 @@ article .meta { border: none; } +.tags-list a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + background-color: var(--black); + color: var(--gold); + border: none; +} + /* Footer */ footer { background-color: var(--footer-bg); @@ -507,6 +590,13 @@ footer a:hover { text-decoration: underline; } +footer a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + color: var(--silver); + text-decoration: underline; +} + /* Pagination */ .pagination { display: flex; @@ -536,6 +626,14 @@ footer a:hover { border: none; } +.pagination a:focus { + outline: 2px solid var(--gold); + outline-offset: 2px; + background-color: var(--black); + color: var(--gold); + border: none; +} + /* Featured images */ .featured-image, .index-image, @@ -599,8 +697,20 @@ figcaption { text-align: center; } -/* Responsive styles */ +/* IMPROVED: Responsive styles with mobile optimizations */ @media (max-width: 768px) { + body { + /* OPTIMIZED: Remove complex background patterns on mobile */ + background-image: none; + background-color: var(--cream); + } + + header { + /* OPTIMIZED: Simplified header background on mobile */ + background-image: none; + background-color: var(--black); + } + .container { margin: 0; width: 100%; @@ -642,6 +752,8 @@ figcaption { padding: 0.8rem 1rem; font-size: 0.8rem; text-align: center; + /* OPTIMIZED: Simplified transitions on mobile */ + transition: background-color 0.2s ease; } article { @@ -659,6 +771,8 @@ figcaption { .tag-image, .archive-image { margin: 1.5rem 0; + /* OPTIMIZED: Simplified decorative elements on mobile */ + padding: 5px; } .pagination { @@ -675,6 +789,7 @@ figcaption { .site-title { font-size: 1.7rem; + letter-spacing: 2px; /* Reduced letter spacing for small screens */ } h1 { @@ -703,6 +818,8 @@ figcaption { width: 100%; box-sizing: border-box; border-bottom: 1px solid var(--gold); + /* OPTIMIZED: Remove transforms on small mobile */ + transform: none !important; } nav a:last-child { @@ -720,6 +837,14 @@ figcaption { font-size: 0.9rem; } + /* OPTIMIZED: Simplified decorative elements on very small screens */ + .featured-image, + .index-image, + .tag-image, + .archive-image { + padding: 2px; + } + footer { text-align: center; } diff --git a/themes/atarist/style.css b/themes/atarist/style.css index 3606b97..f8743d3 100644 --- a/themes/atarist/style.css +++ b/themes/atarist/style.css @@ -1,8 +1,26 @@ /* * Atari ST GEM Theme for BSSG * Recreating the Atari ST's Graphical Environment Manager interface from the 1980s + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for decorative elements + * - Optimized font loading with better fallbacks + * - Enhanced performance while maintaining authentic GEM look */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Atari ST GEM color scheme */ --bg-color: #c0c0c0; @@ -21,15 +39,16 @@ --title-gradient-start: #ffffff; --title-gradient-end: #aaaaff; - /* Typography */ - --font-main: 'Courier New', monospace; - --font-headings: 'Courier New', monospace; - --font-mono: 'Courier New', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-headings: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-mono: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; /* Sizing */ --content-width: 800px; --border-width: 1px; --border-radius: 0; + --transition: 0.2s ease; } /* Base elements */ @@ -83,14 +102,32 @@ header { justify-content: center; font-size: 10px; cursor: pointer; + transition: background-color var(--transition); } +.gem-control:hover { + background-color: var(--dropdown-bg); +} + +.gem-control:focus { + outline: 2px solid var(--title-text); + outline-offset: 2px; +} + +/* TEXT BROWSER FALLBACK: Provide text alternatives for window controls */ .gem-close::after { content: "×"; color: black; font-weight: bold; } +/* TEXT BROWSER FALLBACK: Use "X" when × isn't supported */ +@supports not (content: "×") { + .gem-close::after { + content: "X"; + } +} + .gem-minimize::after { content: "_"; color: black; @@ -103,6 +140,13 @@ header { color: black; } +/* TEXT BROWSER FALLBACK: Use "[]" when □ isn't supported */ +@supports not (content: "□") { + .gem-maximize::after { + content: "[]"; + } +} + header h1 { margin: 0; padding: 0; @@ -149,6 +193,7 @@ nav a { display: inline-block; border-right: var(--border-width) solid var(--border-color); white-space: nowrap; + transition: all var(--transition); } nav a:first-child { @@ -164,6 +209,11 @@ nav a:hover, nav a:focus { color: var(--menu-text); } +nav a:focus { + outline: 2px solid var(--title-text); + outline-offset: 2px; +} + /* Selected menu item */ nav a.active { background-color: var(--menu-highlight); @@ -200,19 +250,19 @@ h3 { article h1 { font-size: 1.4rem; - border-bottom: var(--border-width) solid var(--border-color); - padding-bottom: 5px; + margin-top: 0; } p { margin-bottom: 1rem; - line-height: 1.4; + line-height: 1.5; } /* Links */ a { color: var(--link-color); - text-decoration: none; + text-decoration: underline; + transition: color var(--transition); } a:visited { @@ -220,19 +270,24 @@ a:visited { } a:hover { - text-decoration: underline; + color: var(--menu-highlight); } -/* GEM style button */ +a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; + color: var(--menu-highlight); +} + +/* GEM-style button */ .gem-button { background-color: var(--dropdown-bg); + color: var(--text-color); border: var(--border-width) solid var(--border-color); padding: 3px 10px; - color: var(--text-color); - font-family: var(--font-main); font-size: 0.9rem; cursor: pointer; - display: inline-block; + transition: background-color var(--transition); } .gem-button:active { @@ -240,21 +295,24 @@ a:hover { color: var(--menu-text); } -/* Articles with borders */ +.gem-button:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; +} + article { margin-bottom: 20px; - padding: 10px; - border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 15px; + border-bottom: var(--border-width) solid #e0e0e0; } article:last-child { - margin-bottom: 0; + border-bottom: none; } article .meta { - font-size: 0.85rem; - color: #333; + font-size: 0.9rem; + color: #666; margin-bottom: 10px; display: flex; flex-wrap: wrap; @@ -265,6 +323,18 @@ article .meta { font-style: italic; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .tags { display: flex; flex-wrap: wrap; @@ -278,6 +348,7 @@ article .meta { font-size: 0.75rem; border: var(--border-width) solid var(--border-color); text-decoration: none; + transition: all var(--transition); } .tags a:hover { @@ -285,6 +356,13 @@ article .meta { color: var(--menu-text); } +.tags a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; + background-color: var(--menu-highlight); + color: var(--menu-text); +} + .tags-list { list-style-type: none; padding: 0; @@ -293,6 +371,20 @@ article .meta { gap: 10px; } +.tags-list a { + transition: all var(--transition); +} + +.tags-list a:hover { + background-color: var(--menu-highlight); + color: var(--menu-text); +} + +.tags-list a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; +} + .tag-count { color: #666; font-size: 0.85em; @@ -341,6 +433,21 @@ footer { align-items: center; } +footer a { + color: var(--link-color); + transition: color var(--transition); +} + +footer a:hover { + color: var(--menu-highlight); +} + +footer a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; + color: var(--menu-highlight); +} + /* GEM style scrollbar (simulated with CSS) */ .gem-scrollbar { width: 15px; @@ -391,6 +498,7 @@ footer { text-decoration: none; border: var(--border-width) solid var(--border-color); font-size: 0.9rem; + transition: all var(--transition); } .pagination a:hover { @@ -398,6 +506,13 @@ footer { color: var(--menu-text); } +.pagination a:focus { + outline: 2px solid var(--menu-highlight); + outline-offset: 2px; + background-color: var(--menu-highlight); + color: var(--menu-text); +} + .pagination .page-info { margin: 0 10px; font-size: 0.9rem; @@ -458,7 +573,8 @@ footer { @media (max-width: 480px) { body { - padding: 8px; + padding: 10px; + font-size: 13px; } .container { @@ -470,7 +586,7 @@ footer { } h1 { - font-size: 1.3rem; + font-size: 1.4rem; } h2 { @@ -486,185 +602,200 @@ footer { .tag-image::before, .archive-image::before { font-size: 0.8rem; - padding: 2px 5px; } article .meta { flex-direction: column; - align-items: flex-start; - gap: 8px; + gap: 5px; } .site-title { - font-size: 0.9rem; + font-size: 1rem; } article h1 { font-size: 1.2rem; + line-height: 1.3; } } -/* Site title with GEM-style gradient */ .site-title { - margin: 0; - padding: 0; font-size: 1rem; + margin: 0; font-weight: bold; - font-family: var(--font-headings); - /* Gradient that matches Atari ST GEM aesthetic */ - background: linear-gradient(90deg, var(--title-gradient-start), var(--title-gradient-end)); + color: var(--title-text); + /* GEM-style gradient */ + background: linear-gradient(to bottom, var(--title-gradient-start) 0%, var(--title-gradient-end) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); - display: inline-block; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3); +} + +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title { + color: var(--title-text); + background: none; + } + + .site-title a { + color: var(--title-text); + background: none; + } + + .site-title a:hover { + color: var(--title-gradient-end); + background: none; + } } .site-title a { text-decoration: none; - background: linear-gradient(90deg, var(--title-gradient-start), var(--title-gradient-end)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); - transition: all 0.2s ease; - display: inline-block; + color: inherit; + transition: all var(--transition); } .site-title a:hover { - text-decoration: none; - background: linear-gradient(90deg, var(--title-gradient-end), var(--title-gradient-start)); + background: linear-gradient(to bottom, var(--title-gradient-end) 0%, var(--title-gradient-start) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; } -/* Featured image styles for Atari ST GEM */ +.site-title a:focus { + outline: 2px solid var(--title-text); + outline-offset: 2px; +} + +/* Featured image styles for GEM */ .featured-image { - margin: 15px 0; + display: block; + margin: 20px auto; border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 10px; + background-color: var(--dropdown-bg); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); - padding-top: 20px; /* Space for the title bar */ } .featured-image::before { - content: "IMAGE VIEWER"; - position: absolute; - top: 0; - left: 0; - right: 0; - background-color: var(--title-bar); - color: var(--title-text); - font-size: 0.9rem; - padding: 2px 10px; - font-weight: bold; + content: "*** FEATURED IMAGE ***"; + display: block; text-align: center; + margin-bottom: 10px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-color); + background-color: var(--window-bg); + padding: 2px; + border: var(--border-width) solid var(--border-color); } .featured-image img { display: block; - width: 100%; + margin: 0 auto; + max-width: 100%; height: auto; - margin: 0; - padding: 0; - border: none; - box-shadow: none; + border: var(--border-width) solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); } .featured-image .image-caption { - padding: 5px 10px; - background-color: var(--dropdown-bg); - border-top: var(--border-width) solid var(--border-color); + text-align: center; + margin-top: 10px; font-size: 0.9rem; + font-style: italic; } .index-image { - margin: 15px 0; + display: block; + margin: 20px auto; border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 10px; + background-color: var(--dropdown-bg); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); - padding-top: 20px; /* Space for the title bar */ } .index-image::before { - content: "INDEX IMAGE"; - position: absolute; - top: 0; - left: 0; - right: 0; - background-color: var(--title-bar); - color: var(--title-text); - font-size: 0.9rem; - padding: 2px 10px; - font-weight: bold; + content: "*** INDEX IMAGE ***"; + display: block; text-align: center; + margin-bottom: 10px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-color); + background-color: var(--window-bg); + padding: 2px; + border: var(--border-width) solid var(--border-color); } .index-image img { display: block; - width: 100%; + margin: 0 auto; + max-width: 100%; height: auto; - margin: 0; - padding: 0; - border: none; - box-shadow: none; + border: var(--border-width) solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); } .tag-image { - margin: 15px 0; + display: block; + margin: 20px auto; border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 10px; + background-color: var(--dropdown-bg); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); - padding-top: 20px; /* Space for the title bar */ } .tag-image::before { - content: "TAG IMAGE"; - position: absolute; - top: 0; - left: 0; - right: 0; - background-color: var(--title-bar); - color: var(--title-text); - font-size: 0.9rem; - padding: 2px 10px; - font-weight: bold; + content: "*** TAG IMAGE ***"; + display: block; text-align: center; + margin-bottom: 10px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-color); + background-color: var(--window-bg); + padding: 2px; + border: var(--border-width) solid var(--border-color); } .tag-image img { display: block; - width: 100%; + margin: 0 auto; + max-width: 100%; height: auto; - margin: 0; - padding: 0; - border: none; - box-shadow: none; + border: var(--border-width) solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); } .archive-image { - margin: 15px 0; + display: block; + margin: 20px auto; border: var(--border-width) solid var(--border-color); - background-color: var(--window-bg); + padding: 10px; + background-color: var(--dropdown-bg); position: relative; - box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); - padding-top: 20px; /* Space for the title bar */ } .archive-image::before { - content: "ARCHIVE IMAGE"; - position: absolute; - top: 0; - left: 0; - right: 0; - background-color: var(--title-bar); - color: var(--title-text); - font-size: 0.9rem; - padding: 2px 10px; - font-weight: bold; + content: "*** ARCHIVE IMAGE ***"; + display: block; text-align: center; + margin-bottom: 10px; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text-color); + background-color: var(--window-bg); + padding: 2px; + border: var(--border-width) solid var(--border-color); +} + +.archive-image img { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; + border: var(--border-width) solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); } \ No newline at end of file diff --git a/themes/bauhaus/style.css b/themes/bauhaus/style.css index 1d0dc01..729e4c1 100644 --- a/themes/bauhaus/style.css +++ b/themes/bauhaus/style.css @@ -2,8 +2,24 @@ * Bauhaus Theme for BSSG * Inspired by the Bauhaus design school, emphasizing functionality, * primary colors, geometric shapes, and clean sans-serif typography + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for geometric reading time indicator + * - Optimized performance while maintaining Bauhaus design principles */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + :root { /* Bauhaus color scheme */ --red: #E53935; @@ -102,6 +118,12 @@ header { text-decoration: none; } +.site-title a:focus { + outline: 2px solid var(--tertiary-accent); + outline-offset: 2px; + text-decoration: none; +} + /* Site description */ header p { margin: 0.5rem 0 0 0; @@ -137,6 +159,13 @@ nav a.active { color: var(--nav-hover-text); } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--nav-hover-bg); + color: var(--nav-hover-text); +} + /* Content area */ main { padding: var(--content-padding); @@ -210,6 +239,12 @@ a:hover { border-bottom: 2px solid currentColor; } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-bottom: 2px solid currentColor; +} + /* Articles */ article { margin-bottom: 4rem; @@ -240,16 +275,27 @@ article .meta { padding-left: 1.5rem; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: ""; + content: "Time: "; position: absolute; left: 0; top: 50%; transform: translateY(-50%); + font-style: normal; + font-weight: bold; +} + +/* Modern browsers: Use geometric shape when CSS transforms are supported */ +@supports (transform: translateY(-50%)) and (border-radius: 50%) { + .reading-time::before { + content: ""; width: 1rem; height: 1rem; background-color: var(--tertiary-accent); border-radius: 50%; + font-weight: normal; + } } /* Tags */ @@ -276,6 +322,14 @@ article .meta { border: none; } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--secondary-accent); + color: var(--white); + border: none; +} + /* Tags list page */ .tags-list { list-style-type: none; @@ -307,6 +361,13 @@ article .meta { color: var(--white); } +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--secondary-accent); + color: var(--white); +} + /* Footer */ footer { background-color: var(--footer-bg); @@ -340,6 +401,12 @@ footer a:hover { text-decoration: underline; } +footer a:focus { + outline: 2px solid var(--tertiary-accent); + outline-offset: 2px; + text-decoration: underline; +} + /* Pagination */ .pagination { display: flex; @@ -367,6 +434,14 @@ footer a:hover { border: none; } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--secondary-accent); + color: var(--white); + border: none; +} + /* Featured images */ .featured-image, .index-image, diff --git a/themes/bbs/style.css b/themes/bbs/style.css index f4389c3..9cad0c1 100644 --- a/themes/bbs/style.css +++ b/themes/bbs/style.css @@ -2,9 +2,25 @@ * BBS Theme for BSSG * Inspired by old-school Bulletin Board Systems * Features ANSI colors, ASCII art aesthetics, and terminal feel + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Added text browser fallbacks for gradient text + * - Enhanced font fallbacks with comprehensive system monospace font stacks + * - Optimized performance while maintaining authentic BBS terminal aesthetics */ -@import url('https://fonts.googleapis.com/css2?family=VT323&family=IBM+Plex+Mono:wght@400;700&display=swap'); +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} :root { /* ANSI color scheme */ @@ -24,12 +40,13 @@ --title-color-start: #ffff55; /* ANSI yellow */ --title-color-end: #ff5555; /* ANSI red */ - /* Typography */ - --font-main: 'VT323', monospace; - --font-mono: 'IBM Plex Mono', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-mono: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; /* Sizing */ --content-width: 900px; + --transition: 0.3s ease; } /* Base elements with terminal feel */ @@ -112,6 +129,24 @@ header h1 { position: relative; } +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title { + color: var(--title-color-start); + background: none; + } + + .site-title a { + color: var(--title-color-start); + background: none; + } + + .site-title a:hover { + color: var(--title-color-end); + background: none; + } +} + .site-title a { text-decoration: none; /* Replicate the gradient for links */ @@ -119,7 +154,7 @@ header h1 { -webkit-background-clip: text; background-clip: text; color: transparent; - transition: all 0.3s; + transition: all var(--transition); display: inline-block; } @@ -140,6 +175,24 @@ header h1 { color: var(--title-color-start); } +/* Focus styles for site title */ +.site-title a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + background: linear-gradient(90deg, var(--title-color-end) 0%, var(--title-color-start) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.site-title a:focus::after { + content: "_"; + position: relative; + display: inline-block; + animation: blink 1s step-end infinite; + color: var(--title-color-start); +} + /* BBS menu navigation */ nav { background-color: var(--nav-bg); @@ -160,6 +213,7 @@ nav a { position: relative; display: inline-block; margin-right: 2px; + transition: all var(--transition); } /* BBS-style menu item with brackets and number */ @@ -175,7 +229,7 @@ nav a::after { margin-left: 3px; } -/* Number the menu items like in a BBS */ +/* Numbered menu items */ nav a:nth-child(1)::before { content: "[1-"; } nav a:nth-child(2)::before { content: "[2-"; } nav a:nth-child(3)::before { content: "[3-"; } @@ -187,137 +241,143 @@ nav a:nth-child(8)::before { content: "[8-"; } nav a:nth-child(9)::before { content: "[9-"; } nav a:hover { - color: var(--accent-1); - background-color: var(--bg-color); + background-color: var(--accent-1); + color: var(--bright-text); + text-decoration: none; +} + +nav a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + background-color: var(--accent-1); + color: var(--bright-text); + text-decoration: none; } -/* Active navigation item */ nav a.active { - color: var(--accent-1); - background-color: var(--bg-color); + background-color: var(--accent-1); + color: var(--bright-text); } -/* Special RSS button that looks like a BBS command */ nav a:last-child { - color: var(--header-fg); + margin-right: 0; } nav a:last-child::before { - content: "[R-"; + content: "[X-"; + color: var(--accent-1); } -/* Main content with scanlines */ +/* Main content area */ main { padding: 20px; - position: relative; background-color: var(--bg-color); + min-height: 400px; + position: relative; + border-bottom: 2px dashed var(--border-color); +} + +/* BBS prompt line */ +main::before { + content: "C:\\BSSG> TYPE CONTENT.TXT"; + display: block; + color: var(--accent-1); font-family: var(--font-mono); font-size: 16px; - color: var(--text-color); + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px dashed var(--dim-text); } -/* Scanline effect */ -main::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: repeating-linear-gradient( - 0deg, - rgba(0, 0, 0, 0.15), - rgba(0, 0, 0, 0.15) 1px, - transparent 1px, - transparent 2px - ); - pointer-events: none; - z-index: 1; -} - -/* Terminal typography */ +/* Typography */ h1, h2, h3, h4, h5, h6 { - color: var(--header-fg); - margin-top: 1.5rem; - margin-bottom: 1rem; - font-weight: normal; font-family: var(--font-main); + color: var(--bright-text); + margin: 1.5rem 0 1rem 0; + font-weight: bold; + text-transform: uppercase; } h1 { font-size: 2rem; - text-transform: uppercase; + color: var(--header-fg); } h2 { - font-size: 1.7rem; + font-size: 1.6rem; color: var(--accent-1); - border-bottom: 1px dashed var(--accent-1); - padding-bottom: 5px; + position: relative; + padding-left: 20px; } h2::before { - content: "## "; + content: ">> "; + color: var(--accent-1); } h3 { - font-size: 1.4rem; - color: var(--link-color); + font-size: 1.3rem; + color: var(--link-hover); } h3::before { - content: ">>> "; + content: "> "; + color: var(--accent-1); } p { - margin-bottom: 1rem; + margin-bottom: 1.2rem; + line-height: 1.5; } -/* BBS-style links */ +/* Links */ a { color: var(--link-color); - text-decoration: none; - transition: all 0.2s; + text-decoration: underline; + transition: color var(--transition); } a:hover { color: var(--link-hover); - text-decoration: underline; + text-decoration: none; } -/* ASCII box for articles */ +a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: none; +} + +/* BBS-style articles */ article { - margin-bottom: 40px; - background-color: rgba(0, 0, 0, 0.5); - border: 1px solid var(--border-color); + margin-bottom: 30px; padding: 15px; + border: 1px dashed var(--border-color); position: relative; + background-color: rgba(0, 0, 0, 0.3); } -/* ASCII box top border */ +/* Article header with BBS styling */ article::before { - content: "+-[ MESSAGE ]---------------------------------------------------+"; + content: "+==[ ARTICLE START ]=======================================================+"; display: block; - color: var(--bright-text); + color: var(--accent-1); font-family: var(--font-mono); - font-size: 16px; - margin: -15px -15px 15px -15px; - padding: 5px 15px; - background-color: var(--header-bg); - border-bottom: 1px solid var(--border-color); + font-size: 14px; + margin-bottom: 15px; + margin-top: -5px; } -/* ASCII box bottom border */ +/* Article footer */ article::after { - content: "+---------------------------------------------------------------------+"; + content: "+==[ ARTICLE END ]=========================================================+"; display: block; - color: var(--bright-text); + color: var(--accent-1); font-family: var(--font-mono); - font-size: 16px; - margin: 15px -15px -15px -15px; - padding: 5px 15px; - background-color: var(--header-bg); - border-top: 1px solid var(--border-color); + font-size: 14px; + margin-top: 15px; + margin-bottom: -5px; } article:last-child { @@ -325,23 +385,21 @@ article:last-child { } article h1 { - font-size: 1.5rem; + font-size: 1.8rem; margin-top: 0; - margin-bottom: 10px; - color: var(--accent-1); - font-weight: bold; + margin-bottom: 15px; + color: var(--header-fg); + border-bottom: 1px dashed var(--accent-1); + padding-bottom: 5px; } -/* BBS-style metadata with ANSI colors */ article .meta { font-size: 0.9rem; color: var(--dim-text); - margin-bottom: 20px; + margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 15px; - padding-bottom: 10px; - border-bottom: 1px dashed var(--dim-text); font-family: var(--font-mono); } @@ -376,6 +434,7 @@ article .meta { text-decoration: none; font-family: var(--font-mono); font-size: 0.9em; + transition: color var(--transition); } .tags a:hover { @@ -383,6 +442,13 @@ article .meta { text-decoration: underline; } +.tags a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + .tags-list { list-style-type: none; padding: 0; @@ -404,6 +470,20 @@ article .meta { color: var(--accent-1); } +.tags-list a { + transition: color var(--transition); +} + +.tags-list a:hover { + color: var(--link-hover); +} + +.tags-list a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); +} + .tag-count { color: var(--accent-2); font-size: 0.9em; @@ -549,11 +629,11 @@ img { /* ASCII art top border */ .tag-image::before { - content: "+==[ TAG IMAGE ]============================================================+"; + content: "+==[ TAG IMAGE ]=============================================================+"; display: block; font-family: var(--font-mono); font-size: 14px; - color: var(--link-color); + color: var(--accent-1); margin-bottom: 10px; white-space: nowrap; overflow: hidden; @@ -565,7 +645,7 @@ img { display: block; font-family: var(--font-mono); font-size: 14px; - color: var(--link-color); + color: var(--accent-1); margin-top: 10px; white-space: nowrap; overflow: hidden; @@ -590,11 +670,11 @@ img { /* ASCII art top border */ .archive-image::before { - content: "+==[ ARCHIVE IMAGE ]========================================================+"; + content: "+==[ ARCHIVE IMAGE ]=========================================================+"; display: block; font-family: var(--font-mono); font-size: 14px; - color: var(--accent-2); + color: var(--accent-1); margin-bottom: 10px; white-space: nowrap; overflow: hidden; @@ -606,7 +686,7 @@ img { display: block; font-family: var(--font-mono); font-size: 14px; - color: var(--accent-2); + color: var(--accent-1); margin-top: 10px; white-space: nowrap; overflow: hidden; @@ -621,32 +701,29 @@ img { margin: 0; } -/* BBS-style footer */ +/* BBS footer */ footer { background-color: var(--header-bg); - color: var(--dim-text); + color: var(--header-fg); padding: 15px; - font-size: 0.9rem; text-align: center; border-top: 2px solid var(--border-color); font-family: var(--font-mono); - position: relative; } -/* ASCII art border for footer */ +/* ASCII art footer border */ footer::before { - content: "+------------------------------[ END OF TRANSMISSION ]------------------------------+"; + content: "+==[ SYSTEM STATUS: ONLINE ]=======[ USERS: 1337 ]=======[ UPTIME: 24:7 ]==+"; display: block; color: var(--bright-text); - font-family: var(--font-mono); - font-size: 16px; + font-size: 14px; margin-bottom: 10px; - margin-top: -5px; } footer a { color: var(--link-color); text-decoration: none; + transition: color var(--transition); } footer a:hover { @@ -654,40 +731,56 @@ footer a:hover { text-decoration: underline; } -/* BBS-style pagination */ +footer a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + +/* BBS pagination */ .pagination { display: flex; justify-content: center; - align-items: center; + gap: 10px; margin: 30px 0; - gap: 15px; - font-family: var(--font-main); + font-family: var(--font-mono); } .pagination a { color: var(--link-color); - padding: 5px 10px; text-decoration: none; - border: 1px solid var(--border-color); - background-color: var(--bg-color); - font-weight: bold; + padding: 5px 15px; + border: 1px dashed var(--border-color); + background-color: rgba(0, 0, 0, 0.5); + transition: all var(--transition); } .pagination a:hover { - background-color: var(--header-bg); - color: var(--link-hover); + background-color: var(--accent-1); + color: var(--bright-text); + border-color: var(--accent-1); +} + +.pagination a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + background-color: var(--accent-1); + color: var(--bright-text); + border-color: var(--accent-1); } .pagination .page-info { - color: var(--bright-text); + color: var(--dim-text); + padding: 5px 15px; font-family: var(--font-mono); } -/* Terminal cursor blinking effect */ +/* BBS cursor */ .cursor { display: inline-block; - width: 10px; - height: 18px; + width: 0.6em; + height: 1em; background-color: var(--text-color); animation: blink 1s step-end infinite; vertical-align: text-bottom; @@ -699,43 +792,45 @@ footer a:hover { 50% { opacity: 0; } } -/* Horizontal rule as a BBS divider */ +/* BBS horizontal rule */ hr { border: none; height: 1px; - background-color: var(--dim-text); - margin: 20px 0; + background-color: var(--border-color); + margin: 30px 0; position: relative; } hr::before { - content: "---===[ * ]===---"; + content: "+============================================================================+"; position: absolute; - top: -10px; + top: -8px; left: 50%; transform: translateX(-50%); background-color: var(--bg-color); - padding: 0 15px; color: var(--accent-1); + font-family: var(--font-mono); + font-size: 14px; + padding: 0 10px; } -/* BBS Command prompt */ +/* BBS prompt styling */ .bbs-prompt { - padding: 10px; - margin: 20px 0; - border: 1px solid var(--dim-text); - background-color: rgba(0, 0, 0, 0.5); + color: var(--accent-1); font-family: var(--font-mono); - color: var(--bright-text); + font-weight: bold; + margin: 20px 0; + padding: 10px; + border: 1px dashed var(--border-color); + background-color: rgba(0, 0, 0, 0.5); } .bbs-prompt::before { - content: "BSSG>"; - color: var(--accent-1); - margin-right: 8px; + content: "C:\\BSSG> "; + color: var(--header-fg); } -/* ANSI color classes for text */ +/* ANSI color classes */ .ansi-red { color: var(--accent-1); } .ansi-green { color: var(--text-color); } .ansi-yellow { color: var(--header-fg); } @@ -744,55 +839,51 @@ hr::before { .ansi-cyan { color: var(--link-hover); } .ansi-white { color: var(--bright-text); } -/* Media query for responsive design */ +/* Responsive design */ @media (max-width: 768px) { header::before, header::after, + footer::before, article::before, article::after, - footer::before { + .featured-image::before, + .featured-image::after, + .index-image::before, + .index-image::after, + .tag-image::before, + .tag-image::after, + .archive-image::before, + .archive-image::after, + hr::before { font-size: 12px; - overflow: hidden; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; - max-width: 100%; - box-sizing: border-box; - content: "+-------------[ BSSG BBS ]-------------+"; - } - - header::after, - article::after, - footer::before { - content: "+---------------------------------------+"; } .container { - margin: 10px; - width: auto; + margin: 5px; + border-width: 1px; } header, footer, main { - padding: 15px; + padding: 10px; } nav { flex-direction: column; padding: 5px; - align-items: stretch; } nav a { - margin: 2px 0; - text-align: left; - padding: 8px 15px; - border-bottom: 1px dashed var(--dim-text); - box-sizing: border-box; - overflow: hidden; - text-overflow: ellipsis; + padding: 8px 10px; + margin-bottom: 2px; + text-align: center; + font-size: 0.9rem; } nav a:last-child { - border-bottom: none; + margin-bottom: 0; } article { @@ -800,22 +891,21 @@ hr::before { } pre { - max-width: 100%; - overflow-x: auto; - font-size: 14px; + padding: 10px; + font-size: 0.8rem; } .featured-image, .index-image, .tag-image, .archive-image { - margin: 10px 0; + margin: 15px 0; + padding: 8px; } article .meta { flex-direction: column; - align-items: flex-start; - gap: 8px; + gap: 5px; } .site-title { @@ -823,75 +913,79 @@ hr::before { } h1 { - font-size: 1.8rem; + font-size: 1.6rem; } h2 { - font-size: 1.5rem; - } - - h3 { - font-size: 1.3rem; - } - - .pagination { - flex-direction: column; - align-items: center; - gap: 10px; - } -} - -@media (max-width: 480px) { - body { - font-size: 16px; - padding: 5px; - } - - .container { - margin: 5px; - } - - header, footer, main { - padding: 10px; - } - - pre, code { - font-size: 13px; - } - - .featured-image::before, - .index-image::before, - .tag-image::before, - .archive-image::before, - .featured-image::after, - .index-image::after, - .tag-image::after, - .archive-image::after { - font-size: 11px; - } - - .site-title { - font-size: 1.5rem; - } - - h1 { - font-size: 1.5rem; - } - - h2 { - font-size: 1.3rem; + font-size: 1.4rem; } h3 { font-size: 1.2rem; } + .pagination { + flex-direction: column; + gap: 5px; + align-items: center; + } +} + +@media (max-width: 480px) { + body { + padding: 5px; + font-size: 16px; + } + + .container { + margin: 2px; + } + + header, footer, main { + padding: 8px; + } + + pre, code { + font-size: 0.7rem; + } + + .featured-image::before, + .featured-image::after, + .index-image::before, + .index-image::after, + .tag-image::before, + .tag-image::after, + .archive-image::before, + .archive-image::after, + header::before, + header::after, + footer::before { + font-size: 10px; + } + + .site-title { + font-size: 1.5rem; + } + + h1 { + font-size: 1.4rem; + } + + h2 { + font-size: 1.2rem; + } + + h3 { + font-size: 1.1rem; + } + .tags { flex-direction: column; - align-items: flex-start; + gap: 5px; } nav a { - padding: 6px 10px; + font-size: 0.8rem; + padding: 6px 8px; } } \ No newline at end of file diff --git a/themes/beos/style.css b/themes/beos/style.css index 70a6ba6..b403a5a 100644 --- a/themes/beos/style.css +++ b/themes/beos/style.css @@ -1,8 +1,32 @@ /* * Modern BeOS Theme for BSSG * A contemporary take on the classic BeOS interface + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for gradient text + * - Optimized font loading with better fallbacks + * - Enhanced performance while maintaining BeOS aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + nav a:hover, + .site-title a:hover, + article:hover { + transform: none !important; + } +} + :root { /* Modern BeOS-inspired color scheme */ --bg-color: #e8e8e8; @@ -21,10 +45,10 @@ --title-color-start: #3584e4; --title-color-end: #1d57a5; - /* Typography */ - --font-main: 'Inter', 'Segoe UI', 'Arial', sans-serif; - --font-headings: 'Inter', 'Segoe UI', 'Arial', sans-serif; - --font-mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-headings: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-mono: 'Consolas', 'Courier New', 'Courier', monospace; /* Sizing */ --content-width: 950px; @@ -99,6 +123,21 @@ header::after { transition: var(--transition); border-bottom: none; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + /* ACCESSIBILITY: Focus outline */ + outline: none; +} + +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title a { + color: var(--title-color-start); + background: none; + } + + .site-title a:hover { + color: var(--title-color-end); + background: none; + } } .site-title a:hover { @@ -110,6 +149,17 @@ header::after { border-bottom: none; } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + transform: translateY(-1px); + background: linear-gradient(to bottom, var(--title-color-end), var(--title-color-start)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + header h1 { margin: 0; padding: 0; @@ -153,6 +203,8 @@ nav a { display: inline-block; transition: var(--transition); top: 1px; + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a:hover { @@ -167,6 +219,12 @@ nav a:focus { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Content area with border to connect with tabs */ main { padding: 25px; @@ -208,6 +266,8 @@ a { text-decoration: none; transition: var(--transition); border-bottom: 1px solid transparent; + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:visited { @@ -218,6 +278,13 @@ a:hover { border-bottom: 1px solid currentColor; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-bottom: 1px solid currentColor; +} + /* Articles in modern BeOS window style */ article { margin-bottom: 30px; @@ -274,6 +341,18 @@ article .meta { font-size: 13px; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags styling with proper spacing */ .tags { display: flex; @@ -292,6 +371,8 @@ article .meta { display: inline-block; border-radius: 4px; transition: var(--transition); + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags a:hover { @@ -301,6 +382,15 @@ article .meta { border-bottom: 1px solid var(--border-color); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--title-yellow); + transform: translateY(-2px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -310,6 +400,12 @@ article .meta { list-style-type: none; } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + .tag-count { color: #555; font-size: 12px; @@ -371,6 +467,12 @@ footer { text-align: center; } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Pagination */ .pagination { display: flex; @@ -390,6 +492,8 @@ footer { border-radius: 4px; transition: var(--transition); font-weight: 500; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a:hover { @@ -399,6 +503,15 @@ footer { border-bottom: 1px solid var(--border-color); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--title-yellow); + transform: translateY(-2px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + /* Blockquotes */ blockquote { border-left: 4px solid var(--title-yellow); diff --git a/themes/blackberry/style.css b/themes/blackberry/style.css index b97b5fa..6e7a676 100644 --- a/themes/blackberry/style.css +++ b/themes/blackberry/style.css @@ -1,3 +1,30 @@ +/* + * BlackBerry Theme for BSSG + * Inspired by classic BlackBerry mobile interface + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks for gradient text + * - Optimized font loading with better fallbacks + * - Enhanced performance while maintaining BlackBerry aesthetics + */ + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + article { + animation: none !important; + } +} + :root { /* BlackBerry inspired color scheme */ --bg-color: #000914; @@ -24,10 +51,10 @@ --title-color-start: #57a9ff; --title-color-end: #004080; - /* Typography */ - --font-main: 'SF UI Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - --font-headings: 'SF UI Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - --font-mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --font-headings: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --font-mono: 'Consolas', 'Liberation Mono', 'Menlo', 'Courier New', 'Courier', monospace; /* Spacing and sizing */ --radius: 4px; @@ -192,6 +219,8 @@ nav a { border: 1px solid var(--border-color); position: relative; box-shadow: 0 2px 0 rgba(0,0,0,0.3); + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a::after { @@ -215,6 +244,14 @@ nav a:active { transform: translateY(2px); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--nav-hover); + color: var(--link-hover-color); +} + main { margin-bottom: 3rem; padding: 0 1.5rem; @@ -303,6 +340,18 @@ article .meta { border: 1px solid var(--border-color); } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .summary { margin-bottom: 1.2rem; } @@ -328,6 +377,8 @@ article .tags a { border: 1px solid rgba(0,0,0,0.2); box-shadow: 0 1px 0 rgba(0,0,0,0.3); position: relative; + /* ACCESSIBILITY: Focus outline */ + outline: none; } article .tags a::after { @@ -346,17 +397,34 @@ article .tags a:hover { transform: translateY(-1px); } +/* ACCESSIBILITY: Focus states for tags */ +article .tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); + transform: translateY(-1px); +} + a { color: var(--link-color); text-decoration: none; font-weight: 500; transition: color var(--transition); + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:hover { color: var(--link-hover-color); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + p { margin: 0 0 1.5rem; } @@ -403,6 +471,13 @@ footer a:hover { color: var(--link-hover-color); } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + blockquote { border-left: 4px solid var(--accent-secondary); padding: 1rem 1.5rem; @@ -470,6 +545,8 @@ hr { border: 1px solid var(--border-color); position: relative; box-shadow: 0 2px 0 rgba(0,0,0,0.3); + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a::after { @@ -492,6 +569,13 @@ hr { transform: translateY(2px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -514,6 +598,8 @@ hr { border: 1px solid rgba(0,0,0,0.2); box-shadow: 0 1px 0 rgba(0,0,0,0.3); position: relative; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags-list a::after { @@ -531,6 +617,13 @@ hr { background-color: var(--accent-secondary); } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); +} + .tag-count { background-color: rgba(0, 0, 0, 0.2); padding: 0.1rem 0.4rem; diff --git a/themes/braun/style.css b/themes/braun/style.css index 27d11a8..563bfe6 100644 --- a/themes/braun/style.css +++ b/themes/braun/style.css @@ -4,6 +4,16 @@ * Minimalism, functionality, elegance, and precision */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + :root { /* Braun-inspired color scheme */ --white: #FFFFFF; @@ -104,6 +114,12 @@ header { color: var(--header-text); } +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--header-text); +} + /* Site description - minimal and precise */ header p { margin: calc(var(--grid-size)) 0 0; @@ -139,6 +155,12 @@ nav a.active { font-weight: 500; } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--nav-hover-text); +} + /* Content area with grid precision */ main { min-height: 70vh; @@ -191,6 +213,13 @@ a:hover { border-bottom: 1px solid var(--link-hover); } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + border-bottom: 1px solid var(--link-hover); +} + /* Articles with precise spacing and clean borders */ article { margin-bottom: calc(var(--grid-size) * 8); @@ -219,6 +248,18 @@ article .meta { position: relative; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags with Braun-inspired geometric precision */ .tags { display: flex; @@ -241,6 +282,14 @@ article .meta { border: none; } +.tags a:focus { + outline: 2px solid var(--secondary-accent); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--black); + border: none; +} + /* Tags list page - grid layout */ .tags-list { list-style-type: none; @@ -273,6 +322,14 @@ article .meta { border: none; } +.tags-list a:focus { + outline: 2px solid var(--secondary-accent); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--black); + border: none; +} + /* Footer - clean and precise */ footer { margin-top: calc(var(--grid-size) * 6); @@ -297,6 +354,13 @@ footer a:hover { border-bottom: 1px solid var(--link-hover); } +footer a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + border-bottom: 1px solid var(--link-hover); +} + /* Pagination - clean and precise */ .pagination { display: flex; @@ -323,6 +387,14 @@ footer a:hover { border: none; } +.pagination a:focus { + outline: 2px solid var(--secondary-accent); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--black); + border: none; +} + /* Featured images - clean and precise presentation */ .featured-image, .index-image, diff --git a/themes/brutalist/style.css b/themes/brutalist/style.css index a3af6f2..72b1c4f 100644 --- a/themes/brutalist/style.css +++ b/themes/brutalist/style.css @@ -2,8 +2,19 @@ * Brutalist Theme for BSSG * Raw, minimalist concrete-inspired design * Features harsh typography, exposed elements, and high contrast + * IMPROVED: Enhanced accessibility and text browser support */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + :root { /* Brutalist colors - limited, harsh palette */ --bg-color: #ffffff; @@ -12,14 +23,14 @@ --accent: #ff0000; --text-color: #000000; --link-color: #0000ff; - --link-visited: #551a8b; + --link-visited: #8B0000; /* Dark red for better contrast on white background */ --transition: 0.2s ease; --radius: 0; - /* Typography - harsh, utilitarian */ - --font-main: 'Helvetica Neue', Arial, sans-serif; - --font-headings: 'Arial Black', 'Impact', sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - harsh, utilitarian - IMPROVED fallbacks for text browsers */ + --font-main: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, Arial, sans-serif; + --font-headings: 'Arial Black', 'Impact', -apple-system, BlinkMacSystemFont, Arial, sans-serif; + --font-mono: 'Courier New', 'Monaco', 'Consolas', monospace; /* Sizing */ --content-width: 1200px; @@ -75,7 +86,7 @@ header h1 { font-family: var(--font-headings); } -/* Site title with brutalist gradient style */ +/* Site title with brutalist gradient style - IMPROVED accessibility */ .site-title { margin: 0; padding: 0; @@ -85,33 +96,72 @@ header h1 { letter-spacing: -2px; line-height: 1; font-family: var(--font-headings); - background: linear-gradient(90deg, black 0%, var(--accent) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: black; /* Fallback for text browsers */ border-left: 8px solid var(--accent); padding-left: 10px; display: inline-block; } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title { + background: linear-gradient(90deg, black 0%, var(--accent) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + .site-title a { text-decoration: none; - background: linear-gradient(90deg, black 0%, var(--accent) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: black; /* Fallback for text browsers */ transition: all var(--transition); } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(90deg, black 0%, var(--accent) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + .site-title a:hover { text-decoration: none; - background: linear-gradient(90deg, var(--accent) 0%, black 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--accent); /* Fallback for text browsers */ transform: translateX(5px); } +/* Progressive enhancement for hover gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a:hover { + background: linear-gradient(90deg, var(--accent) 0%, black 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + +.site-title a:focus { + outline: 3px solid var(--accent); + outline-offset: 3px; + text-decoration: none; + color: var(--accent); /* Fallback for text browsers */ + transform: translateX(5px); +} + +/* Progressive enhancement for focus gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a:focus { + background: linear-gradient(90deg, var(--accent) 0%, black 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + /* Navigation with stark contrast */ nav { background-color: black; @@ -134,6 +184,10 @@ nav a { border-right: 1px solid var(--concrete); } +nav a:visited { + color: var(--link-visited); +} + nav a:hover { background-color: var(--accent); color: white; @@ -155,6 +209,20 @@ nav a:last-child:hover { color: var(--accent); } +nav a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + background-color: var(--accent); + color: white; +} + +nav a:last-child:focus { + outline: 3px solid white; + outline-offset: 2px; + background-color: black; + color: var(--accent); +} + /* Content area with visible grid */ main { padding: 30px; @@ -224,6 +292,13 @@ a:hover { text-decoration: underline; } +a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + color: var(--accent); + text-decoration: underline; +} + /* Brutalist articles with visible structure */ article { margin-bottom: 50px; @@ -413,6 +488,13 @@ article .meta { color: white; } +.tags a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + background-color: var(--accent); + color: white; +} + .tags-list { display: flex; flex-wrap: wrap; @@ -505,6 +587,12 @@ footer a:hover { color: var(--accent); } +footer a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + color: var(--accent); +} + .pagination { display: flex; justify-content: space-between; @@ -526,11 +614,23 @@ footer a:hover { border: 2px solid black; } +.pagination a:visited { + color: var(--link-visited); + background-color: var(--concrete); +} + .pagination a:hover { background-color: var(--accent); color: white; } +.pagination a:focus { + outline: 3px solid var(--accent); + outline-offset: 2px; + background-color: var(--accent); + color: white; +} + .pagination a:last-child { margin-left: auto; } diff --git a/themes/c64/style.css b/themes/c64/style.css index 7200c6a..5ae1768 100644 --- a/themes/c64/style.css +++ b/themes/c64/style.css @@ -1,3 +1,31 @@ +/* + * Commodore 64 Theme for BSSG + * Inspired by the classic Commodore 64 computer interface + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system monospace font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic C64 aesthetics + */ + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable blinking animation for reduced motion */ + .site-title a::after { + animation: none !important; + } +} + :root { /* Commodore 64 inspired color scheme */ --bg-color: #4040e0; @@ -16,10 +44,10 @@ --title-color-start: #ffff80; --title-color-end: #ffb080; - /* Typography */ - --font-main: 'C64 Pro', 'VT323', 'Px437 C64 Pro', monospace; - --font-headings: 'C64 Pro', 'VT323', 'Px437 C64 Pro', monospace; - --font-mono: 'C64 Pro', 'VT323', 'Px437 C64 Pro', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-headings: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; + --font-mono: 'Courier New', 'Lucida Console', Monaco, 'Consolas', monospace; /* Spacing and sizing */ --radius: 0; /* C64 didn't have rounded corners */ @@ -28,14 +56,6 @@ --transition: 0.1s ease; } -@font-face { - font-family: 'VT323'; - font-style: normal; - font-weight: 400; - src: url('https://fonts.gstatic.com/s/vt323/v12/pxiKyp0ihIEF2isQFJXGdg.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - *, *::before, *::after { box-sizing: border-box; } @@ -120,6 +140,13 @@ nav a:hover { color: var(--link-hover-color); } +nav a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--accent-secondary); + color: var(--link-hover-color); +} + main { margin-bottom: 3rem; } @@ -177,6 +204,14 @@ article h1::before { color: var(--link-color); } +/* TEXT BROWSER FALLBACK: Reset positioning when absolute positioning isn't supported */ +@supports not (position: absolute) { + article h1::before { + position: static; + left: auto; + } +} + .date-header { color: var(--date-color); font-size: 1.5rem; @@ -203,6 +238,18 @@ article .meta { align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .summary { margin-bottom: 1.2rem; } @@ -232,6 +279,14 @@ article .tags a:hover { transform: translateY(-2px); } +article .tags a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--link-hover-color); + color: var(--bg-color); + transform: translateY(-2px); +} + a { color: var(--link-color); text-decoration: none; @@ -242,6 +297,12 @@ a:hover { color: var(--link-hover-color); } +a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + p { margin: 0 0 1.5rem; } @@ -295,6 +356,12 @@ footer a:hover { color: var(--link-hover-color); } +footer a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + blockquote { border-left: 4px solid var(--link-color); padding: 1rem 1.5rem; @@ -359,6 +426,12 @@ hr { background-color: var(--accent-secondary); } +.pagination a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--accent-secondary); +} + .pagination .page-info { color: var(--date-color); } @@ -388,6 +461,13 @@ hr { color: var(--bg-color); } +.tags-list a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--link-hover-color); + color: var(--bg-color); +} + .tag-count { background-color: var(--accent-color); padding: 0.1rem 0.4rem; @@ -473,6 +553,12 @@ hr { background-color: var(--accent-secondary); } +.archives-nav a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background-color: var(--accent-secondary); +} + /* Responsive styles */ @media (max-width: 768px) { html { @@ -713,6 +799,24 @@ hr { text-shadow: 2px 0 0 var(--accent-color); } +/* TEXT BROWSER FALLBACK: Provide solid color when gradient text isn't supported */ +@supports not (background-clip: text) { + .site-title a { + color: var(--title-color-start); + background: none; + } + + .site-title a:hover { + color: var(--title-color-end); + background: none; + } + + .site-title a:focus { + color: var(--title-color-end); + background: none; + } +} + .site-title a:hover { background: linear-gradient(to right, var(--title-color-end), var(--title-color-start)); -webkit-background-clip: text; @@ -721,6 +825,16 @@ hr { text-shadow: -2px 0 0 var(--accent-color); } +.site-title a:focus { + outline: 2px solid var(--link-hover-color); + outline-offset: 2px; + background: linear-gradient(to right, var(--title-color-end), var(--title-color-start)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: -2px 0 0 var(--accent-color); +} + .site-title a::after { content: "█"; animation: blink 1s step-end infinite; @@ -729,6 +843,14 @@ hr { color: var(--link-color); } +/* TEXT BROWSER FALLBACK: Use simple cursor when block character isn't supported */ +@supports not (animation: blink 1s step-end infinite) { + .site-title a::after { + content: "_"; + animation: none; + } +} + @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } diff --git a/themes/dark/style.css b/themes/dark/style.css index 57fa2e4..764361b 100644 --- a/themes/dark/style.css +++ b/themes/dark/style.css @@ -1,36 +1,53 @@ /* * Dark Theme for BSSG - * A modern dark theme + * A modern dark theme with improved accessibility and compatibility */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + :root { - /* Dark palette */ - --bg-color: #121212; - --card-bg: #1e1e1e; - --text-color: #e0e0e0; - --link-color: #bb86fc; - --link-hover: #d7b8ff; - --header-color: #ffffff; - --border-color: #333333; - --accent-color: #03dac6; - --accent-secondary: #cf6679; - --tag-bg: #2d2d2d; - --tag-text: #bb86fc; - --code-bg: #1e1e1e; - --date-color: #9e9e9e; - --highlight-color: rgba(187, 134, 252, 0.1); + /* Dark palette - improved contrast ratios */ + --bg-color: #0d1117; + --card-bg: #161b22; + --text-color: #e6edf3; + --link-color: #58a6ff; + --link-hover: #79c0ff; + --header-color: #f0f6fc; + --border-color: #30363d; + --accent-color: #39d353; + --accent-secondary: #f85149; + --tag-bg: #21262d; + --tag-text: #58a6ff; + --code-bg: #161b22; + --date-color: #8b949e; + --highlight-color: rgba(88, 166, 255, 0.1); --transition: 0.3s ease; --radius: 8px; - /* Typography */ - --font-main: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --font-headings: 'Inter', sans-serif; - --font-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + /* Typography with better fallbacks */ + --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; + --font-headings: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, 'SFMono-Regular', 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; /* Sizing */ --content-width: 720px; } +/* Text browser and older browser support */ +@supports not (background-clip: text) { + :root { + --gradient-fallback-color: var(--header-color); + } +} + /* Base elements */ html { font-size: 16px; @@ -87,7 +104,7 @@ a { color: var(--link-color); text-decoration: none; transition: color var(--transition); - border-bottom: 1px solid rgba(187, 134, 252, 0.3); + border-bottom: 1px solid rgba(88, 166, 255, 0.3); padding-bottom: 1px; } @@ -96,6 +113,13 @@ a:hover { border-bottom-color: var(--link-hover); } +a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; + color: var(--link-hover); + border-bottom-color: var(--link-hover); +} + /* Header */ header { margin-bottom: 3rem; @@ -115,7 +139,7 @@ header::after { border-radius: var(--radius); } -/* Site title with gradient effect */ +/* Site title - improved accessibility and text browser support */ .site-title { font-family: var(--font-headings); font-weight: 700; @@ -123,30 +147,68 @@ header::after { margin: 0; font-size: 2.5rem; letter-spacing: -0.03em; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: none; + } } .site-title a { text-decoration: none; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); + color: var(--header-color); transition: all var(--transition); } -.site-title a:hover { +/* Progressive enhancement for gradient text links */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + + .site-title a:hover { + background: linear-gradient(120deg, var(--link-hover) 0%, var(--accent-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); + } + + .site-title a:focus { + background: linear-gradient(120deg, var(--link-hover) 0%, var(--accent-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); + } +} + +/* Fallback for browsers without gradient text support */ +@supports not ((background-clip: text) or (-webkit-background-clip: text)) { + .site-title a:hover { + color: var(--link-hover); + transform: translateY(-1px); + } + + .site-title a:focus { + color: var(--link-hover); + transform: translateY(-1px); + } +} + +.site-title a:focus { + outline: 2px solid var(--link-hover); + outline-offset: 2px; text-decoration: none; - background: linear-gradient(120deg, var(--link-hover) 0%, var(--accent-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - transform: translateY(-1px); } header h1 { @@ -154,11 +216,17 @@ header h1 { margin-bottom: 0.5rem; font-size: 2.5rem; letter-spacing: -0.03em; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); + color: var(--header-color); +} + +/* Progressive enhancement for header h1 gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + header h1 { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } } header p { @@ -203,6 +271,16 @@ nav a:hover::after { width: 100%; } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); +} + +nav a:focus::after { + width: 100%; +} + /* Article */ article { margin-bottom: 4rem; @@ -242,6 +320,13 @@ article .meta { font-size: 0.9rem; } +/* Text browser fallback for reading time icon */ +@media (max-width: 0) { /* This targets text browsers */ + .reading-time::before { + content: "[time] "; + } +} + /* Featured Images */ .featured-image { margin: 2rem 0; @@ -266,7 +351,7 @@ article .meta { bottom: 0; left: 0; right: 0; - background: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.8); color: #fff; padding: 0.5rem 1rem; font-size: 0.9rem; @@ -316,6 +401,7 @@ code { font-size: 0.9rem; border-radius: var(--radius); color: var(--accent-color); + border: 1px solid var(--border-color); } pre { @@ -325,12 +411,14 @@ pre { border-radius: var(--radius); margin: 2rem 0; border-left: 3px solid var(--accent-color); + border: 1px solid var(--border-color); } pre code { padding: 0; background-color: transparent; color: var(--text-color); + border: none; } /* Tags */ @@ -350,12 +438,12 @@ pre code { color: var(--tag-text); border-radius: var(--radius); transition: transform var(--transition), background-color var(--transition); - border: none; + border: 1px solid var(--border-color); } .tags a:hover { transform: translateY(-2px); - background-color: rgba(45, 45, 45, 0.8); + background-color: var(--border-color); } .tags-list { @@ -376,6 +464,7 @@ pre code { width: 1.5rem; height: 1.5rem; margin-left: 0.4rem; + border: 1px solid var(--border-color); } img { @@ -450,23 +539,30 @@ blockquote { border-left: 4px solid var(--accent-color); padding: 1rem 1.5rem; margin: 2rem 0; - background-color: rgba(187, 134, 252, 0.05); + background-color: rgba(88, 166, 255, 0.05); border-radius: 0 var(--radius) var(--radius) 0; font-style: italic; position: relative; } blockquote::before { - content: """; + content: "\201C"; font-size: 3rem; font-family: Georgia, serif; color: var(--accent-color); - opacity: 0.2; + opacity: 0.3; position: absolute; top: -1rem; left: 0.5rem; } +/* Text browser fallback for blockquote */ +@media (max-width: 0) { /* This targets text browsers */ + blockquote::before { + content: ""; + } +} + /* Lists */ ul, ol { margin-top: 0; @@ -499,6 +595,13 @@ hr::before { font-size: 0.8rem; } +/* Text browser fallback for hr decoration */ +@media (max-width: 0) { /* This targets text browsers */ + hr::before { + content: "---"; + } +} + /* Pagination - Dark Theme */ .pagination { display: flex; @@ -525,6 +628,13 @@ hr::before { color: var(--bg-color); } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--bg-color); +} + .pagination .page-info { color: var(--date-color); font-size: 0.9rem; @@ -696,6 +806,4 @@ hr::before { margin: 2rem 0 0.75rem; padding: 0.8rem 0; } -} - - \ No newline at end of file +} \ No newline at end of file diff --git a/themes/default/style.css b/themes/default/style.css index 01483a5..ad8622c 100644 --- a/themes/default/style.css +++ b/themes/default/style.css @@ -15,10 +15,10 @@ --highlight-color: #fef3c7; --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05); - /* Typography */ - --font-main: 'Spectral', Georgia, serif; - --font-headings: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; + /* Typography - System fonts for better performance and compatibility */ + --font-main: Georgia, 'Times New Roman', Times, serif; + --font-headings: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; /* Spacing and sizing */ --radius: 10px; @@ -45,29 +45,33 @@ } } -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all transforms and animations */ + .site-title a:hover, + article:hover, + article .tags a:hover, + article .tags a:focus, + .featured-image:hover img, + img:hover, + .tags-list a:hover, + .posts-list article:hover { + transform: none !important; + } + + @keyframes fadeIn { + from, to { opacity: 1; } + } } -@font-face { - font-family: 'Spectral'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,500;0,600;1,400&display=swap'); -} - -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap'); -} +/* Removed external font loading for better performance and text browser compatibility */ *, *::before, *::after { box-sizing: border-box; @@ -125,32 +129,55 @@ header::after { margin: 0; font-size: 2.75rem; letter-spacing: -0.5px; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: 0 1px 1px rgba(0,0,0,0.05); + } } .site-title a { text-decoration: none; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); + color: var(--header-color); transition: all var(--transition); + outline: 2px solid transparent; + outline-offset: 2px; } .site-title a:hover { text-decoration: none; - background: linear-gradient(120deg, var(--link-hover-color) 0%, var(--accent-secondary) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: var(--link-color); transform: translateY(-1px); } +.site-title a:focus { + outline-color: var(--link-color); +} + +/* Progressive enhancement for gradient text on site title links */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: 0 1px 1px rgba(0,0,0,0.05); + } + + .site-title a:hover { + background: linear-gradient(120deg, var(--link-hover-color) 0%, var(--accent-secondary) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } +} + header h1 { font-family: var(--font-headings); font-weight: 700; @@ -158,11 +185,17 @@ header h1 { margin: 0; font-size: 2.75rem; letter-spacing: -0.5px; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0,0,0,0.05); +} + +/* Progressive enhancement for gradient text on header h1 */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + header h1 { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: 0 1px 1px rgba(0,0,0,0.05); + } } header p { @@ -210,6 +243,17 @@ nav a:hover::after { width: 100%; } +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); + transform: translateY(-1px); +} + +nav a:focus::after { + width: 100%; +} + main { margin-bottom: 3.5rem; } @@ -342,6 +386,14 @@ article .meta { margin-right: 0.35rem; } +/* Text browser fallback for reading time icon */ +@supports not (content: "⏱") { + .reading-time::before { + content: "Time: "; + margin-right: 0.35rem; + } +} + .summary { margin-top: 1.5rem; } @@ -372,6 +424,14 @@ article .tags a:hover { transform: translateY(-2px); } +article .tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-text); + color: var(--tag-bg); + transform: translateY(-2px); +} + .featured-image { margin: 2rem 0; border-radius: var(--radius); @@ -449,6 +509,14 @@ a:hover { text-decoration-thickness: 2px; } +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + border-radius: 2px; + color: var(--link-hover-color); + text-decoration-thickness: 2px; +} + p { margin: 1.5rem 0; } @@ -486,6 +554,16 @@ footer { position: relative; } +footer a { + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 2px; +} + +footer a:focus { + outline-color: var(--link-color); +} + footer::before { content: ""; position: absolute; @@ -518,6 +596,13 @@ blockquote::before { left: 0.5rem; } +/* Text browser fallback for blockquote decoration */ +@supports not (content: '"') { + blockquote::before { + content: ""; + } +} + code { font-family: var(--font-mono); font-size: 0.9em; @@ -581,6 +666,13 @@ hr::before { transform: translate(-50%, -50%); } +/* Text browser fallback for hr decoration */ +@supports not (content: "✦") { + hr::before { + content: "*"; + } +} + .pagination { display: flex; justify-content: space-between; @@ -591,6 +683,17 @@ hr::before { font-weight: 500; } +.pagination a { + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 2px; + transition: all var(--transition); +} + +.pagination a:focus { + outline-color: var(--link-color); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -618,6 +721,14 @@ hr::before { transform: translateY(-2px); } +.tags-list a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-text); + color: var(--tag-bg); + transform: translateY(-2px); +} + .tag-count { background-color: rgba(0, 0, 0, 0.1); border-radius: 100px; @@ -636,6 +747,16 @@ hr::before { margin-bottom: 0.5rem; } +.posts-list h2 a, .posts-list h3 a { + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 2px; +} + +.posts-list h2 a:focus, .posts-list h3 a:focus { + outline-color: var(--link-color); +} + article > p:first-of-type::first-letter { initial-letter: 2; -webkit-initial-letter: 2; @@ -972,3 +1093,10 @@ article > p:first-of-type::first-letter { background-color: var(--accent-secondary); color: var(--text-color); } + +.archives-nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); + color: var(--text-color); +} diff --git a/themes/diary/style.css b/themes/diary/style.css index 9524815..cb963f1 100644 --- a/themes/diary/style.css +++ b/themes/diary/style.css @@ -1,8 +1,37 @@ /* * Reflections - A modern, elegant diary theme * Rich visual design with progressive enhancement + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining elegant diary aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable fade-in animations */ + .container, article { + animation: none !important; + } + + /* Disable hover transforms */ + article:hover, figure:hover, .featured-image:hover, + .index-image:hover, .tag-image:hover, .archive-image:hover { + transform: none !important; + } +} + :root { /* Modern, refined color palette */ --bg-color: #faf7f2; @@ -24,11 +53,11 @@ --title-gradient-start: #a2836e; --title-gradient-end: #8fb9aa; - /* Typography - using system fonts + optional web fonts */ - --font-main: 'Spectral', Georgia, 'Times New Roman', serif; - --font-headings: 'Fraunces', 'Playfair Display', Georgia, serif; - --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Monaco, Consolas, monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: Georgia, 'Times New Roman', Times, serif; + --font-headings: Georgia, 'Times New Roman', Times, serif; + --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --font-mono: 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* Spacing and structure */ --content-width: 46rem; @@ -79,38 +108,7 @@ } } -/* Optional web fonts - will fallback gracefully if not available */ -@font-face { - font-family: 'Spectral'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,500;1,400&display=swap'); -} - -@font-face { - font-family: 'Fraunces'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600&display=swap'); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'); -} - -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap'); -} +/* REMOVED: External font loading for better performance and text browser compatibility */ /* Base styles */ * { @@ -300,10 +298,9 @@ article::before { border-radius: 2px; } -/* Optional hover effect */ +/* PERFORMANCE: Simplified hover effect without expensive transforms */ article:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-lg); + box-shadow: var(--shadow-md); /* Reduced shadow complexity */ } article:last-child { @@ -423,6 +420,18 @@ blockquote::before { line-height: 1; } +/* TEXT BROWSER FALLBACK: Simplify quote mark when positioning isn't supported */ +@supports not (position: absolute) { + blockquote::before { + content: '"'; + position: static; + top: auto; + left: auto; + font-size: 1.2rem; + opacity: 1; + } +} + blockquote p:last-child { margin-bottom: 0; } @@ -457,12 +466,10 @@ pre code { /* Images */ figure { margin: var(--spacing-lg) 0; - transition: transform var(--transition-normal); + /* PERFORMANCE: Removed expensive transform transition */ } -figure:hover { - transform: scale(1.01); -} +/* PERFORMANCE: Removed scale transform that causes layout recalculation */ img { max-width: 100%; @@ -503,23 +510,39 @@ figcaption { align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Date: " prefix for date */ .date::before { - content: "📅"; + content: "Date: "; margin-right: 0.35rem; font-size: 1.1em; } +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .date::before { + content: "📅"; + } +} + .reading-time { display: inline-flex; align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏱️"; + content: "Time: "; margin-right: 0.35rem; font-size: 1.1em; } +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱️"; + } +} + /* Tags */ .tags { margin-top: var(--spacing-md); @@ -542,12 +565,12 @@ figcaption { box-shadow: var(--shadow-sm); } +/* PERFORMANCE: Simplified hover without expensive transform */ .tags a:hover, .tags a:focus { background-color: var(--tag-hover); color: var(--accent-primary); - transform: translateY(-2px); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); /* Reduced shadow complexity */ } /* Horizontal rule */ @@ -581,6 +604,21 @@ hr::before { font-size: 1rem; } +/* TEXT BROWSER FALLBACK: Simplify decorative element when positioning isn't supported */ +@supports not (position: absolute) { + hr::before { + content: "*"; + position: static; + left: auto; + top: auto; + transform: none; + width: auto; + height: auto; + border-radius: 0; + background-color: transparent; + } +} + /* Footer */ footer { padding-top: var(--spacing-lg); @@ -630,11 +668,11 @@ footer p { box-shadow: var(--shadow-sm); } +/* PERFORMANCE: Simplified hover without expensive transform */ .pagination a:hover, .pagination a:focus { background-color: var(--bg-secondary); - transform: translateY(-2px); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); /* Reduced shadow complexity */ } .pagination a.prev::before { @@ -662,9 +700,9 @@ footer p { transition: all var(--transition-normal); } +/* PERFORMANCE: Simplified hover without expensive transform */ .archive-list li:hover { - transform: translateX(4px); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); /* Reduced shadow complexity */ } .archive-date { @@ -685,9 +723,9 @@ footer p { border-left: 3px solid var(--accent-primary); } +/* PERFORMANCE: Simplified hover without expensive transform */ .post-summary:hover { - transform: translateY(-3px); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-sm); /* Reduced shadow complexity */ } .post-summary h3 { @@ -735,39 +773,34 @@ footer p { color: var(--accent-primary); } -/* Animation for page load - optional enhancement */ +/* PERFORMANCE OPTIMIZATION: Simplified animations */ @keyframes fadeIn { from { opacity: 0; - transform: translateY(10px); } to { opacity: 1; - transform: translateY(0); } } +/* PERFORMANCE: Removed staggered animations that cause layout thrashing */ .container { - animation: fadeIn 0.5s ease-out; + opacity: 1; /* Default visible state for better performance */ } -/* Animation for article entries - optional */ +/* PERFORMANCE: Simplified article animation without transforms */ article { - opacity: 0; - animation: fadeIn 0.5s ease-out forwards; + opacity: 1; /* Default visible state */ + animation: fadeIn 0.3s ease-out; /* Shorter, simpler animation */ } -article:nth-child(1) { animation-delay: 0.1s; } -article:nth-child(2) { animation-delay: 0.2s; } -article:nth-child(3) { animation-delay: 0.3s; } -article:nth-child(4) { animation-delay: 0.4s; } -article:nth-child(5) { animation-delay: 0.5s; } +/* PERFORMANCE: Removed staggered delays that cause multiple repaints */ /* Ensure content is visible even without animations */ @media (prefers-reduced-motion: reduce) { .container, article { - animation: none; - opacity: 1; + animation: none !important; + opacity: 1 !important; } } @@ -1088,13 +1121,9 @@ article:nth-child(5) { animation-delay: 0.5s; } transition: all var(--transition-normal); } +/* PERFORMANCE: Simplified hover without expensive transform and gradient recalculation */ .site-title a:hover { - background: linear-gradient(120deg, var(--title-gradient-end), var(--title-gradient-start)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - transform: translateY(-1px); - text-shadow: 0px 2px 2px rgba(0,0,0,0.1); + color: var(--accent-secondary); /* Simple color change instead of gradient */ } /* Fallback for browsers that don't support background-clip */ @@ -1141,14 +1170,12 @@ article:nth-child(5) { animation-delay: 0.5s; } margin: 0; } +/* PERFORMANCE: Simplified hover without expensive transform and filter */ .featured-image:hover { - box-shadow: var(--shadow-lg); - transform: translateY(-2px); + box-shadow: var(--shadow-md); /* Reduced shadow complexity */ } -.featured-image:hover img { - filter: brightness(1.03); -} +/* PERFORMANCE: Removed expensive filter effect */ .featured-image .image-caption { padding: var(--spacing-sm) var(--spacing-xs) 0; @@ -1190,9 +1217,9 @@ article:nth-child(5) { animation-delay: 0.5s; } margin: 0; } +/* PERFORMANCE: Simplified hover without expensive transform */ .index-image:hover { - box-shadow: var(--shadow-lg); - transform: translateY(-2px); + box-shadow: var(--shadow-md); /* Reduced shadow complexity */ } .tag-image { @@ -1226,9 +1253,9 @@ article:nth-child(5) { animation-delay: 0.5s; } margin: 0; } +/* PERFORMANCE: Simplified hover without expensive transform */ .tag-image:hover { - box-shadow: var(--shadow-lg); - transform: translateY(-2px); + box-shadow: var(--shadow-md); /* Reduced shadow complexity */ } .archive-image { diff --git a/themes/docs/style.css b/themes/docs/style.css index fbc90fc..ad658cd 100644 --- a/themes/docs/style.css +++ b/themes/docs/style.css @@ -2,8 +2,24 @@ * Documentation Theme for BSSG * A clean, structured theme ideal for technical documentation or guides. * Features clear navigation, excellent code formatting, and readable typography. + * Enhanced with accessibility, performance, and compatibility improvements. */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + html { + scroll-behavior: auto !important; + } +} + :root { /* Color scheme - professional documentation style */ --bg-color: #ffffff; @@ -31,9 +47,9 @@ --tip-bg: #e6f6e6; --tip-border: #a8d8a8; - /* Typography */ - --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + /* Typography - enhanced system fonts for better compatibility */ + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, 'Courier New', Courier, monospace; --font-headings: var(--font-sans); /* Sizing */ @@ -51,12 +67,19 @@ --heading-line-height: 1.3; } -/* Base elements */ +/* Base elements - optimized for accessibility */ html { font-size: 16px; scroll-behavior: smooth; } +/* Progressive enhancement for smooth scrolling */ +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } +} + body { font-family: var(--font-sans); background-color: var(--bg-color); @@ -180,6 +203,18 @@ nav a.active { border-bottom: 2px solid var(--nav-active); } +/* Accessibility: Focus outlines for all interactive elements */ +nav a:focus, +.site-title a:focus, +a:focus, +.tags a:focus, +.pagination a:focus, +.menu-toggle:focus, +.anchor:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Mobile menu toggle button */ .menu-toggle { display: none; diff --git a/themes/flat/style.css b/themes/flat/style.css index c32042a..55ece43 100644 --- a/themes/flat/style.css +++ b/themes/flat/style.css @@ -2,6 +2,7 @@ * Flat Design Theme for BSSG * Inspired by Microsoft Metro/Modern UI design language * Features typography-focused design, solid colors, and minimal decoration + * Enhanced with accessibility, performance, and compatibility improvements */ :root { @@ -33,6 +34,34 @@ --gap-size: 10px; } +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all transforms and animations */ + .site-title a:hover, + .metro-tile:hover, + .metro-button:hover, + .tags a:hover, + .pagination a:hover, + footer a:hover::after, + .featured-image:hover img, + .index-image:hover img, + .tag-image:hover img, + .archive-image:hover img { + transform: none !important; + } + + .progress-indicator { + transition: none !important; + } +} + /* Base elements */ body { font-family: var(--font-main); @@ -74,29 +103,35 @@ header { .site-title a { color: var(--text-on-accent); text-decoration: none; - background: linear-gradient(90deg, var(--title-gradient-start), var(--title-gradient-end)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; display: inline-block; position: relative; transition: all 0.2s ease; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); font-weight: 500; + outline: 2px solid transparent; + outline-offset: 2px; } .site-title a:hover { transform: translateY(-1px); - text-shadow: 0 2px 5px rgba(0, 0, 0, 0.4); } -/* Fallback for browsers that don't support background-clip */ -@supports not (background-clip: text) { +.site-title a:focus { + outline-color: rgba(255, 255, 255, 0.8); +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { .site-title a { - color: white; - background: none; + background: linear-gradient(90deg, var(--title-gradient-start), var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); } + + .site-title a:hover { + text-shadow: 0 2px 5px rgba(0, 0, 0, 0.4); + } } header h1 { @@ -143,6 +178,11 @@ nav a:hover, nav a:focus { border-bottom: 3px solid rgba(255, 255, 255, 0.5); } +nav a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; +} + /* Active navigation item */ nav a.active { color: white; @@ -213,6 +253,12 @@ a:hover { color: var(--accent-hover); } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-hover); +} + /* Metro style button */ .metro-button { display: inline-block; @@ -232,6 +278,12 @@ a:hover { background-color: var(--accent-hover); } +.metro-button:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-hover); +} + /* Articles as flat tiles */ article { margin-bottom: 30px; @@ -274,13 +326,21 @@ article:nth-child(5n+5) .article-header { background-color: var(--tile-color-5); } -article h1 { +article .article-header h1 { font-size: 24px; margin: 0; padding: 0; color: white; } +/* Fix for article content h1 - should use normal text color */ +article .article-content h1 { + color: var(--text-color); + font-size: 24px; + margin: 0 0 1rem 0; + padding: 0; +} + /* Article content with clean padding */ article .article-content { padding: 30px; @@ -324,6 +384,13 @@ article .meta { color: white; } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + color: white; +} + .tags-list { list-style-type: none; padding: 0; @@ -405,6 +472,15 @@ footer a:hover::after { transform: scaleX(1); } +footer a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; +} + +footer a:focus::after { + transform: scaleX(1); +} + /* Metro-style grid layout */ .metro-grid { display: grid; @@ -430,6 +506,12 @@ footer a:hover::after { background-color: var(--accent-hover); } +.metro-tile:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-hover); +} + /* Rotating colors for metro tiles */ .metro-tile:nth-child(5n+1) { background-color: var(--tile-color-1); @@ -481,6 +563,12 @@ footer a:hover::after { background-color: var(--accent-hover); } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-hover); +} + .pagination .page-info { margin: 0 15px; font-size: 14px; diff --git a/themes/gameboy/style.css b/themes/gameboy/style.css index 2d40020..ae028bd 100644 --- a/themes/gameboy/style.css +++ b/themes/gameboy/style.css @@ -2,9 +2,32 @@ * Game Boy Theme for BSSG * A clean retro theme inspired by the original Nintendo Game Boy, * with high readability while maintaining the nostalgic feel + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Game Boy aesthetics */ -@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms */ + .site-title a:hover, + nav a:hover { + transform: none !important; + } +} :root { /* Classic Game Boy palette */ @@ -23,10 +46,10 @@ --accent-color: #8bac0f; --border-color: #8bac0f; - /* Typography */ - --font-main: 'Press Start 2P', monospace; - --font-headings: 'Press Start 2P', monospace; - --font-mono: monospace; + /* Typography - Game Boy pixelated fonts with fallbacks */ + --font-main: 'Courier New', 'Lucida Console', 'Monaco', 'Consolas', 'Liberation Mono', monospace; + --font-headings: 'Courier New', 'Lucida Console', 'Monaco', 'Consolas', 'Liberation Mono', monospace; + --font-mono: 'Courier New', 'Lucida Console', 'Monaco', 'Consolas', 'Liberation Mono', monospace; /* Sizing */ --content-width: 90%; @@ -46,19 +69,21 @@ /* Base elements */ html { - font-size: 14px; /* Smaller base font size for pixelated font */ + font-size: 16px; /* Larger base font size for better Game Boy readability */ background-color: var(--gb-darkest); scroll-behavior: smooth; } body { font-family: var(--font-main); - line-height: 1.5; + line-height: 1.4; /* Tighter line height for pixelated look */ color: var(--text-color); background-color: var(--bg-color); margin: 0; padding: 0; overflow-x: hidden; + font-weight: bold; /* Bold text for authentic Game Boy appearance */ + text-rendering: optimizeSpeed; /* Crisp pixelated rendering */ } .container { @@ -76,7 +101,7 @@ body { overflow: hidden; /* Ensure contained elements don't break the Game Boy screen */ } -/* Enhanced header styling */ +/* Enhanced header styling with Game Boy power indicator */ header { text-align: center; margin-bottom: var(--spacing-xl); @@ -85,6 +110,19 @@ header { position: relative; } +/* Game Boy power indicator */ +header::before { + content: "● POWER"; + position: absolute; + top: -10px; + right: 10px; + font-size: 0.7rem; + color: var(--gb-light); + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; +} + header::after { content: ""; display: block; @@ -98,19 +136,22 @@ header::after { } .site-title { - font-size: 1.5rem; - letter-spacing: -1px; + font-size: 2rem; /* Larger, more prominent Game Boy title */ + letter-spacing: 0px; /* Remove negative spacing for pixelated look */ margin: 0 0 var(--spacing-md) 0; padding: var(--spacing-sm) 0; color: var(--heading-color); - text-shadow: 2px 2px 0 rgba(15, 56, 15, 0.2); + text-shadow: 2px 2px 0 rgba(15, 56, 15, 0.3); /* Stronger shadow for depth */ position: relative; display: inline-block; + font-weight: bold; + text-transform: uppercase; /* Game Boy style uppercase */ } +/* TEXT BROWSER FALLBACK: Simplify decorative elements */ .site-title::before, .site-title::after { - content: "〓"; + content: "*"; color: var(--gb-light); position: absolute; top: 50%; @@ -118,6 +159,15 @@ header::after { font-size: 1rem; } +/* Use Game Boy style decoration when positioning is supported */ +@supports (position: absolute) { + .site-title::before, + .site-title::after { + content: "▓"; /* More pixelated Game Boy style block */ + font-size: 1.2rem; + } +} + .site-title::before { left: -1.5rem; } @@ -139,14 +189,23 @@ header::after { transform: scale(1.05); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + transform: scale(1.05); +} + /* Site description */ header p { color: var(--text-color); - font-size: 0.7rem; + font-size: 0.9rem; /* Larger for better readability */ margin: 0 auto; max-width: 85%; opacity: 0.9; - line-height: 1.8; + line-height: 1.4; /* Consistent with body line height */ + font-weight: normal; /* Less bold for description */ } /* Improved Navigation buttons */ @@ -163,20 +222,22 @@ nav { nav a { display: inline-block; - padding: var(--spacing-xs) var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); /* Larger padding for Game Boy buttons */ background-color: var(--gb-light); color: var(--gb-darkest); text-decoration: none; - font-size: 0.7rem; - border: 2px solid var(--gb-dark); + font-size: 0.9rem; /* Larger font for better readability */ + border: 3px solid var(--gb-dark); /* Thicker border for Game Boy button look */ border-radius: 0; - box-shadow: 2px 2px 0 var(--gb-dark); + box-shadow: 3px 3px 0 var(--gb-dark); /* Stronger shadow for 3D effect */ transform: translateY(0); transition: all var(--animation-speed) ease-in-out; text-align: center; - min-width: 80px; + min-width: 100px; /* Wider buttons */ position: relative; overflow: hidden; + font-weight: bold; + text-transform: uppercase; /* Game Boy style uppercase */ } nav a::after { @@ -197,6 +258,16 @@ nav a:hover::after { nav a:hover, nav a.active { + background-color: var(--gb-dark); + color: var(--bg-color); + transform: translateY(3px); /* Deeper press effect */ + box-shadow: 0 0 0 var(--gb-dark); +} + +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; background-color: var(--gb-dark); color: var(--bg-color); transform: translateY(2px); @@ -218,23 +289,33 @@ h1, h2, h3, h4, h5, h6 { } h1 { - font-size: 1.3rem; - border-bottom: 2px solid var(--accent-color); + font-size: 1.6rem; /* Larger for Game Boy prominence */ + border-bottom: 3px solid var(--accent-color); /* Thicker border */ padding-bottom: var(--spacing-xs); + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; /* Spaced out for pixelated effect */ } h2 { - font-size: 1.1rem; + font-size: 1.3rem; /* Larger for better hierarchy */ + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; } h3 { - font-size: 1rem; + font-size: 1.1rem; /* Larger for readability */ + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; } p { margin: var(--spacing-md) 0; - font-size: 0.8rem; - line-height: 1.7; + font-size: 1rem; /* Larger for better readability */ + line-height: 1.4; /* Consistent pixelated spacing */ + font-weight: normal; /* Less bold for body text */ } /* Links */ @@ -249,6 +330,13 @@ a:hover { color: var(--link-hover); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); +} + /* Article styling */ article { margin-bottom: var(--spacing-xl); @@ -265,18 +353,21 @@ article h1 { flex-wrap: wrap; justify-content: center; gap: var(--spacing-md); - font-size: 0.7rem; + font-size: 0.9rem; /* Larger for readability */ margin: var(--spacing-md) 0 var(--spacing-lg); text-align: center; color: var(--text-color); - opacity: 0.7; + opacity: 0.8; /* Slightly more visible */ + font-weight: bold; + text-transform: uppercase; } /* Lists */ ul, ol { padding-left: 1.5rem; margin: var(--spacing-md) 0; - font-size: 0.8rem; + font-size: 1rem; /* Larger for readability */ + font-weight: normal; } li { @@ -286,34 +377,77 @@ li { /* Code blocks */ pre, code { font-family: var(--font-mono); + font-size: 0.9rem; /* Larger for readability */ + font-weight: bold; + border-radius: 0; /* Sharp corners for pixelated look */ +} + +/* Inline code styling */ +code { background-color: var(--gb-lightest); color: var(--gb-darkest); - border-radius: 2px; - font-size: 0.8rem; -} - -code { padding: 0.1em 0.3em; + border: 1px solid var(--gb-dark); /* Thinner border for inline code */ } +/* Code block styling */ pre { + background-color: var(--gb-lightest); + color: var(--gb-darkest); + border: 2px solid var(--gb-dark); /* Game Boy style border for blocks */ padding: var(--spacing-sm); overflow-x: auto; margin: var(--spacing-md) 0; } +/* Code inside pre blocks - remove all styling to prevent double borders */ pre code { padding: 0; background: transparent; + border: none; /* Remove border from code inside pre */ + font-size: inherit; } -/* Blockquotes */ +/* Blockquotes - Game Boy style */ blockquote { margin: var(--spacing-md) 0; - padding: var(--spacing-sm) var(--spacing-md); - border-left: 4px solid var(--accent-color); - background-color: rgba(139, 172, 15, 0.1); - font-style: italic; + padding: var(--spacing-md); + border: 3px solid var(--gb-dark); + background-color: var(--gb-lightest); + color: var(--gb-darkest); + font-style: normal; /* Remove italic for pixelated look */ + font-weight: bold; + border-radius: 0; /* Sharp corners for Game Boy style */ + box-shadow: 3px 3px 0 var(--gb-dark); + position: relative; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 0.9rem; +} + +/* Game Boy style quote decoration */ +blockquote::before { + content: "▓▓"; + position: absolute; + top: -3px; + left: -3px; + background-color: var(--gb-dark); + color: var(--gb-lightest); + padding: 2px 4px; + font-size: 0.7rem; + line-height: 1; +} + +/* TEXT BROWSER FALLBACK: Simple quote marks */ +@supports not (position: absolute) { + blockquote::before { + content: ">> "; + position: static; + background: none; + color: var(--gb-dark); + padding: 0; + font-size: 1rem; + } } blockquote p:first-child { @@ -338,15 +472,18 @@ img { figcaption, .caption { text-align: center; - font-size: 0.7rem; + font-size: 0.9rem; /* Larger for readability */ margin-top: var(--spacing-sm); padding: var(--spacing-xs); background-color: var(--gb-lightest); color: var(--gb-darkest); - border-radius: 2px; + border-radius: 0; /* Sharp corners for pixelated look */ + border: 2px solid var(--gb-dark); /* Game Boy style border */ display: inline-block; margin-left: auto; margin-right: auto; + font-weight: bold; + text-transform: uppercase; } /* Post list styling */ @@ -388,19 +525,32 @@ figcaption, .tags a { display: inline-block; - padding: var(--spacing-xs); - font-size: 0.65rem; + padding: var(--spacing-xs) var(--spacing-sm); /* More padding */ + font-size: 0.8rem; /* Larger for readability */ background-color: var(--gb-lightest); color: var(--gb-darkest); text-decoration: none; - border: 1px solid var(--gb-dark); + border: 2px solid var(--gb-dark); /* Thicker border */ border-radius: 0; transition: all var(--animation-speed) ease; position: relative; overflow: hidden; + font-weight: bold; + text-transform: uppercase; + box-shadow: 2px 2px 0 var(--gb-dark); /* Game Boy button shadow */ } .tags a:hover { + background-color: var(--gb-dark); + color: var(--gb-lightest); + transform: translateY(2px); /* Deeper press effect */ + box-shadow: 0 0 0 var(--gb-dark); /* Pressed shadow */ +} + +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; background-color: var(--gb-dark); color: var(--gb-lightest); transform: translateY(1px); @@ -426,26 +576,37 @@ footer { .pagination a { display: inline-block; - padding: var(--spacing-xs) var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); /* Larger padding */ background-color: var(--gb-light); color: var(--bg-color); text-decoration: none; - font-size: 0.7rem; - border: 2px solid var(--gb-dark); - box-shadow: 2px 2px 0 var(--gb-dark); + font-size: 0.9rem; /* Larger font */ + border: 3px solid var(--gb-dark); /* Thicker border */ + box-shadow: 3px 3px 0 var(--gb-dark); /* Stronger shadow */ transform: translateY(0); transition: all var(--animation-speed) ease-in-out; - min-width: 80px; + min-width: 100px; /* Wider buttons */ text-align: center; + font-weight: bold; + text-transform: uppercase; } .pagination a:hover { + background-color: var(--gb-dark); + transform: translateY(3px); /* Deeper press effect */ + box-shadow: 0 0 0 var(--gb-dark); +} + +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; background-color: var(--gb-dark); transform: translateY(2px); box-shadow: 0 0 0 var(--gb-dark); } -/* Screen scanlines effect */ +/* Enhanced Game Boy screen scanlines effect */ .container::before { content: ""; position: absolute; @@ -456,12 +617,13 @@ footer { background: linear-gradient( to bottom, - rgba(139, 172, 15, 0.02) 1px, + rgba(139, 172, 15, 0.05) 1px, /* More visible scanlines */ transparent 1px ); - background-size: 100% 4px; + background-size: 100% 3px; /* Tighter scanlines for authentic feel */ pointer-events: none; z-index: 1; + opacity: 0.7; /* More prominent effect */ } /* Enhanced reading time indicator */ @@ -475,11 +637,19 @@ footer { border-radius: 2px; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏲"; + content: "Time: "; margin-right: var(--spacing-xs); } +/* Use Game Boy style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏲"; + } +} + /* Better game boy hover effects */ a:not(.site-title a):not(nav a):not(.pagination a):not(.tags a)::after { content: ""; @@ -661,6 +831,14 @@ a:not(.site-title a):not(nav a):not(.pagination a):not(.tags a):hover::after { } blockquote { - background-color: rgba(15, 56, 15, 0.5); + background-color: var(--gb-darkest); + color: var(--gb-lightest); + border-color: var(--gb-light); + box-shadow: 3px 3px 0 var(--gb-light); + } + + blockquote::before { + background-color: var(--gb-light); + color: var(--gb-darkest); } } \ No newline at end of file diff --git a/themes/glassmorphism/style.css b/themes/glassmorphism/style.css index c73096b..b4b5833 100644 --- a/themes/glassmorphism/style.css +++ b/themes/glassmorphism/style.css @@ -2,43 +2,66 @@ * Glassmorphism Theme for BSSG * Modern frosted glass effect with blur, transparency, and subtle borders * Features: backdrop-filter blur, soft shadows, and subtle gradients + * IMPROVED: Better accessibility, performance, and text browser support */ +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects for reduced motion */ + .container, nav a, .glass-button, .glass-card { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + } + + /* Simplify hover effects */ + .featured-image:hover, .index-image:hover, .tag-image:hover, .archive-image:hover { + transform: none !important; + } +} + :root { - /* Glassmorphism color scheme - updated with cooler tones */ - --glass-bg: rgba(255, 255, 255, 0.1); - --glass-border: rgba(255, 255, 255, 0.2); - --glass-shadow: rgba(0, 0, 0, 0.1); - --glass-highlight: rgba(255, 255, 255, 0.05); + /* Glassmorphism color scheme - IMPROVED contrast ratios */ + --glass-bg: rgba(255, 255, 255, 0.20); /* MAXIMUM CONTRAST: Much higher opacity for better readability */ + --glass-border: rgba(255, 255, 255, 0.25); /* Increased opacity */ + --glass-shadow: rgba(0, 0, 0, 0.15); /* Increased for better definition */ + --glass-highlight: rgba(255, 255, 255, 0.08); --bg-color-1: #0F3443; --bg-color-2: #348F50; --bg-color-3: #56B4D3; - --text-color: rgba(255, 255, 255, 0.9); - --text-color-muted: rgba(255, 255, 255, 0.6); + --text-color: rgba(0, 0, 0, 0.9); /* MAXIMUM contrast: Dark text on light backgrounds */ + --text-color-muted: rgba(0, 0, 0, 0.8); /* MAXIMUM contrast: Slightly muted but still dark */ --accent-color: #00FFCC; --accent-color-hover: #99FFE6; - --link-color: rgba(0, 255, 204, 0.9); + --link-color: rgba(0, 255, 204, 0.95); /* Improved contrast */ --link-hover: rgba(150, 255, 230, 1); --title-gradient-start: rgba(0, 255, 204, 1); --title-gradient-end: rgba(56, 180, 211, 1); - /* Typography */ - --font-main: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-headings: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-mono: 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + /* Typography - Added fallbacks for text browsers */ + --font-main: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-headings: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, 'Courier New', monospace; - /* Sizing */ + /* Sizing - OPTIMIZED blur amount */ --content-width: 1000px; - --blur-amount: 20px; + --blur-amount: 12px; /* Reduced from 20px for better performance */ + --blur-amount-light: 6px; /* For less critical elements */ --border-radius: 16px; --small-radius: 10px; } -/* Base elements with vibrant gradient background */ +/* Base elements with optimized gradient background */ body { font-family: var(--font-main); background: linear-gradient(135deg, var(--bg-color-1) 0%, var(--bg-color-2) 50%, var(--bg-color-3) 100%); - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ color: var(--text-color); margin: 0; padding: 20px; @@ -48,13 +71,14 @@ body { min-height: 100vh; } -/* Container with glass morphism effect */ +/* OPTIMIZED: Container with reduced glass morphism effect */ .container { max-width: var(--content-width); margin: 40px auto; padding: 0; position: relative; background: var(--glass-bg); + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ backdrop-filter: blur(var(--blur-amount)); -webkit-backdrop-filter: blur(var(--blur-amount)); border: 1px solid var(--glass-border); @@ -63,22 +87,41 @@ body { overflow: hidden; } -/* Gradient glass highlight effect */ +/* FALLBACK: For browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(1px)) { + .container { + background: rgba(255, 255, 255, 0.9); /* Light solid fallback background for better contrast */ + } + + nav a { + background: rgba(255, 255, 255, 0.8); + } + + .glass-button { + background: rgba(255, 255, 255, 0.8); + } + + .glass-card { + background: rgba(255, 255, 255, 0.8); + } +} + +/* OPTIMIZED: Simplified gradient glass highlight effect */ .container::before { content: ""; position: absolute; top: 0; left: 0; right: 0; - height: 100px; + height: 80px; /* Reduced height for performance */ background: linear-gradient(to bottom, var(--glass-highlight), transparent); pointer-events: none; border-radius: var(--border-radius) var(--border-radius) 0 0; } -/* Header with deeper glass effect */ +/* Header with optimized glass effect */ header { - background: rgba(255, 255, 255, 0.05); + background: rgba(255, 255, 255, 0.20); /* MAXIMUM CONTRAST: Much higher opacity for better readability */ padding: 30px 40px; position: relative; border-bottom: 1px solid var(--glass-border); @@ -93,7 +136,7 @@ header h1 { opacity: 0.95; } -/* Site title with glassmorphic gradient effect */ +/* Site title with improved glassmorphic gradient effect */ .site-title { margin: 0; padding: 0; @@ -123,14 +166,21 @@ header h1 { border-bottom: none; } -/* Fallback for browsers that don't support background-clip */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-bottom: none; +} + +/* FALLBACK: For browsers that don't support background-clip */ @supports not (background-clip: text) { .site-title a { - color: var(--accent-color); + color: var(--accent-color) !important; + background: none !important; } } -/* Navigation with glass buttons */ +/* OPTIMIZED: Navigation with reduced glass effects */ nav { display: flex; flex-wrap: nowrap; @@ -139,7 +189,7 @@ nav { justify-content: flex-end; position: relative; gap: 12px; - background: rgba(255, 255, 255, 0.03); + background: rgba(255, 255, 255, 0.05); /* Increased opacity */ } nav a { @@ -152,43 +202,48 @@ nav a { display: inline-block; border-radius: 30px; background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ + backdrop-filter: blur(var(--blur-amount-light)); /* Reduced blur for nav items */ + -webkit-backdrop-filter: blur(var(--blur-amount-light)); border: 1px solid var(--glass-border); transition: all 0.2s ease; white-space: nowrap; } nav a:hover, nav a:focus { - background: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.18); /* Increased opacity for better contrast */ transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + /* IMPROVED: Focus outline for accessibility */ + outline: 2px solid var(--accent-color); + outline-offset: 2px; } /* Active navigation with accent glow */ nav a.active { - background: rgba(0, 204, 255, 0.15); - border-color: rgba(0, 204, 255, 0.3); + background: rgba(0, 204, 255, 0.2); /* Increased opacity */ + border-color: rgba(0, 204, 255, 0.4); /* Increased opacity */ box-shadow: 0 0 15px rgba(0, 204, 255, 0.3); } /* RSS in accent color */ nav a:last-child { - background: rgba(0, 204, 255, 0.2); - border-color: rgba(0, 204, 255, 0.3); + background: rgba(0, 204, 255, 0.25); /* Increased opacity */ + border-color: rgba(0, 204, 255, 0.4); /* Increased opacity */ } nav a:last-child:hover { - background: rgba(0, 204, 255, 0.3); + background: rgba(0, 204, 255, 0.35); /* Increased opacity */ } /* Content area with glass effect */ main { padding: 40px; position: relative; + background: rgba(255, 255, 255, 0.15); /* IMPROVED: Better background for content readability */ } -/* Typography with clear modern style */ +/* Typography with improved contrast */ h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); margin-top: 2rem; @@ -212,16 +267,16 @@ h3 { } p { + margin-top: 0; margin-bottom: 1.5rem; - opacity: 0.9; + color: var(--text-color); /* Improved contrast */ } -/* Elegant links with subtle hover effect */ a { color: var(--link-color); text-decoration: none; - transition: all 0.2s; - border-bottom: 1px solid transparent; + border-bottom: 1px solid rgba(0, 255, 204, 0.3); + transition: all 0.2s ease; } a:hover { @@ -229,22 +284,27 @@ a:hover { border-bottom-color: var(--link-hover); } -/* Articles with glass cards */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + border-bottom-color: var(--link-hover); +} + article { - margin-bottom: 30px; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); + margin-bottom: 3rem; + padding: 2rem; + background: rgba(255, 255, 255, 0.25); /* MAXIMUM CONTRAST: Much higher opacity for text readability */ border: 1px solid var(--glass-border); border-radius: var(--small-radius); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); - overflow: hidden; - transition: transform 0.2s, box-shadow 0.2s; + /* OPTIMIZED: Reduced shadow complexity */ + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; } article:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } article:last-child { @@ -252,125 +312,132 @@ article:last-child { } article h1 { - font-size: 1.8rem; - margin: 0; - padding: 25px 30px 15px; + margin-top: 0; + color: var(--text-color); } article .article-content { - padding: 0 30px 30px; + color: var(--text-color); /* Improved contrast */ } article .meta { + color: rgba(255, 255, 255, 1); /* WHITE text on dark background for maximum contrast */ font-size: 0.9rem; - margin-bottom: 20px; + margin-bottom: 1.5rem; display: flex; flex-wrap: wrap; - gap: 15px; - padding: 0 30px 15px; - color: var(--text-color-muted); - border-bottom: 1px solid var(--glass-border); + gap: 1rem; + /* MAXIMUM CONTRAST: Very strong background for perfect metadata visibility */ + background: rgba(0, 0, 0, 0.8); + padding: 0.75rem 1rem; + border-radius: var(--small-radius); + border: 2px solid rgba(255, 255, 255, 0.4); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } + + .reading-time { - opacity: 0.7; + color: rgba(255, 255, 255, 1); /* WHITE text on dark meta background for maximum contrast */ + font-weight: 500; /* IMPROVED: Make it slightly bolder */ } .tags { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 0.5rem; + margin-top: 1rem; } .tags a { - background: rgba(255, 255, 255, 0.1); - color: var(--text-color); - padding: 4px 12px; - font-size: 0.75rem; + background: rgba(0, 0, 0, 0.8); /* Dark background for better contrast */ + color: rgba(0, 255, 204, 1); /* Bright cyan text on dark background */ + padding: 0.25rem 0.75rem; border-radius: 20px; - border: 1px solid var(--glass-border); - transition: all 0.2s; + font-size: 0.8rem; + border: 1px solid rgba(0, 255, 204, 0.5); + border-bottom: 1px solid rgba(0, 255, 204, 0.5); /* Consistent border */ + transition: all 0.2s ease; + font-weight: 500; /* Make text slightly bolder for better readability */ } .tags a:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.3); - transform: translateY(-2px); + background: rgba(0, 0, 0, 0.9); /* Darker background on hover */ + color: rgba(0, 255, 204, 1); /* Keep bright cyan text */ + transform: translateY(-1px); + border: 1px solid rgba(0, 255, 204, 0.8); /* Brighter border on hover */ + border-bottom: 1px solid rgba(0, 255, 204, 0.8); /* Consistent border */ + box-shadow: 0 2px 8px rgba(0, 255, 204, 0.3); /* Add cyan glow on hover */ } .tags-list { - list-style-type: none; - padding: 0; display: flex; flex-wrap: wrap; - gap: 10px; + gap: 1rem; + margin-bottom: 2rem; } .tag-count { - background: rgba(0, 204, 255, 0.2); - color: var(--text-color); - font-size: 0.7em; - margin-left: 5px; - padding: 2px 8px; - border-radius: 10px; + background: rgba(0, 255, 204, 0.2); /* Increased opacity */ + color: var(--accent-color); + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + margin-left: 0.5rem; } -/* Code with frosted glass effect */ code { - font-family: var(--font-mono); - background: rgba(0, 0, 0, 0.2); - color: var(--text-color); - padding: 3px 6px; - font-size: 0.9em; + background: rgba(0, 0, 0, 0.4); /* Improved contrast */ + color: var(--accent-color); + padding: 0.2rem 0.5rem; border-radius: 4px; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); + font-family: var(--font-mono); + font-size: 0.9rem; + border: 1px solid rgba(0, 255, 204, 0.2); } pre { - background: rgba(0, 0, 0, 0.2); - padding: 20px; - overflow-x: auto; - font-size: 0.9em; + background: rgba(0, 0, 0, 0.5); /* Improved contrast */ + color: var(--text-color); + padding: 1.5rem; border-radius: var(--small-radius); - margin: 20px 0; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - border: 1px solid rgba(255, 255, 255, 0.1); + overflow-x: auto; + border: 1px solid var(--glass-border); + margin: 1.5rem 0; } pre code { - background: transparent; + background: none; + border: none; padding: 0; - border-radius: 0; - backdrop-filter: none; - -webkit-backdrop-filter: none; + color: inherit; } img { max-width: 100%; height: auto; - border-radius: 8px; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); - transition: transform 0.2s; + border-radius: var(--small-radius); + margin: 1rem 0; } img:hover { - transform: scale(1.03); + transform: scale(1.02); + transition: transform 0.3s ease; } -/* Featured image with glass effect */ +/* OPTIMIZED: Featured image with reduced glass effects */ .featured-image { - margin: 30px 0; position: relative; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - border: 1px solid var(--glass-border); - border-radius: var(--small-radius); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + margin: 2rem 0; + border-radius: var(--border-radius); overflow: hidden; - transition: all 0.3s ease; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); } .featured-image::before { @@ -378,51 +445,52 @@ img:hover { position: absolute; top: 0; left: 0; - width: 100%; - height: 4px; - background: linear-gradient(to right, var(--title-gradient-start), var(--title-gradient-end)); - z-index: 2; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.1) 100%); + pointer-events: none; + z-index: 1; } .featured-image img { width: 100%; height: auto; display: block; - transition: transform 0.5s ease; - border: none; - margin: 0; + transition: all 0.3s ease; + /* OPTIMIZED: Simplified filter effects */ + filter: brightness(1.05) contrast(1.05); } .featured-image:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); + transform: translateY(-4px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); } .featured-image:hover img { - transform: scale(1.02); + transform: scale(1.02); /* Reduced scale for better performance */ } .featured-image .image-caption { - padding: 15px; - color: var(--text-color-muted); + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.7); /* Improved contrast */ + color: var(--text-color); + padding: 1rem; font-size: 0.9rem; - border-top: 1px solid var(--glass-border); - background: rgba(0, 0, 0, 0.1); - backdrop-filter: blur(var(--blur-amount)); - -webkit-backdrop-filter: blur(var(--blur-amount)); + z-index: 2; } +/* OPTIMIZED: Index image with reduced effects */ .index-image { - margin: 30px 0; position: relative; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - border: 1px solid var(--glass-border); + margin: 1.5rem 0; border-radius: var(--small-radius); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); overflow: hidden; - transition: all 0.3s ease; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .index-image::before { @@ -430,41 +498,38 @@ img:hover { position: absolute; top: 0; left: 0; - width: 100%; - height: 4px; - background: linear-gradient(to right, rgba(150, 255, 230, 1), rgba(0, 150, 200, 1)); - z-index: 2; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.05) 100%); + pointer-events: none; + z-index: 1; } .index-image img { width: 100%; height: auto; display: block; - transition: transform 0.5s ease; - border: none; - margin: 0; + transition: all 0.3s ease; + filter: brightness(1.02) contrast(1.02); } .index-image:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); } .index-image:hover img { - transform: scale(1.02); + transform: scale(1.01); } .tag-image { - margin: 30px 0; position: relative; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - border: 1px solid var(--glass-border); + margin: 1.5rem 0; border-radius: var(--small-radius); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); overflow: hidden; - transition: all 0.3s ease; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .tag-image::before { @@ -472,41 +537,38 @@ img:hover { position: absolute; top: 0; left: 0; - width: 100%; - height: 4px; - background: linear-gradient(to right, rgba(0, 204, 255, 1), rgba(0, 255, 204, 1)); - z-index: 2; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.05) 100%); + pointer-events: none; + z-index: 1; } .tag-image img { width: 100%; height: auto; display: block; - transition: transform 0.5s ease; - border: none; - margin: 0; + transition: all 0.3s ease; + filter: brightness(1.02) contrast(1.02); } .tag-image:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); } .tag-image:hover img { - transform: scale(1.02); + transform: scale(1.01); } .archive-image { - margin: 30px 0; position: relative; - background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - border: 1px solid var(--glass-border); + margin: 1.5rem 0; border-radius: var(--small-radius); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); overflow: hidden; - transition: all 0.3s ease; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .archive-image::before { @@ -514,220 +576,207 @@ img:hover { position: absolute; top: 0; left: 0; - width: 100%; - height: 4px; - background: linear-gradient(to right, rgba(150, 110, 255, 1), rgba(0, 204, 255, 1)); - z-index: 2; + right: 0; + bottom: 0; + background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.05) 100%); + pointer-events: none; + z-index: 1; } .archive-image img { width: 100%; height: auto; display: block; - transition: transform 0.5s ease; - border: none; - margin: 0; + transition: all 0.3s ease; + filter: brightness(1.02) contrast(1.02); } .archive-image:hover { - transform: translateY(-5px); - box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); } .archive-image:hover img { - transform: scale(1.02); + transform: scale(1.01); } -/* Footer with glass effect */ footer { - background: rgba(0, 0, 0, 0.1); - backdrop-filter: blur(var(--blur-amount)); - -webkit-backdrop-filter: blur(var(--blur-amount)); - border-top: 1px solid var(--glass-border); - color: var(--text-color); - padding: 30px 40px; - font-size: 0.9rem; + background: rgba(255, 255, 255, 0.05); + padding: 2rem 40px; text-align: center; - display: flex; - justify-content: space-between; - align-items: center; - border-radius: 0 0 var(--border-radius) var(--border-radius); + border-top: 1px solid var(--glass-border); + color: var(--text-color-muted); + font-size: 0.9rem; } footer a { - color: var(--text-color); - opacity: 0.8; - transition: opacity 0.2s; + color: var(--accent-color); + border-bottom: 1px solid rgba(0, 255, 204, 0.3); } footer a:hover { - opacity: 1; - border-bottom-color: var(--text-color); + color: var(--link-hover); + border-bottom-color: var(--link-hover); } -/* Pagination with glass buttons */ .pagination { display: flex; justify-content: center; align-items: center; - margin: 40px 0 10px; - gap: 15px; + gap: 1rem; + margin: 2rem 0; } .pagination a { - color: var(--text-color); - padding: 10px 20px; - text-decoration: none; - border-radius: 30px; background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); + color: var(--text-color); + padding: 0.75rem 1.5rem; + border-radius: 30px; border: 1px solid var(--glass-border); - transition: all 0.2s; + transition: all 0.2s ease; font-weight: 500; + border-bottom: 1px solid var(--glass-border); /* Consistent border */ } .pagination a:hover { background: rgba(255, 255, 255, 0.15); transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-bottom: 1px solid var(--glass-border); /* Consistent border */ } .pagination .page-info { color: var(--text-color-muted); - font-size: 0.9rem; + font-weight: 500; } -/* Custom glass morphism button */ +/* OPTIMIZED: Glass button component with reduced effects */ .glass-button { - display: inline-block; - color: var(--text-color); background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.8)); - padding: 10px 20px; - border-radius: 30px; + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ + backdrop-filter: blur(var(--blur-amount-light)); + -webkit-backdrop-filter: blur(var(--blur-amount-light)); border: 1px solid var(--glass-border); + border-radius: var(--small-radius); + padding: 0.75rem 1.5rem; + color: var(--text-color); + text-decoration: none; + transition: all 0.2s ease; + display: inline-block; font-weight: 500; - font-size: 0.9rem; - transition: all 0.2s; - cursor: pointer; - text-align: center; } .glass-button:hover { background: rgba(255, 255, 255, 0.15); transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } -/* Glass card with hover effect */ +/* OPTIMIZED: Glass card component with reduced effects */ .glass-card { background: var(--glass-bg); - backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); - -webkit-backdrop-filter: blur(calc(var(--blur-amount) * 0.6)); + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ + backdrop-filter: blur(var(--blur-amount-light)); + -webkit-backdrop-filter: blur(var(--blur-amount-light)); border: 1px solid var(--glass-border); - border-radius: var(--small-radius); - padding: 25px; - margin-bottom: 20px; - transition: transform 0.2s, box-shadow 0.2s; + border-radius: var(--border-radius); + padding: 1.5rem; + margin: 1rem 0; + transition: all 0.3s ease; } .glass-card:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { + body { + padding: 10px; + /* OPTIMIZED: Simplified background on mobile */ + background: linear-gradient(135deg, var(--bg-color-1) 0%, var(--bg-color-2) 100%); + } + + .container { + margin: 20px auto; + border-radius: var(--small-radius); + /* OPTIMIZED: Remove backdrop-filter on mobile for better performance */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(255, 255, 255, 0.9); /* Light solid background for mobile with maximum contrast */ + } + + header, main, footer { + padding: 20px; + } + + .site-title { + font-size: 2rem; + } + + nav { + flex-direction: column; + align-items: center; + gap: 8px; + padding: 16px 20px; + } + + nav a { + width: 100%; + text-align: center; + padding: 12px 16px; + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(255, 255, 255, 0.1); + } + + article h1 { + font-size: 1.8rem; + } + + article .meta { + flex-direction: column; + gap: 0.5rem; + } + + article .article-content { + font-size: 1rem; + } + + footer { + padding: 20px; + } +} + +@media (max-width: 480px) { body { padding: 5px; } - + .container { - margin: 10px 5px; - width: calc(100% - 10px); - max-width: 100%; - border-radius: 12px; + margin: 10px auto; + border-radius: 8px; } - - header, main, footer { - padding: 15px; - width: 100%; - box-sizing: border-box; - } - - header h1 { - font-size: 2rem; - word-break: break-word; - } - + nav { - padding: 10px 15px; - justify-content: center; - flex-wrap: wrap; - gap: 8px; - width: 100%; - box-sizing: border-box; + padding: 12px 16px; } - + nav a { - padding: 8px 16px; + padding: 10px 12px; font-size: 0.85rem; - flex-grow: 0; - text-align: center; } - - article h1 { - font-size: 1.5rem; - padding: 15px 15px 10px; - word-break: break-word; - } - - article .meta { - padding: 0 15px 10px; - } - - article .article-content { - padding: 0 15px 15px; - } - - footer { - flex-direction: column; - gap: 15px; - } -} - -/* Additional breakpoint for very small screens */ -@media (max-width: 480px) { - body { - padding: 0; - } - - .container { - margin: 0; - width: 100%; - border-radius: 0; - } - - nav { - flex-direction: column; - align-items: stretch; - } - - nav a { - width: 100%; - margin: 4px 0; - text-align: center; - box-sizing: border-box; - } - + .pagination { flex-direction: column; - gap: 10px; + gap: 0.5rem; } - + .pagination a { width: 100%; + text-align: center; + padding: 12px; } } diff --git a/themes/ios/style.css b/themes/ios/style.css index ddc0726..f8144b2 100644 --- a/themes/ios/style.css +++ b/themes/ios/style.css @@ -1,6 +1,7 @@ /* * iOS Theme for BSSG * Styled after the modern iOS 17 interface with Dynamic Island elements + * Enhanced with accessibility, performance, and compatibility improvements */ :root { @@ -36,7 +37,7 @@ --font-mono: 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; /* Sizing and spacing */ - --content-width: 700px; + --content-width: 900px; --radius: 14px; --radius-sm: 10px; --radius-lg: 20px; @@ -50,6 +51,38 @@ --spacing-xl: 2rem; } +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + :root { + --transition: 0.01s; + } + + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + .ios-card:hover, + header:hover, + .featured-image:hover, + .featured-image:hover img, + img:hover, + .posts-list article:hover, + .site-title a:hover, + .ios-button:hover, + .tags a:hover, + .pagination a:hover { + transform: none !important; + } + + .dynamic-island { + opacity: 0 !important; + transform: none !important; + } +} + /* Base elements */ html { font-size: 16px; @@ -76,6 +109,14 @@ body { background-color: transparent; } +/* Desktop-specific improvements */ +@media (min-width: 1024px) { + .container { + max-width: 1000px; + padding: 0 var(--spacing-lg); + } +} + /* Card styling for content */ .ios-card { background-color: var(--card-bg); @@ -109,7 +150,7 @@ header:hover { box-shadow: var(--floating-shadow); } -/* iOS style site title with gradient */ +/* iOS style site title with enhanced accessibility */ .site-title { font-family: var(--font-headings); font-weight: 700; @@ -123,34 +164,41 @@ header:hover { .site-title a { text-decoration: none; - background: linear-gradient(135deg, var(--title-gradient-start), var(--title-gradient-end)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--accent-color); display: inline-block; transition: all var(--transition); position: relative; + outline: 2px solid transparent; + outline-offset: 2px; } -.site-title a:hover { - transform: translateY(-1px) scale(1.01); - background: linear-gradient(135deg, var(--title-gradient-end), var(--title-gradient-start)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - -/* Fallback for browsers that don't support background-clip */ -@supports not (background-clip: text) { +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { .site-title a { - color: var(--accent-color); + background: linear-gradient(135deg, var(--title-gradient-start), var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; } .site-title a:hover { - color: var(--teal); + background: linear-gradient(135deg, var(--title-gradient-end), var(--title-gradient-start)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + transform: translateY(-1px) scale(1.01); } } +/* Fallback for browsers without gradient text support */ +.site-title a:hover { + color: var(--teal); +} + +.site-title a:focus { + outline-color: var(--accent-color); +} + header h1 { font-family: var(--font-headings); font-weight: 700; @@ -199,12 +247,19 @@ header:hover .dynamic-island { padding: var(--spacing-xs); margin: 0 auto var(--spacing-sm); transition: color var(--transition); + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: var(--button-radius); } .menu-toggle:hover, .menu-toggle:focus { color: var(--link-active); } +.menu-toggle:focus { + outline-color: var(--accent-color); +} + /* Navigation - iOS style tab bar */ nav { display: flex; @@ -213,12 +268,16 @@ nav { border-radius: var(--radius); margin-bottom: var(--spacing-lg); box-shadow: var(--card-shadow); - justify-content: space-around; + justify-content: center; align-items: center; border: 1px solid var(--border-color); position: relative; z-index: 2; transition: all var(--transition); + width: 100%; + box-sizing: border-box; + flex-wrap: wrap; + gap: 4px; } nav:hover { @@ -232,10 +291,14 @@ nav a { font-size: 0.9rem; transition: all var(--transition); text-align: center; - flex-grow: 1; - padding: var(--spacing-sm) 0; + flex: 0 1 auto; + padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--button-radius); position: relative; + outline: 2px solid transparent; + outline-offset: 2px; + white-space: nowrap; + margin: 0 2px; } nav a::after { @@ -255,6 +318,10 @@ nav a:hover, nav a.active { background-color: rgba(0, 122, 255, 0.1); } +nav a:focus { + outline-color: var(--accent-color); +} + nav a:hover::after, nav a.active::after { width: 30%; } @@ -295,18 +362,24 @@ p { line-height: 1.6; } -/* Links */ +/* Links with enhanced focus states */ a { color: var(--link-color); text-decoration: none; transition: all var(--transition); position: relative; + outline: 2px solid transparent; + outline-offset: 2px; } a:hover, a:active { color: var(--link-active); } +a:focus { + outline-color: var(--link-color); +} + /* iOS-style buttons */ .ios-button { display: inline-block; @@ -322,6 +395,8 @@ a:hover, a:active { cursor: pointer; text-align: center; -webkit-tap-highlight-color: transparent; /* Remove tap highlight on iOS */ + outline: 2px solid transparent; + outline-offset: 2px; } .ios-button:hover, .ios-button:active { @@ -332,6 +407,10 @@ a:hover, a:active { box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3); } +.ios-button:focus { + outline-color: var(--accent-color); +} + .ios-button.secondary { background-color: rgba(0, 122, 255, 0.1); color: var(--accent-color); @@ -342,6 +421,10 @@ a:hover, a:active { box-shadow: 0 4px 12px rgba(0, 122, 255, 0.15); } +.ios-button.secondary:focus { + outline-color: var(--accent-color); +} + .ios-button.destructive { background-color: var(--red); } @@ -351,6 +434,10 @@ a:hover, a:active { box-shadow: 0 4px 12px rgba(255, 59, 48, 0.3); } +.ios-button.destructive:focus { + outline-color: var(--red); +} + /* Article styling */ article { background-color: var(--card-bg); @@ -385,6 +472,14 @@ article .meta { margin-right: var(--spacing-xs); } +/* Text browser fallback for reading time icon */ +@supports not (content: "⏱") { + .reading-time::before { + content: "Time: "; + margin-right: var(--spacing-xs); + } +} + .tags { display: flex; flex-wrap: wrap; @@ -402,12 +497,19 @@ article .meta { text-decoration: none; transition: all var(--transition); border: 1px solid rgba(0, 122, 255, 0.2); + outline: 2px solid transparent; + outline-offset: 2px; } .tags a:hover, .tags a:active { background-color: rgba(0, 122, 255, 0.2); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 122, 255, 0.2); + text-decoration: none; +} + +.tags a:focus { + outline-color: var(--accent-color); } .tags-list { @@ -499,6 +601,13 @@ footer p { footer a { color: var(--accent-color); + outline: 2px solid transparent; + outline-offset: 2px; + transition: outline-color var(--transition); +} + +footer a:focus { + outline-color: var(--accent-color); } /* Date header for post lists */ @@ -690,12 +799,19 @@ hr { align-items: center; justify-content: center; min-width: 100px; + outline: 2px solid transparent; + outline-offset: 2px; } .pagination a:hover, .pagination a:active { background-color: rgba(0, 122, 255, 0.1); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 122, 255, 0.2); + text-decoration: none; +} + +.pagination a:focus { + outline-color: var(--accent-color); } .pagination .page-info { @@ -741,14 +857,20 @@ hr { padding: var(--spacing-sm) var(--spacing-md); color: var(--text-color); font-size: 0.9rem; - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); position: absolute; bottom: 0; left: 0; right: 0; } +/* Optimized backdrop-filter with fallbacks */ +@supports (backdrop-filter: blur(5px)) { + .featured-image .image-caption { + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + } +} + /* Lists with iOS styling */ ul, ol { padding-left: 1.5rem; @@ -787,38 +909,25 @@ ul li, ol li { font-size: 1.1rem; } - /* Mobile navigation */ + /* Mobile navigation - always visible */ .menu-toggle { - display: block; - width: 100%; - background-color: var(--card-bg); - border-radius: var(--radius); - box-shadow: var(--card-shadow); - padding: var(--spacing-sm); - color: var(--accent-color); - font-weight: 600; - margin-bottom: var(--spacing-sm); - border: 1px solid var(--border-color); + display: none; } nav { - max-height: 0; - overflow: hidden; - padding: 0 var(--spacing-sm); flex-direction: column; - transition: max-height var(--transition), padding var(--transition); - margin-bottom: var(--spacing-sm); - } - - nav.open { - max-height: 300px; padding: var(--spacing-sm); + margin-bottom: var(--spacing-lg); + gap: 0; } nav a { - padding: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); width: 100%; - margin: var(--spacing-xs) 0; + margin: 2px 0; + flex: none; + border-radius: var(--button-radius); + text-align: center; } .dynamic-island { diff --git a/themes/italy/style.css b/themes/italy/style.css index 7d71b31..5573ed8 100644 --- a/themes/italy/style.css +++ b/themes/italy/style.css @@ -1,3 +1,35 @@ +/* + * Italy Theme for BSSG + * Authentic Italian design with rich typography and elegant aesthetics + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Italian design aesthetics + */ + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms */ + .site-title a:hover, + nav a:hover, + .tags a:hover, + .pagination a:hover { + transform: none !important; + } +} + :root { /* Authentic Italian color palette */ --bg-color: #fffbf0; /* Venetian plaster */ @@ -27,11 +59,11 @@ --venice-gold: #e6b954; --verona-pink: #e8a598; - /* Typography - distinctly Italian */ - --font-main: 'Cardo', 'Palatino Linotype', 'Book Antiqua', Georgia, serif; - --font-headings: 'Cinzel', 'Trajan Pro', 'Times New Roman', serif; - --font-decorative: 'Cormorant Garamond', 'Garamond', serif; - --font-mono: 'Fira Code', 'Consolas', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Palatino Linotype', 'Book Antiqua', 'Palatino', Georgia, 'Times New Roman', Times, serif; + --font-headings: 'Times New Roman', Times, Georgia, 'Palatino', serif; + --font-decorative: 'Garamond', Georgia, 'Times New Roman', Times, serif; + --font-mono: 'Consolas', 'Liberation Mono', 'Courier New', Courier, monospace; /* Spacing and sizing */ --radius: 8px; @@ -78,8 +110,7 @@ } } -/* Properly import Google Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Cardo:ital,wght@0,400;0,700;1,400&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Fira+Code&display=swap'); +/* REMOVED: External font loading for better performance and text browser compatibility */ *, *::before, *::after { box-sizing: border-box; @@ -185,6 +216,14 @@ header::after { text-shadow: 0 2px 4px rgba(0,0,0,0.1); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + transform: translateY(-2px); + text-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + /* Italian decorative element for site title */ .site-title a::after { content: ""; @@ -293,6 +332,13 @@ nav a:hover { color: var(--link-hover-color); } +/* ACCESSIBILITY: Enhanced focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); +} + nav a:hover::after, nav a.active::after { transform: scaleX(1); transform-origin: left; @@ -369,6 +415,14 @@ a:hover { text-decoration-color: var(--link-hover-color); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover-color); + text-decoration-color: var(--link-hover-color); +} + /* Article styling inspired by Renaissance art */ article { margin-bottom: var(--spacing-xxl); @@ -410,12 +464,20 @@ article .meta { align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏱"; + content: "Time: "; margin-right: var(--spacing-xs); font-style: normal; } +/* Use Italian-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags with Italian color influence */ .tags { display: flex; @@ -443,6 +505,16 @@ article .meta { box-shadow: 0 3px 6px rgba(0,0,0,0.1); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-text); + color: var(--tag-bg); + transform: translateY(-2px); + box-shadow: 0 3px 6px rgba(0,0,0,0.1); +} + /* Featured image with Renaissance framing */ .featured-image { margin: var(--spacing-lg) 0; @@ -497,7 +569,7 @@ blockquote { } blockquote::before { - content: """; + content: "\201C"; font-family: serif; font-size: 3rem; position: absolute; @@ -634,6 +706,16 @@ footer p { box-shadow: 0 3px 6px rgba(0,0,0,0.1); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent-secondary); + color: var(--bg-color); + transform: translateY(-2px); + box-shadow: 0 3px 6px rgba(0,0,0,0.1); +} + .pagination .page-info { color: var(--date-color); font-style: italic; @@ -873,18 +955,7 @@ footer p { } } -/* JavaScript for mobile menu toggle */ -/* -document.addEventListener('DOMContentLoaded', function() { - const menuToggle = document.querySelector('.menu-toggle'); - const nav = document.querySelector('nav'); - - if (menuToggle && nav) { - menuToggle.addEventListener('click', function() { - nav.classList.toggle('open'); - menuToggle.setAttribute('aria-expanded', - nav.classList.contains('open') ? 'true' : 'false'); - }); - } -}); -*/ +/* + * Note: Mobile menu toggle functionality would require JavaScript + * This theme uses CSS-only responsive navigation for better compatibility + */ diff --git a/themes/longform/style.css b/themes/longform/style.css index 3c2d7f8..d6510ab 100644 --- a/themes/longform/style.css +++ b/themes/longform/style.css @@ -2,8 +2,32 @@ * Longform Theme for BSSG * Optimized for reading long articles with highly readable typography, * contained text width, minimal distractions, and reading progress bar + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining longform reading aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable progress bar animation */ + .progress-bar { + transition: none !important; + } +} + :root { /* Color scheme */ --background-color: #ffffff; @@ -20,10 +44,10 @@ --blockquote-bg: #f7fafc; --blockquote-border: #cbd5e1; - /* Typography */ - --font-serif: 'Merriweather', Georgia, 'Times New Roman', serif; - --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; - --font-mono: 'JetBrains Mono', 'Fira Code', 'Menlo', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-serif: Georgia, 'Times New Roman', Times, serif; + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, Arial, sans-serif; + --font-mono: 'Menlo', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* Sizing */ --content-width: 90%; @@ -125,6 +149,13 @@ header { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + color: var(--link-color); +} + /* Site description */ header p { margin: var(--spacing-xs) 0 0; @@ -177,6 +208,17 @@ nav a.active::after { width: 100%; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + color: var(--link-color); +} + +nav a:focus::after { + width: 100%; +} + /* Content area - centered with improved alignment */ main { padding: 0 var(--content-padding) var(--spacing-xl); @@ -256,6 +298,12 @@ a:visited { color: var(--link-visited); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; +} + /* Lists */ ul, ol { margin: var(--spacing-md) 0; @@ -370,11 +418,19 @@ article .article-content { gap: 0.3em; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏱"; + content: "Time: "; font-size: 1.1em; } +/* Use icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱"; + } +} + /* Tags */ .tags { display: flex; @@ -398,6 +454,14 @@ article .article-content { background-color: var(--blockquote-bg); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + color: var(--link-color); + background-color: var(--blockquote-bg); +} + /* Tags list page */ .tags-list { list-style: none; @@ -563,6 +627,15 @@ footer a:hover { border-color: var(--link-color); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 3px solid var(--link-color); + outline-offset: 2px; + background-color: var(--link-color); + color: var(--background-color); + border-color: var(--link-color); +} + /* Utilities */ .text-center { text-align: center; diff --git a/themes/macclassic/style.css b/themes/macclassic/style.css index 59ade03..f89a5d1 100644 --- a/themes/macclassic/style.css +++ b/themes/macclassic/style.css @@ -1,8 +1,34 @@ /* * Mac Classic Theme for BSSG * Styled after the nostalgic MacOS 9 look + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Mac Classic aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms */ + .site-title a:hover, + nav a:hover, + .mac-button:hover { + transform: none !important; + } +} + :root { /* Mac Classic color scheme - improved */ --bg-color: #dedede; @@ -29,27 +55,21 @@ --title-gradient-start: #000000; --title-gradient-end: #333333; - /* Typography */ - --font-main: 'Charcoal', 'Chicago', 'Geneva', 'Helvetica', sans-serif; - --font-headings: 'Charcoal', 'Chicago', 'Geneva', 'Helvetica', sans-serif; - --font-mono: 'Monaco', 'Courier', monospace; + /* Typography - Enhanced font fallbacks for Mac Classic look */ + --font-main: 'Lucida Grande', 'Geneva', 'Verdana', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-headings: 'Lucida Grande', 'Geneva', 'Verdana', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-mono: 'Monaco', 'Menlo', 'Consolas', 'Courier New', 'Liberation Mono', monospace; /* Sizing - increased width for better usage of screen space */ --content-width: 85%; --max-content-width: 960px; } -@font-face { - font-family: 'Chicago'; - src: local('Chicago'), url('https://cdn.jsdelivr.net/gh/dominiklohmann/chicago@v1.0.0/Chicago.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} + /* Base elements */ html { - font-size: 12px; + font-size: 14px; /* Increased from 12px for better readability */ } body { @@ -110,6 +130,23 @@ header::before { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8); } +/* TEXT BROWSER FALLBACK: Simple window controls */ +@supports not (position: absolute) { + header::before { + content: "[X]"; + position: static; + background: none; + border: none; + box-shadow: none; + border-radius: 0; + width: auto; + height: auto; + transform: none; + color: var(--text-color); + font-size: 10px; + } +} + /* Add zoom button */ header::after { content: ""; @@ -125,11 +162,28 @@ header::after { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8); } +/* TEXT BROWSER FALLBACK: Simple zoom control */ +@supports not (position: absolute) { + header::after { + content: "[+]"; + position: static; + background: none; + border: none; + box-shadow: none; + border-radius: 0; + width: auto; + height: auto; + transform: none; + color: var(--text-color); + font-size: 10px; + } +} + /* Site title with Mac Classic style */ .site-title { margin: 0; padding: 0; - font-size: 13px; + font-size: 15px; /* Increased from 13px */ font-weight: bold; text-align: center; flex: 1; @@ -155,10 +209,20 @@ header::after { border-color: #555555; } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background: linear-gradient(to bottom, #999999, #777777); + color: #ffffff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3); + border-color: #555555; +} + header h1 { margin: 0; padding: 0; - font-size: 13px; + font-size: 15px; /* Increased from 13px */ font-weight: bold; text-align: center; color: var(--text-color); @@ -199,9 +263,9 @@ nav::before { nav a { color: var(--text-color); text-decoration: none; - padding: 1px 8px; + padding: 2px 10px; /* Increased padding */ margin: 0 1px; - font-size: 12px; + font-size: 13px; /* Increased from 12px */ font-weight: bold; position: relative; display: inline-block; @@ -216,6 +280,15 @@ nav a:hover { text-shadow: none; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: #000000; + color: #ffffff; + text-shadow: none; +} + nav a.active { background-color: var(--accent-color); color: var(--text-color); @@ -242,23 +315,23 @@ h1, h2, h3, h4, h5, h6 { } h1 { - font-size: 18px; + font-size: 20px; /* Increased from 18px */ } h2 { - font-size: 16px; + font-size: 18px; /* Increased from 16px */ } h3 { - font-size: 14px; + font-size: 16px; /* Increased from 14px */ } h4 { - font-size: 12px; + font-size: 14px; /* Increased from 12px */ } article h1 { - font-size: 18px; + font-size: 20px; /* Increased from 18px */ border-bottom: 1px solid var(--border-color); padding-bottom: 5px; } @@ -286,6 +359,14 @@ a:hover { background-color: var(--highlight-yellow); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + text-decoration: none; + background-color: var(--highlight-yellow); +} + /* Classic Mac button style */ .mac-button { display: inline-block; @@ -313,6 +394,13 @@ a:hover { box-shadow: none; } +/* ACCESSIBILITY: Focus states for Mac buttons */ +.mac-button:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + .mac-button.primary { border-width: 2px; font-weight: bold; @@ -345,6 +433,18 @@ article .meta { font-size: 10px; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use Mac Classic style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags */ .tags { margin-top: 1rem; @@ -366,6 +466,13 @@ article .meta { background-color: var(--accent-color); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: var(--accent-color); +} + .tags-list { margin: 1rem 0; } @@ -620,6 +727,13 @@ hr { background-color: var(--accent-color); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: var(--accent-color); +} + .pagination .current { background-color: var(--selection-bg); color: var(--selection-text); diff --git a/themes/macos9/style.css b/themes/macos9/style.css index e383a98..d012a20 100644 --- a/themes/macos9/style.css +++ b/themes/macos9/style.css @@ -1,8 +1,33 @@ /* * Mac OS 9 Theme for BSSG * Styled after the authentic Mac OS 9.2 look + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Mac OS 9 aesthetics */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms and opacity changes */ + .site-title a:hover::before, + .site-title a:focus::before { + opacity: 0 !important; + } +} + :root { /* Mac OS 9 color scheme */ --bg-color: #3a57a6; /* Mac OS 9 blue background */ @@ -29,26 +54,18 @@ --title-gradient-start: #000000; --title-gradient-end: #666666; - /* Typography */ - --font-main: 'Charcoal', 'Chicago', 'Geneva', 'Helvetica', sans-serif; - --font-headings: 'Charcoal', 'Chicago', 'Geneva', 'Helvetica', sans-serif; - --font-mono: 'Monaco', 'Courier', monospace; + /* Typography - Enhanced font fallbacks for Mac OS 9 look */ + --font-main: 'Lucida Grande', 'Geneva', 'Verdana', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-headings: 'Lucida Grande', 'Geneva', 'Verdana', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-mono: 'Monaco', 'Menlo', 'Consolas', 'Courier New', 'Liberation Mono', monospace; /* Sizing */ --content-width: 680px; } -@font-face { - font-family: 'Chicago'; - src: local('Chicago'), url('https://cdn.jsdelivr.net/gh/dominiklohmann/chicago@v1.0.0/Chicago.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} - /* Base elements */ html { - font-size: 12px; + font-size: 14px; /* Increased from 12px for better readability */ } body { @@ -105,11 +122,27 @@ header::before { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8); } +/* TEXT BROWSER FALLBACK: Simple window control */ +@supports not (position: absolute) { + header::before { + content: "[X]"; + position: static; + background: none; + border: none; + box-shadow: none; + border-radius: 0; + width: auto; + height: auto; + color: var(--text-color); + font-size: 10px; + } +} + /* Site title with Mac OS 9 style */ .site-title { margin: 0; padding: 0; - font-size: 13px; + font-size: 15px; /* Increased from 13px */ font-weight: bold; text-align: center; flex: 1; @@ -147,10 +180,21 @@ header::before { opacity: 1; } +.site-title a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + color: var(--selection-bg); + text-decoration: none; +} + +.site-title a:focus::before { + opacity: 1; +} + header h1 { margin: 0; padding: 0; - font-size: 13px; + font-size: 15px; /* Increased from 13px */ font-weight: bold; text-align: center; color: var(--text-color); @@ -174,9 +218,9 @@ nav { nav a { color: var(--text-color); text-decoration: none; - padding: 2px 10px; + padding: 3px 12px; /* Increased padding */ margin: 0 2px; - font-size: 13px; + font-size: 14px; /* Increased from 13px */ font-weight: normal; border: none; } @@ -193,6 +237,14 @@ nav a.active { border-radius: 0; } +nav a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: #3875d7; + color: #ffffff; + border-radius: 0; +} + /* Content area */ main { padding: 15px; @@ -249,16 +301,23 @@ a:hover { border-bottom: 1px solid currentColor; } +a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + text-decoration: underline; + border-bottom: 1px solid currentColor; +} + /* Mac OS 9 style button */ .mac-button { display: inline-block; - padding: 2px 10px; + padding: 3px 12px; /* Increased padding */ margin: 2px; background: var(--button-face); border: 1px solid #000000; border-radius: 0; font-family: var(--font-main); - font-size: 12px; + font-size: 13px; /* Increased from 12px */ text-align: center; color: var(--text-color); cursor: pointer; @@ -304,6 +363,18 @@ article .meta { border-left: 1px solid #cccccc; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use Mac OS 9 style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .tags { margin: 1em 0; } @@ -325,6 +396,13 @@ article .meta { color: #ffffff; } +.tags a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: #3875d7; + color: #ffffff; +} + .tags-list { list-style: none; padding: 0; @@ -555,6 +633,13 @@ hr { color: #ffffff; } +.pagination a:focus { + outline: 2px solid var(--selection-bg); + outline-offset: 2px; + background-color: #3875d7; + color: #ffffff; +} + .pagination .current { background-color: #3875d7; color: #ffffff; diff --git a/themes/mario/style.css b/themes/mario/style.css index c9a1a9e..e39ecde 100644 --- a/themes/mario/style.css +++ b/themes/mario/style.css @@ -2,15 +2,38 @@ * Super Mario Bros Theme for BSSG * Inspired by the classic Nintendo game with iconic blue sky, green pipes, * brick blocks, question blocks, and classic Mario color palette + * IMPROVED: Better accessibility, performance, and text browser support */ -@font-face { - font-family: 'Press Start 2P'; - src: url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); - font-weight: normal; - font-style: normal; +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects for reduced motion */ + body { + background-image: linear-gradient( + var(--mario-ground) 0%, + var(--mario-ground) 25px, + var(--mario-sky-blue) 25px, + var(--mario-sky-blue) 100% + ) !important; + } + + /* Simplify animations */ + .site-title::before, + .pagination a::before { + animation: none !important; + } } +/* REMOVED: External font loading for better performance and text browser support */ +/* @font-face removed - using system fonts with pixel-style fallbacks */ + :root { /* Mario color palette */ --mario-red: #e52521; @@ -51,10 +74,10 @@ --nav-text: var(--mario-white); --nav-hover: var(--mario-yellow); - /* Typography */ - --font-headings: 'Press Start 2P', monospace; - --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - IMPROVED fallbacks for text browsers */ + --font-headings: 'Courier New', Courier, 'Lucida Console', 'Monaco', monospace; + --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'Courier New', Courier, 'Lucida Console', monospace; /* Sizing */ --content-width: 90%; @@ -68,11 +91,11 @@ --spacing-lg: 2rem; --spacing-xl: 2.5rem; - /* Transitions */ - --transition-fast: 0.2s; + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s; /* Reduced for better performance */ } -/* Base elements */ +/* Base elements - OPTIMIZED background */ html { font-size: 16px; } @@ -80,6 +103,7 @@ html { body { font-family: var(--font-main); background-color: var(--bg-color); + /* OPTIMIZED: Simplified cloud background for better performance */ background-image: /* Ground */ linear-gradient( @@ -88,40 +112,20 @@ body { var(--mario-sky-blue) 25px, var(--mario-sky-blue) 100% ), - /* Clouds */ + /* Simplified clouds - reduced complexity */ radial-gradient( circle at 20% 20%, var(--mario-white) 20px, transparent 20px ), radial-gradient( - circle at 25% 20%, + circle at 70% 15%, var(--mario-white) 25px, transparent 25px - ), - radial-gradient( - circle at 30% 20%, - var(--mario-white) 20px, - transparent 20px - ), - radial-gradient( - circle at 70% 10%, - var(--mario-white) 28px, - transparent 28px - ), - radial-gradient( - circle at 75% 10%, - var(--mario-white) 32px, - transparent 32px - ), - radial-gradient( - circle at 80% 10%, - var(--mario-white) 28px, - transparent 28px ); - background-repeat: repeat-x; - background-size: 100% 100%, 200px 200px, 200px 200px, 200px 200px, 300px 300px, 300px 300px, 300px 300px; - background-position: 0 calc(100% - 40px), 10% 30%, 15% 30%, 20% 30%, 60% 15%, 65% 15%, 70% 15%; + background-repeat: repeat-x, no-repeat, no-repeat; + background-size: 100% 100%, 200px 200px, 250px 250px; + background-position: 0 calc(100% - 40px), 10% 30%, 60% 20%; color: var(--text-color); margin: 0; padding: var(--spacing-lg); @@ -135,6 +139,7 @@ body { padding: 0; background-color: var(--container-bg); border: var(--border-width) solid var(--border-color); + /* OPTIMIZED: Simplified shadow */ box-shadow: var(--pixel-size) var(--pixel-size) 0 var(--mario-brick), calc(var(--pixel-size) * 2) calc(var(--pixel-size) * 2) 0 rgba(0, 0, 0, 0.2); @@ -145,7 +150,7 @@ body { border-top: calc(var(--pixel-size) * 4) solid var(--mario-brick); } -/* Mario pipe-style border on container */ +/* Mario pipe-style border on container - OPTIMIZED */ .container::before, .container::after { content: ""; @@ -170,7 +175,7 @@ body { bottom: 0; } -/* Header with Mario aesthetic */ +/* Header with Mario aesthetic - IMPROVED accessibility */ header { background-color: var(--header-bg); padding: var(--spacing-md) var(--spacing-md) var(--spacing-lg); @@ -179,7 +184,7 @@ header { position: relative; } -/* Question block title effect */ +/* Question block title effect - IMPROVED accessibility */ .site-title { margin: 0; padding: var(--spacing-md); @@ -188,48 +193,64 @@ header { position: relative; background-color: var(--mario-question); border: var(--pixel-size) solid var(--mario-brown); - box-shadow: var(--pixel-size) var(--pixel-size) 0 rgba(0, 0, 0, 0.5); + box-shadow: + inset calc(var(--pixel-size) * -1) calc(var(--pixel-size) * -1) 0 var(--mario-coin), + inset var(--pixel-size) var(--pixel-size) 0 var(--mario-white); font-family: var(--font-headings); + text-transform: uppercase; + letter-spacing: 2px; + border-radius: 4px; + transition: all var(--transition-fast) ease; } +/* OPTIMIZED: Simplified question mark animation */ .site-title::before { content: "?"; position: absolute; top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: var(--mario-white); - font-size: 1.8rem; - opacity: 0.3; - text-shadow: 1px 1px 0 var(--mario-brown); - z-index: 1; + right: calc(var(--pixel-size) * 2); + transform: translateY(-50%); + font-size: 1.2rem; + color: var(--mario-brown); + font-weight: bold; + animation: blink 2s infinite; +} + +/* FALLBACK: Text browser support for question mark */ +@supports not (content: "?") { + .site-title::before { + content: "?"; + } } .site-title a { - color: var(--mario-white); + color: var(--mario-brown); text-decoration: none; - text-shadow: - 1px 1px 0 var(--mario-brown), - -1px -1px 0 var(--mario-brown), - 1px -1px 0 var(--mario-brown), - -1px 1px 0 var(--mario-brown); - position: relative; - z-index: 2; + font-weight: bold; + display: block; + padding-right: calc(var(--pixel-size) * 6); + transition: all var(--transition-fast) ease; } .site-title a:hover { - color: var(--mario-yellow); - animation: jump 0.5s ease-in-out; + color: var(--mario-red); + transform: translateY(-2px); +} + +.site-title a:focus { + outline: 2px solid var(--mario-white); + outline-offset: 2px; + color: var(--mario-red); + transform: translateY(-2px); } /* Site description */ header p { - margin: var(--spacing-sm) auto 0; + margin: var(--spacing-md) 0 0; color: var(--mario-white); - font-size: 1rem; - max-width: 550px; - text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); - line-height: 1.7; + font-size: 0.9rem; + font-weight: bold; + text-shadow: 1px 1px 0 var(--mario-brick); } /* Pipe-style Navigation */ @@ -269,6 +290,17 @@ nav a.active { inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); } +nav a:focus { + outline: 3px solid var(--mario-yellow); + outline-offset: 2px; + color: var(--nav-hover); + background-color: var(--mario-green); + transform: translateY(var(--pixel-size)); + box-shadow: + 0 0 0 rgba(0, 0, 0, 0.5), + inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); +} + nav a.active::before { content: "►"; position: absolute; @@ -338,6 +370,13 @@ a:hover { border-bottom: 2px solid var(--link-hover); } +a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + color: var(--link-hover); + border-bottom: 2px solid var(--link-hover); +} + /* Coin icon for links only in content areas */ main p a::before { content: "•"; @@ -422,6 +461,14 @@ article .meta { border-bottom: none; } +.tags a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + background-color: var(--mario-red); + transform: translateY(-2px); + border-bottom: none; +} + .tags a::before { display: none; } @@ -458,6 +505,15 @@ article .meta { border-bottom: none; } +.tags-list a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + background-color: var(--mario-green); + color: var(--mario-yellow); + transform: scale(1.05); + border-bottom: none; +} + /* Images */ .featured-image, figure { @@ -533,7 +589,7 @@ blockquote { } blockquote::before { - content: """; + content: "\201C"; position: absolute; top: -0.5rem; left: 0.5rem; @@ -605,6 +661,13 @@ footer a:hover { border-bottom: 1px solid var(--mario-white); } +footer a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + color: var(--mario-white); + border-bottom: 1px solid var(--mario-white); +} + /* Pagination */ .pagination { display: flex; @@ -630,6 +693,15 @@ footer a:hover { border-bottom: var(--pixel-size) solid var(--mario-green); } +.pagination a:focus { + outline: 2px solid var(--mario-yellow); + outline-offset: 2px; + background-color: var(--mario-green); + color: var(--mario-yellow); + transform: translateY(-2px); + border-bottom: var(--pixel-size) solid var(--mario-green); +} + .pagination a::before { display: none; } @@ -653,7 +725,7 @@ footer a:hover { } } -/* Responsive adjustments */ +/* IMPROVED: Responsive adjustments with mobile optimizations */ @media (max-width: 768px) { html { font-size: 15px; @@ -661,12 +733,21 @@ footer a:hover { body { padding: var(--spacing-md); + /* OPTIMIZED: Simplified background on mobile */ + background-image: linear-gradient( + var(--mario-ground) 0%, + var(--mario-ground) 25px, + var(--mario-sky-blue) 25px, + var(--mario-sky-blue) 100% + ); } .container { width: 100%; margin-left: 0; margin-right: 0; + /* OPTIMIZED: Simplified shadows on mobile */ + box-shadow: var(--pixel-size) var(--pixel-size) 0 var(--mario-brick); } main { @@ -681,22 +762,35 @@ footer a:hover { nav a { width: 80%; text-align: center; + /* OPTIMIZED: Simplified transitions on mobile */ + transition: background-color var(--transition-fast) ease; } .site-title { font-size: 1.4rem; } + + /* OPTIMIZED: Disable animations on mobile */ + .site-title::before { + animation: none; + } } @media (max-width: 480px) { body { padding: var(--spacing-xs); + /* OPTIMIZED: Remove all background images on small mobile */ + background-image: none; + background-color: var(--mario-sky-blue); } .container { width: 100%; border-radius: 0; box-shadow: none; + /* OPTIMIZED: Simplified borders on small mobile */ + border-left: none; + border-right: none; } main { @@ -714,6 +808,13 @@ footer a:hover { .site-title { font-size: 1.2rem; padding: var(--spacing-sm); + /* OPTIMIZED: Simplified styling on small mobile */ + letter-spacing: 1px; + } + + .site-title::before { + /* OPTIMIZED: Remove question mark on very small screens */ + display: none; } p { diff --git a/themes/material/style.css b/themes/material/style.css index ca9fa4c..2a97731 100644 --- a/themes/material/style.css +++ b/themes/material/style.css @@ -1,6 +1,7 @@ /* * Android Material Design Theme for BSSG * Modern, clean Material Design styling inspired by Google's design language + * Enhanced with accessibility, performance, and compatibility improvements */ :root { @@ -28,10 +29,10 @@ --dp16: rgba(0, 0, 0, 0.15); --dp24: rgba(0, 0, 0, 0.16); - /* Typography */ - --font-main: 'Roboto', 'Segoe UI', 'Arial', sans-serif; - --font-headings: 'Roboto', 'Segoe UI', 'Arial', sans-serif; - --font-mono: 'Roboto Mono', monospace; + /* Typography - Enhanced with better fallbacks */ + --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + --font-headings: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; /* Sizing */ --content-width: 1000px; @@ -49,6 +50,26 @@ --radius: var(--border-radius); } +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + :root { + --transition: 0.01s; + } + + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + .featured-image img, + .material-fab, + article { + transform: none !important; + } +} + /* Material Design pattern for body */ body { font-family: var(--font-main); @@ -71,132 +92,169 @@ body { overflow: hidden; } -/* App bar with primary color */ +/* Clean Material Design header */ header { background-color: var(--primary-color); color: var(--on-primary); - padding: 16px; + padding: 20px 24px; position: relative; - font-size: 1.25rem; - display: flex; - align-items: center; - justify-content: space-between; - box-shadow: 0 4px 6px var(--dp4); + box-shadow: 0 2px 4px var(--dp2), 0 4px 8px var(--dp4); z-index: 2; } -/* Site title with Material-style gradient */ +/* Header content wrapper */ +.header-content { + max-width: var(--content-width); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; +} + +/* Site title - clean and simple */ .site-title { margin: 0; padding: 0; - font-size: 1.5rem; - font-weight: 500; + font-size: 1.75rem; + font-weight: 600; font-family: var(--font-headings); - background: linear-gradient(120deg, var(--on-primary) 0%, rgba(255, 255, 255, 0.8) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + color: var(--on-primary); + letter-spacing: -0.02em; } .site-title a { text-decoration: none; - background: linear-gradient(120deg, var(--on-primary) 0%, rgba(255, 255, 255, 0.8) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + color: var(--on-primary); transition: all var(--transition); + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 4px; + padding: 4px 8px; + margin: -4px -8px; } .site-title a:hover { text-decoration: none; - background: linear-gradient(120deg, rgba(255, 255, 255, 0.9) 0%, var(--secondary-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: rgba(255, 255, 255, 0.9); transform: translateY(-1px); } -/* Menu icons for Material Design */ -.menu-controls { - display: flex; - gap: 16px; +.site-title a:focus { + outline-color: rgba(255, 255, 255, 0.5); } -.material-icon { - width: 24px; - height: 24px; +/* Site description */ +.site-description { + margin: 4px 0 0 0; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.8); + font-weight: 400; + line-height: 1.3; +} + +/* Header actions - simplified */ +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.header-button { + background: none; + border: none; + color: var(--on-primary); + padding: 8px; + border-radius: 50%; + cursor: pointer; + transition: background-color 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; + width: 40px; + height: 40px; display: flex; align-items: center; justify-content: center; - color: var(--on-primary); - cursor: pointer; - border-radius: 50%; - transition: background-color 0.2s; } -.material-icon:hover { +.header-button:hover { background-color: rgba(255, 255, 255, 0.1); } -.material-menu::before { +.header-button:focus { + outline-color: rgba(255, 255, 255, 0.5); +} + +.header-button::before { + font-size: 18px; +} + +.header-menu::before { content: "☰"; - font-size: 18px; } -.material-more::before { - content: "⋮"; - font-size: 18px; +.header-search::before { + content: "🔍"; } -header h1 { - margin: 0; - padding: 0; - font-size: 1.25rem; - font-weight: 500; - font-family: var(--font-headings); - color: var(--on-primary); - background: linear-gradient(120deg, var(--on-primary) 0%, rgba(255, 255, 255, 0.8) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +/* Text browser fallbacks */ +@supports not (content: "☰") { + .header-menu::before { + content: "Menu"; + font-size: 10px; + } + + .header-search::before { + content: "Search"; + font-size: 10px; + } } /* Navigation in Material UI style */ nav { display: flex; - flex-wrap: nowrap; - background-color: var(--primary-color); + flex-wrap: wrap; + background-color: var(--surface); padding: 0; - overflow-x: auto; + overflow: visible; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + border-bottom: 1px solid var(--dp1); + justify-content: center; } nav a { - color: var(--on-primary); + color: var(--primary-color); text-decoration: none; - padding: 15px 20px; + padding: 15px 16px; margin: 0; font-weight: 500; - font-size: 0.95rem; + font-size: 0.9rem; position: relative; display: inline-block; text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; transition: all 0.3s; + flex: 1; + text-align: center; + min-width: 0; + /* Enhanced focus outline for accessibility */ + outline: 2px solid transparent; + outline-offset: -2px; } nav a:hover, nav a:focus { - color: var(--on-primary); - background-color: rgba(255, 255, 255, 0.1); + color: var(--primary-variant); + background-color: rgba(98, 0, 238, 0.08); +} + +nav a:focus { + outline-color: var(--primary-color); } /* Active tab with indicator */ nav a.active { - color: var(--on-primary); + color: var(--primary-variant); + background-color: rgba(98, 0, 238, 0.12); } nav a.active::after { @@ -206,7 +264,7 @@ nav a.active::after { left: 0; width: 100%; height: 2px; - background-color: var(--on-primary); + background-color: var(--primary-color); } /* Media query for responsive design */ @@ -220,12 +278,17 @@ nav a.active::after { nav { flex-direction: row; flex-wrap: wrap; + justify-content: center; } nav a { - flex-grow: 1; + flex: 1; text-align: center; - padding: 12px 10px; + padding: 12px 8px; + font-size: 0.8rem; + min-width: 0; + white-space: normal; + word-break: break-word; } main { @@ -329,11 +392,26 @@ nav a.active::after { } header { - padding: 12px; + padding: 16px 20px; + } + + .header-content { + flex-direction: column; + align-items: flex-start; + gap: 8px; } .site-title { - font-size: 1.25rem; + font-size: 1.5rem; + } + + .site-description { + font-size: 0.85rem; + } + + .header-actions { + align-self: flex-end; + margin-top: -32px; } main { @@ -412,6 +490,8 @@ nav a.active::after { nav a { padding: 10px 6px; font-size: 0.75rem; + white-space: normal; + word-break: break-word; } h1 { @@ -467,7 +547,7 @@ h1, h2, h3, h4, h5, h6 { line-height: 1.2; } -/* Decorative elements for headings */ +/* Decorative elements for headings with text browser fallbacks */ h1::before, h2::before, h3::before, h4::before { content: "#"; display: inline-block; @@ -480,6 +560,14 @@ h1::before, h2::before, h3::before, h4::before { opacity: 0; } +/* Text browser fallback for heading decorations */ +@supports not (transform: translateX(-5px)) { + h1::before, h2::before, h3::before, h4::before { + content: ""; + display: none; + } +} + h1:hover::before, h2:hover::before, h3:hover::before, h4:hover::before { transform: translateX(0); opacity: 0.6; @@ -508,10 +596,13 @@ p { line-height: 1.6; } -/* Links */ +/* Links with enhanced focus states */ a { color: var(--primary-color); text-decoration: none; + outline: 2px solid transparent; + outline-offset: 2px; + transition: outline-color var(--transition); } a:visited { @@ -522,6 +613,10 @@ a:hover { text-decoration: underline; } +a:focus { + outline-color: var(--primary-color); +} + /* Material Design buttons */ .material-button { display: inline-block; @@ -537,11 +632,19 @@ a:hover { cursor: pointer; transition: box-shadow 0.2s, background-color 0.2s; box-shadow: 0 2px 4px var(--dp2); + border: none; + outline: 2px solid transparent; + outline-offset: 2px; } .material-button:hover { box-shadow: 0 4px 8px var(--dp4); background-color: var(--primary-variant); + text-decoration: none; +} + +.material-button:focus { + outline-color: var(--primary-color); } .material-button.secondary { @@ -553,7 +656,11 @@ a:hover { background-color: var(--secondary-variant); } -/* Floating action button */ +.material-button.secondary:focus { + outline-color: var(--secondary-color); +} + +/* Floating action button with text browser fallback */ .material-fab { position: fixed; bottom: 24px; @@ -571,17 +678,32 @@ a:hover { z-index: 10; transition: box-shadow 0.2s, transform 0.2s; cursor: pointer; + border: none; + outline: 2px solid transparent; + outline-offset: 2px; } .material-fab::before { content: "+"; } +/* Text browser fallback for FAB */ +@supports not (content: "+") { + .material-fab::before { + content: "Add"; + font-size: 12px; + } +} + .material-fab:hover { box-shadow: 0 8px 12px var(--dp8), 0 3px 16px var(--dp4); transform: translateY(-2px); } +.material-fab:focus { + outline-color: var(--secondary-color); +} + /* Articles in Material Design style */ article { margin-bottom: 24px; @@ -617,6 +739,14 @@ article .meta { margin-right: 4px; } +/* Text browser fallback for reading time icon */ +@supports not (content: "⏱️") { + .reading-time::before { + content: "Time: "; + margin-right: 4px; + } +} + /* Featured Images in Material Design style */ .featured-image { margin: 16px 0; @@ -696,6 +826,8 @@ article .meta { border-radius: 16px; text-decoration: none; transition: background-color 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; } .tags a:hover { @@ -703,6 +835,10 @@ article .meta { text-decoration: none; } +.tags a:focus { + outline-color: var(--primary-color); +} + code { font-family: var(--font-mono); background-color: rgba(0, 0, 0, 0.05); @@ -758,12 +894,19 @@ footer { text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500; + outline: 2px solid transparent; + outline-offset: 2px; + transition: color var(--transition), outline-color var(--transition); } .bottom-nav-item:hover { color: var(--primary-color); } +.bottom-nav-item:focus { + outline-color: var(--primary-color); +} + /* Pagination in Material Design style */ .pagination { display: flex; @@ -780,6 +923,8 @@ footer { text-decoration: none; border-radius: 4px; transition: background-color 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; } .pagination a:hover { @@ -787,6 +932,10 @@ footer { text-decoration: none; } +.pagination a:focus { + outline-color: var(--primary-color); +} + .pagination .page-info { color: rgba(0, 0, 0, 0.6); font-size: 0.875rem; @@ -803,12 +952,18 @@ footer { margin: 4px; font-size: 0.875rem; transition: background-color 0.2s; + outline: 2px solid transparent; + outline-offset: 2px; } .tags-list .tag:hover { background-color: rgba(98, 0, 238, 0.12); } +.tags-list .tag:focus { + outline-color: var(--primary-color); +} + .tag-count { width: 20px; height: 20px; @@ -817,6 +972,9 @@ footer { font-size: 0.75rem; border-radius: 50%; margin-left: 8px; + display: flex; + align-items: center; + justify-content: center; } /* For posts lists in archives */ diff --git a/themes/mid-century/style.css b/themes/mid-century/style.css index 60d94ec..8e20f24 100644 --- a/themes/mid-century/style.css +++ b/themes/mid-century/style.css @@ -2,8 +2,31 @@ * Mid-Century Modern Theme for BSSG * Inspired by 1950s-60s design aesthetic with clean lines, * organic shapes, and distinctive color palette + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic mid-century modern aesthetics */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable hover transforms and skew effects */ + .site-title::before { + transform: none !important; + } +} + :root { /* Mid-century color scheme */ --teak: #B27C4C; @@ -34,10 +57,10 @@ --footer-text: var(--cream); --border-color: var(--teak); - /* Typography */ - --font-main: 'Futura', 'Trebuchet MS', Arial, sans-serif; - --font-headings: 'Futura', 'Century Gothic', 'Gill Sans', sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for mid-century modern look */ + --font-main: 'Avenir Next', 'Avenir', 'Trebuchet MS', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-headings: 'Avenir Next', 'Avenir', 'Century Gothic', 'Gill Sans', 'Futura', 'Trebuchet MS', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + --font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', 'Liberation Mono', monospace; /* Sizing */ --content-width: 90%; @@ -113,6 +136,21 @@ header { z-index: -1; } +/* TEXT BROWSER FALLBACK: Simple decoration for site title */ +@supports not (position: absolute) { + .site-title::before { + content: "◆ "; + position: static; + background: none; + color: var(--tertiary-accent); + width: auto; + height: auto; + transform: none; + z-index: auto; + font-size: 1rem; + } +} + .site-title a { color: var(--header-text); text-decoration: none; @@ -122,6 +160,12 @@ header { color: var(--accent-color); } +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); +} + /* Site description */ header p { margin: 0.5rem 0 0; @@ -175,6 +219,16 @@ nav a.active::after { width: 50%; } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--nav-hover-bg); +} + +nav a:focus::after { + width: 50%; +} + /* Content area */ main { padding: var(--content-padding); @@ -210,6 +264,16 @@ h1::after { font-size: 1.5rem; } +/* TEXT BROWSER FALLBACK: Simple star for h1 */ +@supports not (position: absolute) { + h1::after { + content: " *"; + position: static; + color: var(--accent-color); + font-size: 1rem; + } +} + h2 { font-size: 2rem; color: var(--accent-color); @@ -246,6 +310,13 @@ a:hover { border-bottom: 2px solid var(--link-hover); } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + border-bottom: 2px solid var(--link-hover); +} + /* Articles with mid-century styling */ article { margin-bottom: 4rem; @@ -305,6 +376,19 @@ article .meta { transform: translateY(-50%); } +/* TEXT BROWSER FALLBACK: Simple "Time: " prefix for reading time */ +@supports not (position: absolute) { + .reading-time { + padding-left: 0; + } + + .reading-time::before { + content: "Time: "; + position: static; + transform: none; + } +} + /* Tags */ .tags { display: flex; @@ -330,6 +414,14 @@ article .meta { border: none; } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--cream); + border: none; +} + /* Tags list page */ .tags-list { list-style-type: none; @@ -364,6 +456,14 @@ article .meta { border: none; } +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + color: var(--cream); + border: none; +} + /* Footer */ footer { background-color: var(--footer-bg); @@ -412,6 +512,14 @@ footer a:hover { border: none; } +footer a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); + text-decoration: underline; + border: none; +} + /* Pagination */ .pagination { display: flex; @@ -441,6 +549,15 @@ footer a:hover { border: none; } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--accent-color); + transform: translateY(-2px); + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1); + border: none; +} + /* Add improved image handling */ img { max-width: 100%; diff --git a/themes/minimal/style.css b/themes/minimal/style.css index a942747..31510c7 100644 --- a/themes/minimal/style.css +++ b/themes/minimal/style.css @@ -1,8 +1,26 @@ /* * Minimal Theme for BSSG * A simple brutalist design + * Enhanced with accessibility and compatibility improvements */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + .featured-image:hover img, + .site-title a:hover, + .pagination a:hover { + transform: none !important; + } +} + :root { /* Minimal palette */ --bg-color: #ffffff; @@ -91,6 +109,12 @@ a:hover { text-decoration: none; } +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-decoration: none; +} + /* Header */ header { margin-bottom: 3rem; @@ -98,44 +122,83 @@ header { padding-bottom: 1rem; } -/* Site title with subtle gradient */ +/* Site title with enhanced accessibility */ .site-title { font-weight: 700; color: var(--header-color); margin: 0; font-size: 2rem; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } } .site-title a { text-decoration: none; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: var(--header-color); transition: all var(--transition); } +/* Progressive enhancement for gradient text on links */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + + .site-title a:hover { + background: linear-gradient(120deg, var(--link-color) 0%, var(--link-visited) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); + } + + .site-title a:focus { + background: linear-gradient(120deg, var(--link-color) 0%, var(--link-visited) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); + } +} + +/* Fallback for browsers without gradient text support */ .site-title a:hover { + color: var(--link-color); +} + +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; text-decoration: none; - background: linear-gradient(120deg, var(--link-color) 0%, var(--link-visited) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - transform: translateY(-1px); + color: var(--link-color); } header h1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0.5rem; - background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: var(--header-color); +} + +/* Progressive enhancement for header h1 gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + header h1 { + background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } } header p { @@ -173,6 +236,15 @@ nav a:hover::after { width: 100%; } +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +nav a:focus::after { + width: 100%; +} + /* Article */ article { margin-bottom: 3.5rem; @@ -212,6 +284,15 @@ article .meta { font-size: 0.9rem; } +/* Text browser fallback for reading time icon */ +@supports not (content: "⏱️") { + .reading-time::before { + content: "Time: "; + margin-right: 0.3rem; + font-size: 0.9rem; + } +} + /* Featured Images */ .featured-image { margin: 1.5rem 0; @@ -324,6 +405,12 @@ pre code { background-color: var(--border-color); } +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--border-color); +} + .tags-list { margin: 1.25rem 0; } @@ -353,6 +440,16 @@ footer { text-align: center; } +footer a { + outline: 2px solid transparent; + outline-offset: 2px; + transition: outline-color var(--transition); +} + +footer a:focus { + outline-color: var(--link-color); +} + /* Date headers for indexes */ .date-header { color: var(--date-color); @@ -375,6 +472,16 @@ footer { margin-bottom: 0.5rem; } +.posts-list h3 a { + outline: 2px solid transparent; + outline-offset: 2px; + transition: outline-color var(--transition); +} + +.posts-list h3 a:focus { + outline-color: var(--link-color); +} + .summary { margin-top: 0.75rem; } @@ -446,6 +553,13 @@ li { transform: translateY(-1px); } +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--border-color); + transform: translateY(-1px); +} + .pagination .page-info { color: var(--date-color); font-size: 0.95rem; diff --git a/themes/mondrian/style.css b/themes/mondrian/style.css index fbddb1f..03a2aa4 100644 --- a/themes/mondrian/style.css +++ b/themes/mondrian/style.css @@ -2,8 +2,26 @@ * Mondrian Theme for BSSG * Inspired by Piet Mondrian's De Stijl artwork with * geometric patterns, primary colors, and black grid lines + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Mondrian geometric aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Mondrian color scheme */ --mondrian-red: #D40920; @@ -32,10 +50,10 @@ --footer-text: var(--mondrian-white); --border-color: var(--mondrian-black); - /* Typography */ - --font-main: 'Helvetica', 'Arial', sans-serif; - --font-headings: 'Helvetica', 'Arial', sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for Mondrian geometric look */ + --font-main: 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', sans-serif; + --font-headings: 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', sans-serif; + --font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', 'Liberation Mono', monospace; /* Sizing */ --content-width: 85%; @@ -94,6 +112,23 @@ header::after { z-index: 0; } +/* TEXT BROWSER FALLBACK: Simple decoration for header */ +@supports not (position: absolute) { + header::after { + content: "■"; + position: static; + background: none; + border: none; + color: var(--mondrian-red); + width: auto; + height: auto; + z-index: auto; + font-size: 1.5rem; + display: inline-block; + margin-left: 1rem; + } +} + /* Site title */ .site-title { font-family: var(--font-headings); @@ -118,6 +153,13 @@ header::after { color: var(--accent-color); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); +} + /* Site description */ header p { margin: 0.75rem auto 0; @@ -172,6 +214,14 @@ nav a.active { color: var(--nav-hover-text); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--nav-hover-bg); + color: var(--nav-hover-text); +} + /* Content area with Mondrian-inspired layout */ main { padding: var(--content-padding); @@ -215,6 +265,19 @@ h2::after { background-color: var(--mondrian-yellow); } +/* TEXT BROWSER FALLBACK: Simple decoration for h2 */ +@supports not (position: absolute) { + h2::after { + content: " ▬"; + position: static; + background: none; + color: var(--mondrian-yellow); + width: auto; + height: auto; + font-size: 1rem; + } +} + h3 { font-size: 1.6rem; margin-top: 2rem; @@ -241,6 +304,14 @@ a:hover { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + article { margin-bottom: 4rem; position: relative; @@ -272,6 +343,18 @@ article .meta { white-space: nowrap; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use Mondrian style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .tags { display: flex; flex-wrap: wrap; @@ -299,6 +382,15 @@ article .meta { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--mondrian-blue); + color: var(--mondrian-white); + transform: translateY(-2px); +} + .tags-list { list-style: none; padding: 0; @@ -333,6 +425,16 @@ article .meta { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--mondrian-blue); + color: var(--mondrian-white); + border-color: var(--mondrian-blue); + transform: translateY(-2px); +} + footer { background-color: var(--footer-bg); color: var(--footer-text); @@ -385,6 +487,14 @@ footer a:hover { text-decoration: none; } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--mondrian-yellow); + outline-offset: 2px; + color: var(--mondrian-yellow); + text-decoration: none; +} + .pagination { display: flex; justify-content: center; @@ -411,6 +521,16 @@ footer a:hover { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--mondrian-blue); + color: var(--mondrian-white); + border-color: var(--mondrian-blue); + transform: translateY(-2px); +} + /* Featured images - Mondrian style */ .featured-image, .index-image, diff --git a/themes/msdos/style.css b/themes/msdos/style.css index 445d9ae..df4e3be 100644 --- a/themes/msdos/style.css +++ b/themes/msdos/style.css @@ -1,8 +1,33 @@ /* * MS-DOS Theme for BSSG * Classic blue command line interface reminiscent of MS-DOS + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic MS-DOS aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable DOS-specific animations */ + .site-title a, + .dos-loading, + .dos-loading::after { + animation: none !important; + } +} + :root { /* MS-DOS classic colors */ --bg-color: #000088; @@ -20,10 +45,10 @@ --title-gradient-start: #ffffff; --title-gradient-end: #aaaaaa; - /* Typography */ - --font-main: 'Courier New', monospace; - --font-headings: 'Courier New', monospace; - --font-mono: 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for MS-DOS terminal look */ + --font-main: 'Consolas', 'Monaco', 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; + --font-headings: 'Consolas', 'Monaco', 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; + --font-mono: 'Consolas', 'Monaco', 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace; /* Sizing */ --content-width: 820px; @@ -98,6 +123,13 @@ header { animation: none; } +/* TEXT BROWSER FALLBACK: Simple prompt for site title */ +@supports not (animation: none) { + .site-title a::before { + content: "> "; + } +} + .site-title a:hover { animation: none; background: linear-gradient(to right, @@ -108,6 +140,19 @@ header { color: transparent; } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + animation: none; + background: linear-gradient(to right, + var(--title-gradient-start), + var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + .site-title a:hover::after { content: "█"; animation: blink 1s step-end infinite; @@ -178,6 +223,14 @@ nav a:hover, nav a:focus { color: var(--menu-highlight-text); } +/* ACCESSIBILITY: Enhanced focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--menu-highlight-bg); + color: var(--menu-highlight-text); +} + /* Selected menu item */ nav a.active { background-color: var(--menu-highlight-bg); @@ -242,6 +295,14 @@ p:after { color: var(--text-color); } +/* TEXT BROWSER FALLBACK: Simple cursor for paragraphs */ +@supports not (animation: blink) { + p:after { + content: "_"; + animation: none; + } +} + @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } @@ -262,6 +323,13 @@ a:hover { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-decoration: underline; +} + /* Articles with command prompt style */ article { margin-bottom: calc(var(--spacing-unit) * 3); @@ -276,6 +344,13 @@ article::before { margin-right: calc(var(--spacing-unit)); } +/* TEXT BROWSER FALLBACK: Simple prompt for articles */ +@supports not (position: relative) { + article::before { + content: "* "; + } +} + article:last-child { margin-bottom: calc(var(--spacing-unit)); border-bottom: none; @@ -323,6 +398,15 @@ article .meta { text-decoration: none; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--menu-highlight-bg); + color: var(--menu-highlight-text); + text-decoration: none; +} + .tags a::before { content: "["; } @@ -417,6 +501,15 @@ footer::before { text-decoration: none; } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--menu-highlight-bg); + color: var(--menu-highlight-text); + text-decoration: none; +} + .pagination .page-info { margin: 0 calc(var(--spacing-unit) * 1.25); font-size: 0.95rem; diff --git a/themes/nes/style.css b/themes/nes/style.css index a7c3540..0040caf 100644 --- a/themes/nes/style.css +++ b/themes/nes/style.css @@ -2,13 +2,31 @@ * NES Theme for BSSG * Retro theme inspired by Nintendo Entertainment System, * using the NES color palette and pixel art aesthetics + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic NES aesthetics */ -@font-face { - font-family: 'Press Start 2P'; - src: url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); - font-weight: normal; - font-style: normal; +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable NES-specific animations */ + .site-title a:hover, + .site-title a:focus { + animation: none !important; + } } :root { @@ -48,10 +66,10 @@ --footer-text: var(--nes-light-gray); --code-bg: var(--nes-dark-gray); - /* Typography */ - --font-main: 'Press Start 2P', monospace; - --font-headings: 'Press Start 2P', monospace; - --font-mono: 'Press Start 2P', monospace; + /* Typography - Enhanced font fallbacks for NES pixelated look */ + --font-main: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + --font-headings: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + --font-mono: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; /* Sizing */ --content-width: 88%; @@ -168,6 +186,13 @@ header { animation: blink 0.5s step-end infinite alternate; } +.site-title a:focus { + outline: 3px solid var(--nes-light-yellow); + outline-offset: 3px; + color: var(--nes-light-yellow); + animation: blink 0.5s step-end infinite alternate; +} + /* Site description */ header p { margin: var(--spacing-sm) auto 0; @@ -214,6 +239,17 @@ nav a.active { inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); } +nav a:focus { + outline: 3px solid var(--nes-light-yellow); + outline-offset: 2px; + color: var(--nes-light-yellow); + background-color: var(--nes-blue); + transform: translateY(var(--pixel-size)); + box-shadow: + 0 0 0 rgba(0, 0, 0, 0.5), + inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); +} + nav a.active::before { content: "►"; position: absolute; @@ -284,6 +320,13 @@ a:hover { animation: blink 0.5s step-end infinite alternate; } +a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + color: var(--link-hover); + animation: blink 0.5s step-end infinite alternate; +} + a::before { content: ""; position: absolute; @@ -359,9 +402,17 @@ article .meta { gap: 0.5em; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "⏱"; - font-size: 1.2em; + content: "Time: "; +} + +/* Use NES-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱"; + font-size: 1.2em; + } } /* Tags */ @@ -389,6 +440,15 @@ article .meta { transform: translateY(2px); } +.tags a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + background-color: var(--nes-green); + color: var(--nes-black); + animation: none; + transform: translateY(2px); +} + .tags a:hover::before { content: ""; display: none; @@ -430,6 +490,17 @@ article .meta { inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); } +.tags-list a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + background-color: var(--nes-green); + color: var(--nes-black); + transform: translateY(var(--pixel-size)); + box-shadow: + 0 0 0 rgba(0, 0, 0, 0.5), + inset var(--pixel-size) var(--pixel-size) 0 rgba(255, 255, 255, 0.1); +} + /* Featured images */ .featured-image, .index-image, @@ -508,7 +579,7 @@ blockquote { } blockquote::before { - content: """; + content: "\201C"; position: absolute; top: 0.1em; left: var(--spacing-sm); @@ -551,6 +622,13 @@ blockquote p:last-child { color: var(--nes-light-yellow); } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list h3 a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + color: var(--nes-light-yellow); +} + /* Summary in post lists */ .summary { background-color: rgba(0, 30, 60, 0.3); @@ -601,6 +679,12 @@ footer a:hover { color: var(--nes-light-yellow); } +footer a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + color: var(--nes-light-yellow); +} + /* Pagination */ .pagination { display: flex; @@ -631,6 +715,16 @@ footer a:hover { animation: none; } +.pagination a:focus { + outline: 2px solid var(--nes-light-yellow); + outline-offset: 2px; + background-color: var(--nes-blue); + color: var(--nes-light-yellow); + transform: translateY(var(--pixel-size)); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.5); + animation: none; +} + .pagination a:hover::before { content: ""; display: none; diff --git a/themes/newspaper/style.css b/themes/newspaper/style.css index e35b90e..aa2fec4 100644 --- a/themes/newspaper/style.css +++ b/themes/newspaper/style.css @@ -1,8 +1,28 @@ /* * Newspaper Theme for BSSG * A traditional newspaper-inspired design + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Fixed gradient text accessibility with progressive enhancement + * - Optimized performance while maintaining authentic newspaper aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Improved newspaper palette */ --bg-color: #f7f4ed; @@ -22,12 +42,12 @@ --title-gradient-start: #000000; --title-gradient-end: #444444; - /* Typography - Added more authentic newspaper fonts */ - --font-main: 'Libre Baskerville', 'Georgia', 'Times New Roman', serif; - --font-headings: 'Playfair Display', 'Times New Roman', serif; - --font-mono: 'Courier New', monospace; - --font-meta: 'Georgia', 'Times New Roman', serif; - --font-masthead: 'Old English Text MT', 'Times New Roman', serif; + /* Typography - Enhanced font fallbacks for newspaper look */ + --font-main: 'Georgia', 'Times New Roman', 'Times', 'Baskerville', 'Palatino', 'Palatino Linotype', 'Book Antiqua', serif; + --font-headings: 'Times New Roman', 'Times', 'Georgia', 'Baskerville', 'Palatino', 'Palatino Linotype', 'Book Antiqua', serif; + --font-mono: 'Courier New', 'Courier', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; + --font-meta: 'Georgia', 'Times New Roman', 'Times', 'Baskerville', 'Palatino', serif; + --font-masthead: 'Times New Roman', 'Times', 'Georgia', 'Baskerville', 'Palatino', serif; /* Sizing */ --content-width: 920px; @@ -41,8 +61,7 @@ --spacing-xl: 2.5rem; } -/* Import Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&display=swap'); + /* Base elements */ html { @@ -141,6 +160,13 @@ a:hover { color: #000; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: #000; +} + /* Enhanced header/masthead */ header { border-bottom: none; @@ -205,6 +231,13 @@ header::after { text-shadow: 2px 2px 3px rgba(0,0,0,0.1); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-shadow: 2px 2px 3px rgba(0,0,0,0.1); +} + /* Fallback for browsers that don't support background-clip */ @supports not (background-clip: text) { .site-title a { @@ -278,6 +311,13 @@ nav a:hover { border-bottom: 2px solid var(--border-color); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + border-bottom: 2px solid var(--border-color); +} + /* Article */ article { margin-bottom: var(--spacing-xl); @@ -325,6 +365,18 @@ article .meta { margin-left: var(--spacing-sm); } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use newspaper-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Code */ code { font-family: var(--font-mono); @@ -374,6 +426,13 @@ pre code { background-color: #d9d6cd; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: #d9d6cd; +} + .tags-list { margin: var(--spacing-md) 0; text-align: center; @@ -489,6 +548,13 @@ footer::after { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list h3 a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-color); +} + .summary { margin-top: var(--spacing-xs); font-style: italic; @@ -544,6 +610,13 @@ li { background-color: var(--tag-bg); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-bg); +} + .pagination .page-info { display: inline-block; padding: var(--spacing-xs) var(--spacing-md); diff --git a/themes/nextstep/style.css b/themes/nextstep/style.css index e1ddf00..830bc67 100644 --- a/themes/nextstep/style.css +++ b/themes/nextstep/style.css @@ -1,8 +1,27 @@ /* * NeXTSTEP Theme for BSSG * Recreating the iconic NeXTSTEP GUI from the late 80s/early 90s - the predecessor to macOS + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Fixed gradient text accessibility with progressive enhancement + * - Optimized performance while maintaining authentic NeXTSTEP aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* NeXTSTEP color scheme */ --bg-color: #bfbfbf; @@ -24,10 +43,10 @@ --title-gradient-start: #ffffff; --title-gradient-end: #cccccc; - /* Typography */ - --font-main: 'Helvetica', 'Arial', sans-serif; - --font-headings: 'Helvetica', 'Arial', sans-serif; - --font-mono: 'Courier', monospace; + /* Typography - Enhanced font fallbacks for NeXTSTEP look */ + --font-main: 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', 'Cantarell', sans-serif; + --font-headings: 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI', 'Roboto', 'Ubuntu', 'Cantarell', sans-serif; + --font-mono: 'Courier New', 'Courier', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; /* Sizing */ --content-width: 820px; @@ -96,8 +115,9 @@ header { cursor: pointer; } +/* TEXT BROWSER FALLBACK: Simple text for window controls */ .next-minimize::after { - content: "_"; + content: "-"; color: black; position: relative; top: -3px; @@ -110,11 +130,22 @@ header { } .next-close::after { - content: "×"; + content: "X"; color: black; font-weight: bold; } +/* Use NeXTSTEP-style symbols when CSS positioning is supported (modern browsers) */ +@supports (position: relative) { + .next-minimize::after { + content: "_"; + } + + .next-close::after { + content: "×"; + } +} + /* NeXTSTEP site title with gradient */ .site-title { margin: 0; @@ -154,6 +185,20 @@ header { } } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--title-gradient-start); + outline-offset: 2px; + text-decoration: none; + background: linear-gradient(to bottom, + var(--title-gradient-start), + var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.9); +} + header h1 { margin: 0; padding: 0; @@ -167,9 +212,8 @@ nav { background-color: var(--window-bg); border-bottom: var(--border-width) solid var(--border-color); display: flex; - flex-wrap: nowrap; + flex-wrap: wrap; padding: 0; - overflow-x: auto; } nav a { @@ -190,10 +234,19 @@ nav a:hover, nav a:focus { color: var(--title-text); } -/* Selected menu item */ +/* ACCESSIBILITY: Enhanced focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Selected menu item - Enhanced contrast for better visibility */ nav a.active { background-color: var(--title-bar); color: var(--title-text); + font-weight: bold; + border-left: 2px solid var(--title-text); + border-right: 2px solid var(--title-text); } /* Content area */ @@ -253,6 +306,13 @@ a:hover { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-decoration: underline; +} + /* NeXTSTEP style button */ .next-button { background-color: var(--button); @@ -272,6 +332,14 @@ a:hover { color: var(--title-text); } +/* ACCESSIBILITY: Focus states for NeXTSTEP buttons */ +.next-button:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--title-bar); + color: var(--title-text); +} + /* Articles with borders */ article { margin-bottom: var(--spacing-xl); @@ -298,6 +366,18 @@ article .meta { font-style: italic; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use NeXTSTEP-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + .tags { display: flex; flex-wrap: wrap; @@ -320,6 +400,15 @@ article .meta { text-decoration: none; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--title-bar); + color: var(--title-text); + text-decoration: none; +} + .tags-list { list-style-type: none; padding: 0; @@ -448,11 +537,28 @@ footer { text-decoration: none; } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--title-bar); + color: var(--title-text); + text-decoration: none; +} + .pagination .page-info { margin: 0 var(--spacing-sm); font-size: 0.9rem; } +/* Media query for medium screens */ +@media (max-width: 1024px) { + nav a { + padding: var(--spacing-xs) var(--spacing-sm); + font-size: 0.85rem; + } +} + /* Media query for responsive design */ @media (max-width: 768px) { .container { @@ -462,16 +568,17 @@ footer { } nav { - flex-direction: row; - flex-wrap: wrap; + flex-direction: column; + flex-wrap: nowrap; } nav a { border-right: none; border-bottom: var(--border-width) solid var(--border-color); - flex: 1 0 auto; - text-align: center; - padding: var(--spacing-sm) var(--spacing-xs); + width: 100%; + text-align: left; + padding: var(--spacing-sm) var(--spacing-sm); + white-space: normal; } footer { diff --git a/themes/nordic-clean/style.css b/themes/nordic-clean/style.css index c677233..c0214ed 100644 --- a/themes/nordic-clean/style.css +++ b/themes/nordic-clean/style.css @@ -2,6 +2,12 @@ * Nordic Clean Theme for BSSG * Inspired by Scandinavian design principles: * Minimalism, airiness, natural colors, and clean typography + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks + * - Optimized performance while maintaining design */ :root { @@ -37,10 +43,10 @@ --footer-text: var(--dark-gray); --border-color: var(--light-gray); - /* Typography */ - --font-main: 'Helvetica Neue', Arial, sans-serif; - --font-headings: 'Helvetica Neue', Arial, sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-headings: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-mono: 'Courier New', 'Courier', monospace; /* Sizing */ --content-width: 820px; @@ -54,6 +60,45 @@ --spacing-xl: 3rem; --spacing-xxl: 4rem; --border-radius: 4px; + + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s; + --transition-base: 0.2s; + --transition-slow: 0.3s; +} + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + nav a::after { + transition: none !important; + } + + .featured-image img, + .index-image img, + .tag-image img, + .archive-image img { + transition: none !important; + } + + .featured-image:hover img, + .index-image:hover img, + .tag-image:hover img, + .archive-image:hover img { + transform: none !important; + } + + .tags-list a:hover, + .pagination a:hover { + transform: none !important; + } } /* Base elements */ @@ -109,13 +154,21 @@ header { .site-title a { color: var(--header-text); text-decoration: none; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .site-title a:hover { color: var(--accent-color); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Site description */ header p { margin: var(--spacing-sm) 0 0; @@ -140,7 +193,9 @@ nav a { font-size: 0.95rem; position: relative; padding-bottom: 2px; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a::after { @@ -153,7 +208,7 @@ nav a::after { background-color: var(--accent-color); transform: scaleX(0); transform-origin: bottom right; - transition: transform 0.3s ease; + transition: transform var(--transition-slow) ease; } nav a:hover { @@ -171,6 +226,12 @@ nav a.active { font-weight: 500; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Content area */ main { min-height: 60vh; @@ -216,8 +277,10 @@ p { a { color: var(--link-color); text-decoration: none; - transition: color 0.2s ease, border-bottom 0.2s ease; + transition: color var(--transition-base) ease, border-bottom var(--transition-base) ease; border-bottom: 1px solid transparent; + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:visited { @@ -229,6 +292,12 @@ a:hover { border-bottom: 1px solid var(--link-hover); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Articles with ample whitespace */ article { margin-bottom: var(--spacing-xl); @@ -254,13 +323,25 @@ article .meta { align-items: center; } -/* Reading time */ +/* Reading time - TEXT BROWSER FALLBACK */ .reading-time { position: relative; display: inline-flex; align-items: center; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags */ .tags { display: flex; @@ -275,7 +356,9 @@ article .meta { font-size: 0.8rem; border-radius: var(--border-radius); border: none; - transition: background-color 0.2s ease, color 0.2s ease; + transition: background-color var(--transition-base) ease, color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags a:hover { @@ -284,6 +367,12 @@ article .meta { border: none; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Tags list page */ .tags-list { list-style-type: none; @@ -307,7 +396,9 @@ article .meta { border-radius: var(--border-radius); text-decoration: none; border: none; - transition: all 0.2s ease; + transition: all var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags-list a:hover { @@ -317,6 +408,12 @@ article .meta { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Footer */ footer { margin-top: var(--spacing-xl); @@ -336,7 +433,9 @@ footer a { color: var(--secondary-accent); text-decoration: none; border: none; - transition: color 0.2s ease, border-bottom 0.2s ease; + transition: color var(--transition-base) ease, border-bottom var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } footer a:hover { @@ -344,6 +443,12 @@ footer a:hover { border-bottom: 1px solid var(--accent-color); } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Pagination */ .pagination { display: flex; @@ -362,7 +467,9 @@ footer a:hover { font-size: 0.9rem; border-radius: var(--border-radius); border: none; - transition: all 0.2s ease; + transition: all var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a:hover { @@ -372,6 +479,12 @@ footer a:hover { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + .pagination .page-info { color: var(--secondary-accent); font-size: 0.9rem; @@ -400,7 +513,7 @@ footer a:hover { height: auto; border-radius: var(--border-radius); margin: 0 auto; - transition: transform 0.3s ease; + transition: transform var(--transition-slow) ease; } .featured-image:hover img, @@ -441,6 +554,12 @@ footer a:hover { margin-bottom: var(--spacing-md); } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Blockquote styling */ blockquote { margin: var(--spacing-lg) 0; diff --git a/themes/osx/style.css b/themes/osx/style.css index 65e23d3..f04d772 100644 --- a/themes/osx/style.css +++ b/themes/osx/style.css @@ -3,6 +3,34 @@ * Styled after macOS Sonoma aesthetics */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable all hover transforms */ + .site-title a:hover, + .site-title a:focus, + .hover-float:hover, + .posts-list article:hover, + .tags a:hover, + .tags a:focus, + .pagination a:hover, + .pagination a:focus, + .mac-button:hover, + .featured-image:hover, + .index-image:hover, + .tag-image:hover, + .archive-image:hover { + transform: none !important; + } +} + :root { /* macOS Sonoma color scheme */ --bg-color: #f5f5f7; @@ -65,14 +93,29 @@ body { margin: var(--spacing-xl) auto; padding: var(--spacing-xl) var(--spacing-lg); background-color: var(--window-bg); - backdrop-filter: blur(var(--frosted-blur)); - -webkit-backdrop-filter: blur(var(--frosted-blur)); box-shadow: var(--card-shadow); border-radius: var(--radius); border: 1px solid var(--border-color); animation: fadeIn 0.3s ease-out; } +/* Progressive enhancement for backdrop-filter */ +@supports (backdrop-filter: blur(10px)) { + .container { + backdrop-filter: blur(10px); /* Reduced from 20px for better performance */ + -webkit-backdrop-filter: blur(10px); + } +} + +/* Disable backdrop-filter on mobile for better performance */ +@media (max-width: 768px) { + .container { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + background-color: var(--window-bg-solid); /* Use solid background on mobile */ + } +} + /* Header styling */ header { margin-bottom: var(--spacing-xl); @@ -115,6 +158,19 @@ header { background-clip: text; } +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 3px; + text-decoration: none; + transform: translateY(-1px); + text-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + background: linear-gradient(110deg, + var(--title-gradient-end), + var(--title-gradient-start)); + -webkit-background-clip: text; + background-clip: text; +} + /* Fallback for browsers that don't support background-clip */ @supports not (background-clip: text) { .site-title a { @@ -180,7 +236,14 @@ nav a:hover { color: var(--accent-color); } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--accent-color); +} + nav a:hover::after, +nav a:focus::after, nav a.active::after { width: 100%; } @@ -247,6 +310,13 @@ a:hover { text-decoration: underline; } +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + /* Buttons */ .mac-button { display: inline-block; @@ -340,6 +410,15 @@ article .meta { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--border-color); + transform: translateY(-2px); + text-decoration: none; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); +} + .tags-list { display: flex; flex-wrap: wrap; @@ -583,6 +662,14 @@ hr { transform: translateY(-2px); } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background-color: var(--code-bg); + border-color: var(--accent-color); + transform: translateY(-2px); +} + .pagination .page-info { color: var(--text-secondary); font-size: 0.9rem; diff --git a/themes/reader-mode/style.css b/themes/reader-mode/style.css index b807367..4415143 100644 --- a/themes/reader-mode/style.css +++ b/themes/reader-mode/style.css @@ -3,6 +3,12 @@ * Simulates browser reader mode with emphasis on text, * white/sepia background, very readable serif font, * and almost no graphic elements + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks + * - Optimized performance while maintaining reading focus */ :root { @@ -18,10 +24,10 @@ --blockquote-bg: #f4ece0; --code-bg: #f4ece0; - /* Typography */ + /* Typography - ENHANCED fallbacks for text browsers */ --font-serif: 'Georgia', 'Times New Roman', serif; --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; - --font-mono: 'Consolas', 'Menlo', monospace; + --font-mono: 'Consolas', 'Menlo', 'Courier New', 'Courier', monospace; /* Sizing for optimal reading */ --content-width: 90%; @@ -34,6 +40,25 @@ --spacing-xxl: 4rem; --line-height: 1.7; --border-radius: 2px; + + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s; + --transition-base: 0.2s; +} + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + html { + scroll-behavior: auto !important; + } } /* Base elements */ @@ -42,6 +67,13 @@ html { scroll-behavior: smooth; } +/* ACCESSIBILITY: Respect reduced motion preference for scroll behavior */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} + body { font-family: var(--font-serif); background-color: var(--background-color); @@ -82,13 +114,21 @@ header { .site-title a { color: var(--heading-color); text-decoration: none; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .site-title a:hover { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Site description */ header p { margin: var(--spacing-xs) 0 0; @@ -117,8 +157,10 @@ nav a { color: var(--muted-color); text-decoration: none; padding: var(--spacing-xs) 0; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; position: relative; + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a:hover, @@ -129,6 +171,12 @@ nav a.active { text-underline-offset: 0.2em; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Content area */ main { min-height: 70vh; @@ -196,7 +244,9 @@ a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:hover { @@ -207,6 +257,12 @@ a:visited { color: var(--link-visited); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Lists */ ul, ol { margin: var(--spacing-md) auto var(--spacing-lg); @@ -314,18 +370,27 @@ article .meta, line-height: 1.5; } -/* Reading time */ +/* Reading time - TEXT BROWSER FALLBACK */ .reading-time { display: flex; align-items: center; gap: 0.3em; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ .reading-time::before { - content: "•"; + content: "Time: "; font-style: normal; } +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "•"; + font-style: normal; + } +} + /* Simplified tags */ .tags { display: flex; @@ -339,13 +404,21 @@ article .meta, color: var(--muted-color); font-style: normal; font-size: 0.85rem; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags a:hover { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Tags list page */ .tags-list { list-style: none; @@ -366,7 +439,9 @@ article .meta, color: var(--text-color); text-decoration: none; font-size: 0.9rem; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags-list a:hover { @@ -374,6 +449,12 @@ article .meta, text-decoration: underline; } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Minimal featured images */ .featured-image, .index-image, @@ -453,7 +534,9 @@ figcaption { .posts-list h3 a { color: var(--heading-color); text-decoration: none; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .posts-list h3 a:hover { @@ -461,6 +544,12 @@ figcaption { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list h3 a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Minimal footer */ footer { margin-top: var(--spacing-xxl); @@ -480,13 +569,21 @@ footer p { footer a { color: var(--muted-color); text-decoration: underline; - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } footer a:hover { color: var(--link-color); } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Pagination - simplified */ .pagination { display: flex; @@ -502,7 +599,9 @@ footer a:hover { color: var(--muted-color); text-decoration: none; padding: var(--spacing-xs) var(--spacing-sm); - transition: color 0.2s ease; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a:hover { @@ -510,6 +609,12 @@ footer a:hover { text-decoration: underline; } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + .pagination .page-info { color: var(--muted-color); font-style: italic; @@ -543,7 +648,9 @@ footer a:hover { cursor: pointer; border-radius: var(--border-radius); color: var(--muted-color); - transition: all 0.2s ease; + transition: all var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .theme-toggle button:hover { @@ -557,6 +664,12 @@ footer a:hover { font-weight: bold; } +/* ACCESSIBILITY: Focus states for theme toggle buttons */ +.theme-toggle button:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Dark mode */ .theme-dark { --background-color: #1a1a1a; diff --git a/themes/skeuomorphic/style.css b/themes/skeuomorphic/style.css index 7e1e78c..afd0efc 100644 --- a/themes/skeuomorphic/style.css +++ b/themes/skeuomorphic/style.css @@ -2,8 +2,34 @@ * Skeuomorphic Theme for BSSG * Featuring realistic textures, 3D effects, and shadows * Inspired by early iOS (pre-iOS 7) and realistic UI elements + * IMPROVED: Better accessibility, performance, and text browser support */ +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects */ + .container, .inner-container, header, nav a, .skeu-button, .pagination a { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important; + } + + /* Simplify backgrounds for reduced motion */ + body, .inner-container, header { + background-image: none !important; + } + + /* Simplify hover effects */ + nav a:hover, .skeu-button:hover, .pagination a:hover { + transform: none !important; + } +} + :root { /* Skeuomorphic color scheme */ --leather-dark: #8B2E00; @@ -34,10 +60,10 @@ --title-gradient-middle: #f8f3e9; --title-gradient-end: #c8b8a5; - /* Typography */ - --font-main: 'Helvetica Neue', Helvetica, Arial, sans-serif; - --font-headings: 'Helvetica Neue', Helvetica, Arial, sans-serif; - --font-mono: 'Menlo', 'Courier New', monospace; + /* Typography - IMPROVED fallbacks for text browsers */ + --font-main: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; + --font-headings: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; + --font-mono: 'Menlo', 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', 'Courier New', monospace; /* Sizing */ --content-width: 820px; @@ -52,17 +78,29 @@ --spacing-xl: 30px; --spacing-xxl: 40px; - /* Transition speeds */ - --transition-fast: 0.2s; - --transition-medium: 0.3s; - --transition-slow: 0.5s; + /* Transition speeds - OPTIMIZED for performance */ + --transition-fast: 0.15s; /* Reduced for better performance */ + --transition-medium: 0.2s; /* Reduced for better performance */ + --transition-slow: 0.3s; /* Reduced for better performance */ } -/* Base elements */ +/* Base elements - OPTIMIZED background */ body { font-family: var(--font-main); background-color: var(--leather-medium); - background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%239C602F' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E"); + /* OPTIMIZED: Simplified pattern for better performance */ + background-image: linear-gradient(45deg, + var(--leather-medium) 25%, + transparent 25%, + transparent 75%, + var(--leather-medium) 75%), + linear-gradient(45deg, + var(--leather-medium) 25%, + transparent 25%, + transparent 75%, + var(--leather-medium) 75%); + background-size: 20px 20px; + background-position: 0 0, 10px 10px; color: var(--text-color); margin: 0; padding: 20px; @@ -70,20 +108,21 @@ body { font-size: 14px; } -/* Container styled as a leather-bound book */ +/* Container styled as a leather-bound book - OPTIMIZED */ .container { max-width: var(--content-width); margin: var(--spacing-lg) auto; background-color: var(--leather-dark); border-radius: var(--border-radius); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); overflow: hidden; position: relative; border: 1px solid var(--leather-dark); padding: var(--spacing-lg); } -/* Leather binding effect with stitching */ +/* OPTIMIZED: Simplified leather binding effect */ .container::before { content: ""; position: absolute; @@ -93,7 +132,7 @@ body { width: 2px; background-color: var(--stitching); z-index: 2; - box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); /* Reduced shadow */ } .container::after { @@ -103,42 +142,45 @@ body { bottom: 0; left: 15px; width: 20px; - background: repeating-linear-gradient( - 0deg, - var(--stitching), - var(--stitching) 5px, - transparent 5px, - transparent 15px - ); + /* SIMPLIFIED: Basic pattern instead of complex repeating gradient */ + background: linear-gradient(to bottom, + var(--stitching) 0%, + transparent 20%, + transparent 80%, + var(--stitching) 100%); z-index: 1; - opacity: 0.5; + opacity: 0.3; /* Reduced opacity for performance */ } -/* Inner container for content with paper texture */ +/* Inner container for content with paper texture - OPTIMIZED */ .inner-container { background-color: var(--paper-bg); border-radius: var(--small-radius); padding: 1px; position: relative; margin-left: var(--spacing-lg); - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23d1cec7' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E"); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + /* SIMPLIFIED: Basic texture pattern */ + background-image: radial-gradient(circle at 20% 50%, rgba(209, 206, 199, 0.1) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(209, 206, 199, 0.1) 0%, transparent 50%); } -/* Polished wood header */ +/* Polished wood header - OPTIMIZED */ header { background: linear-gradient(to bottom, var(--wood-light) 0%, var(--wood-medium) 50%, var(--wood-dark) 100%); color: white; padding: var(--spacing-md) var(--spacing-lg); margin: 0; position: relative; - text-shadow: 0 2px 3px rgba(0, 0, 0, 0.5); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); /* Reduced shadow */ border-bottom: 1px solid var(--wood-dark); border-radius: var(--small-radius) var(--small-radius) 0 0; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); } -/* Wood grain texture for header */ +/* OPTIMIZED: Simplified wood grain texture */ header::before { content: ""; position: absolute; @@ -146,12 +188,16 @@ header::before { right: 0; bottom: 0; left: 0; - background-image: url("data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h200v200H0V0zm160 160h40v40h-40v-40zm-20-40h20v20h-20v-20zm40 20h20v20h-20v-20zM20 120h20v20H20v-20zm20-40h20v20H40V80zm60 40h20v20h-20v-20zm-40-40h20v20H60V80zm60 0h20v20h-20V80zm20-40h20v20h-20V40zm-20 0h20v20h-20V40zM40 40h20v20H40V40zm40-20h20v20H80V20zm40 0h20v20h-20V20zm20 20h20v20H20V20zm80 160h20v20h-20v-20z' fill='%23825333' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E"); + /* SIMPLIFIED: Basic wood grain pattern */ + background-image: linear-gradient(90deg, + transparent 0%, + rgba(130, 83, 51, 0.1) 50%, + transparent 100%); border-radius: var(--small-radius) var(--small-radius) 0 0; - opacity: 0.5; + opacity: 0.3; /* Reduced opacity */ } -/* Embossed site title with gradient and beveled 3D effect */ +/* Site title with improved accessibility - IMPROVED */ .site-title { margin: 0; padding: 0; @@ -162,24 +208,41 @@ header::before { } .site-title a { - color: var(--text-color); + color: var(--text-color); /* Fallback for text browsers */ text-decoration: none; - background: linear-gradient(to bottom, - var(--title-gradient-start), - var(--title-gradient-middle) 50%, - var(--title-gradient-end)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; display: inline-block; position: relative; - text-shadow: - 0 1px 0 rgba(255, 255, 255, 0.8), - 0 -1px 1px rgba(0, 0, 0, 0.5); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6), 0 -1px 1px rgba(0, 0, 0, 0.3); letter-spacing: 0.03em; + transition: all var(--transition-medium); } -/* Beveled edge effect */ +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(to bottom, + var(--title-gradient-start), + var(--title-gradient-middle) 50%, + var(--title-gradient-end)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + +.site-title a:hover { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8), 0 -1px 1px rgba(0, 0, 0, 0.5); + transform: translateY(-1px); /* Reduced transform for better performance */ +} + +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8), 0 -1px 1px rgba(0, 0, 0, 0.5); + transform: translateY(-1px); +} + +/* OPTIMIZED: Simplified beveled edge effect */ .site-title a::before { content: ""; position: absolute; @@ -190,37 +253,25 @@ header::before { z-index: -1; border-radius: 4px; background: var(--wood-medium); - box-shadow: - inset 0 2px 3px rgba(255, 255, 255, 0.4), - inset 0 -2px 3px rgba(0, 0, 0, 0.4); + /* SIMPLIFIED: Basic bevel effect */ + box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.3), inset 0 -1px 2px rgba(0, 0, 0, 0.3); opacity: 0; - transition: opacity 0.3s ease; + transition: opacity var(--transition-medium); } .site-title a:hover::before { - opacity: 1; + opacity: 0.5; /* Reduced opacity for better performance */ } -/* Fallback for browsers that don't support background-clip */ +/* FALLBACK: For browsers that don't support background-clip */ @supports not (background-clip: text) { .site-title a { - color: var(--wood-light); - text-shadow: - 0 1px 0 rgba(255, 255, 255, 0.3), - 0 -1px 1px rgba(0, 0, 0, 0.5); + color: var(--text-color) !important; + background: none !important; } } -header h1 { - margin: 0; - padding: 0; - font-size: 1.8rem; - font-weight: normal; - position: relative; - z-index: 1; -} - -/* Navigation with iOS-style tabs */ +/* Navigation with iOS-style tabs - IMPROVED accessibility */ nav { background: linear-gradient(to bottom, var(--paper-bg) 0%, var(--paper-dark) 100%); padding: var(--spacing-sm) var(--spacing-sm) 5px; @@ -229,7 +280,8 @@ nav { overflow-x: auto; justify-content: space-around; border-bottom: 1px solid var(--wood-dark); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); position: relative; } @@ -244,7 +296,8 @@ nav a { display: inline-block; border-radius: 5px; background: linear-gradient(to bottom, var(--button-top) 0%, var(--button-bottom) 100%); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.8); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.6); border: 1px solid var(--button-border); text-shadow: 0 1px 0 white; white-space: nowrap; @@ -253,7 +306,7 @@ nav a { text-align: center; } -/* Shine effect for glass buttons */ +/* OPTIMIZED: Simplified shine effect */ nav a::after { content: ""; position: absolute; @@ -261,25 +314,32 @@ nav a::after { left: 0; right: 0; height: 50%; - background: linear-gradient(to bottom, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0) 100%); + background: linear-gradient(to bottom, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 100%); border-radius: 5px 5px 0 0; pointer-events: none; } nav a:hover, nav a:focus { background: linear-gradient(to bottom, #f9f9f9 0%, #e8e8e8 100%); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 1); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.8); } -/* Pressed button effect */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Pressed button effect - OPTIMIZED */ nav a.active, nav a:active { background: linear-gradient(to bottom, #d9d9d9 0%, #e9e9e9 100%); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); + /* OPTIMIZED: Simplified shadow */ + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); padding-top: 9px; padding-bottom: 7px; } -/* Make RSS button special */ +/* Make RSS button special - IMPROVED accessibility */ nav a:last-child { background: linear-gradient(to bottom, #7eb6e2 0%, #5a9bd1 100%); color: white; @@ -291,11 +351,16 @@ nav a:last-child:hover { background: linear-gradient(to bottom, #90c1e8 0%, #6ba6d9 100%); } +nav a:last-child:focus { + outline: 2px solid #ffffff; + outline-offset: 2px; +} + nav a:last-child:active { background: linear-gradient(to bottom, #5a9bd1 0%, #7eb6e2 100%); } -/* Main content area with paper texture */ +/* Main content area with paper texture - OPTIMIZED */ main { padding: var(--spacing-lg); background-color: var(--paper-bg); @@ -303,7 +368,7 @@ main { position: relative; } -/* Paper texture and subtle lines */ +/* OPTIMIZED: Simplified paper texture */ main::before { content: ""; position: absolute; @@ -311,12 +376,13 @@ main::before { left: 0; right: 0; bottom: 0; - background: repeating-linear-gradient( - 0deg, - transparent, - transparent 23px, - #e0ded8 24px - ); + /* SIMPLIFIED: Basic line pattern instead of complex repeating gradient */ + background: linear-gradient(to bottom, + transparent 0%, + transparent 95%, + rgba(224, 222, 216, 0.3) 95%, + rgba(224, 222, 216, 0.3) 100%); + background-size: 100% 24px; z-index: 0; pointer-events: none; } @@ -354,12 +420,12 @@ p { z-index: 1; } -/* Links styled as underlining ink */ +/* Links styled as underlining ink - IMPROVED accessibility */ a { color: var(--link-color); text-decoration: none; border-bottom: 1px solid rgba(0, 102, 204, 0.3); - transition: all 0.2s; + transition: all var(--transition-fast); position: relative; z-index: 1; } @@ -374,7 +440,14 @@ a:hover { border-bottom-color: rgba(0, 136, 255, 0.5); } -/* Skeuomorphic button */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: #0088ff; + border-bottom-color: rgba(0, 136, 255, 0.5); +} + +/* Skeuomorphic button - IMPROVED accessibility */ .skeu-button { display: inline-block; padding: 8px 18px; @@ -385,12 +458,16 @@ a:hover { border-radius: 5px; background: linear-gradient(to bottom, var(--button-top) 0%, var(--button-bottom) 100%); border: 1px solid var(--button-border); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.8); + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.6); position: relative; cursor: pointer; margin: 5px 0; + text-decoration: none; + transition: all var(--transition-fast); } +/* OPTIMIZED: Simplified shine effect */ .skeu-button::after { content: ""; position: absolute; @@ -398,7 +475,7 @@ a:hover { left: 0; right: 0; height: 50%; - background: linear-gradient(to bottom, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0) 100%); + background: linear-gradient(to bottom, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 100%); border-radius: 5px 5px 0 0; pointer-events: none; } @@ -407,9 +484,16 @@ a:hover { background: linear-gradient(to bottom, #f9f9f9 0%, #e8e8e8 100%); } +.skeu-button:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background: linear-gradient(to bottom, #f9f9f9 0%, #e8e8e8 100%); +} + .skeu-button:active { background: linear-gradient(to bottom, #d9d9d9 0%, #e9e9e9 100%); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); + /* OPTIMIZED: Simplified shadow */ + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); padding-top: 9px; padding-bottom: 7px; } @@ -653,36 +737,92 @@ footer a:hover { box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { + body { + padding: 10px; + /* OPTIMIZED: Simplified background on mobile */ + background-image: none; + background-color: var(--leather-medium); + } + .container { margin: var(--spacing-sm); padding: var(--spacing-sm); + /* OPTIMIZED: Simplified shadows on mobile */ + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .inner-container { margin-left: var(--spacing-sm); + /* OPTIMIZED: Simplified background on mobile */ + background-image: none; + } + + header { + padding: var(--spacing-md); + /* OPTIMIZED: Simplified background on mobile */ + background-image: none; + } + + main::before { + /* OPTIMIZED: Remove paper lines on mobile */ + display: none; } nav { flex-wrap: wrap; justify-content: center; + padding: var(--spacing-md); } nav a { margin: var(--spacing-xs); flex-grow: 1; text-align: center; + /* OPTIMIZED: Simplified effects on mobile */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.4); + } + + nav a::after { + /* OPTIMIZED: Remove shine effect on mobile */ + display: none; } footer { flex-direction: column; gap: var(--spacing-sm); padding: var(--spacing-sm); + /* OPTIMIZED: Simplified background on mobile */ + background-image: none; } article { padding: var(--spacing-md); + /* OPTIMIZED: Simplified shadows on mobile */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } + + .skeu-button::after { + /* OPTIMIZED: Remove shine effect on mobile */ + display: none; + } + + .tags a::after { + /* OPTIMIZED: Remove shine effect on mobile */ + display: none; + } + + .featured-image, .index-image, .tag-image, .archive-image { + /* OPTIMIZED: Simplified shadows on mobile */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); + transform: none; /* Remove rotation on mobile */ + } + + .featured-image:hover, .index-image:hover, .tag-image:hover, .archive-image:hover { + /* OPTIMIZED: Simplified hover effects on mobile */ + transform: none; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25); } } diff --git a/themes/stefano/style.css b/themes/stefano/style.css index 5600667..a7b236f 100644 --- a/themes/stefano/style.css +++ b/themes/stefano/style.css @@ -2,8 +2,27 @@ * Stefano Theme for BSSG * Based on Stefano Marinelli's personal website design * https://stefano.dragas.it/ + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining elegant design aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Color palette from user's website */ --primary: #1a3a5f; @@ -40,10 +59,10 @@ --accent-secondary: #2d6d8a; --quote-bg: #f7f7f7; - /* Typography */ - --font-main: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --font-headings: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + /* Typography - Enhanced font fallbacks for elegant design */ + --font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --font-headings: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --font-mono: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', 'Courier New', 'Courier', monospace; /* Sizing */ --content-width: 840px; @@ -62,8 +81,7 @@ --line-height-heading: 1.2; } -/* Import Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); + /* Base elements */ html { @@ -148,6 +166,13 @@ a:hover { color: var(--link-hover); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--link-hover); +} + /* Header/Site Title */ header { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); @@ -219,6 +244,18 @@ header p { transform: translateY(-1px); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; + text-decoration: none; + background: linear-gradient(120deg, var(--accent) 0%, rgba(255,255,255,0.9) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + transform: translateY(-1px); +} + /* Navigation */ nav { display: flex; @@ -230,9 +267,10 @@ nav { nav a { margin: 0 var(--spacing-sm); padding: var(--spacing-xs) 0; - color: white; + color: rgba(255, 255, 255, 0.95); position: relative; font-weight: 600; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } nav a::after { @@ -246,10 +284,27 @@ nav a::after { transition: var(--transition); } +nav a:hover { + color: white; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); +} + nav a:hover::after { width: 100%; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; + color: white; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); +} + +nav a:focus::after { + width: 100%; +} + /* Article */ article { margin-bottom: var(--spacing-xxl); @@ -284,6 +339,18 @@ article .meta { margin-left: var(--spacing-sm); } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use elegant icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Code */ code { font-family: var(--font-mono); @@ -332,6 +399,15 @@ pre code { transform: translateY(-3px); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--tag-hover-bg); + color: var(--tag-hover-text); + transform: translateY(-3px); +} + .tags-list { margin: var(--spacing-sm) 0; } @@ -396,6 +472,13 @@ footer { color: var(--accent); } +/* ACCESSIBILITY: Focus states for social links */ +.social-link:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; + color: var(--accent); +} + .copyright { font-size: 0.9rem; color: rgba(255, 255, 255, 0.7); @@ -452,6 +535,13 @@ footer { color: var(--accent); } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list h3 a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--accent); +} + .summary { margin-top: var(--spacing-xs); } @@ -535,6 +625,15 @@ ul li::before { transform: translateY(-3px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--accent); + color: white; + transform: translateY(-3px); +} + .pagination .page-info { margin: 0 var(--spacing-sm); color: var(--date-color); diff --git a/themes/swiss-design/style.css b/themes/swiss-design/style.css index 1c41853..2e198fc 100644 --- a/themes/swiss-design/style.css +++ b/themes/swiss-design/style.css @@ -2,6 +2,12 @@ * Swiss Design Theme for BSSG * Based on International Typographic Style with focus on grids, * clean sans-serif typography, and strong visual hierarchy + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Added text browser fallbacks + * - Optimized performance while maintaining Swiss design principles */ :root { @@ -23,10 +29,10 @@ --link-hover: var(--dark-gray); --border-color: var(--medium-gray); - /* Typography */ - --font-main: 'Helvetica Neue', Helvetica, Arial, sans-serif; - --font-headings: 'Helvetica Neue', Helvetica, Arial, sans-serif; - --font-mono: 'Courier New', monospace; + /* Typography - ENHANCED fallbacks for text browsers */ + --font-main: 'Helvetica Neue', Helvetica, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-headings: 'Helvetica Neue', Helvetica, -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; + --font-mono: 'Courier New', 'Courier', monospace; /* Sizing */ --content-width: 90%; @@ -57,8 +63,20 @@ --line-height-normal: 1.5; --line-height-relaxed: 1.6; - /* Transitions */ - --transition-fast: 0.2s; + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s; + --transition-base: 0.2s; +} + +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } } /* Reset and base elements */ @@ -117,12 +135,21 @@ header { .site-title a { color: var(--heading-color); text-decoration: none; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .site-title a:hover { color: var(--red); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Site description - minimalist */ header p { margin: var(--spacing-xs) 0 0; @@ -155,6 +182,9 @@ nav a { text-transform: uppercase; letter-spacing: 1px; position: relative; + transition: color var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } nav a:hover, @@ -162,6 +192,12 @@ nav a.active { color: var(--red); } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Content area with grid structure */ main { padding: 0 var(--spacing-md) var(--spacing-xl); @@ -205,6 +241,8 @@ a { text-decoration: none; transition: all var(--transition-fast) ease; border-bottom: 1px solid transparent; + /* ACCESSIBILITY: Focus outline */ + outline: none; } a:visited { @@ -216,6 +254,12 @@ a:hover { border-bottom: 1px solid var(--link-hover); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Swiss-style articles with strong structure */ article { margin-bottom: var(--grid-row-gap); @@ -245,11 +289,23 @@ article .meta { padding-bottom: var(--spacing-sm); } -/* Reading time - minimal style */ +/* Reading time - minimal style with TEXT BROWSER FALLBACK */ .reading-time { position: relative; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Hide the prefix when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags with Swiss precision */ .tags { display: flex; @@ -266,6 +322,9 @@ article .meta { letter-spacing: 1px; font-weight: 500; border: none; + transition: all var(--transition-fast) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags a:hover { @@ -274,6 +333,12 @@ article .meta { border: none; } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Tags list page - strong grid */ .tags-list { list-style-type: none; @@ -300,6 +365,8 @@ article .meta { border: none; transition: all var(--transition-fast) ease; text-align: center; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .tags-list a:hover { @@ -308,6 +375,12 @@ article .meta { border: none; } +/* ACCESSIBILITY: Focus states for tags list */ +.tags-list a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Footer - clean and minimal */ footer { background-color: var(--light-gray); @@ -325,6 +398,9 @@ footer a { color: var(--link-color); text-decoration: none; border-bottom: 1px solid transparent; + transition: all var(--transition-base) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } footer a:hover { @@ -332,6 +408,12 @@ footer a:hover { color: var(--link-hover); } +/* ACCESSIBILITY: Focus states for footer links */ +footer a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Pagination - Swiss precision */ .pagination { display: flex; @@ -352,6 +434,8 @@ footer a:hover { font-size: var(--text-xs); border: none; transition: all var(--transition-fast) ease; + /* ACCESSIBILITY: Focus outline */ + outline: none; } .pagination a:hover { @@ -360,6 +444,12 @@ footer a:hover { border: none; } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Featured images - clean presentation */ .featured-image, .index-image, @@ -404,6 +494,12 @@ footer a:hover { margin-bottom: var(--spacing-sm); } +/* ACCESSIBILITY: Focus states for post list links */ +.posts-list a:focus { + outline: 2px solid var(--red); + outline-offset: 2px; +} + /* Grid-based layout for larger screens */ @media (min-width: 768px) { .posts-list { diff --git a/themes/terminal/style.css b/themes/terminal/style.css index c048cad..3bd2787 100644 --- a/themes/terminal/style.css +++ b/themes/terminal/style.css @@ -23,10 +23,10 @@ --title-gradient-start: #33ff33; /* Bright terminal green */ --title-gradient-end: #00cc00; /* Darker terminal green */ - /* Typography */ - --font-main: 'VT323', 'Courier New', monospace; - --font-headings: 'VT323', 'Courier New', monospace; - --font-mono: 'VT323', 'Courier New', monospace; + /* Typography - System monospace fonts for terminal aesthetic */ + --font-main: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; + --font-headings: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; + --font-mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; /* Sizing */ --content-width: 800px; @@ -56,8 +56,39 @@ --transition-normal: 0.3s ease; } -/* Import VT323 font - classic terminal style */ -@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); +/* Removed external font loading for better performance and text browser compatibility */ + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all transforms and animations */ + .site-title a:hover, + .featured-image:hover img, + .index-image:hover img, + .tag-image:hover img, + .archive-image:hover img, + .pagination a:hover { + transform: none !important; + } + + @keyframes flicker { + from, to { opacity: 1; } + } + + @keyframes blink { + from, to { opacity: 1; } + } + + main::after { + animation: none !important; + } +} /* Base elements */ html { @@ -140,40 +171,46 @@ header { } .site-title a { - background: linear-gradient(to right, - var(--title-gradient-start) 0%, - var(--highlight-color) 50%, - var(--title-gradient-end) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--header-color); text-decoration: none; border: none; padding: 0; - text-shadow: 0 0 8px rgba(51, 255, 51, 0.6); position: relative; + outline: 2px solid transparent; + outline-offset: 2px; } .site-title a:hover { - background: linear-gradient(to right, - var(--highlight-color) 0%, - var(--title-gradient-start) 50%, - var(--highlight-color) 100%); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: flicker 0.5s infinite alternate; + color: var(--highlight-color); border: none; } -/* Fallback for browsers that don't support background-clip */ -@supports not (background-clip: text) { +.site-title a:focus { + outline-color: var(--link-color); +} + +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { .site-title a { - color: var(--header-color); + background: linear-gradient(to right, + var(--title-gradient-start) 0%, + var(--highlight-color) 50%, + var(--title-gradient-end) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + text-shadow: 0 0 8px rgba(51, 255, 51, 0.6); } .site-title a:hover { - color: var(--highlight-color); + background: linear-gradient(to right, + var(--highlight-color) 0%, + var(--title-gradient-start) 50%, + var(--highlight-color) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: flicker 0.5s infinite alternate; } } @@ -234,6 +271,13 @@ nav a:hover { color: var(--bg-color); } +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--dim-color); + color: var(--bg-color); +} + /* Content area */ main { position: relative; @@ -318,6 +362,11 @@ a:hover, a:focus { background-color: rgba(51, 255, 51, 0.1); } +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + /* Article styling */ article { margin-bottom: var(--spacing-xxl); @@ -358,6 +407,16 @@ article .meta { content: "]"; } +/* Text browser fallbacks for reading time brackets */ +@supports not (content: "[") { + .reading-time::before { + content: ""; + } + .reading-time::after { + content: ""; + } +} + /* Tags */ .tags { margin-top: var(--spacing-lg); @@ -383,6 +442,13 @@ article .meta { color: var(--bg-color); } +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--dim-color); + color: var(--bg-color); +} + .tags-list { margin: var(--spacing-md) 0; } @@ -400,6 +466,16 @@ article .meta { content: ")"; } +/* Text browser fallbacks for tag count parentheses */ +@supports not (content: "(") { + .tag-count::before { + content: ""; + } + .tag-count::after { + content: ""; + } +} + /* Code blocks */ code { font-family: var(--font-mono); @@ -743,6 +819,13 @@ hr::before { color: var(--dim-color); } +/* Text browser fallback for hr decoration */ +@supports not (content: "----------------------------") { + hr::before { + content: ""; + } +} + /* Selection style */ ::selection { background-color: var(--text-color); @@ -880,6 +963,13 @@ body { color: var(--bg-color); } +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--dim-color); + color: var(--bg-color); +} + .pagination .page-info { color: var(--dim-color); font-size: var(--text-sm); diff --git a/themes/text-only/style.css b/themes/text-only/style.css index 147ab04..fbf8c3e 100644 --- a/themes/text-only/style.css +++ b/themes/text-only/style.css @@ -2,8 +2,19 @@ * Text-Only Theme for BSSG * A step beyond minimalism - uses browser defaults with only * essential typography for readability. Lightning-fast loading. + * Enhanced with accessibility and compatibility improvements. */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + /* CSS Variables for consistency */ :root { /* Colors */ @@ -96,6 +107,12 @@ nav a:hover { text-decoration: underline; } +nav a:focus { + outline: 2px solid var(--color-link); + outline-offset: 2px; + text-decoration: underline; +} + /* Simple content area */ main { margin-bottom: var(--spacing-lg); @@ -136,6 +153,12 @@ a:hover { text-decoration: underline; } +a:focus { + outline: 2px solid var(--color-link); + outline-offset: 2px; + text-decoration: underline; +} + /* Ultra-minimal article styling */ article { margin-bottom: var(--spacing-lg); @@ -232,6 +255,12 @@ hr { margin-top: var(--spacing-lg); } +.pagination a:focus { + outline: 2px solid var(--color-link); + outline-offset: 2px; + text-decoration: underline; +} + /* Bare minimum responsive adjustments */ @media (max-width: 600px) { body { diff --git a/themes/tty/style.css b/themes/tty/style.css index f6112de..a9df816 100644 --- a/themes/tty/style.css +++ b/themes/tty/style.css @@ -2,20 +2,29 @@ * Terminal Theme for BSSG * Simulates an old teletype/terminal output with monospace text * on a simple background, with a character-by-character "print" effect. + * Enhanced with accessibility, performance, and compatibility improvements. */ -@font-face { - font-family: 'VT323'; - src: url('https://fonts.googleapis.com/css2?family=VT323&display=swap'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Source Code Pro'; - src: url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap'); - font-weight: normal; - font-style: normal; +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all terminal effects for reduced motion */ + .tty-effect { + animation: none !important; + visibility: visible !important; + opacity: 1 !important; + } + + body::before, + .container::before { + display: none !important; + } } :root { @@ -37,10 +46,10 @@ --code-bg: rgba(0, 85, 0, 0.3); --cursor-color: var(--terminal-green); - /* Typography */ - --font-main: 'VT323', 'Source Code Pro', monospace; - --font-headings: 'VT323', 'Source Code Pro', monospace; - --font-mono: 'Source Code Pro', monospace; + /* Typography - system monospace fonts for terminal aesthetic */ + --font-main: 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; + --font-headings: 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; + --font-mono: 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; /* Font sizes */ --text-xs: 0.8rem; @@ -85,34 +94,42 @@ body { line-height: var(--line-height); margin: 0; padding: var(--spacing-sm); - /* Terminal scanlines effect */ - background-image: linear-gradient( - rgba(0, 255, 0, 0.03) 1px, - transparent 1px - ); - background-size: 100% 2px; position: relative; - /* CRT glow effect */ - box-shadow: 0 0 20px rgba(0, 255, 0, 0.05) inset; } -/* CRT flicker animation */ -body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: radial-gradient( - ellipse at center, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 0.2) 80%, - rgba(0, 0, 0, 0.4) 100% - ); - pointer-events: none; - z-index: 2; - opacity: 0.8; +/* Progressive enhancement for terminal effects */ +@supports (background-image: linear-gradient(rgba(0, 255, 0, 0.03) 1px, transparent 1px)) { + body { + /* Terminal scanlines effect - reduced intensity */ + background-image: linear-gradient( + rgba(0, 255, 0, 0.02) 1px, + transparent 1px + ); + background-size: 100% 3px; + /* CRT glow effect - reduced intensity */ + box-shadow: 0 0 10px rgba(0, 255, 0, 0.03) inset; + } +} + +/* Progressive enhancement for CRT vignette effect */ +@supports (background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.2) 80%)) { + body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient( + ellipse at center, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.1) 80%, + rgba(0, 0, 0, 0.2) 100% + ); + pointer-events: none; + z-index: 2; + opacity: 0.5; + } } /* Scrollbars - webkit */ @@ -145,23 +162,26 @@ body::before { overflow: hidden; } -.container::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: repeating-linear-gradient( - 0deg, - rgba(0, 0, 0, 0.1), - rgba(0, 0, 0, 0.1) 1px, - rgba(0, 0, 0, 0) 1px, - rgba(0, 0, 0, 0) 2px - ); - pointer-events: none; - z-index: 1; - opacity: 0.3; +/* Progressive enhancement for container scanlines */ +@supports (background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 1px)) { + .container::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.05), + rgba(0, 0, 0, 0.05) 1px, + rgba(0, 0, 0, 0) 1px, + rgba(0, 0, 0, 0) 3px + ); + pointer-events: none; + z-index: 1; + opacity: 0.2; + } } /* Terminal header */ @@ -262,18 +282,36 @@ main { } /* Typography for terminal */ -h1, h2, h3, h4, h5, h6 { +h1, h2, h3 { font-weight: normal; margin: var(--spacing-md) 0 var(--spacing-xs) 0; line-height: 1.3; position: relative; - padding-left: 1.2em; + padding-left: 2em; /* Sufficient padding for 1-3 # symbols */ } -h1::before, h2::before, h3::before, h4::before, h5::before, h6::before { +h4, h5, h6 { + font-weight: normal; + margin: var(--spacing-md) 0 var(--spacing-xs) 0; + line-height: 1.3; + position: relative; + padding-left: 3.5em; /* Increased padding for 4-6 # symbols */ +} + +h1::before, h2::before, h3::before { position: absolute; left: 0; + top: 0; color: var(--terminal-amber); + width: 1.8em; /* Sufficient width for 1-3 # symbols */ +} + +h4::before, h5::before, h6::before { + position: absolute; + left: 0; + top: 0; + color: var(--terminal-amber); + width: 3.2em; /* Sufficient width for 4-6 # symbols */ } h1::before { content: "#"; } @@ -325,14 +363,25 @@ a:hover { border-bottom: 1px solid var(--link-hover); } +/* Accessibility: Focus outlines for all interactive elements */ +a:focus, +nav a:focus, +.tags a:focus, +.pagination a:focus { + outline: 2px solid var(--terminal-amber); + outline-offset: 2px; + background-color: rgba(255, 176, 0, 0.1); +} + /* Article styling */ article { margin-bottom: var(--spacing-lg); position: relative; } +/* Text browser fallback for article header */ article::before { - content: "cat article.txt"; + content: "Article:"; color: var(--terminal-green); display: block; margin-bottom: var(--spacing-sm); @@ -340,6 +389,13 @@ article::before { border-bottom: 1px dashed var(--border-color); } +/* Progressive enhancement for terminal command */ +@supports (content: "cat article.txt") { + article::before { + content: "cat article.txt"; + } +} + article h1 { margin-top: var(--spacing-sm); } @@ -362,11 +418,19 @@ article .meta { gap: 0.3em; } +/* Text browser fallback for reading time */ .reading-time::before { - content: "⧗"; + content: "Time: "; font-size: var(--text-base); } +/* Progressive enhancement for decorative symbol */ +@supports (content: "⧗") { + .reading-time::before { + content: "⧗"; + } +} + /* Tags */ .tags { display: flex; @@ -409,8 +473,9 @@ pre { position: relative; } +/* Text browser fallback for code header */ pre::before { - content: "$ cat code.txt"; + content: "Code:"; color: var(--terminal-green); display: block; margin-bottom: var(--spacing-xs); @@ -418,6 +483,13 @@ pre::before { border-bottom: 1px dashed var(--border-color); } +/* Progressive enhancement for terminal command */ +@supports (content: "$ cat code.txt") { + pre::before { + content: "$ cat code.txt"; + } +} + pre code { background-color: transparent; padding: 0; @@ -433,8 +505,9 @@ blockquote { position: relative; } +/* Text browser fallback for blockquote decorations */ blockquote::before { - content: "echo '"; + content: "Quote: "; position: absolute; top: 0; left: 0; @@ -444,7 +517,7 @@ blockquote::before { } blockquote::after { - content: "'"; + content: ""; position: absolute; bottom: 0; right: 0; @@ -453,6 +526,17 @@ blockquote::after { padding: 0.2em 0.4em; } +/* Progressive enhancement for terminal command */ +@supports (content: "echo '") { + blockquote::before { + content: "echo '"; + } + + blockquote::after { + content: "'"; + } +} + blockquote p { margin: var(--spacing-xs) 0; padding-left: 0; @@ -462,25 +546,39 @@ blockquote p::before { content: none; } -/* Lists */ +/* Lists with proper spacing to prevent overlap */ ul, ol { margin: var(--spacing-sm) 0; - padding-left: 3em; + padding-left: 4em; /* Increased to prevent overlap */ position: relative; } ul::before, ol::before { position: absolute; left: 0; + top: 0; color: var(--terminal-dim-green); + width: 3.5em; /* Ensure sufficient width */ } +/* Text browser fallbacks for list headers */ ul::before { - content: "ls:"; + content: "List:"; } ol::before { - content: "seq:"; + content: "List:"; +} + +/* Progressive enhancement for terminal commands */ +@supports (content: "ls:") { + ul::before { + content: "ls:"; + } + + ol::before { + content: "seq:"; + } } li { @@ -507,8 +605,9 @@ figure { position: relative; } +/* Text browser fallback for figure header */ figure::before { - content: "cat image.jpg"; + content: "Image:"; color: var(--terminal-green); display: block; margin-bottom: var(--spacing-xs); @@ -516,6 +615,13 @@ figure::before { border-bottom: 1px dashed var(--border-color); } +/* Progressive enhancement for terminal command */ +@supports (content: "cat image.jpg") { + figure::before { + content: "cat image.jpg"; + } +} + figcaption { font-size: var(--text-sm); padding: var(--spacing-xs); @@ -540,7 +646,7 @@ figcaption { .posts-list h3 { margin-top: 0; font-size: var(--text-md); - padding-left: 1.2em; + padding-left: 1.5em; /* Increased to prevent overlap */ position: relative; } @@ -548,8 +654,10 @@ figcaption { .posts-list h3::before { position: absolute; left: 0; + top: 0; color: var(--terminal-amber); content: "##"; + width: 1.3em; /* Ensure sufficient width */ } .posts-list article p:last-child { @@ -566,14 +674,22 @@ footer { position: relative; } +/* Text browser fallback for footer */ footer::before { - content: "exit"; + content: "End"; color: var(--terminal-green); display: block; text-align: left; margin-bottom: var(--spacing-sm); } +/* Progressive enhancement for terminal command */ +@supports (content: "exit") { + footer::before { + content: "exit"; + } +} + footer p { margin: var(--spacing-xs) 0; padding-left: 0; @@ -653,8 +769,12 @@ footer p::before { margin-bottom: var(--spacing-xs); } - h1, h2, h3, h4, h5, h6, p { - padding-left: 0.8em; + h1, h2, h3, p { + padding-left: 1.5em; /* Reduced but still sufficient padding for mobile */ + } + + h4, h5, h6 { + padding-left: 2.5em; /* Reduced but still sufficient padding for mobile */ } h1 { diff --git a/themes/vaporwave/style.css b/themes/vaporwave/style.css index e889b9b..056c457 100644 --- a/themes/vaporwave/style.css +++ b/themes/vaporwave/style.css @@ -2,14 +2,39 @@ * Vaporwave/Synthwave Theme for BSSG * 80s retro futurism with neon colors and grid designs * Inspired by retrowave aesthetics and digital dreamscapes + * IMPROVED: Better accessibility, performance, and text browser support */ -@import url('https://fonts.googleapis.com/css2?family=VT323&family=Monoton&display=swap'); +/* Removed Google Fonts import - using system font alternatives for better performance and privacy */ + +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects for reduced motion */ + .container { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + } + + body::before { + display: none !important; + } + + header h1, .site-title { + animation: none !important; + } +} :root { - /* Vaporwave/Synthwave color scheme */ - --bg-color: #0b032d; - --grid-color: rgba(255, 65, 221, 0.3); + /* Vaporwave/Synthwave color scheme - IMPROVED contrast ratios */ + --bg-color: #0a0a0a; /* Darker for better contrast */ + --grid-color: rgba(255, 65, 221, 0.2); /* Reduced opacity for performance */ --text-color: #ffffff; --text-shadow: #ff00ea; --neon-pink: #ff71ce; @@ -25,13 +50,13 @@ --border-color: var(--neon-purple); --accent-color: var(--neon-blue); --accent-secondary: var(--neon-pink); - --quote-bg: rgba(0, 0, 0, 0.4); + --quote-bg: rgba(0, 0, 0, 0.6); /* Improved contrast */ --header-color: var(--neon-green); - /* Typography */ - --font-main: 'VT323', monospace; - --font-headings: 'Monoton', cursive; - --font-mono: 'VT323', monospace; + /* Typography - Using system fonts for better performance and privacy */ + --font-main: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; + --font-headings: 'Impact', 'Arial Black', 'Franklin Gothic Bold', 'Helvetica Neue', sans-serif; + --font-mono: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; /* Font Sizes */ --text-xs: 0.8rem; @@ -58,14 +83,14 @@ --line-height-tight: 1.3; --line-height-loose: 1.8; - /* Transitions */ - --transition-fast: 0.2s; - --transition-medium: 0.3s; - --transition-slow: 0.5s; + /* Transitions - Reduced for performance */ + --transition-fast: 0.15s; + --transition-medium: 0.2s; + --transition-slow: 0.3s; /* Sizing */ --content-width: 1000px; - --grid-size: 30px; + --grid-size: 40px; /* Larger grid for better performance */ } html, body { @@ -80,59 +105,63 @@ html, body { box-sizing: inherit; } -/* Base elements with retro grid background */ +/* Base elements with optimized grid background */ body { font-family: var(--font-main); background-color: var(--bg-color); + /* OPTIMIZED: Simplified background for better performance */ background-image: - linear-gradient(0deg, rgba(150, 0, 150, 0.3) 0%, rgba(0, 0, 80, 0.3) 100%), - linear-gradient(to right, var(--grid-color) 2px, transparent 2px), - linear-gradient(to bottom, var(--grid-color) 2px, transparent 2px); + linear-gradient(0deg, rgba(150, 0, 150, 0.2) 0%, rgba(0, 0, 80, 0.2) 100%), + linear-gradient(to right, var(--grid-color) 1px, transparent 1px), + linear-gradient(to bottom, var(--grid-color) 1px, transparent 1px); background-size: 100% 100%, var(--grid-size) var(--grid-size), var(--grid-size) var(--grid-size); background-position: 0 0, 0 0, 0 0; - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ color: var(--text-color); margin: 0; padding: var(--spacing-xl); line-height: var(--line-height); font-size: var(--text-md); - text-shadow: 0 0 5px var(--text-shadow); + /* IMPROVED: Reduced text shadow for better readability */ + text-shadow: 0 0 3px var(--text-shadow); overflow-x: hidden; box-sizing: border-box; } -/* Sun/horizon effect */ +/* OPTIMIZED: Simplified sun/horizon effect */ body::before { content: ""; position: fixed; bottom: 0; left: 0; right: 0; - height: 40vh; + height: 30vh; /* Reduced height for performance */ background: linear-gradient(to top, var(--sunset-orange) 0%, var(--sunset-pink) 40%, rgba(0, 0, 0, 0) 100%); - opacity: 0.2; + opacity: 0.15; /* Reduced opacity for better text contrast */ z-index: -1; pointer-events: none; } -/* Chrome container with retro feel */ +/* OPTIMIZED: Chrome container with reduced effects */ .container { max-width: var(--content-width); margin: var(--spacing-xl) auto; - background-color: rgba(0, 0, 0, 0.7); + background-color: rgba(0, 0, 0, 0.8); /* Improved contrast */ border: 1px solid var(--neon-purple); + /* OPTIMIZED: Reduced shadow complexity */ box-shadow: - 0 0 20px rgba(185, 103, 255, 0.5), - 0 0 40px rgba(185, 103, 255, 0.3), - inset 0 0 30px rgba(185, 103, 255, 0.3); + 0 0 15px rgba(185, 103, 255, 0.4), + 0 0 30px rgba(185, 103, 255, 0.2); overflow: hidden; padding: 0; position: relative; - backdrop-filter: blur(5px); + /* CONDITIONAL: Only apply backdrop-filter if motion is not reduced */ + backdrop-filter: blur(3px); /* Reduced blur for performance */ + -webkit-backdrop-filter: blur(3px); box-sizing: border-box; } -/* Chrome trim for container */ +/* OPTIMIZED: Simplified chrome trim */ .container::after { content: ""; position: absolute; @@ -154,7 +183,7 @@ header { border-bottom: 3px solid var(--neon-pink); } -/* Horizontal lines effect */ +/* OPTIMIZED: Simplified horizontal lines effect */ header::before { content: ""; position: absolute; @@ -165,9 +194,9 @@ header::before { background: repeating-linear-gradient( to bottom, transparent, - transparent 2px, - rgba(255, 255, 255, 0.05) 2px, - rgba(255, 255, 255, 0.05) 4px + transparent 3px, /* Larger lines for better performance */ + rgba(255, 255, 255, 0.03) 3px, + rgba(255, 255, 255, 0.03) 6px ); pointer-events: none; } @@ -179,15 +208,16 @@ header h1 { font-weight: normal; font-family: var(--font-headings); letter-spacing: 5px; + /* IMPROVED: Better text shadow for readability */ text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green), - 0 0 15px var(--neon-green), - 0 0 20px var(--neon-green); - animation: neon 1.5s ease-in-out infinite alternate; + 0 0 3px var(--neon-green), + 0 0 6px var(--neon-green), + 0 0 9px var(--neon-green); + /* CONDITIONAL: Animation only if motion is not reduced */ + animation: neon 2s ease-in-out infinite alternate; } -/* Site title with animated gradient effect */ +/* Site title with optimized gradient effect */ .site-title { margin: 0; padding: 0; @@ -198,17 +228,25 @@ header h1 { background: linear-gradient( 90deg, var(--neon-blue) 0%, - var(--neon-pink) 25%, - var(--neon-purple) 50%, - var(--neon-green) 75%, - var(--neon-blue) 100% + var(--neon-green) 50%, + var(--neon-pink) 100% ); - background-size: 400% 100%; + background-size: 200% 100%; background-clip: text; -webkit-background-clip: text; color: transparent; - text-shadow: none; - animation: gradient-shift 8s linear infinite; + /* FALLBACK: For browsers that don't support background-clip */ + color: var(--neon-green); + /* CONDITIONAL: Animation only if motion is not reduced */ + animation: gradient-shift 4s ease-in-out infinite; +} + +/* FALLBACK: Ensure text is visible in text browsers */ +@supports not (background-clip: text) { + .site-title { + color: var(--neon-green) !important; + background: none !important; + } } .site-title a { @@ -216,136 +254,104 @@ header h1 { background: linear-gradient( 90deg, var(--neon-blue) 0%, - var(--neon-pink) 25%, - var(--neon-purple) 50%, - var(--neon-green) 75%, - var(--neon-blue) 100% + var(--neon-green) 50%, + var(--neon-pink) 100% ); - background-size: 400% 100%; + background-size: 200% 100%; background-clip: text; -webkit-background-clip: text; color: transparent; - text-shadow: none; - animation: gradient-shift 8s linear infinite; + /* FALLBACK: For browsers that don't support background-clip */ + color: var(--neon-green); transition: all var(--transition-medium); + display: inline-block; } .site-title a:hover { - transform: translateY(-3px) scale(1.03); + transform: translateY(-2px); filter: brightness(1.2); } +/* OPTIMIZED: Simplified gradient animation */ @keyframes gradient-shift { - 0% { + 0%, 100% { background-position: 0% 50%; } - 100% { - background-position: 400% 50%; + 50% { + background-position: 100% 50%; } } -/* Navigation with neon buttons */ nav { - background-color: rgba(0, 0, 0, 0.8); display: flex; - flex-wrap: nowrap; - overflow-x: auto; - padding: var(--spacing-md); + flex-wrap: wrap; justify-content: center; - position: relative; - gap: var(--spacing-lg); + align-items: center; + padding: var(--spacing-lg) var(--spacing-xl); + background: rgba(0, 0, 0, 0.3); + gap: var(--spacing-md); } nav a { - color: var(--neon-blue); + color: var(--text-color); text-decoration: none; - padding: var(--spacing-sm) var(--spacing-xl); - font-size: var(--text-md); + padding: var(--spacing-sm) var(--spacing-lg); + font-size: var(--text-base); font-weight: normal; position: relative; display: inline-block; - border: 2px solid var(--neon-blue); - white-space: nowrap; - text-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px var(--neon-blue); - box-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px rgba(1, 205, 254, 0.5), - inset 0 0 5px rgba(1, 205, 254, 0.2); + border: 1px solid var(--neon-blue); + background: rgba(0, 0, 0, 0.5); transition: all var(--transition-fast); - transform: perspective(500px) rotateX(10deg); + text-transform: uppercase; + letter-spacing: 2px; + /* IMPROVED: Better text shadow for readability */ + text-shadow: 0 0 3px var(--neon-blue); } nav a:hover, nav a:focus { - color: var(--neon-pink); - border-color: var(--neon-pink); - text-shadow: - 0 0 5px var(--neon-pink), - 0 0 10px var(--neon-pink); - box-shadow: - 0 0 5px var(--neon-pink), - 0 0 10px rgba(255, 113, 206, 0.5), - inset 0 0 5px rgba(255, 113, 206, 0.2); - transform: perspective(500px) rotateX(0deg) scale(1.1); + background: rgba(1, 205, 254, 0.2); + border-color: var(--neon-blue); + /* OPTIMIZED: Simplified glow effect */ + box-shadow: 0 0 10px var(--neon-blue); + transform: translateY(-1px); + /* IMPROVED: Focus outline for accessibility */ + outline: 2px solid var(--neon-blue); + outline-offset: 2px; } -/* Active navigation item */ +/* Active navigation with accent glow */ nav a.active { - color: var(--neon-yellow); - border-color: var(--neon-yellow); - text-shadow: - 0 0 5px var(--neon-yellow), - 0 0 10px var(--neon-yellow); - box-shadow: - 0 0 5px var(--neon-yellow), - 0 0 10px rgba(255, 251, 150, 0.5), - inset 0 0 5px rgba(255, 251, 150, 0.2); + background: rgba(185, 103, 255, 0.3); + border-color: var(--neon-purple); + box-shadow: 0 0 10px var(--neon-purple); } -/* RSS button with different color */ +/* RSS in accent color */ nav a:last-child { - color: var(--neon-green); - border-color: var(--neon-green); - text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green); - box-shadow: - 0 0 5px var(--neon-green), - 0 0 10px rgba(5, 255, 161, 0.5), - inset 0 0 5px rgba(5, 255, 161, 0.2); + background: rgba(255, 113, 206, 0.2); + border-color: var(--neon-pink); + text-shadow: 0 0 3px var(--neon-pink); } nav a:last-child:hover { - color: var(--neon-purple); - border-color: var(--neon-purple); - text-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px var(--neon-purple); - box-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px rgba(185, 103, 255, 0.5), - inset 0 0 5px rgba(185, 103, 255, 0.2); + background: rgba(255, 113, 206, 0.4); + box-shadow: 0 0 10px var(--neon-pink); } -/* Main content area */ main { padding: var(--spacing-2xl); position: relative; - min-height: 500px; } -/* Typography with retro neon effect */ h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); - margin-top: var(--spacing-xl); - margin-bottom: var(--spacing-md); + margin-top: var(--spacing-2xl); + margin-bottom: var(--spacing-lg); + color: var(--text-color); font-weight: normal; letter-spacing: 2px; - color: var(--neon-pink); - text-shadow: - 0 0 5px var(--neon-pink), - 0 0 10px var(--neon-pink); + line-height: var(--line-height-tight); } h1 { @@ -355,62 +361,57 @@ h1 { h2 { font-size: var(--text-2xl); color: var(--neon-blue); - text-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px var(--neon-blue); + text-shadow: 0 0 5px var(--neon-blue); } h3 { font-size: var(--text-xl); - color: var(--neon-purple); - text-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px var(--neon-purple); + color: var(--neon-pink); + text-shadow: 0 0 5px var(--neon-pink); } p { margin-bottom: var(--spacing-lg); - text-shadow: 0 0 5px rgba(255, 255, 255, 0.5); + line-height: var(--line-height); } -/* Neon links */ a { - color: var(--neon-green); + color: var(--neon-blue); text-decoration: none; - text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green); + border-bottom: 1px solid rgba(1, 205, 254, 0.5); transition: all var(--transition-fast); + /* IMPROVED: Better text shadow for readability */ + text-shadow: 0 0 2px var(--neon-blue); } a:visited { color: var(--neon-purple); - text-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px var(--neon-purple); + border-bottom-color: rgba(185, 103, 255, 0.5); + text-shadow: 0 0 2px var(--neon-purple); } a:hover { - color: var(--neon-pink); - text-shadow: - 0 0 5px var(--neon-pink), - 0 0 10px var(--neon-pink), - 0 0 15px var(--neon-pink); + color: var(--neon-green); + border-bottom-color: var(--neon-green); + text-shadow: 0 0 5px var(--neon-green); + /* IMPROVED: Focus outline for accessibility */ +} + +a:focus { + outline: 2px solid var(--neon-green); + outline-offset: 2px; + color: var(--neon-green); + border-bottom-color: var(--neon-green); } -/* Articles with chrome effect */ article { margin-bottom: var(--spacing-3xl); padding: var(--spacing-xl); - background-color: rgba(0, 0, 0, 0.6); - border: 1px solid var(--neon-blue); - box-shadow: - 0 0 10px rgba(1, 205, 254, 0.3), - inset 0 0 20px rgba(1, 205, 254, 0.2); + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); position: relative; } -/* CRT scanline effect */ article::before { content: ""; position: absolute; @@ -421,12 +422,11 @@ article::before { background: repeating-linear-gradient( to bottom, transparent, - transparent 2px, - rgba(0, 0, 0, 0.05) 3px, - rgba(0, 0, 0, 0.05) 5px + transparent 4px, /* Larger lines for better performance */ + rgba(255, 255, 255, 0.02) 4px, + rgba(255, 255, 255, 0.02) 8px ); pointer-events: none; - z-index: 1; } article:last-child { @@ -434,143 +434,125 @@ article:last-child { } article h1 { - font-size: var(--text-2xl); margin-top: 0; - margin-bottom: var(--spacing-xl); - position: relative; - z-index: 2; + color: var(--neon-green); + text-shadow: 0 0 5px var(--neon-green); } article .meta { - font-size: var(--text-base); - margin-bottom: var(--spacing-xl); - display: flex; - flex-wrap: wrap; - gap: var(--spacing-lg); - padding-bottom: var(--spacing-md); - border-bottom: 1px solid var(--neon-blue); - position: relative; - z-index: 2; + color: var(--neon-yellow); + font-size: var(--text-sm); + margin-bottom: var(--spacing-lg); + text-transform: uppercase; + letter-spacing: 1px; + /* IMPROVED: Better contrast for readability */ + text-shadow: 0 0 2px var(--neon-yellow); } .reading-time { color: var(--neon-yellow); - text-shadow: - 0 0 5px var(--neon-yellow), - 0 0 10px var(--neon-yellow); + font-size: var(--text-sm); + text-shadow: 0 0 2px var(--neon-yellow); } .tags { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-md); + margin-top: var(--spacing-lg); } .tags a { - background-color: rgba(5, 255, 161, 0.1); - color: var(--neon-green); - padding: var(--spacing-xs) var(--spacing-md); - font-size: var(--text-sm); - border: 1px solid var(--neon-green); - position: relative; - z-index: 2; + background: rgba(185, 103, 255, 0.2); + color: var(--neon-purple); + padding: var(--spacing-xs) var(--spacing-sm); + margin-right: var(--spacing-sm); + border: 1px solid var(--neon-purple); + text-transform: uppercase; + font-size: var(--text-xs); + letter-spacing: 1px; + border-bottom: none; + text-shadow: 0 0 2px var(--neon-purple); } .tags a:hover { - background-color: rgba(5, 255, 161, 0.3); - color: var(--text-color); + background: rgba(185, 103, 255, 0.4); + box-shadow: 0 0 5px var(--neon-purple); } .tags-list { - list-style-type: none; - padding: 0; display: flex; flex-wrap: wrap; - gap: var(--spacing-md); + gap: var(--spacing-sm); + margin-bottom: var(--spacing-xl); } .tag-count { - background-color: rgba(255, 113, 206, 0.3); + background: rgba(255, 113, 206, 0.2); color: var(--neon-pink); + padding: var(--spacing-xs); + border-radius: 50%; font-size: var(--text-xs); - margin-left: var(--spacing-sm); - padding: 0 var(--spacing-sm); - border: 1px solid var(--neon-pink); + min-width: 20px; + text-align: center; + text-shadow: 0 0 2px var(--neon-pink); } -/* Code with retro terminal look */ code { - font-family: var(--font-mono); - background-color: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.6); color: var(--neon-green); padding: var(--spacing-xs) var(--spacing-sm); - font-size: var(--text-base); - border: 1px solid var(--neon-green); - text-shadow: - 0 0 3px var(--neon-green), - 0 0 6px var(--neon-green); + font-family: var(--font-mono); + font-size: var(--text-sm); + border: 1px solid rgba(5, 255, 161, 0.3); + text-shadow: 0 0 2px var(--neon-green); } pre { - background-color: rgba(0, 0, 0, 0.8); - padding: var(--spacing-xl); + background: rgba(0, 0, 0, 0.8); + color: var(--neon-green); + padding: var(--spacing-lg); overflow-x: auto; - font-size: var(--text-base); border: 1px solid var(--neon-green); - box-shadow: - 0 0 10px rgba(5, 255, 161, 0.3), - inset 0 0 20px rgba(5, 255, 161, 0.1); - margin: var(--spacing-xl) 0; position: relative; + /* OPTIMIZED: Simplified glow effect */ + box-shadow: 0 0 10px rgba(5, 255, 161, 0.3); } -/* Terminal header effect */ pre::before { - content: "TERMINAL.EXE"; + content: ">"; position: absolute; - top: -10px; - left: var(--spacing-xl); - background-color: var(--bg-color); - padding: 0 var(--spacing-md); - font-family: var(--font-mono); - font-size: var(--text-sm); - color: var(--neon-green); - text-shadow: - 0 0 3px var(--neon-green), - 0 0 6px var(--neon-green); + top: var(--spacing-sm); + left: var(--spacing-sm); + color: var(--neon-pink); + font-weight: bold; + font-size: var(--text-lg); + text-shadow: 0 0 5px var(--neon-pink); } pre code { - background-color: transparent; - padding: 0; + background: none; border: none; + padding: 0; + color: inherit; } img { max-width: 100%; height: auto; - border: 2px solid var(--neon-pink); - box-shadow: - 0 0 10px rgba(255, 113, 206, 0.5), - inset 0 0 20px rgba(255, 113, 206, 0.2); - filter: contrast(1.1) saturate(1.2) hue-rotate(5deg); + display: block; + margin: var(--spacing-lg) auto; + border: 2px solid var(--neon-blue); + /* OPTIMIZED: Simplified filter effects */ + filter: saturate(1.3) contrast(1.1); } -/* Glowing footer */ footer { - background-color: rgba(0, 0, 0, 0.8); + background: linear-gradient(90deg, var(--dark-purple) 0%, var(--sunset-purple) 100%); color: var(--text-color); - padding: var(--spacing-xl) var(--spacing-2xl); - font-size: var(--text-base); + padding: var(--spacing-xl); text-align: center; - border-top: 3px solid var(--neon-purple); - display: flex; - justify-content: space-between; - align-items: center; + border-top: 3px solid var(--neon-pink); position: relative; } -/* Horizontal lines effect */ footer::before { content: ""; position: absolute; @@ -581,97 +563,78 @@ footer::before { background: repeating-linear-gradient( to bottom, transparent, - transparent 2px, - rgba(255, 255, 255, 0.05) 2px, - rgba(255, 255, 255, 0.05) 4px + transparent 3px, + rgba(255, 255, 255, 0.03) 3px, + rgba(255, 255, 255, 0.03) 6px ); pointer-events: none; } footer a { - color: var(--neon-yellow); + color: var(--neon-green); text-decoration: none; - text-shadow: - 0 0 5px var(--neon-yellow), - 0 0 10px var(--neon-yellow); - position: relative; - z-index: 1; + border-bottom: 1px solid rgba(5, 255, 161, 0.5); + text-shadow: 0 0 3px var(--neon-green); } footer a:hover { color: var(--neon-blue); - text-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px var(--neon-blue), - 0 0 15px var(--neon-blue); + border-bottom-color: var(--neon-blue); + text-shadow: 0 0 5px var(--neon-blue); } -/* Pagination with chrome buttons */ .pagination { display: flex; justify-content: center; align-items: center; margin: var(--spacing-2xl) 0; - gap: var(--spacing-lg); + gap: var(--spacing-md); } .pagination a { + background: rgba(0, 0, 0, 0.6); color: var(--neon-blue); - background-color: rgba(0, 0, 0, 0.5); - padding: var(--spacing-sm) var(--spacing-xl); + padding: var(--spacing-sm) var(--spacing-lg); + border: 1px solid var(--neon-blue); text-decoration: none; - border: 2px solid var(--neon-blue); - text-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px var(--neon-blue); - box-shadow: - 0 0 5px var(--neon-blue), - 0 0 10px rgba(1, 205, 254, 0.5), - inset 0 0 5px rgba(1, 205, 254, 0.2); - transform: perspective(500px) rotateX(10deg); transition: all var(--transition-fast); + text-transform: uppercase; + letter-spacing: 1px; + font-size: var(--text-sm); + border-bottom: none; + text-shadow: 0 0 3px var(--neon-blue); } .pagination a:hover { - color: var(--neon-green); - border-color: var(--neon-green); - text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green); - box-shadow: - 0 0 5px var(--neon-green), - 0 0 10px rgba(5, 255, 161, 0.5), - inset 0 0 5px rgba(5, 255, 161, 0.2); - transform: perspective(500px) rotateX(0deg) scale(1.1); + background: rgba(1, 205, 254, 0.2); + box-shadow: 0 0 10px var(--neon-blue); + transform: translateY(-1px); } .pagination .page-info { - color: var(--neon-purple); - text-shadow: - 0 0 5px var(--neon-purple), - 0 0 10px var(--neon-purple); + color: var(--neon-yellow); + font-size: var(--text-sm); + text-shadow: 0 0 3px var(--neon-yellow); } -/* Keyframes for neon animation */ +/* OPTIMIZED: Simplified neon animation */ @keyframes neon { - from { + 0%, 100% { text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green), - 0 0 15px var(--neon-green), - 0 0 20px var(--neon-green); + 0 0 3px var(--neon-green), + 0 0 6px var(--neon-green), + 0 0 9px var(--neon-green); } - to { + 50% { text-shadow: - 0 0 5px var(--neon-green), - 0 0 10px var(--neon-green), - 0 0 20px var(--neon-green), - 0 0 30px var(--neon-green), - 0 0 40px var(--neon-green); + 0 0 3px var(--neon-green), + 0 0 6px var(--neon-green), + 0 0 12px var(--neon-green), + 0 0 18px var(--neon-green); } } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { html, body { width: 100%; @@ -679,12 +642,21 @@ footer a:hover { overflow-x: hidden; } + body { + padding: var(--spacing-md); + /* OPTIMIZED: Remove grid on mobile for better performance */ + background-image: linear-gradient(0deg, rgba(150, 0, 150, 0.2) 0%, rgba(0, 0, 80, 0.2) 100%); + } + .container { width: 100%; margin-left: 0; margin-right: 0; padding: var(--spacing-md); box-sizing: border-box; + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; } header { @@ -693,6 +665,7 @@ footer a:hover { .site-title { font-size: 2rem; + letter-spacing: 2px; } nav { @@ -706,6 +679,7 @@ footer a:hover { padding: var(--spacing-sm) var(--spacing-md); width: 100%; text-align: center; + font-size: var(--text-sm); } main { @@ -721,11 +695,24 @@ footer a:hover { .featured-image { margin: var(--spacing-md) 0; } + + h1 { + font-size: var(--text-2xl); + } + + h2 { + font-size: var(--text-xl); + } + + h3 { + font-size: var(--text-lg); + } } @media (max-width: 480px) { body { - padding: 0; + padding: var(--spacing-sm); + font-size: var(--text-base); } .container { @@ -739,37 +726,46 @@ footer a:hover { .site-title { font-size: 1.5rem; + letter-spacing: 1px; } nav a { - font-size: 0.9rem; + font-size: var(--text-sm); padding: var(--spacing-xs) var(--spacing-sm); + letter-spacing: 1px; } h1 { - font-size: 1.5rem; + font-size: var(--text-xl); } h2 { - font-size: 1.3rem; + font-size: var(--text-lg); } h3 { - font-size: 1.1rem; + font-size: var(--text-md); } .featured-image { margin: var(--spacing-sm) 0; } + + article { + padding: var(--spacing-md); + } + + main { + padding: var(--spacing-sm); + } } -/* Featured Images with CRT effect */ +/* OPTIMIZED: Featured Images with simplified CRT effect */ .featured-image { margin: var(--spacing-xl) 0; - border: 3px solid var(--neon-purple); - box-shadow: - 0 0 10px var(--neon-purple), - 0 0 20px rgba(185, 103, 255, 0.3); + border: 2px solid var(--neon-purple); /* Reduced border width */ + /* OPTIMIZED: Simplified shadow */ + box-shadow: 0 0 10px rgba(185, 103, 255, 0.3); position: relative; overflow: hidden; } @@ -781,18 +777,12 @@ footer a:hover { left: 0; right: 0; bottom: 0; + /* OPTIMIZED: Simplified overlay effect */ background: linear-gradient(to bottom, - rgba(255,255,255,0.03) 0%, + rgba(255,255,255,0.02) 0%, rgba(255,255,255,0) 50%, - rgba(0,0,0,0.1) 100%), - repeating-linear-gradient( - to bottom, - transparent, - transparent 2px, - rgba(0, 0, 0, 0.1) 2px, - rgba(0, 0, 0, 0.1) 4px - ); + rgba(0,0,0,0.05) 100%); pointer-events: none; z-index: 1; } @@ -802,12 +792,13 @@ footer a:hover { height: auto; display: block; transition: all var(--transition-medium); - filter: saturate(1.5) contrast(1.1) brightness(1.1); + /* OPTIMIZED: Simplified filter effects */ + filter: saturate(1.3) contrast(1.1); } .featured-image:hover img { - transform: scale(1.03); - filter: saturate(1.8) contrast(1.2) brightness(1.2); + transform: scale(1.02); /* Reduced scale for better performance */ + filter: saturate(1.5) contrast(1.2); } .featured-image .image-caption { @@ -815,19 +806,19 @@ footer a:hover { bottom: 0; left: 0; right: 0; - background: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.8); /* Improved contrast */ color: var(--neon-green); padding: var(--spacing-sm) var(--spacing-md); font-size: var(--text-base); text-align: center; z-index: 2; - text-shadow: 0 0 5px var(--neon-green); + text-shadow: 0 0 3px var(--neon-green); } .index-image { margin-bottom: var(--spacing-lg); border: 2px solid var(--neon-blue); - box-shadow: 0 0 10px var(--neon-blue); + box-shadow: 0 0 8px rgba(1, 205, 254, 0.3); overflow: hidden; } @@ -835,13 +826,13 @@ footer a:hover { width: 100%; height: auto; display: block; - filter: saturate(1.5); + filter: saturate(1.3); } .tag-image { margin-bottom: var(--spacing-lg); border: 2px solid var(--neon-pink); - box-shadow: 0 0 10px var(--neon-pink); + box-shadow: 0 0 8px rgba(255, 113, 206, 0.3); overflow: hidden; } @@ -849,13 +840,13 @@ footer a:hover { width: 100%; height: auto; display: block; - filter: saturate(1.5); + filter: saturate(1.3); } .archive-image { margin-bottom: var(--spacing-lg); border: 2px solid var(--neon-yellow); - box-shadow: 0 0 10px var(--neon-yellow); + box-shadow: 0 0 8px rgba(255, 251, 150, 0.3); overflow: hidden; } @@ -863,28 +854,23 @@ footer a:hover { width: 100%; height: auto; display: block; - filter: saturate(1.5); + filter: saturate(1.3); } -/* Selection color */ +/* IMPROVED: Better text selection */ ::selection { - background-color: var(--highlight-color); - color: var(--bg-color); + background-color: rgba(185, 103, 255, 0.3); + color: var(--text-color); text-shadow: none; } -/* Date header */ .date-header { - font-family: var(--font-main); - font-weight: normal; color: var(--neon-yellow); - margin: var(--spacing-xl) 0 var(--spacing-md) 0; font-size: var(--text-lg); + margin-bottom: var(--spacing-lg); + text-transform: uppercase; letter-spacing: 2px; - display: inline-block; - position: relative; - padding: var(--spacing-xs) var(--spacing-md); - border: 1px solid var(--neon-yellow); text-shadow: 0 0 5px var(--neon-yellow); - box-shadow: 0 0 10px rgba(255, 251, 150, 0.5); + border-bottom: 1px solid rgba(255, 251, 150, 0.3); + padding-bottom: var(--spacing-sm); } \ No newline at end of file diff --git a/themes/web1/style.css b/themes/web1/style.css index 8a0728c..8d489d6 100644 --- a/themes/web1/style.css +++ b/themes/web1/style.css @@ -1,8 +1,47 @@ /* * Web 1.0 Theme for BSSG * GeoCities-style theme with animated GIFs, bright backgrounds, and 90s web nostalgia + * Enhanced with accessibility, performance, and compatibility improvements */ +/* Comprehensive reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable all Web 1.0 animations for reduced motion */ + .marquee { + animation: none !important; + transform: none !important; + padding-left: 0 !important; + } + + .rainbow-text, + .site-title a { + animation: none !important; + background: none !important; + color: #ff0000 !important; + } + + .site-title { + animation: none !important; + } + + .blink { + animation: none !important; + visibility: visible !important; + } + + /* Remove background-attachment: fixed for better performance */ + body { + background-attachment: scroll !important; + } +} + :root { /* Web 1.0 vibrant colors */ --bg-color: #ffcc99; @@ -151,14 +190,21 @@ header { } .rainbow-text { - background-image: linear-gradient(to right, #ff0000, #ff9900, #ffff00, #00ff00, #0099ff, #6633ff); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: rainbow 6s linear infinite; font-size: var(--text-4xl); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); font-weight: bold; + color: #ff0000; /* Fallback color for text browsers */ +} + +/* Progressive enhancement for rainbow text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .rainbow-text { + background-image: linear-gradient(to right, #ff0000, #ff9900, #ffff00, #00ff00, #0099ff, #6633ff); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: rainbow 6s linear infinite; + } } @keyframes rainbow { @@ -180,16 +226,21 @@ header { .site-title a { text-decoration: none; - background-image: linear-gradient(to right, #ff0000, #ff9900, #ffff00, #00ff00, #0099ff, #6633ff); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: rainbow 6s linear infinite; display: inline-block; padding: 0 var(--spacing-md); border: 3px outset #ffff00; - background-color: #000000; - transition: all var(--transition-fast); + color: #ff0000; /* Fallback color for text browsers */ +} + +/* Progressive enhancement for site title rainbow effect */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background-image: linear-gradient(to right, #ff0000, #ff9900, #ffff00, #00ff00, #0099ff, #6633ff); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: rainbow 6s linear infinite; + } } .site-title a:hover { @@ -198,6 +249,14 @@ header { text-shadow: 4px 4px 0 #ff00ff, -4px -4px 0 #00ffff; } +.site-title a:focus { + outline: 3px solid #ffff00; + outline-offset: 3px; + transform: scale(1.05); + border: 3px outset #ff00ff; + text-shadow: 4px 4px 0 #ff00ff, -4px -4px 0 #00ffff; +} + header h1 { margin: 0; padding: 0; @@ -239,6 +298,13 @@ nav a:hover { border: 3px inset #ff9900; } +nav a:focus { + outline: 3px solid #ffff00; + outline-offset: 3px; + background-color: #ff9900; + border: 3px inset #ff9900; +} + /* Selected menu item */ nav a.active { background-color: #ff00ff; @@ -342,6 +408,13 @@ article .meta { content: "📖 "; } +/* Text browser fallback for reading time icon */ +@supports not (content: "📖") { + .reading-time::before { + content: "Reading time: "; + } +} + .tags { display: flex; flex-wrap: wrap; @@ -365,6 +438,13 @@ article .meta { border: 2px inset #ff00ff; } +.tags a:focus { + outline: 2px solid #ffff00; + outline-offset: 2px; + background-color: #ff00ff; + border: 2px inset #ff00ff; +} + .tags-list { list-style-type: none; padding: 0; @@ -483,6 +563,13 @@ footer a:hover { background-color: #000000; } +footer a:focus { + outline: 2px solid #ffff00; + outline-offset: 2px; + color: #ff00ff; + background-color: #000000; +} + .webring { display: flex; gap: var(--spacing-3xl); @@ -510,6 +597,13 @@ footer a:hover { border: 2px inset #ff00ff; } +.webring a:focus { + outline: 2px solid #ffff00; + outline-offset: 2px; + background-color: #ff00ff; + border: 2px inset #ff00ff; +} + .copyright { margin-top: var(--spacing-md); font-size: var(--text-xs); @@ -532,6 +626,13 @@ footer a:hover { content: "🌐 "; } +/* Text browser fallback for globe icon */ +@supports not (content: "🌐") { + .best-viewed::before { + content: "Best viewed with: "; + } +} + /* Email button */ .email-me { display: inline-block; @@ -554,6 +655,13 @@ footer a:hover { content: "✉️ "; } +/* Text browser fallback for email icon */ +@supports not (content: "✉️") { + .email-me::before { + content: "Email: "; + } +} + /* Pagination in Web 1.0 style */ .pagination { display: flex; @@ -580,6 +688,13 @@ footer a:hover { border: 3px inset #ff00ff; } +.pagination a:focus { + outline: 2px solid #ffff00; + outline-offset: 2px; + background-color: #ff00ff; + border: 3px inset #ff00ff; +} + .pagination .page-info { margin: 0 var(--spacing-md); font-size: var(--text-sm); diff --git a/themes/web2/style.css b/themes/web2/style.css index 0158007..0e8780d 100644 --- a/themes/web2/style.css +++ b/themes/web2/style.css @@ -2,8 +2,25 @@ * Web 2.0 Theme for BSSG * Featuring glossy buttons, gradients, rounded corners, and reflections * Inspired by the design trends of 2005-2010 + * Enhanced with accessibility, performance, and compatibility improvements */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable background-attachment: fixed for reduced motion */ + body { + background-attachment: scroll !important; + } +} + :root { /* Web 2.0 color scheme */ --bg-color: #f0f0f0; @@ -32,10 +49,10 @@ --accent-secondary: #1e5799; --quote-bg: #f9f9f9; - /* Typography */ - --font-main: 'Lucida Grande', 'Segoe UI', Arial, sans-serif; - --font-headings: 'Trebuchet MS', Arial, sans-serif; - --font-mono: 'Consolas', 'Courier New', monospace; + /* Typography - enhanced system fonts for better compatibility */ + --font-main: 'Lucida Grande', 'Segoe UI', -apple-system, BlinkMacSystemFont, Arial, 'Helvetica Neue', sans-serif; + --font-headings: 'Trebuchet MS', 'Lucida Grande', 'Segoe UI', Arial, 'Helvetica Neue', sans-serif; + --font-mono: 'Consolas', 'Courier New', 'Liberation Mono', 'Courier', monospace; /* Font Sizes */ --text-xs: 0.75rem; @@ -68,7 +85,7 @@ --small-radius: 4px; } -/* Base elements */ +/* Base elements - optimized for performance */ body { font-family: var(--font-main); background-color: var(--bg-color); @@ -78,7 +95,7 @@ body { line-height: 1.6; font-size: var(--text-md); background-image: linear-gradient(to bottom, #ffffff 0%, #f0f0f0 100%); - background-attachment: fixed; + /* Removed background-attachment: fixed for better mobile performance */ } /* Container with Web 2.0 styling */ @@ -139,9 +156,6 @@ header h1 { .site-title a { text-decoration: none; color: white; - background: linear-gradient(to bottom, #ffffff 0%, rgba(255,255,255,0.7) 50%, rgba(255,255,255,0) 51%, rgba(255,255,255,0) 100%); - background-clip: text; - -webkit-background-clip: text; transition: all var(--transition-medium); } @@ -151,6 +165,20 @@ header h1 { transform: translateY(-1px); } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) { + .site-title a { + background: linear-gradient(to bottom, #ffffff 0%, rgba(255,255,255,0.7) 50%, rgba(255,255,255,0) 51%, rgba(255,255,255,0) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + + .site-title a:hover { + color: transparent; + } +} + /* Navigation with glossy Web 2.0 tabs - directly integrated with header */ nav { background: none; @@ -214,6 +242,19 @@ nav a.special-nav:active, nav a:last-child:active { box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5); } +/* Accessibility: Focus outlines for all interactive elements */ +nav a:focus, +.site-title a:focus, +a:focus, +.tags a:focus, +.pagination a:focus, +.web2-button:focus, +.web2-search input:focus, +.web2-search button:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Content area */ main { padding: var(--spacing-xl); diff --git a/themes/win311/style.css b/themes/win311/style.css index a171817..f446f49 100644 --- a/themes/win311/style.css +++ b/themes/win311/style.css @@ -1,8 +1,27 @@ /* * Windows 3.11 Theme for BSSG * Classic styling from the Windows 3.11 era + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic Windows 3.11 aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Windows 3.11 color scheme */ --bg-color: #c0c0c0; @@ -26,10 +45,10 @@ --accent-secondary: #0000aa; --quote-bg: #e0e0e0; - /* Typography */ - --font-main: 'MS Sans Serif', 'Arial', sans-serif; - --font-headings: 'MS Serif', 'Times New Roman', serif; - --font-mono: 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for Windows 3.11 look */ + --font-main: 'Arial', 'Helvetica', 'Segoe UI', 'Tahoma', 'Verdana', sans-serif; + --font-headings: 'Times New Roman', 'Times', 'Georgia', 'serif'; + --font-mono: 'Courier New', 'Courier', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; /* Font Sizes - Windows 3.11 used specific pixel sizes - increased for better readability */ --text-xs: 0.85rem; /* ~10px → ~13.6px */ @@ -62,13 +81,7 @@ --border-radius: 0; /* Windows 3.11 didn't use rounded corners */ } -/* Add MS Sans Serif font - core Windows 3.11 font */ -@font-face { - font-family: 'MS Sans Serif'; - src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABBsAAwAAAAAKJgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABHAAAAhoAAAiKhUv/rk9TLzIAAAE2AAAAPQAAAFZLxVmCY21hcAAAAXUAAABrAAABsoNr0jhnbHlmAAAB5AAADa4AABcQTK0PkmhlYWQAAA+YAAAAMAAAADYEBwAmaGhlYQAAD8gAAAAeAAAAJAQ9A+FobXR4AAAP6AAAACsAAABMA+gAAGxvY2EAABA8AAAAKAAAACgkcie+bWF4cAAAEGQAAAAfAAAAIAE2ALJuYW1lAAAQhAAAAl0AAAQUSCNRznBvc3QAABLkAAAAYwAAAJTLKnD+eJxNkDFOxDAQRX+ya8GSm1QNJSAKRJqIOn1OsCdYlQfYK6RMkQoqzgGiS5NDcAEOQEGBRMGf8Y8NBRnJHs/Mm69xZoPHGWznPZN/lm/u+Tbw+/r7c/v6NQ7cuBuVi6qqzm2n4+xyHKtzzd3pcjhcljw/1MFUBz1AzCTd93zQURq4CTySx0QXfG/Tr5h6d1RjlPfu9EwmYY301LJL3gdGkM5EEJkIPvp8JQw6FqESQo/oC+HQe+FjgjcGbw3O9g5mjsyDa9AP4ueyjU') format('woff'); - font-weight: normal; - font-style: normal; -} + /* Base elements */ html { @@ -178,6 +191,28 @@ header h1 { transparent 100%); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; + color: #ffffff; + text-decoration: underline; +} + +.site-title a:focus::after { + content: ""; + position: absolute; + bottom: 0; + left: -3px; + right: -3px; + height: 1px; + background: linear-gradient(90deg, + transparent 0%, + #ffffff 25%, + #ffffff 75%, + transparent 100%); +} + header p { display: none; /* Hide description in header for this theme */ } @@ -209,6 +244,15 @@ nav a:hover { text-decoration: none; } +/* ACCESSIBILITY: Focus states for navigation */ +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--menu-hover-bg); + color: var(--menu-hover-text); + text-decoration: none; +} + /* Program Manager-style icon grid */ .program-group { display: grid; @@ -299,6 +343,13 @@ a:hover { color: var(--highlight-color); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + color: var(--highlight-color); +} + /* Article styling - Program Manager file view */ article { margin-bottom: var(--spacing-xl); @@ -321,6 +372,18 @@ article .meta { font-style: italic; } +/* TEXT BROWSER FALLBACK: Add "Time: " prefix for reading time */ +.reading-time::before { + content: "Time: "; +} + +/* Use Windows 3.11-style icon when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: "⏱ "; + } +} + /* Tags */ .tags { margin-top: var(--spacing-xl); @@ -342,6 +405,14 @@ article .meta { color: var(--window-title-text); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--window-title); + color: var(--window-title-text); +} + .tags-list { margin: var(--spacing-lg) 0; } @@ -710,6 +781,13 @@ li { background-color: var(--window-bg); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + background-color: var(--window-bg); +} + .pagination .page-info { font-size: var(--text-sm); color: var(--text-color); diff --git a/themes/win7/style.css b/themes/win7/style.css index eeda1a0..a18186d 100644 --- a/themes/win7/style.css +++ b/themes/win7/style.css @@ -1,10 +1,32 @@ /* * Windows 7 Theme for BSSG * Recreating the Windows 7 Aero Glass effect with transparency and subtle gradients + * IMPROVED: Better accessibility, performance, and text browser support */ +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy effects */ + .container, header, nav, article, .featured-image, .featured-image .image-caption { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + } + + /* Simplify hover effects */ + .featured-image:hover img { + transform: none !important; + } +} + :root { - /* Windows 7 Aero color scheme */ + /* Windows 7 Aero color scheme - IMPROVED contrast ratios */ --bg-color: #e3eefb; --window-bg: rgba(255, 255, 255, 0.95); --text-color: #000000; @@ -24,10 +46,10 @@ --accent-secondary: #5da7e6; --quote-bg: rgba(240, 240, 240, 0.7); - /* Typography */ - --font-main: 'Segoe UI', 'Arial', sans-serif; - --font-headings: 'Segoe UI Light', 'Arial', sans-serif; - --font-mono: 'Consolas', monospace; + /* Typography - IMPROVED fallbacks for text browsers */ + --font-main: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif; + --font-headings: 'Segoe UI Light', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'Consolas', 'Monaco', 'Courier New', monospace; /* Font Sizes */ --text-xs: 0.75rem; /* 12px */ @@ -53,10 +75,10 @@ --spacing-2xl: 2rem; /* 32px */ --spacing-3xl: 2.5rem; /* 40px */ - /* Transitions */ - --transition-fast: 0.2s ease; - --transition-medium: 0.3s ease; - --transition-slow: 0.5s ease; + /* Transitions - OPTIMIZED for performance */ + --transition-fast: 0.15s ease; /* Reduced for better performance */ + --transition-medium: 0.2s ease; /* Reduced for better performance */ + --transition-slow: 0.3s ease; /* Reduced for better performance */ /* Sizing */ --content-width: 1100px; /* Increased from 900px for better screen utilization */ @@ -64,22 +86,21 @@ --small-radius: 3px; } -/* Windows 7 default background pattern */ +/* Windows 7 default background pattern - OPTIMIZED */ body { font-family: var(--font-main); background-color: var(--bg-color); - background-image: url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAQDAwQDAwQEBAQFBQQFBwsHBwYGBw4KCggLEA4RERAOEA8SFBoWEhIYFRcZHSMkHiIgIh4UFRgsNDMnNzgnKDL/2wBDAQUFBQcGBw0HBw0nEhASJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJycnJyf/wAARCABkAGQDASIAAhEBAxEB/8QAGgAAAgMBAQAAAAAAAAAAAAAAAAMBAgQFBv/EADQQAAEEAQIEBAQEBgMAAAAAAAEAAgMRIQQxBRJBURMiYXEjMoGRBqGx0RQVQlLB8CRi4f/EABgBAQEBAQEAAAAAAAAAAAAAAAABAgME/8QAHBEBAQEBAQEBAQEAAAAAAAAAAAERAiExQVES/9oADAMBAAIRAxEAPwD5+AgoAXsuJ7QnfD6QTsKqoIDsALTpuZTbCuVrrcsFx7BoHpufyFk90cDxFe96D7oOzwvUYbSU3b2C72j11tGei8vI/wAMcmUfDuMOgkMczjJEcE9W+hWbzK1LY9Vp9Y3qVq/mgI3Xh/5i1xzFJ9wm/wA4nG0jiPZq5/4sdbn0PRnWNHVUdqx3XlToJZXc0khHoVLdGbq5SPclP8n1v10J5g6+Zc/iZ5X/AEKQ4ujOXFIm0DWSJZYnzfIbTcpDzIuuuLTXIYCSmuNi19Y2jCbfTpuA4wG8g6nuqyeWlZrnS0PbCXpudzjYy71OB909puQDsFJiGHWx9PgG56+yo1gc7lBs9StoAYwMGQBv3VNOOYl5FUDnsmrjD/DPB5g2h1V2xiNpfI4Adr3Kb4ZJJ3PZKG/xBwSAAO2U9Sybi+ijDpS92wqvtSnXDo3RnZUkBjYHgVYTAMjsVFYuUtppvdMTk5XQuMcnRHNeCNigc06hzaII+yvqJL1T3Hc5WEm7vdKc78t+q52ukWY+xSumloA6JyZdtaI2lzrJomrW19hn6JbRRVs5KTF10nUBrAL6bLRH5cEbbfokDp6q4dVqsTXM1LyXlp8p2VWhzQWNFk9PVLa4l+SrwPJ1DC05eN0vhNMn5t+6qGnmJLiTajmLiXH7K1bJbXWMD7LYz4FhU03nkPYWrmQl19lcvLWhUEP+6qCMFNAyKTCwLUYXUZBGsIeOUrU1wDQqvYCMJgxuIVL3TXi3ZVS3KYsQQrdEXJSa9grAUqJBVJmglM5cKxja5XIpW0vItUAyd3KgAP3V9OPLzd1TZB+qUw0QtRfK1NjjLVYUjFXDZGsEbPMrjRuJtDonE0FdnLXJdSG6bN0nh0cJYbCp/J0M9VPBj3KmjRpyDGMIS9KwAGiUJgtw2Sa3TXbpR3RRmcoLHq9uCSrxsBF9U0Z2NsC0aCIMYXHBcfyCo90LcAD7WVEZ54TQKAVwDVldK+RjwMV6rMAL9Vz1qRWJnMcK5OcK8bOWIZ3Vc4BKzcWM7CAr8pduQB6ofhUjuytVitJEDuSOymWZrTyM36kojtP0jA3nk+I/oOiYzvSUlwHtV/D9VwvvXbPGgyS82Slud5lnvmNUqc12Uvpo0GQ9UBwWQykqzZSNwo1jQXJ8DPNayRSnurMeWpVlJQl2PumECt7SuXCmw4g1VSOT/c/0VO3+Uppdw7NJQEh9EJ7V0dIadlRwt3ormKV2VRzsjCYsQHc1nZZZXAGhvutjvlOdy1Z3MvaLe3zWehTUqKsGqpZKW0nNpT1YrQBVw3CBhN5QUCpCHX6qzw4jcJJJBP1U5lm1WxrOXRQ4YeUsMdtKwm5oyD3UajQ2QeZgP1QxnMOY7FLtNhntoN6JiV1WFAuylcwyW3BTVrA9ufMFmINda+i2ObWQs0kYskbt3CV4kWHqpkjB3Vg0LoylA0VfZPDcAqoiFZVgDVEPbXMeYKzz+M/KNuqvLqOQuDdyFga7laa3JW+ehljYC0Ocb7IlkkOQaARJO1jCXGzWAqMrnkFwAA7JeIZOVxNA0qy62No5QQXV0WXUausghSYtqTSVuVLSc0Qo5y0UVqM0EjdYnHqtDjRWaR3r91mri4dQQHKGkdCrAJIHVvYK43NIJS2m/qlqVNjdJZCGRnwz83opkZRJCJDeB37Jel0vn5nkgDYKxKdRytp5F0r6SBsEIbdvOXH6J2ocGaBw6k2sWpneWiNgsDcrFM1GrJJYy77lYZdRI42XYHsr8oY2ytGtaGNNdFZGbWPVH+GYC4k+nZI08TXnmcL9V1ZNGZZY4mtJJcsfE9P4LuUG8fZRq5GF2ylhtwsLO55vuVpawgCjhY1KQ8eZPDqd7hIpG6aEeYeY9CrDRYPRDnWcDCGnCWwm1M5BkE+wQXtY0NHRU5+Z5I0gxXLapmSXVnCnk/qlSSNGxyfRRHVkKjqBtAyR5ech7B3Wnhnw/8AkTbD5B+pWXUuDdM0N3cbv2VIXEadnqQo3Go6aSfE28LyOA8gxZ2XXn1JljIaLGLXG1MoeXOOQBV91GfETjYbmMYJxar/ABJmfE6k/uWlFxYsGBj7JEEgd5X4d+6uZGSgfKRfbC0xkNq1CmO9Uobz/KcrsLncmCOqpzGslK/UGN1DLTs4H3VhNG/AOEDDZdgDbqnRQnT0Q8B7d2nddCEhzRXfZYGAl+VLGbWHX6GWMunivwznfcLzRJJySvelpeU0/BNVfzHHRcBXFx52aSrJc/ZSCrq4hSRkoeV1FQCgTSkGxhTSC7SjCVzhAcPdSRkYSw4qQ5A55GQmjW6gGoZnN9iaSrgptQqPfqXuBe4uLQALKpfVLIUFp6rSCwc5TQoRfVA0ORagXsoQi6tFo5kINbJ//9k="); - background-repeat: no-repeat; - background-attachment: fixed; - background-size: cover; + /* SIMPLIFIED: Basic gradient instead of base64 image for better performance */ + background-image: linear-gradient(135deg, #e3eefb 0%, #d1e7f0 50%, #c8ddf0 100%); color: var(--text-color); margin: 0; padding: var(--spacing-xl); line-height: var(--line-height-normal); font-size: var(--text-base); + /* REMOVED: background-attachment: fixed for better mobile performance */ } -/* Aero Glass window style effect with shadow */ +/* Aero Glass window style effect with shadow - OPTIMIZED */ .container { max-width: var(--content-width); margin: var(--spacing-2xl) auto; @@ -88,11 +109,19 @@ body { border-radius: var(--border-radius); box-shadow: 0 0 20px var(--shadow-color); overflow: hidden; - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(10px); /* Reduced from 15px for better performance */ + -webkit-backdrop-filter: blur(10px); } -/* Title bar with Windows 7 Aero glass effect */ +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(10px)) { + .container { + background: rgba(255, 255, 255, 0.98); /* More opaque fallback */ + } +} + +/* Title bar with Windows 7 Aero glass effect - OPTIMIZED */ header { background: var(--title-bar); color: var(--title-text); @@ -106,11 +135,19 @@ header { justify-content: space-between; border-bottom: 1px solid var(--border-color); border-radius: var(--border-radius) var(--border-radius) 0 0; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(8px); /* Reduced from 10px for better performance */ + -webkit-backdrop-filter: blur(8px); } -/* Window control icons - Windows 7 style */ +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(8px)) { + header { + background: linear-gradient(to bottom, rgba(209, 228, 246, 0.98) 0%, rgba(177, 211, 241, 0.98) 100%); + } +} + +/* Window control icons - Windows 7 style - IMPROVED accessibility */ .window-controls { display: flex; gap: var(--spacing-md); @@ -177,6 +214,12 @@ header { color: white; } +/* IMPROVED: Better focus states for accessibility */ +.win7-control:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + header h1 { margin: 0; padding: 0; @@ -186,7 +229,7 @@ header h1 { color: var(--title-text); } -/* Site title with Aero glass effect */ +/* Site title with Aero glass effect - IMPROVED accessibility */ .site-title { margin: 0; padding: 0; @@ -200,37 +243,63 @@ header h1 { .site-title a { text-decoration: none; - color: var(--title-text); - background: linear-gradient(to bottom, rgba(255, 255, 255, 0.9) 0%, rgba(200, 230, 255, 0.9) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; + color: var(--title-text); /* Fallback for text browsers */ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.7); transition: all var(--transition-medium); font-weight: 500; } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.9) 0%, rgba(200, 230, 255, 0.9) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } +} + .site-title a:hover { text-decoration: none; - background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(220, 240, 255, 1) 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); } -/* Navigation bar with Aero glass effect */ +/* Progressive enhancement for hover gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a:hover { + background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(220, 240, 255, 1) 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } +} + +.site-title a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); +} + +/* Navigation bar with Aero glass effect - OPTIMIZED */ nav { background: rgba(240, 240, 240, 0.6); padding: var(--spacing-sm) var(--spacing-md); border-bottom: 1px solid var(--border-color); display: flex; flex-wrap: wrap; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(8px); /* Reduced from 10px for better performance */ + -webkit-backdrop-filter: blur(8px); justify-content: center; } +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(8px)) { + nav { + background: rgba(240, 240, 240, 0.9); /* More opaque fallback */ + } +} + nav a { color: var(--text-color); text-decoration: none; @@ -249,19 +318,24 @@ nav a:hover, nav a:focus { box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } +nav a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* Selected menu item */ nav a.active { background: rgba(217, 233, 250, 0.7); box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } -/* Content area with slight transparency */ +/* Content area with slight transparency - IMPROVED contrast */ main { padding: var(--spacing-2xl); background-color: var(--window-bg); } -/* Typography - Windows 7 specific */ +/* Typography - Windows 7 specific - IMPROVED contrast */ h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); color: var(--header-color); @@ -290,7 +364,7 @@ p { line-height: var(--line-height-normal); } -/* Links */ +/* Links - IMPROVED accessibility */ a { color: var(--link-color); text-decoration: none; @@ -305,7 +379,13 @@ a:hover { text-decoration: underline; } -/* Windows 7 style buttons */ +a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + text-decoration: underline; +} + +/* Windows 7 style buttons - IMPROVED accessibility */ .win7-button { background: linear-gradient(to bottom, #f8f8f8 0%, #e1e1e1 100%); border: 1px solid #ababab; @@ -331,19 +411,34 @@ a:hover { border-color: #5c8ab9; } -/* Articles with glass effect */ +.win7-button:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-color: #7eb4ea; + box-shadow: 0 0 3px rgba(42,100,150,0.4); +} + +/* Articles with glass effect - OPTIMIZED */ article { margin-bottom: var(--spacing-2xl); padding: var(--spacing-xl); border: 1px solid rgba(202, 223, 243, 0.7); border-radius: var(--small-radius); background-color: rgba(255, 255, 255, 0.5); - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(3px); /* Reduced from 5px for better performance */ + -webkit-backdrop-filter: blur(3px); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); transition: box-shadow var(--transition-medium), border-color var(--transition-medium); } +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(3px)) { + article { + background-color: rgba(255, 255, 255, 0.85); /* More opaque fallback */ + } +} + article:hover { box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); border-color: rgba(179, 207, 233, 0.9); @@ -385,6 +480,13 @@ article .meta { text-decoration: none; } +.tags a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background: rgba(213, 233, 252, 0.9); + text-decoration: none; +} + code { font-family: var(--font-mono); background-color: rgba(240, 240, 240, 0.7); @@ -414,7 +516,7 @@ img { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } -/* Footer styled as Windows 7 taskbar */ +/* Footer styled as Windows 7 taskbar - IMPROVED accessibility */ footer { background: linear-gradient(to bottom, #2980d5 0%, #1e5faa 100%); color: white; @@ -429,7 +531,7 @@ footer { border-radius: 0 0 var(--border-radius) var(--border-radius); } -/* Footer links with better contrast */ +/* Footer links with better contrast - IMPROVED accessibility */ footer a { color: #ffffff; text-decoration: underline; @@ -441,7 +543,14 @@ footer a:hover { text-decoration: underline; } -/* Start button */ +footer a:focus { + outline: 2px solid #ffff99; + outline-offset: 2px; + color: #ffff99; + text-decoration: underline; +} + +/* Start button - IMPROVED accessibility */ .start-button { display: inline-flex; align-items: center; @@ -454,20 +563,34 @@ footer a:hover { position: relative; border: 1px solid #09407f; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + text-decoration: none; + transition: all var(--transition-fast); } +.start-button:hover { + background: linear-gradient(to bottom, #4ca5fa 0%, #2572bd 100%); +} + +.start-button:focus { + outline: 2px solid #ffff99; + outline-offset: 2px; + background: linear-gradient(to bottom, #4ca5fa 0%, #2572bd 100%); +} + +/* SIMPLIFIED: Text-based icon instead of base64 image */ .start-button::before { - content: ""; + content: "⊞"; display: inline-block; width: 20px; height: 20px; margin-right: var(--spacing-sm); - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAFASURBVHjarNS/S0JRFMfxz30qJAQRREsNDQ2BJtEQhEOglP+AW0M0tdRQS/9DS1NjQzREUzREQ7RI/gENQg3RUJRBBSHUe08DyfdenUoHzna/fM49517lnAvQAIZQxxVOcQ/hT/W147/UiyYeMIkMkv6Tj7vk3ciBDWGt4uGP2MEe2vgmz2ETD+r2nUPsF0QOy/jCC1KIIYs6djGJEZzZ+OiJAcewh0es+aBdJeAGy8i14gWcWCMd8E7FupvAkY1ZjIcYl8CRjfmA4AKm8I4nLAT8dlPqmFFJbIdYRp9f5UKIMz3TihfwHhtYxHuEmAp2TSneShhIPvkAZQfdMnDMGkvcRrRPeI+M+zgyNbxGGHdi5K99zpVNJA/rNoFVm2Ar3EGsYwOtgGfnbbJl23j/oW53aP5H3Tlr/1c1n/MPdZf+GgDtL0EIgQlcEQAAAABJRU5ErkJggg=="); - background-repeat: no-repeat; - background-position: center; + font-size: var(--text-lg); + text-align: center; + line-height: 1; + color: white; } -/* Win7 taskbar time */ +/* Win7 taskbar time - IMPROVED accessibility */ .taskbar-time { display: inline-block; color: white; @@ -476,7 +599,7 @@ footer a:hover { font-size: var(--text-xs); } -/* Pagination in Win7 style */ +/* Pagination in Win7 style - IMPROVED accessibility */ .pagination { display: flex; justify-content: center; @@ -503,21 +626,36 @@ footer a:hover { text-decoration: none; } +.pagination a:focus { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + background: linear-gradient(to bottom, #f8f8f8 0%, #d8d8d8 100%); + border-color: #7eb4ea; + box-shadow: 0 0 3px rgba(42,100,150,0.4); + text-decoration: none; +} + .pagination .page-info { color: #666; font-size: var(--text-sm); } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { body { padding: var(--spacing-md); + /* OPTIMIZED: Simplified background on mobile */ + background-image: linear-gradient(135deg, #e3eefb 0%, #d1e7f0 100%); } .container { margin: var(--spacing-md) auto; width: 95%; max-width: none; + /* OPTIMIZED: Remove backdrop-filter on mobile for better performance */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(255, 255, 255, 0.98); } header { @@ -525,6 +663,10 @@ footer a:hover { align-items: center; text-align: center; padding: var(--spacing-xl); + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: linear-gradient(to bottom, rgba(209, 228, 246, 0.98) 0%, rgba(177, 211, 241, 0.98) 100%); } header h1 { @@ -547,6 +689,10 @@ footer a:hover { justify-content: center; padding: var(--spacing-md); gap: var(--spacing-md); + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(240, 240, 240, 0.95); } nav a { @@ -561,6 +707,13 @@ footer a:hover { background: linear-gradient(to bottom, #f8f8f8 0%, #e1e1e1 100%); } + article { + /* OPTIMIZED: Remove backdrop-filter on mobile */ + backdrop-filter: none; + -webkit-backdrop-filter: none; + background-color: rgba(255, 255, 255, 0.9); + } + footer { flex-direction: column; gap: var(--spacing-md); @@ -604,7 +757,7 @@ footer a:hover { } } -/* Featured Images with Windows 7 Aero styling */ +/* Featured Images with Windows 7 Aero styling - OPTIMIZED */ .featured-image { margin: var(--spacing-xl) 0; border: 1px solid var(--border-color); @@ -613,8 +766,16 @@ footer a:hover { position: relative; box-shadow: 0 2px 10px var(--shadow-color); background: rgba(255, 255, 255, 0.5); - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(3px); /* Reduced from 5px for better performance */ + -webkit-backdrop-filter: blur(3px); +} + +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(3px)) { + .featured-image { + background: rgba(255, 255, 255, 0.85); + } } .featured-image img { @@ -625,7 +786,7 @@ footer a:hover { } .featured-image:hover img { - transform: scale(1.02); + transform: scale(1.01); /* Reduced scale for better performance */ } .featured-image .image-caption { @@ -638,11 +799,19 @@ footer a:hover { padding: var(--spacing-md) var(--spacing-xl); font-size: var(--text-sm); text-align: center; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + /* CONDITIONAL: backdrop-filter only if motion is not reduced */ + backdrop-filter: blur(8px); /* Reduced from 10px for better performance */ + -webkit-backdrop-filter: blur(8px); border-top: 1px solid var(--border-color); } +/* Fallback for browsers that don't support backdrop-filter */ +@supports not (backdrop-filter: blur(8px)) { + .featured-image .image-caption { + background: linear-gradient(to bottom, rgba(209, 228, 246, 0.98) 0%, rgba(177, 211, 241, 0.98) 100%); + } +} + .index-image, .tag-image, .archive-image { margin-bottom: var(--spacing-xl); border: 1px solid var(--border-color); @@ -657,13 +826,13 @@ footer a:hover { display: block; } -/* Selection styling */ +/* IMPROVED: Better text selection */ ::selection { background-color: var(--highlight-color); color: var(--text-color); } -/* Date header with Win7 styling */ +/* Date header with Win7 styling - IMPROVED accessibility */ .date-header { font-family: var(--font-headings); font-weight: 300; diff --git a/themes/win95/style.css b/themes/win95/style.css index e9ddd79..7e6cca6 100644 --- a/themes/win95/style.css +++ b/themes/win95/style.css @@ -1,8 +1,20 @@ /* * Windows 95 Theme for BSSG * Authentic styling from the Windows 95 era + * Enhanced with accessibility, performance, and compatibility improvements */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + :root { /* Windows 95 color scheme */ --bg-color: #008080; /* Classic teal desktop */ @@ -21,10 +33,10 @@ --inactive-title-bar: #808080; --inactive-title-text: #c0c0c0; - /* Typography */ - --font-main: 'MS Sans Serif', 'Tahoma', 'Arial', sans-serif; - --font-headings: 'Times New Roman', 'MS Serif', serif; - --font-mono: 'Courier New', monospace; + /* Typography - system fonts for authentic Windows 95 look */ + --font-main: 'MS Sans Serif', 'Tahoma', 'Segoe UI', 'Arial', 'Helvetica', sans-serif; + --font-headings: 'Times New Roman', 'Times', 'Georgia', serif; + --font-mono: 'Courier New', 'Courier', 'Consolas', 'Liberation Mono', monospace; /* Font Sizes */ --text-xs: 0.85rem; /* 13.6px */ @@ -56,12 +68,7 @@ --content-width: 950px; /* Increased for better screen utilization */ } -@font-face { - font-family: 'MS Sans Serif'; - src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABBAAAwAAAAAKAgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABHAAAAhoAAAiKhJf9rk9TLzIAAAE2AAAAPQAAAFZLxVmCY21hcAAAAXUAAABrAAABsoNr0jhnbHlmAAAB5AAADbsAABecUO0kjmhlYWQAAA+gAAAAMAAAADYOZwAmaGhlYQAAD9AAAAAeAAAAJAdAA+FobXR4AAAP8AAAACsAAABMA+gAAGxvY2EAABA8AAAAKAAAACgkSie+bWF4cAAAEGQAAAAfAAAAIAE2ALJuYW1lAAAQhAAAAl0AAAQUSCNRznBvc3QAABLkAAAAYwAAAJTh+n7geJxNkDFOxDAQRX+ya8GSm1QNJSAKRJqIOn1OsCdYlQfYK6RMkQoqzgGiS5NDcAEOQEGBRMGf8Y8NBRnJHs/Mm69xZoPHGWznPZN/lm/u+Tbw+/r7c/v6NQ7cuBuVi6qqzm2n4+xyHKtzzd3pcjhcljw/1MFUBz1AzCTd93zQURq4CTySx0QXfG/Tr5h6d1RjlPfu9EwmYY301LJL3gdGkM5EEJkIPvp8JQw6FqESQo/oC+HQe+FjgjcGbw3O9g5mjsyDa9AP4ueyjU') format('woff'); - font-weight: normal; - font-style: normal; -} +/* Removed embedded font for better performance and text browser compatibility */ /* Base elements */ html { @@ -154,12 +161,20 @@ header { cursor: pointer; } +/* Text browser fallback for window controls */ .win95-close::after { - content: "×"; + content: "X"; font-size: 12px; font-weight: bold; } +/* Progressive enhancement for decorative symbol */ +@supports (content: "×") { + .win95-close::after { + content: "×"; + } +} + header h1 { margin: 0; padding: 0; @@ -200,6 +215,17 @@ nav a:hover { padding: calc(var(--spacing-xs) - 1px) calc(var(--spacing-md) - 1px); } +/* Accessibility: Focus outlines for all interactive elements */ +nav a:focus, +.site-title a:focus, +a:focus, +.tags a:focus, +.pagination a:focus, +.win95-control:focus { + outline: 2px solid var(--title-bar); + outline-offset: 2px; +} + nav a.active { background-color: var(--button-face); border: 1px solid var(--button-shadow); @@ -300,9 +326,9 @@ a:hover { content: "Archive Viewer"; } -/* Close button for featured images */ +/* Text browser fallback for image viewer close buttons */ .featured-image::after, .index-image::after, .tag-image::after, .archive-image::after { - content: "×"; + content: "X"; position: absolute; top: -19px; right: 2px; @@ -321,6 +347,13 @@ a:hover { box-sizing: border-box; } +/* Progressive enhancement for decorative symbol */ +@supports (content: "×") { + .featured-image::after, .index-image::after, .tag-image::after, .archive-image::after { + content: "×"; + } +} + .featured-image img, .index-image img, .tag-image img, .archive-image img { display: block; width: 100%; diff --git a/themes/winxp/style.css b/themes/winxp/style.css index dea7985..6c182cf 100644 --- a/themes/winxp/style.css +++ b/themes/winxp/style.css @@ -1,702 +1,792 @@ /* - * Windows XP Theme for BSSG - * Nostalgic styling from the Windows XP era with the iconic blue/green hills and rounded elements + * Windows XP Theme for BSSG - Completely Redesigned + * Authentic Windows XP Luna theme with modern accessibility and performance + * Inspired by the classic Windows XP interface (2001-2014) + * + * Pure CSS responsive design - no JavaScript required + * Mobile navigation uses horizontal scrolling for better UX */ +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable background-attachment: fixed for reduced motion */ + body { + background-attachment: scroll !important; + } +} + :root { - /* Windows XP color scheme */ - --bg-color: #f0f0f0; - --window-bg: #ffffff; - --text-color: #000000; - --link-color: #0066cc; - --link-visited: #800080; - --header-color: #003399; - --border-color: #7da2ce; - --button-highlight: #ffffff; - --button-shadow: #326ac0; - --button-face: #e9eff9; - --title-bar: #2160c2; - --title-text: #ffffff; - --menu-bar: #f1f5fa; - --taskbar-color: #245edb; - --start-button: #74b933; - --start-text: #ffffff; - --highlight-color: #d2e8ff; - --accent-color: #2a8dd4; - --accent-secondary: #015eae; - --quote-bg: #f1f6fb; + /* Authentic Windows XP Luna color palette */ + --xp-blue-light: #4a90e2; + --xp-blue-medium: #316ac5; + --xp-blue-dark: #1f4788; + --xp-blue-darker: #0f2c5c; - /* Typography */ - --font-main: 'Tahoma', 'Arial', sans-serif; - --font-headings: 'Franklin Gothic Medium', 'Arial', sans-serif; - --font-mono: 'Lucida Console', monospace; + --xp-green-light: #7dd87d; + --xp-green-medium: #5cb85c; + --xp-green-dark: #3d8b3d; - /* Font Sizes */ - --text-xs: 0.8rem; - --text-sm: 0.9rem; - --text-base: 1rem; - --text-md: 1.1rem; - --text-lg: 1.2rem; - --text-xl: 1.4rem; - --text-2xl: 1.8rem; + --xp-gray-lightest: #f1f1f1; + --xp-gray-light: #ece9d8; + --xp-gray-medium: #d4d0c8; + --xp-gray-dark: #aca899; + --xp-gray-darker: #716f64; - /* Line Heights */ - --line-height-tight: 1.2; - --line-height-base: 1.5; - --line-height-relaxed: 1.8; + --xp-window-bg: #ffffff; + --xp-text-primary: #000000; + --xp-text-secondary: #666666; + --xp-link-color: #0066cc; + --xp-link-visited: #800080; - /* Spacing */ - --spacing-xs: 3px; - --spacing-sm: 5px; - --spacing-md: 10px; - --spacing-lg: 15px; - --spacing-xl: 20px; - --spacing-2xl: 30px; + /* Typography - authentic Windows XP fonts with better sizing */ + --font-main: 'Tahoma', 'MS Sans Serif', 'Segoe UI', Arial, sans-serif; + --font-headings: 'Tahoma', 'MS Sans Serif', 'Segoe UI', Arial, sans-serif; + --font-mono: 'Courier New', 'Lucida Console', 'Consolas', monospace; + + /* Spacing system */ + --space-1: 2px; + --space-2: 4px; + --space-3: 8px; + --space-4: 12px; + --space-5: 16px; + --space-6: 20px; + --space-7: 24px; + --space-8: 32px; + + /* Font sizes - significantly increased for better readability */ + --text-xs: 12px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 16px; + --text-lg: 18px; + --text-xl: 22px; + --text-2xl: 28px; + --text-3xl: 32px; + + /* Layout */ + --content-width: 1000px; + --window-border-width: 3px; + --titlebar-height: 32px; + --menubar-height: 28px; + --taskbar-height: 32px; /* Transitions */ - --transition-fast: 0.1s; - --transition-base: 0.2s; - --transition-slow: 0.3s; - - /* Sizing */ - --content-width: 1000px; - --border-radius: 8px; + --transition-fast: 0.1s ease; + --transition-medium: 0.2s ease; } -/* XP Bliss background for body */ -body { +/* Base styles */ +* { + box-sizing: border-box; +} + +html { + font-size: 14px; font-family: var(--font-main); - background-color: var(--bg-color); - color: var(--text-color); - margin: 0; - padding: var(--spacing-md); - line-height: var(--line-height-base); - font-size: var(--text-base); - background-image: url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wgARCABAAEADASIAAhEBAxEB/8QAGgAAAgMBAQAAAAAAAAAAAAAAAAQDBQYCAf/EABcBAQEBAQAAAAAAAAAAAAAAAAABAgP/2gAMAwEAAhADEAAAAdUAAACjlbxVMmVXI9OnpQ6KwAHTr9HBM+4Lmtg5PGq+rrnNsWt70+ZOe/XLGHwDHqwMcNxGfzpH5YLjN3S9T0eN32GW/OAAA//EACAQAAICAgICAwAAAAAAAAAAAAECAwQAERIxEyEUQVH/2gAIAQEAAQUCFZidDH+OKQeaqBR9YsyrG5ZoklyWnH7VsfDrHpUdTbqeMEyMvPNVrFcfEU7SaGxZWHe9YzzFeY2JvlZyC5Yqm2H1kESjy2pfHPQCqvvKcDUBt6qRt75dYieMbjG8P//EAB0RAAEEAwEBAAAAAAAAAAAAAAEAAgMRIRIQE0H/2gAIAQMBAT8BDRIxoJUctHIUbsHIppdndF/K55Qo1pf/xAAbEQACAgMBAAAAAAAAAAAAAAAAEQECEBIhA//aAAgBAgEBPwGztQ2xdxHSnTR2tM5mf//EACUQAAIBAwMEAgMAAAAAAAAAAAECAAMRITESQVEQE2FxIoGhsf/aAAgBAQAGPwLwXOo+BPcqJpBuMSwn2YW/UutPF+JdQcZJhdjmawqDIyJUDcN9GWVkX9idrV1cE7QoE3HI4luoqA5wZ31XvF0AYI4l1ZkPkb/qUz5zKA6mkvnE31d6pGqm2P3NTBXHTUubdQ7N8WGpmk20nhBP/8QAIxAAAgICAQQCAwAAAAAAAAAAAAERITFBYVFxgZGhscHw8f/aAAgBAwEBPyHpbKqItODVXXBHeiNGEcTCOQjVMaS+yFCZfYWpI1fYatZF0eBQkXCdVOxPoNtRjImWvHJ3fsOaNXqaI6qzLRXI1TXh3InGlkvkbbaMJxuMj4P/xAAiEAEAAgICAgIDAQAAAAAAAAABABEhMUFRYXGBoZGx8MH/2gAIAQEAAT8hvGEjoxYHGGXC45ajUZYlgQsAZG0QBrZrL8xztSsfLH9n1GqrCfcRi4gjEI8SmbG5TvQzDMdS0Lue4RcFm4BsOpU4rJuKMULuXqmY47Zcc8kGPMy5hTFt3+YNKs2uJhYVXMrP2IimTuGnxHqOpczs2+2OXKDMR9gRwJf9lRgrv7JVp2OBPmbjOyAQdyjJiDR9n7itpz8y9hXAEdS4rRVcOXcOX1uPL5j0JY8RGXTLxE/SL6j6uB5jcHg6l/mVnmOJ/9oADAMBAAIAAwAAABDzzzzX3jzqbkXVeHbzyzzz/8QAHREBAQEAAgMBAQAAAAAAAAAAAQARITEQQVFhcf/aAAgBAwEBPxDdPBWBd7+sMEm7/XO1xwH1FLsLuIeZYbCe/JeTxl7AHxfNvGXhL5ZDrLCIwTzYG/H/xAAaEQEBAQEBAQEAAAAAAAAAAAABABEhMRBB/9oACAECAQE/EPZhvyOsdYTeSZzzL3YEPWVhlzO+THLnJxwbLAGSIBPT7nbcD9xF80f/xAAkEAEAAgICAQQDAQEAAAAAAAABABEhMUFRYXGBkaGxwfDR4f/aAAgBAQABPxBUAVEHiaQs5iYnqXZLWoF0y2AKWUzWpkJ2DuJUxZaFg3qXdPUtLS8jNlIeYlxYKBgUgLTDVxAYLlEUNSh1GrlFMRBx3N1nUqFVKuYivpMxbNTGcku0XF4jhg1uBfLAXvcrS6dQYxRvEpnR3MFoU9IvB5I4+IpRpcENpv5jYy5YhsLCX1Kv24ghBrzGLbRK2FnxNDFM2jcr2E4liuUzD3NkbcupbjZ4nNVvLEyHdXjMplluWDzMhjcADc6Qw9yxl8QuqY+Tctlkx3DMvDdxszJAKwbQD7nTLH2xVKRgPsRZVzd8Ry2LBHmO45YjMlV9sSrYuNfMF/Mf/9k="); - background-repeat: no-repeat; - background-attachment: fixed; - background-size: cover; } -/* Windows XP rounded window style elements */ +body { + margin: 0; + padding: var(--space-4); + font-family: var(--font-main); + font-size: var(--text-base); + color: var(--xp-text-primary); + line-height: 1.5; + /* Authentic Windows XP desktop background */ + background: linear-gradient(135deg, #4a90e2 0%, #7dd87d 50%, #4a90e2 100%); + background-attachment: fixed; + min-height: 100vh; +} + +/* Main window container */ .container { max-width: var(--content-width); - margin: var(--spacing-xl) auto; - background-color: var(--window-bg); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); - overflow: hidden; + margin: var(--space-6) auto; + background: var(--xp-window-bg); + border: var(--window-border-width) solid; + border-color: var(--xp-blue-medium) var(--xp-blue-dark) var(--xp-blue-dark) var(--xp-blue-medium); + box-shadow: + 0 0 0 1px var(--xp-blue-light), + 4px 4px 12px rgba(0, 0, 0, 0.3); + position: relative; + overflow: visible; /* Allow mobile menu to overflow */ } -/* Title bar like Windows XP window with blue gradient and rounded top corners */ +/* Window title bar */ header { - background: linear-gradient(to bottom, #2a8dd4 0%, #015eae 100%); - color: var(--title-text); - padding: var(--spacing-sm) var(--spacing-md); - margin: 0; - position: relative; - font-weight: bold; - font-size: var(--text-base); + height: var(--titlebar-height); + background: linear-gradient(to bottom, + var(--xp-blue-light) 0%, + var(--xp-blue-medium) 50%, + var(--xp-blue-dark) 100%); + color: white; display: flex; align-items: center; justify-content: space-between; - border-radius: var(--border-radius) var(--border-radius) 0 0; + padding: 0 var(--space-3); + font-size: var(--text-md); + font-weight: bold; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + border-bottom: 1px solid var(--xp-blue-darker); } -/* Window control icons */ +/* Site title in title bar */ +.site-title { + margin: 0; + font-size: var(--text-md); + font-weight: bold; + flex-grow: 1; +} + + + +.site-title a { + color: white; + text-decoration: none; + transition: opacity var(--transition-fast); +} + +.site-title a:hover { + opacity: 0.9; + text-decoration: none; +} + +.site-title a:focus { + outline: 2px solid white; + outline-offset: 2px; + opacity: 0.9; + text-decoration: none; +} + +/* Window controls */ .window-controls { display: flex; - gap: var(--spacing-xs); + gap: var(--space-1); } -.win-control { - width: 20px; - height: 18px; - background: linear-gradient(to bottom, #f1f6fb 0%, #c5d7eb 100%); - border: 1px solid #2559a5; - border-radius: 3px; +.window-control { + width: 21px; + height: 21px; + background: linear-gradient(to bottom, #f0f0f0 0%, #d0d0d0 100%); + border: 1px solid #808080; display: flex; align-items: center; justify-content: center; font-size: var(--text-xs); + font-weight: bold; cursor: pointer; transition: all var(--transition-fast); } -.win-minimize::after { - content: "_"; - position: relative; - top: -4px; - color: #000; +.window-control:hover { + background: linear-gradient(to bottom, #f8f8f8 0%, #e0e0e0 100%); } -.win-maximize::after { - content: "□"; - position: relative; - color: #000; - font-size: 10px; +.window-control:active { + background: linear-gradient(to bottom, #d0d0d0 0%, #f0f0f0 100%); } -.win-close { - background: linear-gradient(to bottom, #f3a0a0 0%, #e46363 100%); +.window-control:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 1px; } -.win-close::after { - content: "×"; - color: #000; - font-weight: bold; +/* Text browser fallbacks for window controls */ +.minimize::after { content: "_"; } +.maximize::after { content: "□"; } +.close::after { content: "×"; color: #800000; } + +/* Progressive enhancement for better symbols */ +@supports (content: "🗕") { + .minimize::after { content: "🗕"; } +} +@supports (content: "🗖") { + .maximize::after { content: "🗖"; } +} +@supports (content: "🗙") { + .close::after { content: "🗙"; } } -header h1 { - margin: 0; - padding: 5px; - font-size: 1.2rem; - font-weight: bold; - font-family: var(--font-main); - color: var(--title-text); - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); -} - -/* Site title with Windows XP style */ -.site-title { - margin: 0; - padding: 0; - font-size: 1.2rem; - font-weight: bold; - font-family: var(--font-main); - flex-grow: 1; -} - -.site-title a { - color: var(--title-text); /* Fallback */ - text-decoration: none; - background: linear-gradient(to right, #ffffff 0%, #f0f0f0 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - text-shadow: 0 1px 2px rgba(0, 0, 0, 1); /* Increased shadow opacity */ - display: inline-block; - transition: all 0.2s; - font-weight: bold; - /* Extra depth with subtle outline */ - -webkit-text-stroke: 0.5px rgba(0, 0, 0, 0.3); -} - -.site-title a:hover { - background: linear-gradient(to right, #ffffff 0%, #ffffff 100%); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - transform: translateY(-1px); - text-shadow: 0 1px 3px rgba(0, 0, 0, 1), 0 0 5px rgba(255, 255, 255, 0.8); - -webkit-text-stroke: 0.5px rgba(0, 0, 0, 0.5); -} - -/* Navigation bar in XP style */ +/* Menu bar */ nav { - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - padding: var(--spacing-sm) 0; - border-bottom: 1px solid var(--border-color); + height: var(--menubar-height); + background: linear-gradient(to bottom, var(--xp-gray-lightest) 0%, var(--xp-gray-light) 100%); + border-bottom: 1px solid var(--xp-gray-dark); display: flex; - flex-wrap: wrap; - justify-content: center; + align-items: center; + padding: 0 var(--space-3); + font-size: var(--text-base); + overflow-x: auto; + overflow-y: hidden; } nav a { - color: var(--text-color); + color: var(--xp-text-primary); text-decoration: none; - padding: var(--spacing-sm) var(--spacing-md); - margin: 0 var(--spacing-xs); - border-radius: 3px; - font-size: var(--text-sm); - position: relative; - display: inline-block; - white-space: nowrap; + padding: var(--space-3) var(--space-4); + margin: 0 1px; + border-radius: 2px; transition: all var(--transition-fast); + white-space: nowrap; + font-size: var(--text-base); + min-height: 24px; + display: flex; + align-items: center; } -nav a:hover, nav a:focus { - background: linear-gradient(to bottom, #f5f9fd 0%, #cee2fc 100%); - border: 1px solid var(--border-color); - padding: 3px 9px; +nav a:hover { + background: linear-gradient(to bottom, #e3f2fd 0%, #bbdefb 100%); + border: 1px solid #90caf9; + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); +} + +nav a:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; + background: linear-gradient(to bottom, #e3f2fd 0%, #bbdefb 100%); + border: 1px solid #90caf9; + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); } -/* Selected menu item */ nav a.active { - background: linear-gradient(to bottom, #e3effd 0%, #afd2fa 100%); - border: 1px solid var(--border-color); - padding: 3px 9px; + background: linear-gradient(to bottom, #d1e7dd 0%, #a3cfbb 100%); + border: 1px solid #75b798; + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); } -/* Content area */ +/* Main content area */ main { - padding: var(--spacing-lg); - background-color: var(--window-bg); + padding: var(--space-5); + background: var(--xp-window-bg); + min-height: 400px; } /* Typography */ h1, h2, h3, h4, h5, h6 { font-family: var(--font-headings); - color: var(--header-color); - margin-top: 1.5rem; - margin-bottom: 0.75rem; - line-height: var(--line-height-tight); + color: var(--xp-text-primary); + margin: var(--space-6) 0 var(--space-4) 0; + line-height: 1.2; + font-weight: bold; } -h1 { - font-size: var(--text-2xl); - color: var(--title-bar); -} +h1 { font-size: var(--text-2xl); } +h2 { font-size: var(--text-xl); } +h3 { font-size: var(--text-lg); } +h4 { font-size: var(--text-md); } -h2 { - font-size: var(--text-xl); - color: var(--title-bar); -} - -h3 { - font-size: var(--text-lg); +article h1 { + margin-top: 0; + color: var(--xp-blue-dark); } p { - margin-bottom: 1rem; - line-height: var(--line-height-base); + margin: 0 0 var(--space-4) 0; + line-height: 1.5; } /* Links */ a { - color: var(--link-color); - text-decoration: none; - transition: color var(--transition-base); + color: var(--xp-link-color); + text-decoration: underline; + transition: color var(--transition-fast); } a:visited { - color: var(--link-visited); + color: var(--xp-link-visited); } a:hover { - text-decoration: underline; + color: var(--xp-blue-dark); + text-decoration: none; } -/* XP Style buttons */ -.winxp-button { - background: linear-gradient(to bottom, #f1f6fb 0%, #c5d7eb 100%); - border: 1px solid var(--border-color); - border-radius: 3px; - padding: var(--spacing-sm) var(--spacing-lg); - color: var(--text-color); +a:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; + color: var(--xp-blue-dark); + text-decoration: none; +} + +/* XP-style buttons */ +.xp-button { + background: linear-gradient(to bottom, var(--xp-gray-lightest) 0%, var(--xp-gray-light) 100%); + border: 1px solid var(--xp-gray-dark); + padding: var(--space-2) var(--space-4); font-family: var(--font-main); font-size: var(--text-sm); - font-weight: normal; + color: var(--xp-text-primary); cursor: pointer; - margin: var(--spacing-xs); transition: all var(--transition-fast); + text-decoration: none; + display: inline-block; } -.winxp-button:active { - background: linear-gradient(to bottom, #cde1f9 0%, #9bc1f1 100%); +.xp-button:hover { + background: linear-gradient(to bottom, #f8f8f8 0%, var(--xp-gray-lightest) 100%); + border-color: var(--xp-blue-medium); } -/* Articles in XP window style */ +.xp-button:active { + background: linear-gradient(to bottom, var(--xp-gray-light) 0%, var(--xp-gray-lightest) 100%); +} + +.xp-button:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; +} + +/* Articles */ article { - margin-bottom: var(--spacing-xl); - padding: var(--spacing-md); - border: 1px solid #d2e1f9; - border-radius: 5px; - background-color: #f9fafc; + margin-bottom: var(--space-8); + padding-bottom: var(--space-6); + border-bottom: 1px solid var(--xp-gray-medium); +} + +article:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; } article .meta { - font-size: var(--text-sm); - color: #666; - margin-bottom: var(--spacing-md); + color: var(--xp-text-secondary); + font-size: var(--text-base); + margin: var(--space-3) 0 var(--space-4) 0; display: flex; flex-wrap: wrap; - gap: var(--spacing-md); + gap: var(--space-4); } .reading-time { - color: #666; font-style: italic; - font-size: var(--text-sm); } +/* Text browser fallback for reading time icon */ +.reading-time::before { + content: "Time: "; +} + +/* Progressive enhancement for reading time icon */ +@supports (content: "⏱") { + .reading-time::before { + content: "⏱ "; + } +} + +/* Tags */ .tags { display: flex; flex-wrap: wrap; - gap: var(--spacing-sm); + gap: var(--space-2); + margin: var(--space-4) 0; } .tags a { - background: linear-gradient(to bottom, #f5f9fd 0%, #cee2fc 100%); - color: var(--text-color); - padding: var(--spacing-xs) var(--spacing-md); - font-size: var(--text-xs); - border-radius: 8px; + background: linear-gradient(to bottom, #e3f2fd 0%, #bbdefb 100%); + color: var(--xp-text-primary); + padding: var(--space-2) var(--space-4); + font-size: var(--text-sm); text-decoration: none; - border: 1px solid #c5d7eb; - transition: all var(--transition-base); + border: 1px solid #90caf9; + border-radius: 2px; + transition: all var(--transition-fast); } .tags a:hover { - background: linear-gradient(to bottom, #e3effd 0%, #afd2fa 100%); + background: linear-gradient(to bottom, #bbdefb 0%, #90caf9 100%); + text-decoration: none; } +.tags a:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; + background: linear-gradient(to bottom, #bbdefb 0%, #90caf9 100%); + text-decoration: none; +} + +/* Code styling */ code { font-family: var(--font-mono); - background-color: #f0f0f0; - padding: var(--spacing-xs) var(--spacing-sm); - border-radius: 3px; - font-size: var(--text-sm); + background: var(--xp-gray-lightest); + padding: var(--space-2) var(--space-3); + border: 1px solid var(--xp-gray-medium); + font-size: var(--text-base); } pre { - background-color: #f5f5f5; - padding: var(--spacing-md); - border-radius: 5px; + background: var(--xp-gray-lightest); + border: 1px solid var(--xp-gray-medium); + padding: var(--space-4); overflow-x: auto; - border: 1px solid #ddd; - font-size: var(--text-sm); + margin: var(--space-4) 0; + font-size: var(--text-base); } pre code { - background-color: transparent; + background: none; + border: none; padding: 0; } +/* Images */ img { max-width: 100%; height: auto; - border-radius: 3px; - border: 1px solid #d2e1f9; + border: 1px solid var(--xp-gray-medium); + margin: var(--space-4) 0; } -/* Footer styled as XP taskbar */ -footer { - background: linear-gradient(to bottom, #3c82d0 0%, #245edb 100%); - color: white; - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--text-sm); - text-align: center; - display: flex; - justify-content: space-between; - align-items: center; - border-top: 1px solid #1752cf; - border-radius: 0 0 var(--border-radius) var(--border-radius); +/* Blockquotes */ +blockquote { + margin: var(--space-4) 0; + padding: var(--space-4); + background: #f8f9fa; + border-left: 4px solid var(--xp-blue-medium); + border: 1px solid var(--xp-gray-medium); } -/* Footer links with better contrast */ -footer a { - color: #ffffff; - text-decoration: underline; +blockquote p:last-child { + margin-bottom: 0; +} + +/* Tables */ +table { + width: 100%; + border-collapse: collapse; + margin: var(--space-4) 0; + border: 1px solid var(--xp-gray-medium); +} + +th, td { + padding: var(--space-3); + text-align: left; + border: 1px solid var(--xp-gray-medium); +} + +th { + background: linear-gradient(to bottom, var(--xp-gray-lightest) 0%, var(--xp-gray-light) 100%); font-weight: bold; } +tbody tr:nth-child(even) { + background: #f8f9fa; +} + +tbody tr:hover { + background: #e3f2fd; +} + +/* Footer as taskbar */ +footer { + min-height: var(--taskbar-height); + background: linear-gradient(to bottom, + var(--xp-blue-medium) 0%, + var(--xp-blue-dark) 50%, + var(--xp-blue-darker) 100%); + color: white; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: var(--space-4); + padding: var(--space-3); + font-size: var(--text-sm); + border-top: 1px solid var(--xp-blue-light); + text-align: center; +} + +footer a { + color: white; + text-decoration: underline; +} + footer a:hover { color: #ffff99; - text-decoration: underline; + text-decoration: none; +} + +footer a:focus { + outline: 2px solid white; + outline-offset: 2px; + color: #ffff99; + text-decoration: none; } /* Start button */ .start-button { - display: inline-flex; - align-items: center; - background: linear-gradient(to bottom, #8cd343 0%, #4fad0f 100%); + background: linear-gradient(to bottom, var(--xp-green-light) 0%, var(--xp-green-medium) 100%); color: white; font-weight: bold; - padding: var(--spacing-sm) var(--spacing-md) var(--spacing-sm) var(--spacing-sm); - border-radius: 10px; - font-size: var(--text-sm); - position: relative; - border: 1px solid #306110; + padding: var(--space-3) var(--space-5); + border: 1px solid var(--xp-green-dark); + border-radius: 4px; + font-size: var(--text-base); + text-decoration: none; transition: all var(--transition-fast); + white-space: nowrap; + flex-shrink: 0; } -.start-button::before { - content: ""; - display: inline-block; - width: 20px; - height: 18px; - margin-right: 5px; - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAFMSURBVHjatNQ/S5thFAbw3zs1KhLTgINDRxcXB6GEDg7SoUvpIhH8CIKfICLoUGinLn4CB+ks+AE6FOzk5NCpDiJIQTQmgZpzOiTvGyPPm7w5cLiHe+5/59xzuUIIIYTVCPsYxRCG0I9zrGEe2yW8iNdoRTh/tPCYGruQc1hAExsYxigmcIuDWD8vTxQZNrGHPQxgHD3oxkHUnMX8UdawXWvQjXGZYB6HcXA2Ts05M/vLPR5KxJsYQCWLtAst7OAMT/ADffiOKm7i+5eMuBb36x+P7xVvGMMQnmMAX9GHe3SgC68wlV9HHr/PjLbwExPxqT5iAbP4jpuYqC43LNd6nD3BKeqYjvH+YBfX2Iqa07xhPWp3MYkp1DAdP/+iFE+bNjj7iR+J8RXM4DWe5gtfYixzWMQVvmEZJ1HzCa/ShjmOIqzHxvhXTfg7AESQI1XUvK6jAAAAAElFTkSuQmCC"); - background-repeat: no-repeat; - background-position: center; +.start-button:hover { + background: linear-gradient(to bottom, var(--xp-green-medium) 0%, var(--xp-green-dark) 100%); + text-decoration: none; } -/* XP taskbar time */ -.taskbar-time { - display: inline-block; - background: #0c327a; - color: white; - padding: var(--spacing-sm) var(--spacing-md); - border-radius: 3px; - border-top: 1px solid #001a69; - border-left: 1px solid #001a69; - border-right: 1px solid #5986d6; - border-bottom: 1px solid #5986d6; - font-size: var(--text-sm); +.start-button:focus { + outline: 2px solid white; + outline-offset: 2px; + text-decoration: none; } -/* Pagination in XP style */ +/* Pagination */ .pagination { display: flex; justify-content: center; align-items: center; - margin: var(--spacing-xl) 0; - gap: var(--spacing-md); + gap: var(--space-3); + margin: var(--space-6) 0; + padding: var(--space-4); + background: var(--xp-gray-lightest); + border: 1px solid var(--xp-gray-medium); } .pagination a { - background: linear-gradient(to bottom, #f1f6fb 0%, #c5d7eb 100%); - color: var(--text-color); - padding: var(--spacing-sm) var(--spacing-md); + background: linear-gradient(to bottom, var(--xp-gray-lightest) 0%, var(--xp-gray-light) 100%); + color: var(--xp-text-primary); + padding: var(--space-2) var(--space-4); text-decoration: none; - border-radius: 3px; - border: 1px solid var(--border-color); + border: 1px solid var(--xp-gray-dark); transition: all var(--transition-fast); } .pagination a:hover { - background: linear-gradient(to bottom, #e3effd 0%, #afd2fa 100%); + background: linear-gradient(to bottom, #f8f8f8 0%, var(--xp-gray-lightest) 100%); + border-color: var(--xp-blue-medium); + text-decoration: none; +} + +.pagination a:focus { + outline: 2px solid var(--xp-blue-medium); + outline-offset: 2px; + background: linear-gradient(to bottom, #f8f8f8 0%, var(--xp-gray-lightest) 100%); + border-color: var(--xp-blue-medium); text-decoration: none; } .pagination .page-info { - color: #666; - font-size: var(--text-sm); - padding: 0 var(--spacing-md); + color: var(--xp-text-secondary); + font-size: var(--text-base); } -/* Media query for responsive design */ +/* Featured images with XP window styling */ +.featured-image, .index-image, .tag-image, .archive-image { + margin: var(--space-6) 0; + border: 2px solid var(--xp-gray-medium); + background: var(--xp-gray-lightest); + padding: var(--space-2); +} + +.featured-image img, .index-image img, .tag-image img, .archive-image img { + width: 100%; + height: auto; + border: 1px solid var(--xp-gray-dark); + margin: 0; +} + +/* Selection styling */ +::selection { + background: var(--xp-blue-light); + color: white; +} + +/* Responsive design */ @media (max-width: 768px) { - .container { - margin: var(--spacing-md); - width: auto; - } - - header { - display: flex; - flex-direction: column; - align-items: center; - padding: var(--spacing-md) var(--spacing-md) var(--spacing-sm) var(--spacing-md); - } - - .site-title { - margin-bottom: var(--spacing-sm); - text-align: center; - } - - nav { - padding: var(--spacing-sm); - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: var(--spacing-xs); - width: 100%; - background: linear-gradient(to bottom, #e9eff9 0%, #d2e2f9 100%); - border-radius: var(--border-radius); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); - } - - nav a { - margin: 2px; - padding: var(--spacing-xs) var(--spacing-sm); - text-align: center; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(to bottom, #f5f9fd 0%, #e9eff9 100%); - border: 1px solid #b9d1f7; - border-radius: 5px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; - height: 35px; - box-sizing: border-box; - overflow: hidden; - white-space: nowrap; - } - - nav a:hover, nav a:focus, nav a.active { - padding: var(--spacing-xs) var(--spacing-sm); - background: linear-gradient(to bottom, #e3effd 0%, #afd2fa 100%); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); - transform: translateY(1px); - } - - main { - padding: var(--spacing-md); - } - - footer { - flex-wrap: wrap; - justify-content: center; - gap: var(--spacing-md); - padding: var(--spacing-md); - } - - .start-button { - margin-bottom: var(--spacing-sm); - } -} - -@media (max-width: 480px) { - :root { - --content-width: 100%; + html { + font-size: 15px; /* Larger base font for tablets */ } body { - padding: var(--spacing-xs); + padding: var(--space-2); } - + .container { - margin: var(--spacing-xs); - box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + margin: var(--space-3) 0; } header { - padding: var(--spacing-sm); - } - - .site-title { - font-size: 1rem; - } - - nav { - display: flex; - flex-direction: column; - padding: var(--spacing-xs); - gap: var(--spacing-xs); - width: 100%; - } - - nav a { - margin: 1px 0; - width: 100%; - padding: var(--spacing-xs) var(--spacing-sm); - height: 32px; - font-size: 0.85rem; - box-sizing: border-box; - } - - nav a:hover, nav a:focus, nav a.active { - width: 100%; - padding: var(--spacing-xs) var(--spacing-sm); + padding: 0 var(--space-2); + height: auto; + min-height: var(--titlebar-height); + flex-wrap: wrap; } .window-controls { display: none; } - h1 { - font-size: var(--text-xl); + /* Navigation becomes horizontally scrollable on tablets */ + nav { + padding: var(--space-2) var(--space-3); + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + scrollbar-color: var(--xp-gray-dark) var(--xp-gray-light); } - h2 { - font-size: var(--text-lg); + nav::-webkit-scrollbar { + height: 6px; + } + + nav::-webkit-scrollbar-track { + background: var(--xp-gray-light); + } + + nav::-webkit-scrollbar-thumb { + background: var(--xp-gray-dark); + border-radius: 3px; + } + + nav a { + padding: var(--space-3) var(--space-4); + font-size: var(--text-md); + min-width: max-content; + } + + nav a:hover, nav a:focus { + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); + } + + nav a.active { + padding: calc(var(--space-3) - 1px) calc(var(--space-4) - 1px); + } + + main { + padding: var(--space-5); + } + + footer { + padding: var(--space-4); + gap: var(--space-3); } } -/* Featured Images with Windows XP styling */ -.featured-image { - margin: var(--spacing-xl) 0; - border: 1px solid var(--border-color); - padding: var(--spacing-sm); - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - border-radius: var(--spacing-sm); - position: relative; - box-shadow: 0 var(--spacing-xs) var(--spacing-md) rgba(0, 0, 0, 0.1); +@media (max-width: 480px) { + html { + font-size: 16px; /* Even larger base font for phones */ + } + + :root { + /* Increase spacing for mobile */ + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + } + + header { + padding: 0 var(--space-3); + } + + .site-title { + font-size: var(--text-lg); + } + + /* Navigation stacks vertically on phones for better accessibility */ + nav { + flex-direction: column; + height: auto; + padding: var(--space-3); + overflow: visible; + } + + nav a { + padding: var(--space-4) var(--space-4); + font-size: var(--text-lg); + margin: 1px 0; + text-align: center; + min-width: auto; + width: 100%; + } + + nav a:hover, nav a:focus { + padding: calc(var(--space-4) - 1px) calc(var(--space-4) - 1px); + } + + nav a.active { + padding: calc(var(--space-4) - 1px) calc(var(--space-4) - 1px); + } + + main { + padding: var(--space-4); + } + + h1 { font-size: var(--text-3xl); } + h2 { font-size: var(--text-2xl); } + h3 { font-size: var(--text-xl); } + h4 { font-size: var(--text-lg); } + + footer { + padding: var(--space-4); + gap: var(--space-2); + font-size: var(--text-base); + } + + .start-button { + padding: var(--space-4) var(--space-5); + font-size: var(--text-md); + } + + .pagination { + flex-direction: column; + gap: var(--space-3); + } + + .pagination a { + padding: var(--space-4) var(--space-5); + font-size: var(--text-md); + } } -.featured-image img { - width: 100%; - height: auto; - display: block; - border: 1px solid #d8d8d8; - border-radius: var(--spacing-xs); - transition: all var(--transition-base); -} - -.featured-image:hover img { - opacity: 0.95; -} - -.featured-image .image-caption { - background: linear-gradient(to bottom, #2a8dd4 0%, #015eae 100%); - color: white; - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--text-sm); - text-align: center; - margin-top: var(--spacing-sm); - border-radius: var(--spacing-xs); - border: 1px solid var(--border-color); - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); -} - -.index-image { - margin-bottom: 1.2rem; - border: 1px solid var(--border-color); - padding: 3px; - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - border-radius: 3px; -} - -.index-image img { - width: 100%; - height: auto; - display: block; - border: 1px solid #d8d8d8; - border-radius: 2px; -} - -.tag-image { - margin-bottom: 1.2rem; - border: 1px solid var(--border-color); - padding: 3px; - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - border-radius: 3px; -} - -.tag-image img { - width: 100%; - height: auto; - display: block; - border: 1px solid #d8d8d8; - border-radius: 2px; -} - -.archive-image { - margin-bottom: 1.2rem; - border: 1px solid var(--border-color); - padding: 3px; - background: linear-gradient(to bottom, #f1f6fb 0%, #e4ecf7 100%); - border-radius: 3px; -} - -.archive-image img { - width: 100%; - height: auto; - display: block; - border: 1px solid #d8d8d8; - border-radius: 2px; -} - -/* Selection styling */ -::selection { - background-color: var(--highlight-color); - color: var(--text-color); -} - -/* Date header with Windows XP styling */ -.date-header { - font-family: var(--font-headings); - font-weight: bold; - color: var(--title-text); - margin: var(--spacing-xl) 0 var(--spacing-md) 0; - font-size: var(--text-md); - display: inline-block; - position: relative; - padding: var(--spacing-sm) var(--spacing-md); - background: linear-gradient(to bottom, #2a8dd4 0%, #015eae 100%); - border-radius: var(--spacing-sm); - border: 1px solid var(--border-color); - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +/* Print styles */ +@media print { + body { + background: white; + padding: 0; + } + + .container { + border: none; + box-shadow: none; + margin: 0; + } + + header, footer, .window-controls { + display: none; + } + + nav { + display: none; + } + + main { + padding: 0; + } + + a { + color: black; + text-decoration: underline; + } + + .tags { + display: none; + } } \ No newline at end of file diff --git a/themes/y2k/style.css b/themes/y2k/style.css index 0d814df..6de4790 100644 --- a/themes/y2k/style.css +++ b/themes/y2k/style.css @@ -2,35 +2,62 @@ * Y2K Theme for BSSG * Turn of the millennium aesthetic with bold colors and digital futurism * Featuring bubble effects, gradients, and elements from early 2000s web design + * IMPROVED: Better accessibility, performance, and text browser support */ -@import url('https://fonts.googleapis.com/css2?family=VT323&family=Titillium+Web:wght@400;700&display=swap'); +/* Reduced motion support - CRITICAL for accessibility */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + /* Disable performance-heavy animations */ + .site-title a { + animation: none !important; + } + + .marquee-content { + animation: none !important; + transform: none !important; + } + + /* Simplify hover effects */ + nav a:hover, .site-title a:hover { + transform: none !important; + } +} + +/* OPTIMIZED: Using system fonts instead of external Google Fonts for better performance and privacy */ +/* Removed Google Fonts import - using system font alternatives */ :root { - /* Y2K color scheme - bold and digital */ - --bg-color: #CCCCFF; - --bg-secondary: #FFCCFF; - --text-color: #333366; - --text-light: #666699; - --link-color: #FF3399; - --link-hover: #FF66CC; - --accent-blue: #33CCFF; - --accent-pink: #FF66FF; - --accent-yellow: #FFFF66; - --header-color: #6633CC; - --border-color: #9999CC; - --nav-bg: #EEEEFF; - --bubble-gradient-1: rgba(255, 102, 255, 0.5); - --bubble-gradient-2: rgba(102, 204, 255, 0.5); - --highlight-color: #EEDDFF; + /* Y2K color scheme - IMPROVED contrast ratios */ + --bg-color: #E6E6FF; /* Lighter for better contrast */ + --bg-secondary: #FFE6FF; /* Lighter for better contrast */ + --text-color: #1A1A4D; /* Darker for better contrast */ + --text-light: #4D4D80; /* Improved contrast */ + --link-color: #CC0066; /* Darker for better contrast */ + --link-hover: #FF3399; + --accent-blue: #0099CC; /* Darker for better contrast */ + --accent-pink: #FF33CC; + --accent-yellow: #FFCC00; /* Darker for better contrast */ + --header-color: #4D1A99; /* Darker for better contrast */ + --border-color: #8080B3; /* Improved contrast */ + --nav-bg: #F0F0FF; + --bubble-gradient-1: rgba(255, 102, 255, 0.3); /* Reduced opacity for performance */ + --bubble-gradient-2: rgba(102, 204, 255, 0.3); /* Reduced opacity for performance */ + --highlight-color: #E6CCFF; --accent-color: var(--accent-blue); --accent-secondary: var(--accent-pink); - --quote-bg: #EEEEFF; + --quote-bg: #F0F0FF; - /* Typography */ - --font-main: 'Titillium Web', 'Arial', sans-serif; - --font-digital: 'VT323', monospace; - --font-mono: 'Courier New', monospace; + /* Typography - Using system fonts for better performance and privacy */ + --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; + --font-digital: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; + --font-mono: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace; /* Font Sizes */ --text-xs: 0.8rem; @@ -56,10 +83,10 @@ --spacing-2xl: 30px; --spacing-3xl: 40px; - /* Transitions */ + /* Transitions - OPTIMIZED for performance */ --transition-fast: 0.1s; - --transition-base: 0.2s; - --transition-slow: 0.3s; + --transition-base: 0.15s; /* Reduced for better performance */ + --transition-slow: 0.2s; /* Reduced for better performance */ /* Sizing */ --content-width: 960px; @@ -67,7 +94,7 @@ --button-radius: 25px; } -/* Base elements with background gradient */ +/* Base elements with optimized background gradient */ body { font-family: var(--font-main); background: linear-gradient(135deg, var(--bg-color) 0%, var(--bg-secondary) 100%); @@ -76,14 +103,14 @@ body { padding: var(--spacing-xl); line-height: var(--line-height-base); font-size: var(--text-base); - background-attachment: fixed; + /* REMOVED: background-attachment: fixed for better mobile performance */ } -/* Container with early 2000s styling */ +/* Container with early 2000s styling - OPTIMIZED */ .container { max-width: var(--content-width); margin: var(--spacing-xl) auto; - background-color: rgba(255, 255, 255, 0.7); + background-color: rgba(255, 255, 255, 0.8); /* Improved contrast */ border: 3px solid var(--border-color); border-radius: var(--border-radius); overflow: hidden; @@ -91,7 +118,7 @@ body { position: relative; } -/* Digital stars in the background */ +/* OPTIMIZED: Simplified digital pattern */ .container::before { content: ""; position: absolute; @@ -99,12 +126,16 @@ body { left: 0; right: 0; bottom: 0; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cpath d='M20 0L20 40M0 20L40 20M8 8L32 32M32 8L8 32' stroke='%239999CC' stroke-width='0.5' stroke-dasharray='1,4' stroke-linecap='round'/%3E%3C/svg%3E"); - opacity: 0.2; + /* SIMPLIFIED: Basic pattern instead of complex SVG */ + background-image: + linear-gradient(45deg, transparent 40%, rgba(153, 153, 204, 0.1) 50%, transparent 60%), + linear-gradient(-45deg, transparent 40%, rgba(153, 153, 204, 0.1) 50%, transparent 60%); + background-size: 20px 20px; + opacity: 0.3; pointer-events: none; } -/* Y2K header with gradient */ +/* Y2K header with gradient - OPTIMIZED */ header { background: linear-gradient(to right, var(--header-color), var(--accent-blue)); color: white; @@ -115,17 +146,17 @@ header { border-bottom: 3px solid var(--border-color); } -/* Bubble effect behind header */ +/* OPTIMIZED: Simplified bubble effect */ header::before { content: ""; position: absolute; - width: 200px; - height: 200px; + width: 150px; /* Reduced size for performance */ + height: 150px; border-radius: 50%; background: radial-gradient(circle, var(--bubble-gradient-1) 0%, var(--bubble-gradient-2) 100%); - top: -100px; + top: -75px; right: -50px; - opacity: 0.6; + opacity: 0.4; /* Reduced opacity */ z-index: 0; } @@ -139,7 +170,7 @@ header h1 { z-index: 1; } -/* Site title with Y2K futuristic styling */ +/* Site title with Y2K futuristic styling - IMPROVED accessibility */ .site-title { margin: 0; padding: 0; @@ -148,38 +179,54 @@ header h1 { position: relative; z-index: 1; letter-spacing: 1px; + color: white; /* Fallback color */ } .site-title a { text-decoration: none; - background: linear-gradient(45deg, - var(--accent-blue) 0%, - var(--accent-pink) 30%, - var(--link-color) 60%, - var(--accent-yellow) 100% - ); - background-size: 200% 200%; - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: white; /* Fallback for text browsers */ text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2); display: inline-block; - animation: y2k-gradient 5s ease infinite; transition: all var(--transition-slow); } +/* Progressive enhancement for gradient text */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + .site-title a { + background: linear-gradient(45deg, + var(--accent-blue) 0%, + var(--accent-pink) 30%, + var(--link-color) 60%, + var(--accent-yellow) 100% + ); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + /* CONDITIONAL: Animation only if motion is not reduced */ + animation: y2k-gradient 5s ease infinite; + } +} + .site-title a:hover { - transform: scale(1.05) rotate(-1deg); + transform: scale(1.02) rotate(-0.5deg); /* Reduced for better performance */ text-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3); } -@keyframes y2k-gradient { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } +.site-title a:focus { + outline: 3px solid var(--accent-yellow); + outline-offset: 3px; + transform: scale(1.02) rotate(-0.5deg); + text-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3); } -/* Y2K-style bubble buttons navigation */ +/* OPTIMIZED: Simplified gradient animation */ +@keyframes y2k-gradient { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +/* Y2K-style bubble buttons navigation - OPTIMIZED */ nav { background-color: var(--nav-bg); padding: var(--spacing-md); @@ -212,7 +259,15 @@ nav a { nav a:hover { background: linear-gradient(to bottom, white, var(--accent-yellow)); - transform: scale(1.05); + transform: scale(1.02); /* Reduced scale for better performance */ + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); +} + +nav a:focus { + outline: 3px solid var(--accent-yellow); + outline-offset: 3px; + background: linear-gradient(to bottom, white, var(--accent-yellow)); + transform: scale(1.02); box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); } @@ -238,10 +293,10 @@ nav a:last-child:hover { main { padding: var(--spacing-2xl); position: relative; - background-color: rgba(255, 255, 255, 0.5); + background-color: rgba(255, 255, 255, 0.6); /* Improved contrast */ } -/* Y2K-style typography */ +/* Y2K-style typography - IMPROVED contrast */ h1, h2, h3, h4, h5, h6 { color: var(--header-color); margin-top: 1.5rem; @@ -258,14 +313,21 @@ h1 { h2 { font-size: var(--text-xl); - background: linear-gradient(to right, var(--header-color), var(--accent-blue)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--header-color); /* Fallback color */ padding-bottom: var(--spacing-sm); border-bottom: 2px dotted var(--border-color); } +/* Progressive enhancement for h2 gradient */ +@supports (background-clip: text) or (-webkit-background-clip: text) { + h2 { + background: linear-gradient(to right, var(--header-color), var(--accent-blue)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } +} + h3 { font-size: var(--text-lg); color: var(--accent-blue); @@ -276,7 +338,7 @@ p { line-height: var(--line-height-base); } -/* Y2K-style links with hover effect */ +/* Y2K-style links with hover effect - IMPROVED contrast */ a { color: var(--link-color); text-decoration: none; @@ -289,10 +351,17 @@ a:hover { text-decoration: underline; } -/* Digital counter effect */ +a:focus { + outline: 2px solid var(--accent-yellow); + outline-offset: 2px; + color: var(--link-hover); + text-decoration: underline; +} + +/* Digital counter effect - IMPROVED contrast */ .digital-counter { font-family: var(--font-digital); - background-color: #000033; + background-color: #000066; /* Darker for better contrast */ color: #00FF00; padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--spacing-sm); @@ -303,10 +372,10 @@ a:hover { box-shadow: inset 0 0 5px rgba(0, 255, 0, 0.5); } -/* Y2K-style articles with bubble corners */ +/* Y2K-style articles with bubble corners - OPTIMIZED */ article { margin-bottom: var(--spacing-3xl); - background-color: rgba(255, 255, 255, 0.7); + background-color: rgba(255, 255, 255, 0.8); /* Improved contrast */ border: 2px solid var(--border-color); border-radius: 15px; padding: var(--spacing-xl); @@ -315,27 +384,27 @@ article { overflow: hidden; } -/* Decorative Y2K corner bubbles */ +/* OPTIMIZED: Simplified decorative bubbles */ article::before { content: ""; position: absolute; - width: 100px; - height: 100px; + width: 80px; /* Reduced size for performance */ + height: 80px; border-radius: 50%; - background: radial-gradient(circle, rgba(255, 102, 255, 0.2) 0%, rgba(255, 255, 255, 0) 70%); - top: -50px; - left: -50px; + background: radial-gradient(circle, rgba(255, 102, 255, 0.15) 0%, rgba(255, 255, 255, 0) 70%); + top: -40px; + left: -40px; } article::after { content: ""; position: absolute; - width: 80px; - height: 80px; + width: 60px; /* Reduced size for performance */ + height: 60px; border-radius: 50%; - background: radial-gradient(circle, rgba(102, 204, 255, 0.2) 0%, rgba(255, 255, 255, 0) 70%); - bottom: -40px; - right: -40px; + background: radial-gradient(circle, rgba(102, 204, 255, 0.15) 0%, rgba(255, 255, 255, 0) 70%); + bottom: -30px; + right: -30px; } article:last-child { @@ -365,209 +434,235 @@ article .meta { background-color: var(--header-color); color: white; padding: var(--spacing-xs) var(--spacing-md); - border-radius: var(--spacing-md); - font-size: var(--text-xs); + border-radius: var(--spacing-sm); + font-size: var(--text-sm); + display: inline-block; } .tags { display: flex; flex-wrap: wrap; - gap: var(--spacing-md); + gap: var(--spacing-sm); + margin-top: var(--spacing-lg); } .tags a { - background: linear-gradient(to bottom, white, #f0f0ff); - color: var(--text-color); + background: linear-gradient(to bottom, var(--accent-pink), var(--link-color)); + color: white; padding: var(--spacing-xs) var(--spacing-md); + border-radius: var(--button-radius); font-size: var(--text-xs); - text-decoration: none; - border-radius: 12px; - border: 1px solid var(--border-color); - transition: all var(--transition-base); + text-transform: uppercase; + letter-spacing: 1px; + border: 1px solid rgba(255, 255, 255, 0.5); + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); } .tags a:hover { - background: linear-gradient(to bottom, white, var(--accent-yellow)); - transform: scale(1.05); + background: linear-gradient(to bottom, var(--link-hover), var(--accent-pink)); + transform: translateY(-1px); /* Reduced for better performance */ +} + +.tags a:focus { + outline: 2px solid var(--accent-yellow); + outline-offset: 2px; + background: linear-gradient(to bottom, var(--link-hover), var(--accent-pink)); + transform: translateY(-1px); } .tags-list { - list-style-type: none; - padding: 0; display: flex; flex-wrap: wrap; - gap: 10px; + gap: var(--spacing-lg); + margin-bottom: var(--spacing-xl); } .tag-count { - background-color: var(--accent-pink); + background: var(--accent-blue); color: white; - font-size: 0.75em; - margin-left: 5px; - padding: 0 5px; - border-radius: 8px; + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: 50%; + font-size: var(--text-xs); + font-weight: bold; + margin-left: var(--spacing-sm); + min-width: 20px; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.5); } -/* Y2K code blocks */ code { - font-family: var(--font-mono); - background-color: #000033; - color: #00FF00; + background: rgba(0, 0, 102, 0.1); /* Improved contrast */ + color: var(--header-color); padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--spacing-xs); + font-family: var(--font-mono); font-size: var(--text-sm); - border-radius: 3px; + border: 1px solid var(--border-color); } pre { - background-color: #000033; - color: #00FF00; + background: rgba(0, 0, 102, 0.05); /* Improved contrast */ + color: var(--text-color); padding: var(--spacing-lg); - overflow-x: auto; - font-size: var(--text-sm); border-radius: var(--spacing-md); - margin: var(--spacing-xl) 0; - border: 2px solid #333366; - font-family: var(--font-mono); + overflow-x: auto; + border: 2px solid var(--border-color); + margin: var(--spacing-lg) 0; } pre code { - background-color: transparent; + background: none; + border: none; padding: 0; + color: inherit; } -/* Y2K images with bubbly border */ img { max-width: 100%; height: auto; - border: 3px solid var(--accent-blue); - border-radius: var(--spacing-lg); - padding: var(--spacing-sm); - background-color: white; - box-shadow: 3px 3px var(--spacing-md) rgba(0, 0, 0, 0.2); + border-radius: var(--spacing-md); + margin: var(--spacing-lg) 0; + border: 2px solid var(--border-color); + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.1); } -/* Y2K-style footer */ footer { background: linear-gradient(to right, var(--header-color), var(--accent-blue)); color: white; - padding: var(--spacing-xl) var(--spacing-2xl); - font-size: var(--text-sm); + padding: var(--spacing-xl); text-align: center; border-top: 3px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; position: relative; - overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + gap: var(--spacing-xl); } -/* Bubble decoration for footer */ +/* OPTIMIZED: Simplified footer decorations */ footer::before { content: ""; position: absolute; - width: 150px; - height: 150px; + width: 100px; + height: 100px; border-radius: 50%; - background: radial-gradient(circle, var(--bubble-gradient-1) 0%, transparent 70%); - bottom: -80px; - left: 10%; + background: radial-gradient(circle, var(--bubble-gradient-1) 0%, rgba(255, 255, 255, 0) 70%); + top: -50px; + left: var(--spacing-xl); opacity: 0.3; } footer::after { content: ""; position: absolute; - width: 120px; - height: 120px; + width: 80px; + height: 80px; border-radius: 50%; - background: radial-gradient(circle, var(--bubble-gradient-2) 0%, transparent 70%); - top: -60px; - right: 10%; + background: radial-gradient(circle, var(--bubble-gradient-2) 0%, rgba(255, 255, 255, 0) 70%); + bottom: -40px; + right: var(--spacing-xl); opacity: 0.3; } footer a { - color: white; + color: var(--accent-yellow); text-decoration: none; font-weight: bold; - position: relative; - z-index: 1; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); } footer a:hover { - color: var(--accent-yellow); + color: white; + text-decoration: underline; +} + +footer a:focus { + outline: 2px solid var(--accent-yellow); + outline-offset: 2px; + color: white; text-decoration: underline; } -/* Y2K-style pagination */ .pagination { display: flex; justify-content: center; align-items: center; + gap: var(--spacing-lg); margin: var(--spacing-2xl) 0; - gap: var(--spacing-md); } .pagination a { - background: linear-gradient(to bottom, var(--accent-blue), #0099cc); - color: white; - padding: var(--spacing-md) var(--spacing-lg); - text-decoration: none; + background: linear-gradient(to bottom, white, var(--nav-bg)); + color: var(--text-color); + padding: var(--spacing-md) var(--spacing-xl); border-radius: var(--button-radius); - border: 2px solid #0099cc; - font-weight: bold; - box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); - transition: all var(--transition-base); -} - -.pagination a:hover { - background: linear-gradient(to bottom, #66ddff, #00aadd); - transform: scale(1.05); - text-decoration: none; -} - -.pagination .page-info { - background-color: rgba(255, 255, 255, 0.7); - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--spacing-lg); border: 2px solid var(--border-color); - font-family: var(--font-digital); - color: var(--header-color); + text-decoration: none; + font-weight: bold; + transition: all var(--transition-base); + text-transform: uppercase; + letter-spacing: 1px; font-size: var(--text-sm); } -/* Y2K-style horizontal rule with stars */ -hr { - border: none; - height: 20px; - background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath d='M10 0l2.4 7.4H20l-6.2 4.5 2.4 7.4-6.2-4.5-6.2 4.5 2.4-7.4L0 7.4h7.6z' fill='%239999CC'/%3E%3C/svg%3E"); - background-repeat: repeat-x; - background-position: center; - margin: var(--spacing-xl) 0; +.pagination a:hover { + background: linear-gradient(to bottom, white, var(--accent-yellow)); + transform: scale(1.02); + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); } -/* Y2K-style divider */ -.y2k-divider { - display: flex; - align-items: center; - margin: var(--spacing-xl) 0; +.pagination a:focus { + outline: 3px solid var(--accent-yellow); + outline-offset: 3px; + background: linear-gradient(to bottom, white, var(--accent-yellow)); + transform: scale(1.02); + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); } -.y2k-divider::before, -.y2k-divider::after { - content: ""; - flex: 1; - height: 2px; - background: linear-gradient(to right, transparent, var(--accent-pink), transparent); -} - -.y2k-divider span { - padding: 0 var(--spacing-md); +.pagination .page-info { + font-family: var(--font-digital); + background-color: var(--header-color); + color: white; + padding: var(--spacing-md) var(--spacing-lg); + border-radius: var(--spacing-md); font-size: var(--text-md); color: var(--accent-pink); } -/* Marquee effect - classic Y2K */ +/* Y2K horizontal rule */ +hr { + border: none; + height: 3px; + background: linear-gradient(to right, var(--accent-blue), var(--accent-pink), var(--accent-yellow)); + margin: var(--spacing-2xl) 0; + border-radius: var(--spacing-xs); +} + +/* Y2K divider with decorative elements */ +.y2k-divider { + text-align: center; + margin: var(--spacing-2xl) 0; + position: relative; +} + +.y2k-divider::before, +.y2k-divider::after { + content: "◆◇◆"; + color: var(--accent-pink); + font-size: var(--text-lg); + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.y2k-divider span { + background: var(--bg-color); + padding: 0 var(--spacing-lg); + color: var(--header-color); + font-weight: bold; +} + +/* OPTIMIZED: Marquee effect with reduced motion support */ .marquee { background-color: var(--accent-blue); color: white; @@ -583,16 +678,32 @@ hr { .marquee-content { display: inline-block; - animation: marquee 15s linear infinite; + /* CONDITIONAL: Animation only if motion is not reduced */ + animation: marquee 20s linear infinite; /* Slower for better performance */ } +/* Text browser fallback for marquee */ +@media (max-width: 0) { /* This targets text browsers */ + .marquee-content { + animation: none; + transform: none; + } +} + +/* OPTIMIZED: Simplified marquee animation */ @keyframes marquee { 0% { transform: translateX(100%); } 100% { transform: translateX(-100%); } } -/* Media query for responsive design */ +/* IMPROVED: Better responsive design */ @media (max-width: 768px) { + body { + padding: var(--spacing-md); + /* OPTIMIZED: Simplified background on mobile */ + background: linear-gradient(135deg, var(--bg-color) 0%, var(--bg-secondary) 100%); + } + .container { margin: var(--spacing-md); } @@ -676,7 +787,7 @@ hr { } } -/* Featured Images with Y2K styling */ +/* OPTIMIZED: Featured Images with Y2K styling */ .featured-image { margin: 1.5rem 0; border: 3px solid var(--border-color); @@ -688,15 +799,16 @@ hr { padding: var(--spacing-sm); } +/* OPTIMIZED: Simplified bubble effect */ .featured-image::before { content: ""; position: absolute; - width: 150px; - height: 150px; + width: 100px; /* Reduced size for performance */ + height: 100px; border-radius: 50%; - background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0) 70%); - top: -50px; - left: -50px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 70%); + top: -30px; + left: -30px; opacity: 0.5; z-index: 1; pointer-events: none; @@ -712,8 +824,8 @@ hr { } .featured-image:hover img { - transform: scale(1.02); - filter: brightness(1.05); + transform: scale(1.01); /* Reduced scale for better performance */ + filter: brightness(1.02); } .featured-image .image-caption { @@ -787,13 +899,13 @@ hr { border: 1px solid rgba(255, 255, 255, 0.7); } -/* Selection styling */ +/* IMPROVED: Better text selection */ ::selection { background-color: var(--highlight-color); color: var(--text-color); } -/* Date header with Y2K styling */ +/* Date header with Y2K styling - IMPROVED contrast */ .date-header { font-family: var(--font-main); font-weight: bold; diff --git a/themes/zxspectrum/style.css b/themes/zxspectrum/style.css index 84d55c3..2c87add 100644 --- a/themes/zxspectrum/style.css +++ b/themes/zxspectrum/style.css @@ -1,8 +1,37 @@ /* * ZX Spectrum Theme for BSSG * Bold colors with black background inspired by the iconic home computer + * + * IMPROVEMENTS APPLIED: + * - Added comprehensive reduced motion support + * - Enhanced accessibility with focus outlines + * - Removed external font loading for better performance and text browser compatibility + * - Enhanced font fallbacks with comprehensive system font stacks + * - Added text browser fallbacks for decorative elements + * - Optimized performance while maintaining authentic ZX Spectrum aesthetics */ +/* REDUCED MOTION SUPPORT - Disable animations for users who prefer reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } + + /* Disable ZX Spectrum-specific animations */ + .site-title a { + animation: none !important; + } + + /* Disable pixel overlay effect for reduced motion */ + body::before { + display: none !important; + } +} + :root { /* ZX Spectrum color palette */ --black: #000000; @@ -33,10 +62,10 @@ --accent-secondary: var(--bright-magenta); --quote-bg: var(--black); - /* Typography */ - --font-main: 'ZXSpectrumFont', 'Courier New', monospace; - --font-headings: 'ZXSpectrumFont', 'Courier New', monospace; - --font-mono: 'ZXSpectrumFont', 'Courier New', monospace; + /* Typography - Enhanced font fallbacks for ZX Spectrum look */ + --font-main: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + --font-headings: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + --font-mono: 'Courier New', 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; /* Sizing and spacing system */ --content-width: 820px; @@ -72,11 +101,7 @@ --border-thick: 4px; } -/* Pixelated effect for the entire document */ -@font-face { - font-family: 'ZXSpectrumFont'; - src: url('data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABF0ABAAAAAACgwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCQVNFAAARUAAAAOwAAAGOm5pjl0dQT1MAAAGQAAAAMQAAAD5VD0ORR1NVQgAAEbwAAAGIAAADHMULLGVPUy8yAAAB+AAAAFcAAABga1GRJGNTSEQAAA9AAAATRAAAAVRR+wjZY21hcAAAEcAAAADeAAABwtW+K9tjdnQgAAAP1AAAABYAAAAWBe4NdmZwZ20AAA8oAAABAgAAAXMGWZw3Z2FzcAAAEUQAAAAIAAAACAAAABBnbHlmAAAQUAAABcoAAApEh4BFcmhlYWQAAAJEAAAANgAAADYLf8ThaGhlYQAAAngAAAAdAAAAJA5dBWloaGVhAAACmAAAABkAAAAkCngFDWhtdHgAAALUAAAAWQAAAGhJ5QMmbG9jYQAAEAQAAAA2AAAANkhGQ+ZtYXhwAAACsAAAACAAAAAgAPcAl25hbWUAABMcAAABAQAAAkoMr0x5cG9zdAAAFFAAAAB0AAAAsDlLRORwcmVwAAAPwAAAAFgAAABYb9ZwSnjaY2BkYGAA4nKrMoXx/DZfGeSZXwBFGK6suTIJos9iM/7/834KcxJzEMjlYGACiQIA66AMgXjaY2BkYGCO+V/CEMX8goHh/3/mJAagCApgBgCAdQVCA+xgAAAAAABQAAAYAAB42mNgZj7LOIGBlYGBqYtpDwMDQw+EZnzAYMjIRRTMwcDAxMDKzAADjAIMCBCQ5prC0MAQyBzEnPQ/iyGKOYlhOlCYgYF5fhlDAgMDAGDSDh0AAAB42mNgYGBmgGAZBkYGELgD5DGC+SwMDcxgmgwoBOReYQv6n8XwnP+/AUgtZZBmkGGQY5AHivyHiP//D1LGCNGsCNKMBExXQnkMbACHWRk8AAAAAAAAAAAAAADVAPIBfAIGAkgCvAMKA2ADjAPgBCQEeATgBUgFegWCBdQF8gYeBmgGmAb+ByIHWgeWB7YH9AgkCFwIhAjyCVQJrgn4Cl4KngrgCyILYAuwC94MPAyODL4M+A1IDXQNpg3kDhgOQA56DsQO+g8qD1YPeA+oD9YQBhBAEI4QvBEOETQRXhGSEcgSCBJSEpYTBBNCE3AAAAABAAAAGABUAAkAAAAAAAIAFAAkAHMAAABeC3AAAAAAeNqNkM9qE1EUxr9JpmA10IWb5O63qIiztKkmhdIuijtFK1QQF93lJpnak8kkTGaCG7fufIC+gK/g0ifQR3DjE3TrL9Mmkha60Jlz7/3ud77vnHsAL/ADEee7A+CZ8QgveTKOsIOrwGO8ws/AU7jCn8DTKIN54Bns4GPgWbzB58BzqPE88Dze8k/gBVTxFZPsEg3jCGt4H3iMZ/gaeIw3RANPocK7wNOY4UfgGSyj13PZ8T6Gcva1q2vjlDFWvZJHsxfdFWJuXTqWyhY1qEXdnRJ9k0k1MPEyt0pvG33t5vbeeOEaYR9n/OxYp1ImXlvr0wPdm0S63Ntp7YzW/2w3iXPCd8ytsU6tnVeZiWXoGG0LV2oT23TDOfHIpFmmQmeOJh7OUb+2tdwxn8SamDQsT9Mxvj5UhqX/P/PoXqbZkY7Lml70jpKpXVKlJCnb5UMzz4bTYZK0y6iyNYaIkeDQWUwwa2XRMUlzqbpJOY6nZrFCXtWf7dPCLCcmzXztZbXWxDQoG3bvrFiJ7hFKZMjtAi/J9ujaRBmK7hgG59TsNHGNM25KOcpx1qAeYIUzPJbkptGmI6e9zJF83WTK3QnKf3JXd8K9ZbYOu7V0IG9GZzkHF/W08YR6eWl7fS86FPGe/hYdbDn24WDY7fLe0v2v3PUydjY4XT+5kBzxLpZEKP8Aqcmm9wB42nWRR0/DQBCF39S0AgkEvRMSUkhhx6FUCRAfSAiBIwc+J+vExnIcxfHvYe+IOZD27c7MzpsyBlC/JsQo6GJYsZIxTOAkWXZYV2BxLXl8Yl2ZTdKaE+JQWEeRbRTb6Ni4ZVtDhW11bKSo96OoyHsQxbn8hqJYyR8oir/6NUVhIL+iKBbya4rCJ39IUVjyegqFIRsGjMnX4LsVvMKCT1HEoKJ4hA2P4o3O9UX+0rlh5AqVcuX+1H7iCZlxfIFLXOEaN3hCS0rIaHlGBy94xRs+pM5MyqoWZubMN7KZibmpDSPN3M1Ek5mnmY2ZXzNHZqFmoc1CzRW7Bnc1YvqtLaSxgUMcYQ9FbJLMWDcnuUwG/A9SvstAeNpVzLsOQFAAQNHbvr7F04XoSUSCGOwWiUkxSGxWu/9/NF7OcgCPEgRJCURpWUVOU1ZRU9fkdPUNREMjCcnUTDa3UBStLK1t7OyVHXQdVVSFFidnF1c3dw9PL2+fvY+vn39AYFDwIST0GBYeERldiI29pKRe09Kzsr8jAAAAuAH/hbAEjQAAACwALAAsACwAeAC0AQgBcAIKAkoCpAMMlA+mhQAAeNq1Vc1vG0UUf29mdmZ3vWuv7V3HeeLYSfw5sWM7jms7aZOUNFE+mjRqSUupIqJWREIIqagSHwcUkCohoA0gLiAQEicuIMS/AJdK3Ljm1IQTp1wo9r5ZO3ZJicSJHc3uzG/ee/N+7/d7awAAH78HYLAMd0EO7sNBAEnTZ9kyJ+yzZOm0acMJkfY+lslJEzTj0+Y2ckTOchSF5g7Q8Kk/9aCLQxMEq6DBKTgJAI5sIRLq00b2yKakZ/6kz/LluIMaDof3hnvDR8NteDb8cng+3EHsF+kXcfqMf4X+Bf0lSYkpCQSPNW1YsZ1UyrJSTNEeKJZlqZZVSWA7PZzSHMR0cDOVNu2KVcr4lCw17dihqm9XKxYq11KVdKWWqW6kE2nLKW1v8YRTcQ61JcvOpG+UbcvJ1Aq4ZGfrFSdXLRU3Nnd2P4QNKFCGWp2rV9Hv2iU0+R3oYKXKpVLaxqQrONksZ7LpbDZXcXIyktdz9UZho7nZ2mnvo92XM02Xy9nNDdrFRiEFt+EOfARD+IZMfkteJ0nyLbkgZ8g9ksBaIk6CFXrX3Aw5B7fgFD4Fw7yPxmRpnkbdmb1R8WavVz3LlD17J3O+KudLmXyg08HsZDLVslfXhO6rJb8WCYRisWAoFJwQhFAsHo1HhWgy2I9FY0+fh0fPBDmFfyv+bHO5YXbDRbKn6jX9Gu4pSkNV67jHe6+pqrrKe6+e9VSYg9PwOtwYvT9/RfwuAE5K8ZjH1EfhJ1t/8JMQElvgMVnvZ4iQ8Ah/g5/HcFDwGBt0FIYBPPbAmAUw4uDjD1j1fhr7PsY/MXTwWPc45scxXhnHUsCMSPUTi/IA+2kzllvYbsHzMKLg8ORHmMVtW9juYPsA203ccXDcwfYDtttjm3cwRsSHGa3QQv2l1NzQ6kbBsOLJiVBsIhwJBGKq8jwZDUyQUCQSCoR0UbvQtXN+oWw+EaHiuLGTnFdkn6woEz61KfOVU49XrqSXnYXW9uL5tYXVlaXzi/XmbK1Wr9XL5dnZpfWlwWzrmVpwMb1irM5pXPzs8vHp6uRkNRgKhYLBaOxMsVhpuIk35ouOXkuLhRXJPzlpmqbKy6aWkROLPvGRmZqyV+aX1i+fvvz9++9cunoGjh1bmZ21ZwqFWq3V6nc7Syvzyxur3fPV+trcTKFg56t6pVI+4efVxDJ9cVJR2mBE4xFdCqj84eSEqsuRgK4fDyRDIT2oRyICjzOH/aXDvsIxiJEgaHAOiV5DLmoo8CUOVSc/C9dRX8HuBO4bHKZxlK5YHRdcvZaQbWyXOcjCFWR3nW8iA3fhDlJmHzHOCe/JQdHRO9g9wCq7OLLPVXYFdvHsVZR4DZEjvuJ+Uyo9P9Ove5vX6aNxd1frnXMDRUdHjJESrp7vYAyXnHO3I/XwwT+XJVkJiEI8nQiJ4rTs97+j+H0zfklSBEl5VyGbyoIiCqqqHCOCQMRQNB6NhJOxSHpSoRLxaUIwIitKLBVOTcVTQVkJpHuHPaTPtHlOZF4TdnSR5ZGYHMQBTfJoKVLAiFP/9n+DPDP6wAZ/AK/BO9CBPrwNr8ClcbxLXu9gjcsdC4RrcKJPeqgmCb+FqXxE+PFnPPPQ+LyOUeTIXRGCB16GN+DNw73GIkTJO0/w14OM/xpkfPeYYLHn+MPn+L/P8UPP8RcBvs3/u4A88j9//k7+o0LfwCptNBrjf44mwqo1DFIUn/Y0bVoTNFVVSMDHB5KikyTfVCXG+CZrqtQcT8kn7JQmaJomRKcaS+3lsNgv2unLp1sLZ5cW6s2F+eW2Ua41ZyvFxtzJ5vKJZqvddE3XjBu3+ot9N9dsGu5co7WyHPz8/MbZxfOzi7Vza7P1xvz8XL1mz8y4y5dPtdyTrc7g2p1B21icddxmfXbGrpbKxbppJDQtFg3GYhOxaFDScqnMrb2LBzd+7O4N9m+++8bNL28NpZASTigJVU8mFTWRSCSSU8fDsdgn79MvpJDoV5KJePyjXU3TjUg0FOUHEjm2xhLRcCQi0pAe1YOqJoXT6XAkORWOylpSNI14Qt8VFWkqIQUn49FwJKTwkygmVH0qlohpSU3WeBpyIhoKhSRZFBUpqGoTkURICyrRqUBQnEpNhUPaZDyQDKrx8LEPNCUQjvB06jQaSk5p0qSqJaNqJJKMB4JhfSqqwf8QzaUxAHjah1j5ZZXaJgAAPgwYiK9JcL/70ZUu3ZfSF+neoW+kmUn3pEiH7oEUE6kLGYD7CW9vW0Db8fYGMPa+0cUA0J3hGQZgOZbQNO5wbRpHuN4wP37jeII/LS7qwbX7H343+A4j4K7GCEHqJ9bnr/GP9QMbhjP8w9oGwPcXgLcniwQEyOiCvAG8lZb4W2VIDQDpRlJ83Vta5E2nLQfRIVxaWuDQx55gOVLJt/cBACCpfBsA8CKTv1NnSZcUAKqvqzulBvBu6YCYrqsDbJkaNvZgdEXQAW+DLdKz4TBvAxpO7P0cLYU75S2Vo22ZRE9mEhstTY6ZkuQZBmm2pLKR8LS8nLAYhtSSrOSNWUlOLSnJbEL8ykgkFEyKX5hzRZF+TYRDfkkrKkmzmEgw6pPWlESmGDN5+w8FTWVC7JOjWYf4I5Iyj4/D3X4+6PULIaeQ8HqDO1KvWyzpY/79BW96JFVw+sTRhMcrkQVvwicuSmXeUt43JvSX8n5v0SnJwdK+V5KKYo83KfgkBY/YJ5ZK4vygTyztA4+cYsL7RyogPCZ9whcmXH2hT2Qlpa8WEzPlmZnKZcXLXF9ZnrE7s1lnoZqrVOazC4ZzsuasJCbgzeJlS5Vj82PzmVKmvAxPFyvzVefExPLyxNj8fKVYXJiAT6vVqhAWJifw5vLYDHxcnJutGBnDwMuyvLIyu7w8W1lZkeWxsbGZGcPIFwqTk0bBKBQmJwuGkc/D59VqFT4lDQmSvZnO5pK59M7TJz89faG/0I5kkxmyocvK+GFRLm2XFtR2oagf1lTtu25Rb9cVzSQNcg56ByY/iXEIg/Pn+8MbD4KT8KZBg1sGDfYd9EsODZYcGqQcNOg70FgxqcGJiQbvH1igqwJl46YvIFhgP1hgO1hg7aBCzUGFkoMKRQcNbh00KDhokHfQoHvQIHvQIOcY10p+DWsZSduzZlrGrI2MJm/KJEtxvBQnS3G8FCdLcbwUr57gK8E/gntN8o+/KI9I8jfvnbU2XkJQ4kl4wz2hpAEfjMr/UgP/AcNMGMABAAAACgAKAAoADAAB//8AAwijYGRgYOADYgkGEwZmBiwGMgYKDGq7jhyYv294/MHt/0/+/z1XaA4AA+0MFAAAAHjaY2BiYGBgYFBjSGFgYghjEGJgYmBgcAViCyBmg/IUwSrhv0LjL1Djn9ik+0qM0oyMjD8E3RgD+F8o+r8z8j6hEvkPQqiEvxIj74PfD0T/0vZ+E/zI+Ifp3Scm3rdMn98z/bj/gPvVg4tM/79fYfgLAFMPJIYAAQAB//8AD3jalVVbbBRVGP7OmZ2Z3ZnZO93d9tLd9n5p2e12l9JCbSktyFIKlIsIpYAQMEYk0ggYlfgAeOFFECPBeHkwmqgvJD5IIIHwQJNGggQMMYEHeTDxATHGA6Tbc2Z2u6Wok8w5/3/5zn/5vvP9BxTgifuc3sUJKKgbgIAIlbZE85Vyvd3wEJUrnXIXe0IPQ28kIJGD2r5F7p5dC5uyRncqzrOHf9+kDX7wXpx89/WBt7HMp1vHB+cM8CNAUBeAdSfPHVeVZnseRmZrvhQf9RkI4DtOvvRYK7c9fmRKnKhwVEW5Wu+08xKW27XO7OpoN4VLu1nq1rHVbgsjFTi4YOPx81fWZQz1/MLnl8/oAfl0RNyS4xbPRSN9yfQ9cxA+uyAMXGZXwANagKEqMCoIAKTAIHt54BLtXIBoCn7bnS1i95FsUUTUyGIH+Nd3xtOTPOSrMAxQUCgC3RnVDa7qgBY8MnUwhCZG0I+iH0UvigMoOihCKEygWED+P+4wBIxRDjc7tCMfZDtm9HgUUYr+Rnq3y5VKlwnSHHYb3aq4GQcqbdXHjX0pebTSp0ZqdL1zJX+KHKWm+diFl85ub+VeHh3eXXg50JEqfV+dWXsokKKl+YMzEzLIscx8cdNDZSXkTzI9GUulvdqcWGoq57d9+cVoL0s6MxvK55y2TYgmMqLjkKqG1YSYN+OuVd+mZxZntmV91x/CJZbST9zN9BQ4IZQD4H2eDX8Yt8kxF8c3+q8RD48X1EQb69UOzpYH9BpzXfK7+h5XMQQhOSjwJhMRcTPsCftsIRXSFIJbxO6xbduzrQyOcaKKG+0B2c08VnGUjWGl2aRGFY4LDWUG3YrwsHI4sNIh9fZ6gO+e17yR6f3jF/T13/3iq4lH53snJrdvK10t7nvn8q3Rz24N8+tjqw8dPn5C3njzk7N3Lnx9+4eBZx42H72xb9/4G0OpTYe+unVwYu+pS4f89GWDy8WDzJewYVsAGUUURVzRQBBQklF0USQIhjEKAkGMiYgCRaxGxajL6fCh16G4JdWTdKZcdjeuJEJ93qfcXn12ZPZUomTj1Mzn+w92/8p+ff+dtYvzwQ/W1vXrT1NWqozg1DK1bHKZdPo1HEtrNlbD0aiLGzxWwwJPMQrUH2N0MCGoyziGVwgoPdWiIQeXgK5jd4J/LDDyZWRZObLhD2bIBmOQwsQR+JZK4JXnTm3ceWHm1vL6sRXlwXwf7+2lz+TevbqnKkm/xJfnMxRu/fLh7OT7p7c5+e7R/WvW+r97tG3d6ZNr9XVnJrftGc41N33/YImUlv2JsxMjr5jxpPnUu+qFXz7xhh7kYcaELa4uZQolLSk3/2FaY4IgwZ0iM7ZA8rADojq9gZlhqGCXO6q6dHAYNzslcpQTGK85jbqgXO4M8RWQUPywxkJhhb8RjgUZA59qizCU6QptRe0/YGM+xSIu1UQRyJoVTDpnRKJ9Xs8CVzRiF3miGvG+TP5fXM1aGqv+LDQrVj1ZaE6s2rmkEkPWbFnE0oJ6ONdL4WzNvUQjnK1bIEYxuOBsjzLsYdLhH80JZ/vMIZYQDu0a8kCDMzdMJbIgwX0MBwhHIABdLM7wLOUGGGGQxbcIlOzCK3qI6QRt43f3+8y7uYj7IpvM9Jk3L4VywdCc0+Pq7bZzL7s7LzYfr5vzODsvZLPpzveNm/n+Zv7G4+KN/GAjz4d01lCNl1kYsKxbORMRuRV1Pam6kX1FcAjYE0XvkoAnKGKA2BEXbmJ9HjG6pN+0Q4ZIsEgpuEqOhKxYDL2FRjWGqXYVa/rvGQXEZhmVAZUJqPbp0jtv0U1jnk1vHD19ZH2Mb9KTYl3fs3Y2nYp6X+Sbt51f2ru/UWg2G6cGD82vmqXzqcZANfztH2MDX8YIv5Uu+/aN2Zfu5VOF6D11+0rPlS9K5XPRwpWu7/ZefXWLnr0Rja1u8rsnD8L/S24SY/I4ifjjEwAA') format('woff'); -} + body, input, button, select, textarea { font-family: var(--font-main); @@ -155,6 +180,14 @@ header h1 { text-shadow: 0 0 8px var(--bright-white), 0 0 15px var(--bright-cyan); } +/* ACCESSIBILITY: Focus states for site title */ +.site-title a:focus { + outline: 2px solid var(--bright-white); + outline-offset: 2px; + color: var(--bright-white); + text-shadow: 0 0 8px var(--bright-white), 0 0 15px var(--bright-cyan); +} + /* Navigation with ZX Spectrum colors */ nav { margin: var(--spacing-md) 0; @@ -262,6 +295,15 @@ a:hover { text-shadow: 0 0 4px var(--bright-white); } +/* ACCESSIBILITY: Focus states for general links */ +a:focus { + outline: 2px solid var(--bright-yellow); + outline-offset: 2px; + color: var(--bright-white); + background-color: var(--blue); + text-shadow: 0 0 4px var(--bright-white); +} + /* Articles with ZX Spectrum border */ article { margin-bottom: var(--spacing-lg); @@ -322,6 +364,14 @@ article .meta { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for tags */ +.tags a:focus { + outline: 2px solid var(--bright-yellow); + outline-offset: 2px; + background-color: var(--bright-cyan); + transform: translateY(-2px); +} + code { font-family: var(--font-mono); background-color: var(--blue); @@ -387,6 +437,14 @@ footer { transform: translateY(-2px); } +/* ACCESSIBILITY: Focus states for pagination */ +.pagination a:focus { + outline: 2px solid var(--bright-yellow); + outline-offset: 2px; + background-color: var(--bright-cyan); + transform: translateY(-2px); +} + .pagination .page-info { color: var(--bright-white); background-color: var(--blue); From 70760825b07241f384c2932581af4195f199d8ad Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Wed, 18 Jun 2025 19:26:13 +0200 Subject: [PATCH 05/21] Preparing for 0.31 - related posts, theme fixes, etc --- README.md | 9 + config.sh | 6 +- locales/de.sh | 3 +- locales/en.sh | 3 +- locales/es.sh | 3 +- locales/fr.sh | 3 +- locales/it.sh | 3 +- locales/ja.sh | 3 +- locales/pt.sh | 3 +- locales/zh.sh | 3 +- scripts/bssg.sh | 2 +- scripts/build/cache.sh | 6 + scripts/build/config_loader.sh | 7 + scripts/build/generate_posts.sh | 304 ++++++++++++++++++++++---------- scripts/build/main.sh | 14 ++ scripts/build/related_posts.sh | 251 ++++++++++++++++++++++++++ themes/default/style.css | 103 +++++++++++ themes/italy/style.css | 32 ++++ 18 files changed, 657 insertions(+), 101 deletions(-) create mode 100644 scripts/build/related_posts.sh diff --git a/README.md b/README.md index 02ba8ea..150de18 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ - Admin interface for managing posts and scheduling publications (planned for future release) - Standalone post editor with modern Ghost-like interface for visual content creation - Creates tag index pages +- Related Posts: Automatically suggests related posts based on shared tags at the end of each post - Author index pages with conditional navigation menu - Archives by year and month for chronological browsing - Dynamic menu generation based on available pages @@ -721,6 +722,10 @@ RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be included in the RSS feed description instead of the excerpt. Useful for readers that consume entire posts via RSS. ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags//rss.xml`. +# Related Posts configuration +ENABLE_RELATED_POSTS=true # Options: "true", "false". If set to "true" (default), related posts based on shared tags will be shown at the end of each post. +RELATED_POSTS_COUNT=3 # Number of related posts to display (default: 3, recommended maximum: 5). + # Deployment configuration DEPLOY_AFTER_BUILD="false" # Options: "true", "false". Automatically deploy after a successful build. DEPLOY_SCRIPT="" # Path to the deployment script to execute if DEPLOY_AFTER_BUILD is true. @@ -1133,6 +1138,10 @@ RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be included in the RSS feed description instead of the excerpt. Useful for readers that consume entire posts via RSS. ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags//rss.xml`. +# Related Posts configuration +ENABLE_RELATED_POSTS=true # Options: "true", "false". If set to "true" (default), related posts based on shared tags will be shown at the end of each post. +RELATED_POSTS_COUNT=3 # Number of related posts to display (default: 3, recommended maximum: 5). + # Multi-author configuration ENABLE_AUTHOR_PAGES=false # Options: "true", "false". If set to "true", author index pages will be generated. ENABLE_AUTHOR_RSS=false # Options: "true", "false". If set to "true", RSS feeds will be generated for each author. diff --git a/config.sh b/config.sh index a08d033..c94c2d4 100644 --- a/config.sh +++ b/config.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # BSSG - Configuration File -# Version 0.30 +# Version 0.31 # Contains all configurable parameters for the static site generator # Developed by Stefano Marinelli (stefano@dragas.it) # @@ -65,6 +65,10 @@ MARKDOWN_PROCESSOR="commonmark" # Options: "pandoc", "commonmark", or "markdown. # Language Configuration SITE_LANG="en" # Default language code (e.g., en, es, fr). See locales/ directory. +# Related Posts Configuration +ENABLE_RELATED_POSTS=true # Enable or disable related posts feature +RELATED_POSTS_COUNT=3 # Number of related posts to show (default: 3) + # Server Configuration (for 'bssg.sh server' command) # These are the defaults used by 'bssg.sh server' if not overridden by command-line options. BSSG_SERVER_PORT_DEFAULT="8000" # Default port for the local development server diff --git a/locales/de.sh b/locales/de.sh index 3ef2cf0..5b6e572 100644 --- a/locales/de.sh +++ b/locales/de.sh @@ -47,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d Min. Lesezeit" export MSG_MINUTE="Minute" export MSG_MINUTES="Minuten" export MSG_UPDATED_ON="Aktualisiert am" -export MSG_BACK_TO_TOP="Nach oben" \ No newline at end of file +export MSG_BACK_TO_TOP="Nach oben" +export MSG_RELATED_POSTS="Ähnliche Beiträge" \ No newline at end of file diff --git a/locales/en.sh b/locales/en.sh index 606e881..9b15347 100644 --- a/locales/en.sh +++ b/locales/en.sh @@ -47,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min read" export MSG_MINUTE="minute" export MSG_MINUTES="minutes" export MSG_UPDATED_ON="Updated on" -export MSG_BACK_TO_TOP="Back to Top" \ No newline at end of file +export MSG_BACK_TO_TOP="Back to Top" +export MSG_RELATED_POSTS="Related Posts" \ No newline at end of file diff --git a/locales/es.sh b/locales/es.sh index 8351c2f..33e6f33 100644 --- a/locales/es.sh +++ b/locales/es.sh @@ -47,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de lectura" export MSG_MINUTE="minuto" export MSG_MINUTES="minutos" export MSG_UPDATED_ON="Actualizado el" -export MSG_BACK_TO_TOP="Volver arriba" \ No newline at end of file +export MSG_BACK_TO_TOP="Volver arriba" +export MSG_RELATED_POSTS="Artículos relacionados" \ No newline at end of file diff --git a/locales/fr.sh b/locales/fr.sh index af14657..8d88750 100644 --- a/locales/fr.sh +++ b/locales/fr.sh @@ -47,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de lecture" export MSG_MINUTE="minute" export MSG_MINUTES="minutes" export MSG_UPDATED_ON="Mis à jour le" -export MSG_BACK_TO_TOP="Retour en haut" \ No newline at end of file +export MSG_BACK_TO_TOP="Retour en haut" +export MSG_RELATED_POSTS="Articles connexes" \ No newline at end of file diff --git a/locales/it.sh b/locales/it.sh index 20275cf..04f0169 100644 --- a/locales/it.sh +++ b/locales/it.sh @@ -47,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min di lettura" export MSG_MINUTE="minuto" export MSG_MINUTES="minuti" export MSG_UPDATED_ON="Aggiornato il" -export MSG_BACK_TO_TOP="Torna su" \ No newline at end of file +export MSG_BACK_TO_TOP="Torna in cima" +export MSG_RELATED_POSTS="Articoli correlati" \ No newline at end of file diff --git a/locales/ja.sh b/locales/ja.sh index 92a9f7d..6c14574 100644 --- a/locales/ja.sh +++ b/locales/ja.sh @@ -47,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="読了時間 %d分" export MSG_MINUTE="分" export MSG_MINUTES="分" export MSG_UPDATED_ON="更新日" -export MSG_BACK_TO_TOP="トップに戻る" \ No newline at end of file +export MSG_BACK_TO_TOP="トップに戻る" +export MSG_RELATED_POSTS="関連記事" \ No newline at end of file diff --git a/locales/pt.sh b/locales/pt.sh index 6313ddf..5c0f7d5 100644 --- a/locales/pt.sh +++ b/locales/pt.sh @@ -47,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de leitura" export MSG_MINUTE="minuto" export MSG_MINUTES="minutos" export MSG_UPDATED_ON="Atualizado em" -export MSG_BACK_TO_TOP="Voltar ao topo" \ No newline at end of file +export MSG_BACK_TO_TOP="Voltar ao topo" +export MSG_RELATED_POSTS="Posts Relacionados" \ No newline at end of file diff --git a/locales/zh.sh b/locales/zh.sh index 10cd77b..26a491e 100644 --- a/locales/zh.sh +++ b/locales/zh.sh @@ -47,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="阅读时间 %d 分钟" export MSG_MINUTE="分钟" export MSG_MINUTES="分钟" export MSG_UPDATED_ON="更新于" -export MSG_BACK_TO_TOP="返回顶部" \ No newline at end of file +export MSG_BACK_TO_TOP="返回顶部" +export MSG_RELATED_POSTS="相关文章" \ No newline at end of file diff --git a/scripts/bssg.sh b/scripts/bssg.sh index 092510a..16089f8 100755 --- a/scripts/bssg.sh +++ b/scripts/bssg.sh @@ -109,7 +109,7 @@ chmod +x scripts/*.sh 2>/dev/null || true # Function to display help information show_help() { - echo "BSSG - Bash Static Site Generator (v0.30)" + echo "BSSG - Bash Static Site Generator (v0.31)" echo "=========================================" echo "" echo "Usage: $0 [--config ] command [options]" diff --git a/scripts/build/cache.sh b/scripts/build/cache.sh index 11ab1fd..8836073 100755 --- a/scripts/build/cache.sh +++ b/scripts/build/cache.sh @@ -184,6 +184,12 @@ clean_stale_cache() { # Also remove tag and archive pages to force their regeneration find "${OUTPUT_DIR:-output}/tags" -name "*.html" -type f -delete 2>/dev/null || true find "${OUTPUT_DIR:-output}/archives" -name "*.html" -type f -delete 2>/dev/null || true + + # Clean related posts cache when posts are removed + if [ -d "${CACHE_DIR}/related_posts" ]; then + echo -e "${YELLOW}Cleaning related posts cache due to post removal...${NC}" + rm -rf "${CACHE_DIR}/related_posts" + fi fi echo -e "${GREEN}Cache cleaned!${NC}" diff --git a/scripts/build/config_loader.sh b/scripts/build/config_loader.sh index 3be4620..3ff16e5 100755 --- a/scripts/build/config_loader.sh +++ b/scripts/build/config_loader.sh @@ -46,6 +46,10 @@ ENABLE_AUTHOR_PAGES="${ENABLE_AUTHOR_PAGES:-true}" # Generate author index pages ENABLE_AUTHOR_RSS="${ENABLE_AUTHOR_RSS:-false}" # Generate RSS feed for each author SHOW_AUTHORS_MENU_THRESHOLD="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" # Minimum authors to show menu +# Related Posts Configuration Defaults +ENABLE_RELATED_POSTS="${ENABLE_RELATED_POSTS:-true}" # Enable or disable related posts feature +RELATED_POSTS_COUNT="${RELATED_POSTS_COUNT:-3}" # Number of related posts to show + # --- Backup Directory --- Added --- BACKUP_DIR="${BACKUP_DIR:-backup}" # Default backup location @@ -214,6 +218,7 @@ BSSG_CONFIG_VARS_ARRAY=( BACKUP_DIR CACHE_DIR DEPLOY_AFTER_BUILD DEPLOY_SCRIPT ARCHIVES_LIST_ALL_POSTS + ENABLE_RELATED_POSTS RELATED_POSTS_COUNT # Add any other custom config variables here if needed BSSG_SERVER_PORT_DEFAULT BSSG_SERVER_HOST_DEFAULT # Server defaults ) @@ -266,6 +271,8 @@ export CACHE_DIR export DEPLOY_AFTER_BUILD export DEPLOY_SCRIPT export ARCHIVES_LIST_ALL_POSTS +export ENABLE_RELATED_POSTS +export RELATED_POSTS_COUNT # Server defaults export export BSSG_SERVER_PORT_DEFAULT diff --git a/scripts/build/generate_posts.sh b/scripts/build/generate_posts.sh index 22592e3..146a7a9 100755 --- a/scripts/build/generate_posts.sh +++ b/scripts/build/generate_posts.sh @@ -11,6 +11,8 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s source "$(dirname "$0")/content.sh" || { echo >&2 "Error: Failed to source content.sh from generate_posts.sh"; exit 1; } # shellcheck source=cache.sh disable=SC1091 source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_posts.sh"; exit 1; } # For file_needs_rebuild checks etc. +# shellcheck source=related_posts.sh disable=SC1091 +source "$(dirname "$0")/related_posts.sh" || { echo >&2 "Error: Failed to source related_posts.sh from generate_posts.sh"; exit 1; } # For related posts functionality # --- Post Generation Functions --- START --- @@ -295,15 +297,15 @@ convert_markdown() { post_meta_reading_time=$(printf "${MSG_READING_TIME_TEMPLATE:-%d min read}" "$reading_time") local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" local post_meta="
    " - post_meta+="

    " - post_meta+="${MSG_PUBLISHED_ON:-Published on}: ${MSG_BY:-by} $display_author_name" + post_meta+="

    " + post_meta+="${MSG_PUBLISHED_ON:-Published on}: ${MSG_BY:-by} $display_author_name" post_meta+="

    " if [ "$formatted_date" != "$formatted_lastmod" ]; then - post_meta+="

    " + post_meta+="

    " post_meta+="${MSG_UPDATED_ON:-Updated on}: • $post_meta_reading_time" post_meta+="

    " else - post_meta+="

    $post_meta_reading_time

    " + post_meta+="

    $post_meta_reading_time

    " fi post_meta+="
    " @@ -314,9 +316,27 @@ convert_markdown() { image_html="
    \"$alt_text\"
    ${image_caption:-$title}
    " fi + # Generate related posts if enabled and tags exist + local related_posts_html="" + if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && [ -n "$tags" ]; then + echo -e "${BLUE}DEBUG: Generating related posts for $slug with tags: $tags${NC}" + related_posts_html=$(generate_related_posts "$slug" "$tags" "$date" "${RELATED_POSTS_COUNT:-3}") + else + echo -e "${BLUE}DEBUG: Skipping related posts for $slug - ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}, tags=$tags${NC}" + fi + # Construct article body local final_html="${header_content}" - final_html+=$(printf '
    \n

    %s

    \n%s\n%s\n%s\n%s\n
    \n' "$title" "$post_meta" "$image_html" "$html_content" "$tags_html") + final_html+='
    '$'\n' + final_html+="

    $title

    "$'\n' + final_html+="$post_meta"$'\n' + final_html+="$image_html"$'\n' + final_html+="$html_content"$'\n' + final_html+="$tags_html"$'\n' + if [ -n "$related_posts_html" ]; then + final_html+="$related_posts_html"$'\n' + fi + final_html+='
    '$'\n' # Replace placeholders in footer content local current_year=$(date +'%Y') @@ -361,38 +381,191 @@ process_all_markdown_files() { fi echo -e "Checking ${GREEN}$total_file_count${NC} potential posts listed in index." - # --- Start Change: Clear previous modified tags and authors lists --- - echo "Clearing previous modified tags list: $modified_tags_list" >&2 # Debug message - echo "Clearing previous modified authors list: $modified_authors_list" >&2 # Debug message - rm -f "$modified_tags_list" - rm -f "$modified_authors_list" - touch "$modified_tags_list" # Ensure file exists even if empty - touch "$modified_authors_list" # Ensure file exists even if empty - # --- End Change --- + # --- OPTIMIZATION: Quick check if any posts need rebuilding --- + local needs_pass1=false + local posts_needing_rebuild=0 + + # Only do expensive Pass 1 if related posts are enabled AND posts might need rebuilding + if [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then + echo -e "${BLUE}DEBUG: Related posts enabled, starting quick scan...${NC}" + # Quick scan to see if ANY posts need rebuilding before doing expensive Pass 1 + echo -e "${YELLOW}Quick scan: Checking if any posts need rebuilding...${NC}" + + while IFS= read -r line; do + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" + + # Basic check if it looks like a post + if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then + continue + fi + + # Calculate expected output path + local year month day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + year=$(date +%Y); month=$(date +%m); day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; + url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" + local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" + + # Quick rebuild check + common_rebuild_check "$output_html_file" + local common_result=$? + local needs_rebuild=false + + if [ $common_result -eq 0 ]; then + needs_rebuild=true + else + local input_time=$(get_file_mtime "$file") + local output_time=$(get_file_mtime "$output_html_file") + if (( input_time > output_time )); then + needs_rebuild=true + fi + fi + + if $needs_rebuild; then + posts_needing_rebuild=$((posts_needing_rebuild + 1)) + needs_pass1=true + # Early exit optimization: if we find posts needing rebuild, we need Pass 1 + break + fi + done < "$file_index" + + echo -e "Quick scan result: ${GREEN}$posts_needing_rebuild${NC} posts need rebuilding" + fi + + # --- PASS 1: Only run if needed (posts need rebuilding AND related posts enabled) --- + if [ "$needs_pass1" = true ] && [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then + echo -e "${BLUE}DEBUG: Both needs_pass1=true and ENABLE_RELATED_POSTS=true, running Pass 1...${NC}" + echo -e "${YELLOW}Pass 1: Identifying modified tags for related posts cache invalidation...${NC}" + + # Clear previous modified tags lists + rm -f "$modified_tags_list" + rm -f "$modified_authors_list" + touch "$modified_tags_list" # Ensure file exists even if empty + touch "$modified_authors_list" # Ensure file exists even if empty + + while IFS= read -r line; do + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" + + # Basic check if it looks like a post + if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then + continue + fi + + # Calculate expected output path + local year month day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + year=$(date +%Y); month=$(date +%m); day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; + url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" + local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" + + # Perform the rebuild check here + common_rebuild_check "$output_html_file" + local common_result=$? + local needs_rebuild=false + + if [ $common_result -eq 0 ]; then + needs_rebuild=true # Common checks failed (config changed, template newer, output missing) + else # common_result is 2 (output exists and newer than templates/locale) + local input_time=$(get_file_mtime "$file") + local output_time=$(get_file_mtime "$output_html_file") + if (( input_time > output_time )); then + needs_rebuild=true # Input file is newer + fi + fi + + # If post needs rebuilding, add its tags to the modified list + if $needs_rebuild; then + local new_tags="$tags" + local old_tags="" + # Try to get old tags from the previous index snapshot + if [ -f "$file_index_prev" ]; then + old_tags=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f6) + fi + + # Combine old and new tags + local combined_tags="${old_tags},${new_tags}" + + if [ -n "$combined_tags" ]; then + # Split by comma, trim, filter empty, sort unique, and add each tag on a new line + echo "$combined_tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . | sort -u >> "$modified_tags_list" + fi + + # Track modified authors (similar logic to tags) + local new_author="$author_name" + local old_author="" + if [ -f "$file_index_prev" ]; then + old_author=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f11) + fi + + # Add both old and new authors to the modified list (if they exist) + if [ -n "$old_author" ] && [ "$old_author" != "" ]; then + echo "$old_author" >> "$modified_authors_list" + fi + if [ -n "$new_author" ] && [ "$new_author" != "" ]; then + echo "$new_author" >> "$modified_authors_list" + fi + fi + done < "$file_index" + + # Unique sort the modified tags and authors lists + if [ -f "$modified_tags_list" ]; then + local temp_tags_list=$(mktemp) + sort -u "$modified_tags_list" > "$temp_tags_list" + mv "$temp_tags_list" "$modified_tags_list" + fi + + if [ -f "$modified_authors_list" ]; then + local temp_authors_list=$(mktemp) + sort -u "$modified_authors_list" > "$temp_authors_list" + mv "$temp_authors_list" "$modified_authors_list" + fi + + # Invalidate related posts cache if there are modified tags + if [ -f "$modified_tags_list" ] && [ -s "$modified_tags_list" ]; then + # Source related posts functions if not already loaded + if ! command -v invalidate_related_posts_cache_for_tags > /dev/null 2>&1; then + # shellcheck source=related_posts.sh disable=SC1091 + source "$(dirname "$0")/related_posts.sh" || { echo -e "${RED}Error: Failed to source related_posts.sh${NC}"; exit 1; } + fi + + # Create a temporary file to capture the list of invalidated posts + RELATED_POSTS_INVALIDATED_LIST="${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list" + > "$RELATED_POSTS_INVALIDATED_LIST" # Create empty file + + # Call the invalidation function with the output file + invalidate_related_posts_cache_for_tags "$modified_tags_list" "$RELATED_POSTS_INVALIDATED_LIST" + + # Export the list for use in pass 2 + export RELATED_POSTS_INVALIDATED_LIST + fi + else + echo -e "${BLUE}DEBUG: Pass 1 skipped - needs_pass1=$needs_pass1, ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}${NC}" + fi + + # --- PASS 2: Process posts with proper rebuild flags --- + echo -e "${YELLOW}Pass 2: Processing posts...${NC}" # Pre-filter files that need rebuilding local files_to_process_list=() local files_to_process_count=0 local skipped_count=0 - # Get template/locale mtimes once (requires utils.sh and cache.sh to be sourced) - # IMPORTANT: Assumes get_file_mtime, TEMPLATES_DIR, THEME, LOCALE_DIR, SITE_LANG are available - local template_dir="${TEMPLATES_DIR:-templates}" - if [ -d "$template_dir/${THEME:-default}" ]; then - template_dir="$template_dir/${THEME:-default}" - fi - local header_template="$template_dir/header.html" - local footer_template="$template_dir/footer.html" - local active_locale_file="" - if [ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]; then - active_locale_file="${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" - elif [ -f "${LOCALE_DIR:-locales}/en.sh" ]; then - active_locale_file="${LOCALE_DIR:-locales}/en.sh" - fi - local header_time=$(get_file_mtime "$header_template") - local footer_time=$(get_file_mtime "$footer_template") - local locale_time=$(get_file_mtime "$active_locale_file") - while IFS= read -r line; do local file filename title date lastmod tags slug image image_caption description author_name author_email IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" @@ -419,8 +592,6 @@ process_all_markdown_files() { local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" # Perform the rebuild check here - # IMPORTANT: Requires common_rebuild_check, get_file_mtime to be available - # Requires BSSG_CONFIG_CHANGED_STATUS to be exported by main.sh common_rebuild_check "$output_html_file" local common_result=$? local needs_rebuild=false @@ -435,48 +606,17 @@ process_all_markdown_files() { fi fi + # Check if this post needs rebuilding due to related posts cache invalidation + if [ "$needs_rebuild" = false ] && [ -n "${RELATED_POSTS_INVALIDATED_LIST:-}" ] && [ -f "$RELATED_POSTS_INVALIDATED_LIST" ]; then + if grep -Fxq "$slug" "$RELATED_POSTS_INVALIDATED_LIST" 2>/dev/null; then + needs_rebuild=true # Related posts cache was invalidated + echo -e "Rebuilding ${GREEN}$(basename "$file")${NC} due to related posts cache invalidation" + fi + fi + if $needs_rebuild; then files_to_process_list+=("$line") files_to_process_count=$((files_to_process_count + 1)) - # --- Start Change: Track ALL modified tags and authors (old and new) --- - # 'tags' variable holds the NEW tags from the current file_index line - local new_tags="$tags" - local old_tags="" - # Try to get old tags from the previous index snapshot - if [ -f "$file_index_prev" ]; then - # Grep for the exact file path ($file), assuming it's the first field - # Extract the 6th field (tags) - old_tags=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f6) - fi - - # Combine old and new tags - local combined_tags="${old_tags},${new_tags}" - - #echo "Tracking combined tags for modified file: $file -> Old: '$old_tags' New: '$new_tags' Combined: '$combined_tags'" >&2 # Debug message - - if [ -n "$combined_tags" ]; then - # Split by comma, trim, filter empty, sort unique, and add each tag on a new line - echo "$combined_tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . | sort -u >> "$modified_tags_list" - fi - - # Track modified authors (similar logic to tags) - local new_author="$author_name" - local old_author="" - # Try to get old author from the previous index snapshot - if [ -f "$file_index_prev" ]; then - # Grep for the exact file path ($file), assuming it's the first field - # Extract the 11th field (author_name) - old_author=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f11) - fi - - # Add both old and new authors to the modified list (if they exist) - if [ -n "$old_author" ] && [ "$old_author" != "" ]; then - echo "$old_author" >> "$modified_authors_list" - fi - if [ -n "$new_author" ] && [ "$new_author" != "" ]; then - echo "$new_author" >> "$modified_authors_list" - fi - # --- End Change --- else # Only print skip message if not rebuilding echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}" @@ -484,24 +624,6 @@ process_all_markdown_files() { fi done < "$file_index" - # --- Start Change: Unique sort the modified tags and authors lists (redundant now but safe) --- - if [ -f "$modified_tags_list" ]; then - echo "Sorting and making modified tags list unique: $modified_tags_list" >&2 # Debug message - local temp_tags_list=$(mktemp) - # Sort unique again just in case duplicates were added somehow - sort -u "$modified_tags_list" > "$temp_tags_list" - mv "$temp_tags_list" "$modified_tags_list" - fi - - if [ -f "$modified_authors_list" ]; then - echo "Sorting and making modified authors list unique: $modified_authors_list" >&2 # Debug message - local temp_authors_list=$(mktemp) - # Sort unique to remove duplicates - sort -u "$modified_authors_list" > "$temp_authors_list" - mv "$temp_authors_list" "$modified_authors_list" - fi - # --- End Change --- - # Check if any files need processing if [ $files_to_process_count -eq 0 ]; then echo -e "${GREEN}All $total_file_count posts are up to date.${NC}" @@ -512,7 +634,6 @@ process_all_markdown_files() { echo -e "Found ${GREEN}$files_to_process_count${NC} posts needing processing out of $total_file_count (Skipped: $skipped_count)." # Define a function for processing a single file line from the *filtered* list - # Note: This function now assumes the file *needs* processing. process_single_file_for_rebuild() { local line="$1" @@ -564,6 +685,7 @@ process_all_markdown_files() { export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME MARKDOWN_PROCESSOR MARKDOWN_PL_PATH DATE_FORMAT TIMEZONE SHOW_TIMEZONE export MSG_PUBLISHED_ON MSG_UPDATED_ON MSG_READING_TIME_TEMPLATE # Export needed locale messages export CONFIG_HASH_FILE BSSG_CONFIG_CHANGED_STATUS # Export status for common_rebuild_check + export ENABLE_RELATED_POSTS RELATED_POSTS_COUNT # Export related posts configuration # Process filtered lines in parallel printf "%s\n" "${files_to_process_list[@]}" | parallel --jobs "$cores" --will-cite process_single_file_for_rebuild {} || { echo -e "${RED}Parallel post processing failed.${NC}"; exit 1; } diff --git a/scripts/build/main.sh b/scripts/build/main.sh index 7e0de0a..099d925 100755 --- a/scripts/build/main.sh +++ b/scripts/build/main.sh @@ -299,6 +299,15 @@ if [ "${HAS_PARALLEL:-false}" = true ]; then echo "Core parallel exports complete." fi +# --- Related Posts Cache Invalidation --- START --- +# This will be handled during post processing for better timing +RELATED_POSTS_INVALIDATED_LIST="" +if [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then + # Export the variable for use by generate_posts.sh + export RELATED_POSTS_INVALIDATED_LIST +fi +# --- Related Posts Cache Invalidation --- END --- + # --- Generate Content HTML --- # Source and run Post Generator # shellcheck source=generate_posts.sh @@ -427,6 +436,11 @@ rm -f "${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt" # Remove the frontmatter changes marker if it exists rm -f "${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" +# Clean up related posts temporary files to prevent unnecessary cache invalidation on next build +rm -f "${CACHE_DIR:-.bssg_cache}/modified_tags.list" +rm -f "${CACHE_DIR:-.bssg_cache}/modified_authors.list" +rm -f "${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list" + # --- Final Cleanup --- END --- # --- Deployment --- START --- diff --git a/scripts/build/related_posts.sh b/scripts/build/related_posts.sh new file mode 100644 index 0000000..8fe0b86 --- /dev/null +++ b/scripts/build/related_posts.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env bash +# +# BSSG - Related Posts Module +# Functions for generating related posts based on shared tags +# + +# Source dependencies +# shellcheck source=utils.sh disable=SC1091 +source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from related_posts.sh"; exit 1; } +# shellcheck source=cache.sh disable=SC1091 +source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from related_posts.sh"; exit 1; } + +# --- Related Posts Functions --- START --- + +# Generate related posts for a given post based on shared tags +# Args: $1=current_post_slug $2=current_post_tags $3=current_post_date $4=max_results (optional, default=3) +# Returns: HTML snippet with related posts +generate_related_posts() { + local current_slug="$1" + local current_tags="$2" + local current_date="$3" + local max_results="${4:-3}" + + # Validate inputs + if [[ -z "$current_slug" || -z "$current_tags" ]]; then + return 0 # No related posts if missing essential data + fi + + # Check cache first + local cache_file="${CACHE_DIR:-.bssg_cache}/related_posts/${current_slug}.html" + local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + + # Create cache directory if it doesn't exist + mkdir -p "$(dirname "$cache_file")" + + # Check if cache is valid (newer than file index) + if [[ -f "$cache_file" && -f "$file_index" ]]; then + if [[ "$cache_file" -nt "$file_index" ]] && [[ "${FORCE_REBUILD:-false}" != true ]]; then + cat "$cache_file" + return 0 + fi + fi + + # Generate related posts + local related_posts_html="" + related_posts_html=$(compute_related_posts "$current_slug" "$current_tags" "$current_date" "$max_results") + + # Cache the result + echo "$related_posts_html" > "$cache_file" + + # Output the result + echo "$related_posts_html" +} + +# Core algorithm to compute related posts +compute_related_posts() { + local current_slug="$1" + local current_tags="$2" + local current_date="$3" + local max_results="$4" + + local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + + if [[ ! -f "$file_index" ]]; then + return 0 # No posts to compare against + fi + + # Convert current tags to array for comparison + IFS=',' read -ra current_tags_array <<< "$current_tags" + local current_tags_clean=() + for tag in "${current_tags_array[@]}"; do + tag=$(echo "$tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # Trim whitespace + if [[ -n "$tag" ]]; then + current_tags_clean+=("$tag") + fi + done + + # If no valid tags, return empty + if [[ ${#current_tags_clean[@]} -eq 0 ]]; then + return 0 + fi + + # Process all posts and calculate similarity scores + local temp_results=$(mktemp) + + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + # Skip current post + if [[ "$slug" == "$current_slug" ]]; then + continue + fi + + # Skip posts without tags or date + if [[ -z "$tags" || -z "$date" ]]; then + continue + fi + + # Calculate similarity score based on shared tags + local score=0 + IFS=',' read -ra post_tags_array <<< "$tags" + + for post_tag in "${post_tags_array[@]}"; do + post_tag=$(echo "$post_tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # Trim whitespace + if [[ -n "$post_tag" ]]; then + for current_tag in "${current_tags_clean[@]}"; do + if [[ "$post_tag" == "$current_tag" ]]; then + score=$((score + 1)) + break + fi + done + fi + done + + # Only consider posts with at least one shared tag + if [[ $score -gt 0 ]]; then + # Store: score|date|title|slug|description + echo "${score}|${date}|${title}|${slug}|${description}" >> "$temp_results" + fi + + done < "$file_index" + + # Sort by score (descending), then by date (descending), limit results + local sorted_results="" + if [[ -s "$temp_results" ]]; then + sorted_results=$(sort -t'|' -k1,1nr -k2,2r "$temp_results" | head -n "$max_results") + fi + rm -f "$temp_results" + + # Generate HTML output + if [[ -z "$sorted_results" ]]; then + return 0 # No related posts found + fi + + local html_output="" + html_output+=''$'\n' + + echo "$html_output" +} + +# Clean related posts cache (called when posts are modified) +clean_related_posts_cache() { + local cache_dir="${CACHE_DIR:-.bssg_cache}/related_posts" + if [[ -d "$cache_dir" ]]; then + echo -e "${YELLOW}Cleaning related posts cache...${NC}" + rm -rf "$cache_dir" + mkdir -p "$cache_dir" + fi +} + +# Invalidate related posts cache for posts that share tags with modified posts +# Args: $1=path to modified tags list file, $2=optional output file for invalidated post slugs +invalidate_related_posts_cache_for_tags() { + local modified_tags_file="$1" + local invalidated_output_file="$2" + local cache_dir="${CACHE_DIR:-.bssg_cache}/related_posts" + local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + + if [[ ! -f "$modified_tags_file" || ! -d "$cache_dir" || ! -f "$file_index" ]]; then + return 0 + fi + + # Read modified tags into array + local modified_tags=() + while IFS= read -r tag; do + if [[ -n "$tag" ]]; then + modified_tags+=("$tag") + fi + done < "$modified_tags_file" + + if [[ ${#modified_tags[@]} -eq 0 ]]; then + return 0 + fi + + echo -e "${YELLOW}Invalidating related posts cache for posts with modified tags...${NC}" + + # Find posts that have any of the modified tags and remove their cache + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + if [[ -n "$tags" && -n "$slug" ]]; then + IFS=',' read -ra post_tags_array <<< "$tags" + local should_invalidate=false + + for post_tag in "${post_tags_array[@]}"; do + post_tag=$(echo "$post_tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [[ -n "$post_tag" ]]; then + for modified_tag in "${modified_tags[@]}"; do + if [[ "$post_tag" == "$modified_tag" ]]; then + should_invalidate=true + break 2 + fi + done + fi + done + + if [[ "$should_invalidate" == true ]]; then + local cache_file="$cache_dir/${slug}.html" + if [[ -f "$cache_file" ]]; then + rm -f "$cache_file" + echo -e " Invalidated cache for post: ${GREEN}$slug${NC}" + fi + + # Write the slug to the output file if provided + if [[ -n "$invalidated_output_file" ]]; then + echo "$slug" >> "$invalidated_output_file" + fi + fi + fi + done < "$file_index" +} + +# --- Related Posts Functions --- END --- + +# Export functions for use by other scripts +export -f generate_related_posts compute_related_posts clean_related_posts_cache invalidate_related_posts_cache_for_tags \ No newline at end of file diff --git a/themes/default/style.css b/themes/default/style.css index ad8622c..bf8f1d9 100644 --- a/themes/default/style.css +++ b/themes/default/style.css @@ -850,6 +850,15 @@ article > p:first-of-type::first-letter { .tags-list { gap: 0.7rem; } + + .related-posts { + margin-top: 2rem; + padding-top: 1.5rem; + } + + .related-post { + padding: 1.2rem; + } } /* Medium-small screens */ @@ -925,6 +934,19 @@ article > p:first-of-type::first-letter { blockquote { padding: 1.1rem 1.3rem; } + + .related-posts { + margin-top: 1.5rem; + padding-top: 1.2rem; + } + + .related-post { + padding: 1rem; + } + + .related-post h4 { + font-size: 1rem; + } } /* Small screens */ @@ -1100,3 +1122,84 @@ article > p:first-of-type::first-letter { background-color: var(--accent-secondary); color: var(--text-color); } + +/* Related Posts Styles */ +.related-posts { + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid var(--border-color); +} + +.related-posts h3 { + font-family: var(--font-headings); + font-size: 1.5rem; + font-weight: 600; + color: var(--header-color); + margin-bottom: 1.5rem; + position: relative; +} + +.related-posts h3::after { + content: ""; + position: absolute; + bottom: -0.5rem; + left: 0; + width: 60px; + height: 2px; + background: linear-gradient(90deg, var(--link-color), var(--accent-secondary)); + border-radius: var(--radius); +} + +.related-posts-list { + display: grid; + gap: 1.5rem; + margin-top: 1.5rem; +} + +.related-post { + padding: 1.5rem; + background-color: var(--accent-color); + border-radius: var(--radius); + border-left: 4px solid var(--accent-secondary); + transition: all var(--transition); + position: relative; +} + +.related-post:hover { + transform: translateY(-2px); + box-shadow: var(--card-shadow); + border-left-color: var(--link-color); +} + +.related-post h4 { + margin: 0 0 0.75rem 0; + font-family: var(--font-headings); + font-size: 1.1rem; + font-weight: 600; + line-height: 1.4; +} + +.related-post h4 a { + color: var(--header-color); + text-decoration: none; + outline: 2px solid transparent; + outline-offset: 2px; + border-radius: 2px; +} + +.related-post h4 a:hover { + color: var(--link-color); + text-decoration: underline; +} + +.related-post h4 a:focus { + outline-color: var(--link-color); + color: var(--link-color); +} + +.related-post p { + margin: 0; + color: var(--date-color); + font-size: 0.9rem; + line-height: 1.5; +} diff --git a/themes/italy/style.css b/themes/italy/style.css index 5573ed8..d98c459 100644 --- a/themes/italy/style.css +++ b/themes/italy/style.css @@ -456,6 +456,38 @@ article .meta { align-items: center; } +/* Page meta styling for post pages */ +.page-meta { + font-family: var(--font-decorative); + margin-bottom: var(--spacing-lg); +} + +.page-meta .meta { + color: var(--date-color); + font-style: italic; + margin: 0 0 var(--spacing-xs) 0; + font-size: 0.9rem; +} + +.page-meta .meta:first-child { + font-size: 0.95rem; + margin-bottom: var(--spacing-sm); +} + +.page-meta .meta time { + font-weight: 500; +} + +.page-meta .meta strong { + color: var(--text-color); + font-weight: 600; +} + +.page-meta .reading-time { + font-size: 0.85rem; + margin: 0; +} + .reading-time { font-style: normal; color: var(--date-color); From 1c7aab7b71014610b60a8be3f54f06a4b108fba9 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Sat, 21 Jun 2025 16:09:12 +0200 Subject: [PATCH 06/21] Removed Google leftovers --- templates/header.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/templates/header.html b/templates/header.html index ef24012..c892f8d 100644 --- a/templates/header.html +++ b/templates/header.html @@ -17,10 +17,6 @@ - - - - {{schema_json_ld}} {{custom_css_link}} From 7ebd09eeeb3d37d76a70c576fabe936866098644 Mon Sep 17 00:00:00 2001 From: James Seward Date: Mon, 23 Jun 2025 11:29:57 +0100 Subject: [PATCH 07/21] Disable colour output for non-terms or if NO_COLOR is set --- scripts/build/config_loader.sh | 17 ++++++++++++----- scripts/build/utils.sh | 20 ++++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/scripts/build/config_loader.sh b/scripts/build/config_loader.sh index 3ff16e5..3b3da4b 100755 --- a/scripts/build/config_loader.sh +++ b/scripts/build/config_loader.sh @@ -85,10 +85,17 @@ if [ -f "$UTILS_SCRIPT" ]; then else # Define basic color functions as fallback if utils.sh is missing # Needed for messages printed *before* utils.sh is sourced, or if it fails. - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[0;33m' - NC='\033[0m' # No Color + if [[ -t 1 ]] && [[ -z $NO_COLOR ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + NC='\033[0m' # No Color + else + RED="" + GREEN="" + YELLOW="" + NC="" + fi print_error() { echo -e "${RED}Error: $1${NC}" >&2; } print_warning() { echo -e "${YELLOW}Warning: $1${NC}"; } print_success() { echo -e "${GREEN}$1${NC}"; } @@ -299,4 +306,4 @@ export MSG_MONTH_09 MSG_MONTH_10 MSG_MONTH_11 MSG_MONTH_12 # Fallback using compgen (use with caution, might export unintended vars) # compgen -v MSG_ | while read -r var; do export "$var"; done -# --- Export All Variables --- END --- \ No newline at end of file +# --- Export All Variables --- END --- diff --git a/scripts/build/utils.sh b/scripts/build/utils.sh index 15a0cd8..f8f5d8a 100755 --- a/scripts/build/utils.sh +++ b/scripts/build/utils.sh @@ -5,11 +5,19 @@ # # Colors for output messages -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' +if [[ -t 1 ]] && [[ -z $NO_COLOR ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BLUE='\033[0;34m' + NC='\033[0m' +else + RED="" + GREEN="" + YELLOW="" + BLUE="" + NC="" +fi # --- Printing Functions --- START --- print_error() { @@ -332,4 +340,4 @@ export -f html_escape export -f print_error export -f print_warning export -f print_success -export -f print_info \ No newline at end of file +export -f print_info From 1f59e618793bd01c9af6c0c4f9ac08a6c63bca0a Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Thu, 17 Jul 2025 08:55:06 +0200 Subject: [PATCH 08/21] Added pre-compression, thoughtful theme, Cyber-Dark theme, removed some Google fonts leftovers --- README.md | 71 +- config.sh | 1 + scripts/build/config_loader.sh | 11 +- scripts/build/main.sh | 40 ++ scripts/build/utils.sh | 2 +- themes/cyber-dark/style.css | 41 ++ themes/thoughtful/style.css | 1102 ++++++++++++++++++++++++++++++++ 7 files changed, 1247 insertions(+), 21 deletions(-) create mode 100644 themes/cyber-dark/style.css create mode 100644 themes/thoughtful/style.css diff --git a/README.md b/README.md index 150de18..2e08c31 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ - Dynamic menu generation based on available pages - Support for primary and secondary pages with automatic menu organization - Generates sitemap.xml and RSS feed with timezone support +- Asset pre-compression: Can automatically create gzipped versions of text-based files (`.html`, `.css`, `.xml`, `.js`) during the build for servers that support serving pre-compressed content. - Clean design - No JavaScript required (except for admin interface) - Works well without images @@ -691,44 +692,77 @@ The `config.sh` file contains the default configuration settings for the site ge ```bash # Directory configuration -SRC_DIR="src" # Source directory for posts -PAGES_DIR="pages" # Source directory for pages -DRAFTS_DIR="drafts" # Source directory for drafts (posts and pages) -OUTPUT_DIR="output" # Where the generated site is placed +SRC_DIR="src" +PAGES_DIR="pages" # Directory for static pages +OUTPUT_DIR="output" TEMPLATES_DIR="templates" THEMES_DIR="themes" STATIC_DIR="static" +DRAFTS_DIR="drafts" # Directory for drafts THEME="default" +CACHE_DIR=".bssg_cache" # Default cache directory location (relative to BSSG root) # Build configuration -CLEAN_OUTPUT=false +CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild +REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh) +REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh) +PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build. + +# Customization +CUSTOM_CSS="" # Optional: Path to custom CSS file relative to output root (e.g., "/css/custom.css"). File should be placed in STATIC_DIR. # Site information -SITE_TITLE="My Journal" -SITE_DESCRIPTION="A personal journal and introspective newspaper" -SITE_URL="http://localhost" +SITE_TITLE="My new BSSG site" +SITE_DESCRIPTION="A complete SSG - written in bash" +SITE_URL="http://localhost:8000" AUTHOR_NAME="Anonymous" AUTHOR_EMAIL="anonymous@example.com" # Content configuration DATE_FORMAT="%Y-%m-%d %H:%M:%S %z" -TIMEZONE="local" # Options: "local", "GMT", or a specific timezone - # Affects how dates are displayed in the generated site based on system interpretation. -SHOW_TIMEZONE="false" # Options: "true", "false". Determines if the timezone offset (e.g., +0200) is shown in displayed dates. +TIMEZONE="local" # Options: "local", "GMT", or a specific timezone like "America/New_York" +SHOW_TIMEZONE="false" # Options: "true", "false". Whether to display the timezone in rendered dates. POSTS_PER_PAGE=10 -ENABLE_ARCHIVES=true # Enable or disable archives by year/month -URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. -RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be included in the RSS feed description instead of the excerpt. Useful for readers that consume entire posts via RSS. -ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags//rss.xml`. +RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". Include full post content in RSS feed. +RSS_FILENAME="rss.xml" # The filename for the main RSS feed (e.g., feed.xml, rss.xml) +ENABLE_ARCHIVES=true # Enable or disable archive pages +ENABLE_AUTHOR_PAGES=false # Enable or disable author pages (default: false) +ENABLE_AUTHOR_RSS=false # Enable or disable author-specific RSS feeds (default: false) +SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2) +URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs. Available: Year, Month, Day, slug +ENABLE_TAG_RSS=true # Enable or disable tag-specific RSS feed generation (default: true) -# Related Posts configuration -ENABLE_RELATED_POSTS=true # Options: "true", "false". If set to "true" (default), related posts based on shared tags will be shown at the end of each post. -RELATED_POSTS_COUNT=3 # Number of related posts to display (default: 3, recommended maximum: 5). +# Archive Page Configuration +ARCHIVES_LIST_ALL_POSTS="false" # Options: "true", "false". If true, list all posts on the main archive page. + +# Page configuration +PAGE_URL_FORMAT="slug" # Format for page URLs. Available: slug, filename (without ext) + +# Markdown processing configuration +MARKDOWN_PROCESSOR="commonmark" # Options: "pandoc", "commonmark", or "markdown.pl" + +# Language Configuration +SITE_LANG="en" # Default language code (e.g., en, es, fr). See locales/ directory. + +# Related Posts Configuration +ENABLE_RELATED_POSTS=true # Enable or disable related posts feature +RELATED_POSTS_COUNT=3 # Number of related posts to show (default: 3) + +# Server Configuration (for 'bssg.sh server' command) +# These are the defaults used by 'bssg.sh server' if not overridden by command-line options. +BSSG_SERVER_PORT_DEFAULT="8000" # Default port for the local development server +BSSG_SERVER_HOST_DEFAULT="localhost" # Default host for the local development server # Deployment configuration DEPLOY_AFTER_BUILD="false" # Options: "true", "false". Automatically deploy after a successful build. DEPLOY_SCRIPT="" # Path to the deployment script to execute if DEPLOY_AFTER_BUILD is true. + +# Terminal colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color ``` #### Date Format Examples @@ -895,6 +929,7 @@ BSSG includes a variety of themes to customize the look of your site. Themes are - `docs` - A clean, structured theme ideal for technical documentation with excellent code formatting and clear navigation - `longform` - Optimized for reading long articles with highly readable typography, contained text width, and minimal distractions - `reader-mode` - Simulates browser reader mode with almost total emphasis on text, sepia background, very readable serif font, and minimal graphic elements +- `thoughtful` - A warm, accessible, and performant theme for personal reflection blogs and thoughtful writing - `text-only` - A step beyond minimalism using browser defaults with clean base typography for readability and lightning-fast loading #### Special Themes diff --git a/config.sh b/config.sh index c94c2d4..8a32838 100644 --- a/config.sh +++ b/config.sh @@ -27,6 +27,7 @@ CACHE_DIR=".bssg_cache" # Default cache directory location (relative to BSSG roo CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh) REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh) +PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build. # Customization CUSTOM_CSS="" # Optional: Path to custom CSS file relative to output root (e.g., "/css/custom.css"). File should be placed in STATIC_DIR. diff --git a/scripts/build/config_loader.sh b/scripts/build/config_loader.sh index 3b3da4b..233741f 100755 --- a/scripts/build/config_loader.sh +++ b/scripts/build/config_loader.sh @@ -226,6 +226,7 @@ BSSG_CONFIG_VARS_ARRAY=( DEPLOY_AFTER_BUILD DEPLOY_SCRIPT ARCHIVES_LIST_ALL_POSTS ENABLE_RELATED_POSTS RELATED_POSTS_COUNT + PRECOMPRESS_ASSETS # Add any other custom config variables here if needed BSSG_SERVER_PORT_DEFAULT BSSG_SERVER_HOST_DEFAULT # Server defaults ) @@ -274,12 +275,13 @@ export ENABLE_AUTHOR_PAGES export ENABLE_AUTHOR_RSS export SHOW_AUTHORS_MENU_THRESHOLD export BACKUP_DIR -export CACHE_DIR +export CACHE_DIR export DEPLOY_AFTER_BUILD -export DEPLOY_SCRIPT +export DEPLOY_SCRIPT export ARCHIVES_LIST_ALL_POSTS export ENABLE_RELATED_POSTS export RELATED_POSTS_COUNT +export PRECOMPRESS_ASSETS # Server defaults export export BSSG_SERVER_PORT_DEFAULT @@ -307,3 +309,8 @@ export MSG_MONTH_09 MSG_MONTH_10 MSG_MONTH_11 MSG_MONTH_12 # Fallback using compgen (use with caution, might export unintended vars) # compgen -v MSG_ | while read -r var; do export "$var"; done # --- Export All Variables --- END --- + +# --- Final Path Adjustments (after all sourcing) --- START --- +# Ensure relevant directory paths are exported if not already absolute. +# ... existing code ... +# --- Final Path Adjustments (after all sourcing) --- END --- \ No newline at end of file diff --git a/scripts/build/main.sh b/scripts/build/main.sh index 099d925..ec2db28 100755 --- a/scripts/build/main.sh +++ b/scripts/build/main.sh @@ -443,6 +443,46 @@ rm -f "${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list" # --- Final Cleanup --- END --- +# --- Pre-compress Assets --- START --- +precompress_assets() { + # Check if pre-compression is enabled in the config. + if [ ! "${PRECOMPRESS_ASSETS:-false}" = "true" ]; then + return + fi + + echo "Starting pre-compression of assets..." + + # 1. Cleanup: Remove any .gz file that does not have a corresponding original file. + # This handles cases where original files were deleted. + # Using -print0 and read -d '' to safely handle filenames with spaces or special chars. + find "${OUTPUT_DIR}" -type f -name "*.gz" -print0 | while IFS= read -r -d '' gzfile; do + original_file="${gzfile%.gz}" + if [ ! -f "$original_file" ]; then + echo "Removing stale compressed file: $gzfile" + rm -- "$gzfile" + fi + done + + # 2. Compression: Compress text files if they are new or have been updated. + # We target .html, .css, .xml and .js files. + find "${OUTPUT_DIR}" -type f \( -name "*.html" -o -name "*.css" -o -name "*.xml" -o -name "*.js" \) -print0 | while IFS= read -r -d '' file; do + gzfile="${file}.gz" + # Compress if the .gz file doesn't exist, or if the original file is newer. + if [ ! -f "$gzfile" ] || [ "$file" -nt "$gzfile" ]; then + echo "Compressing: $file" + # Use gzip with best compression (-9) and write to stdout, then redirect. + # This is a robust way to handle output and overwriting. + gzip -c -9 -- "$file" > "$gzfile" + fi + done + + echo "Asset pre-compression finished." +} + +# Execute the asset compression. +precompress_assets +# --- Pre-compress Assets --- END --- + # --- Deployment --- START --- deploy_now="false" if [[ "${CMD_DEPLOY_OVERRIDE:-unset}" == "true" ]]; then # Use default value for safety diff --git a/scripts/build/utils.sh b/scripts/build/utils.sh index f8f5d8a..80ad813 100755 --- a/scripts/build/utils.sh +++ b/scripts/build/utils.sh @@ -340,4 +340,4 @@ export -f html_escape export -f print_error export -f print_warning export -f print_success -export -f print_info +export -f print_info \ No newline at end of file diff --git a/themes/cyber-dark/style.css b/themes/cyber-dark/style.css new file mode 100644 index 0000000..4b379a3 --- /dev/null +++ b/themes/cyber-dark/style.css @@ -0,0 +1,41 @@ +/* + * Cyber-Dark theme by Nigel Swan + * for BSSG, https://bssg.dragas.net + */ +:root { + --highlight1: lightseagreen; + --highlight2: #e441e1; + --bright: #fff; + --text: #cecece; + --muted-text: #999999; + --background: #121212; + --border: #121212; + --blockquote: #222; + background-color: var(--background); +} + +a { font-family:sans-serif; color:var(--highlight2); text-decoration:none; } +body { font-size:1.1em; color:var(--text); padding:0.2em; font-family:sans-serif; max-width:60em; margin:auto; line-height:1.5; } +h1 { font-size:2em; color:var(--highlight1); text-shadow:0 0 20px; } +h2 { font-size:1.7em; color:var(--highlight1); text-shadow:0 0 20px; } +h3 { font-size:1.4em; color:var(--highlight1); text-shadow:0 0 20px; } +nav { display:block; text-align:center; padding-top:0.8em; padding-bottom:3.5em; } +nav { a { padding-left:0.5em; padding-right:0.5em; text-decoration:underline var(--highlight1); text-shadow:0 0 9px var(--highlight1); } } +hr { border:1px solid var(--highlight2); } +p { padding-top:0.5em; padding-bottom:0.5em; } +header { text-align:center; margin:auto; } +header { p { text-shadow:0 0 10px var(--highlight2); } } +img { display:block; max-width: 100%; margin: auto; padding-top: 20px; padding-bottom: 20px; } +.posts-list { h3 { a { color:var(--highlight1); text-decoration: underline var(--highlight2); text-shadow:0 0 15px var(--highlight2); } } } +.featured-image.index-image { img { display:block; max-width: 100%; max-height:640px; margin: auto; padding-top: 20px; padding-bottom: 20px; } } +.image-caption { color:var(--muted-text); text-align: center; } +.site-title { a { text-shadow:0 0 30px var(--highlight2); text-decoration:underline 2px var(--highlight2); font-size:3em; font-weight:bold; color:var(--highlight1); }} +.meta, .page-meta { font-size:0.8em; color:var(--muted-text); } +.tag::before { content:"#"; } +.tags-list a::before { white-space:pre; content:'\A'; } +.summary { max-width:99%; margin:auto; padding-top:1em; padding-bottom:2em; } +article { padding-bottom:1em; } +blockquote { background:var(--blockquote); color:var(--bright); max-width:90%; padding:1em; border-radius:0.5em; margin:auto; display:flex; } +pre { background:var(--blockquote); max-width:90%; padding:1em; border-radius:0.5em; margin:auto; display:flex; } +code { font-family:monospace; color:var(--highlight1); text-shadow:0 0 5px var(--highlight2); } +footer { text-align:center; font-size:0.8em; } diff --git a/themes/thoughtful/style.css b/themes/thoughtful/style.css new file mode 100644 index 0000000..8a61de6 --- /dev/null +++ b/themes/thoughtful/style.css @@ -0,0 +1,1102 @@ +/* + * Thoughtful Theme for BSSG + * A warm, accessible, and performant theme + * for personal reflection blogs and thoughtful writing + * + * DESIGN PHILOSOPHY: + * - Warm, natural color palette that invites contemplation + * - Typography that balances elegance with personality + * - Rhythm and flow that supports narrative reading + * - Subtle visual elements that enhance without distraction + * - Full accessibility compliance with graceful degradation + * - Optimized performance for all devices + */ + +:root { + /* Color palette - Warm and contemplative with WCAG AA+ contrast */ + --primary-bg: #faf8f3; + --secondary-bg: #f2efe7; + --surface-bg: #ffffff; + --accent-bg: #e8e2d4; + + /* Text colors - High contrast for accessibility */ + --text-primary: #2c2620; + --text-secondary: #3d3730; + --text-tertiary: #4f4940; + --text-muted: #6b635a; + + /* Accent colors - WCAG AA compliant */ + --accent-primary: #8b4513; + --accent-secondary: #b8651e; + --link-color: #1a4480; + --link-hover: #0f2c5c; + --link-visited: #4a2c5a; + + /* Semantic colors */ + --border-light: #e0d7c5; + --border-medium: #c9baa5; + --border-dark: #a69780; + --shadow-light: rgba(45, 41, 38, 0.08); + --shadow-medium: rgba(45, 41, 38, 0.12); + --highlight: rgba(139, 69, 19, 0.08); + + /* Typography - Text browser friendly */ + --font-primary: Georgia, 'Times New Roman', 'Liberation Serif', serif; + --font-secondary: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Liberation Sans', Arial, sans-serif; + --font-mono: 'Liberation Mono', 'Courier New', Courier, monospace; + + /* Typography scale */ + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; + + /* Spacing system - Rhythmic and harmonious */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + --space-16: 4rem; + --space-20: 5rem; + --space-24: 6rem; + + /* Layout */ + --content-width: min(65ch, 90vw); + --wide-content-width: min(75ch, 95vw); + --narrow-content-width: min(45ch, 85vw); + + /* Design tokens */ + --line-height-tight: 1.25; + --line-height-normal: 1.6; + --line-height-relaxed: 1.75; + --border-radius-sm: 4px; + --border-radius-md: 8px; + --border-radius-lg: 12px; + + /* Performance-optimized transitions */ + --transition-fast: 150ms ease-out; + --transition-normal: 200ms ease-out; + --transition-slow: 300ms ease-out; +} + +/* Dark theme variant - WCAG AA+ compliant */ +.theme-dark { + --primary-bg: #1a1814; + --secondary-bg: #242019; + --surface-bg: #2d2926; + --accent-bg: #3a3530; + + /* High contrast text for accessibility */ + --text-primary: #f5f2eb; + --text-secondary: #e0dcd5; + --text-tertiary: #cac5be; + --text-muted: #b4afa8; + + --accent-primary: #d2813f; + --accent-secondary: #e6a85c; + --link-color: #7eb3ff; + --link-hover: #a8c7ff; + --link-visited: #c4a3e8; + + --border-light: #3a3530; + --border-medium: #4a453f; + --border-dark: #5a534c; + --shadow-light: rgba(0, 0, 0, 0.2); + --shadow-medium: rgba(0, 0, 0, 0.3); + --highlight: rgba(210, 129, 63, 0.12); +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + transform: none !important; + } +} + +/* Base styles */ +* { + box-sizing: border-box; +} + +html { + font-size: clamp(18px, 2.5vw, 22px); + scroll-behavior: smooth; + scroll-padding-top: var(--space-8); +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-primary); + font-size: var(--text-base); + line-height: var(--line-height-normal); + color: var(--text-primary); + background-color: var(--primary-bg); + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Removed expensive background effects for better performance */ + min-height: 100vh; +} + +/* Container and layout */ +.container { + width: var(--content-width); + margin: 0 auto; + padding: var(--space-6) var(--space-4) var(--space-16); + position: relative; +} + +/* Header */ +header { + margin-bottom: var(--space-16); + text-align: center; + position: relative; +} + +header::after { + content: ''; + position: absolute; + bottom: calc(-1 * var(--space-8)); + left: 50%; + transform: translateX(-50%); + width: 60px; + height: 2px; + background: linear-gradient(90deg, transparent, var(--accent-primary), transparent); + border-radius: 1px; +} + +.site-title { + font-family: var(--font-primary); + font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl)); + font-weight: 400; + margin: 0 0 var(--space-4); + letter-spacing: -0.02em; + line-height: var(--line-height-tight); + color: var(--text-primary); + position: relative; +} + +.site-title a { + color: inherit; + text-decoration: none; + transition: color var(--transition-normal); + position: relative; + display: inline-block; +} + +.site-title a::before { + content: ''; + position: absolute; + bottom: -4px; + left: 0; + width: 0; + height: 2px; + background: var(--accent-primary); + transition: width var(--transition-normal); +} + +.site-title a:hover { + color: var(--accent-primary); +} + +.site-title a:hover::before { + width: 100%; +} + +.site-title a:focus { + outline: 2px solid var(--link-color); + outline-offset: 4px; + border-radius: var(--border-radius-sm); +} + +header p { + font-family: var(--font-secondary); + font-size: var(--text-lg); + font-style: italic; + color: var(--text-secondary); + margin: 0; + max-width: var(--narrow-content-width); + margin-left: auto; + margin-right: auto; + line-height: var(--line-height-relaxed); +} + +/* Navigation */ +nav { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: var(--space-6); + margin-top: var(--space-8); + font-family: var(--font-secondary); + font-size: var(--text-sm); + font-weight: 500; + letter-spacing: 0.025em; + text-transform: uppercase; +} + +nav a { + color: var(--text-tertiary); + text-decoration: none; + padding: var(--space-2) var(--space-3); + border-radius: var(--border-radius-sm); + transition: all var(--transition-normal); + position: relative; + overflow: hidden; +} + +nav a::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--highlight); + transition: left var(--transition-normal); + z-index: -1; +} + +nav a:hover { + color: var(--accent-primary); + transform: translateY(-1px); +} + +nav a:hover::before { + left: 0; +} + +nav a.active { + color: var(--accent-primary); + background: var(--highlight); +} + +nav a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Main content */ +main { + min-height: 60vh; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-primary); + color: var(--text-primary); + line-height: var(--line-height-tight); + margin: 0 0 var(--space-6); + font-weight: 400; + letter-spacing: -0.015em; +} + +h1 { + font-size: clamp(var(--text-3xl), 4vw, var(--text-4xl)); + margin-bottom: var(--space-8); + text-align: center; + position: relative; + padding-bottom: var(--space-6); +} + +h1::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 40px; + height: 2px; + background: var(--accent-secondary); + border-radius: 1px; +} + +h2 { + font-size: var(--text-2xl); + margin-top: var(--space-16); + margin-bottom: var(--space-6); + color: var(--accent-primary); + position: relative; + padding-left: var(--space-4); +} + +h2::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 60%; + background: var(--accent-secondary); + border-radius: 2px; +} + +h3 { + font-size: var(--text-xl); + margin-top: var(--space-12); + color: var(--text-primary); +} + +h4 { + font-size: var(--text-lg); + margin-top: var(--space-10); + font-style: italic; + color: var(--text-secondary); +} + +p { + margin: 0 0 var(--space-6); + line-height: var(--line-height-relaxed); + hyphens: auto; + hanging-punctuation: first last; +} + +/* First paragraph enhancement */ +article .article-content > p:first-of-type { + font-size: var(--text-lg); + line-height: var(--line-height-normal); + color: var(--text-secondary); + margin-bottom: var(--space-8); +} + +article .article-content > p:first-of-type::first-letter { + float: left; + font-size: 4rem; + line-height: 0.8; + margin: 0.1em 0.1em 0 0; + color: var(--accent-primary); + font-weight: 400; + text-shadow: 2px 2px 4px var(--shadow-light); +} + +/* Links */ +a { + color: var(--link-color); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 0.2em; + transition: all var(--transition-normal); +} + +a:hover { + color: var(--link-hover); + text-decoration-thickness: 2px; +} + +a:visited { + color: var(--link-visited); +} + +a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; + border-radius: var(--border-radius-sm); +} + +/* Lists */ +ul, ol { + margin: var(--space-6) 0 var(--space-8); + padding-left: var(--space-8); +} + +li { + margin-bottom: var(--space-3); + line-height: var(--line-height-relaxed); +} + +li::marker { + color: var(--accent-secondary); +} + +/* Blockquotes */ +blockquote { + margin: var(--space-12) 0; + padding: var(--space-8) var(--space-10); + background: linear-gradient(135deg, var(--secondary-bg), var(--accent-bg)); + border-left: 4px solid var(--accent-primary); + border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0; + font-style: italic; + font-size: var(--text-lg); + line-height: var(--line-height-relaxed); + color: var(--text-secondary); + box-shadow: 0 4px 6px var(--shadow-light); + position: relative; +} + +blockquote::before { + content: '"'; + position: absolute; + top: var(--space-4); + left: var(--space-4); + font-size: 3rem; + color: var(--accent-primary); + opacity: 0.3; + line-height: 1; +} + +blockquote p:first-child { + margin-top: 0; +} + +blockquote p:last-child { + margin-bottom: 0; +} + +blockquote cite { + display: block; + margin-top: var(--space-4); + font-size: var(--text-sm); + color: var(--text-muted); + text-align: right; + font-style: normal; +} + +blockquote cite::before { + content: '— '; +} + +/* Code blocks */ +pre, code { + font-family: var(--font-mono); + font-size: var(--text-sm); + border-radius: var(--border-radius-sm); +} + +code { + background: var(--accent-bg); + padding: 0.2em 0.4em; + color: var(--text-primary); + border: 1px solid var(--border-light); +} + +pre { + background: var(--secondary-bg); + padding: var(--space-6); + margin: var(--space-8) 0; + overflow-x: auto; + line-height: var(--line-height-normal); + border: 1px solid var(--border-medium); + box-shadow: inset 0 2px 4px var(--shadow-light); +} + +pre code { + background: none; + padding: 0; + border: none; +} + +/* Horizontal rule */ +hr { + border: none; + height: 2px; + background: linear-gradient(90deg, transparent, var(--accent-primary), transparent); + margin: var(--space-16) auto; + width: 60%; + border-radius: 1px; +} + +/* Article layout */ +article { + margin-bottom: var(--space-24); +} + +article h1 { + margin-bottom: var(--space-4); +} + +/* Article meta */ +.meta, .page-meta { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: var(--space-2); + color: var(--text-muted); + font-family: var(--font-secondary); + font-size: var(--text-sm); + margin: var(--space-4) 0 var(--space-16); + text-align: center; + line-height: var(--line-height-normal); +} + +.meta > * { + display: inline-flex; + align-items: center; + gap: var(--space-1); +} + +.meta > *:not(:last-child)::after { + content: '•'; + margin: 0 var(--space-3); + color: var(--accent-secondary); + font-weight: bold; +} + +/* Reading time - TEXT BROWSER FALLBACK */ +.reading-time { + display: inline-flex; + align-items: center; + gap: var(--space-2); +} + +/* Fallback for text browsers - shows "Reading time:" before emoji */ +.reading-time::before { + content: 'Reading time: ⏱'; + font-style: normal; + color: var(--accent-secondary); +} + +/* Hide fallback text when CSS transforms are supported (modern browsers) */ +@supports (transform: translateX(0)) { + .reading-time::before { + content: '⏱'; + font-style: normal; + color: var(--accent-secondary); + } +} + +/* TEXT BROWSER FALLBACK: Ensure content is readable without CSS */ +@media (max-width: 0) { + .reading-time::before { + content: 'Reading time: '; + font-style: normal; + } +} + +/* ACCESSIBILITY: Enhanced for screen readers */ +.reading-time[aria-label]::before { + content: '⏱'; +} + +/* Tags */ +.tags { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + justify-content: center; + margin-top: var(--space-6); +} + +.tags a { + background: var(--accent-bg); + color: var(--text-tertiary); + text-decoration: none; + padding: var(--space-1) var(--space-3); + border-radius: var(--border-radius-lg); + font-size: var(--text-xs); + font-family: var(--font-secondary); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: all var(--transition-normal); + border: 1px solid var(--border-light); +} + +.tags a:hover { + background: var(--accent-primary); + color: var(--surface-bg); + transform: translateY(-1px); + box-shadow: 0 2px 4px var(--shadow-medium); +} + +.tags a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Images */ +img { + max-width: 100%; + height: auto; + border-radius: var(--border-radius-md); + box-shadow: 0 4px 6px var(--shadow-light); +} + +.featured-image, .index-image, .tag-image, .archive-image { + margin: var(--space-12) auto; + text-align: center; +} + +figcaption { + font-size: var(--text-sm); + font-style: italic; + color: var(--text-muted); + text-align: center; + margin-top: var(--space-4); + font-family: var(--font-secondary); +} + +/* Pull quotes */ +.pull-quote { + font-family: var(--font-primary); + font-size: var(--text-2xl); + line-height: var(--line-height-normal); + font-style: italic; + color: var(--accent-primary); + text-align: center; + margin: var(--space-16) auto; + padding: var(--space-8) var(--space-6); + max-width: var(--narrow-content-width); + position: relative; + background: linear-gradient(135deg, var(--highlight), transparent); + border-radius: var(--border-radius-lg); +} + +.pull-quote::before, +.pull-quote::after { + content: '"'; + font-size: 4rem; + position: absolute; + color: var(--accent-secondary); + opacity: 0.3; + line-height: 1; +} + +.pull-quote::before { + top: 0; + left: var(--space-4); +} + +.pull-quote::after { + bottom: 0; + right: var(--space-4); +} + +/* Post list */ +.posts-list { + margin-top: var(--space-16); +} + +.posts-list article { + margin-bottom: var(--space-16); + padding: var(--space-10) var(--space-8) var(--space-8); + background: var(--surface-bg); + border-radius: var(--border-radius-lg); + box-shadow: 0 2px 4px var(--shadow-light); + transition: all var(--transition-normal); + border: 1px solid var(--border-light); + text-align: center; + position: relative; + overflow: hidden; +} + +.posts-list article::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); +} + +.posts-list article:hover { + /* Use opacity and scale instead of transform for better performance */ + box-shadow: 0 8px 16px var(--shadow-medium); +} + +.posts-list article:last-child { + border-bottom: 1px solid var(--border-light); +} + +.posts-list h3 { + margin: 0 0 var(--space-6); + font-size: var(--text-2xl); + text-align: center; + line-height: var(--line-height-tight); +} + +.posts-list h3 a { + color: var(--text-primary); + text-decoration: none; + transition: color var(--transition-normal); + display: block; +} + +.posts-list h3 a:hover { + color: var(--accent-primary); +} + +.posts-list h3 a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +.posts-list .meta { + margin: var(--space-4) 0 var(--space-8); +} + +.posts-list .featured-image, +.posts-list .index-image { + margin: var(--space-8) 0; +} + +.posts-list p { + text-align: left; + margin-top: var(--space-8); + font-size: var(--text-lg); + line-height: var(--line-height-relaxed); + color: var(--text-secondary); +} + +/* Footer */ +footer { + margin-top: var(--space-24); + padding: var(--space-12) 0 var(--space-8); + text-align: center; + border-top: 1px solid var(--border-medium); + background: linear-gradient(135deg, var(--secondary-bg), var(--accent-bg)); + color: var(--text-muted); + font-family: var(--font-secondary); + font-size: var(--text-sm); +} + +footer p { + margin: var(--space-2) 0; +} + +footer a { + color: var(--text-secondary); + transition: color var(--transition-normal); +} + +footer a:hover { + color: var(--accent-primary); +} + +footer a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Pagination */ +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + margin: var(--space-16) 0 var(--space-12); + font-family: var(--font-secondary); + font-size: var(--text-sm); + gap: var(--space-4); +} + +.pagination a { + background: var(--surface-bg); + color: var(--text-secondary); + text-decoration: none; + padding: var(--space-3) var(--space-6); + border-radius: var(--border-radius-md); + border: 1px solid var(--border-medium); + transition: all var(--transition-normal); + box-shadow: 0 2px 4px var(--shadow-light); +} + +.pagination a:hover { + background: var(--accent-primary); + color: var(--surface-bg); + transform: translateY(-1px); + box-shadow: 0 4px 8px var(--shadow-medium); +} + +.pagination a:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +.page-info { + color: var(--text-muted); + font-style: italic; +} + +/* Theme toggle */ +.theme-toggle { + position: fixed; + top: var(--space-4); + right: var(--space-4); + display: flex; + gap: var(--space-2); + z-index: 1000; + background: var(--surface-bg); + padding: var(--space-2); + border-radius: var(--border-radius-lg); + box-shadow: 0 4px 6px var(--shadow-medium); + border: 1px solid var(--border-light); +} + +.theme-toggle button { + background: none; + border: 1px solid var(--border-medium); + padding: var(--space-2) var(--space-3); + cursor: pointer; + border-radius: var(--border-radius-sm); + color: var(--text-muted); + font-family: var(--font-secondary); + font-size: var(--text-xs); + font-weight: 500; + transition: all var(--transition-normal); +} + +.theme-toggle button:hover { + background: var(--highlight); + color: var(--text-primary); +} + +.theme-toggle button.active { + background: var(--accent-primary); + color: var(--surface-bg); + border-color: var(--accent-primary); +} + +.theme-toggle button:focus { + outline: 2px solid var(--link-color); + outline-offset: 2px; +} + +/* Responsive design */ +@media (max-width: 768px) { + :root { + --content-width: min(90vw, 100%); + } + + .container { + padding: var(--space-4) var(--space-3) var(--space-12); + } + + header::after { + bottom: calc(-1 * var(--space-6)); + } + + h1::after { + width: 30px; + } + + h2 { + margin-top: var(--space-12); + padding-left: var(--space-3); + } + + .meta, .page-meta { + flex-direction: column; + gap: var(--space-1); + text-align: center; + } + + .meta > *:not(:last-child)::after { + content: ''; + margin: 0; + } + + blockquote { + padding: var(--space-6); + margin: var(--space-8) 0; + } + + .pull-quote { + font-size: var(--text-xl); + padding: var(--space-6) var(--space-4); + margin: var(--space-12) auto; + } + + nav { + gap: var(--space-4); + margin-top: var(--space-6); + } + + .theme-toggle { + top: var(--space-2); + right: var(--space-2); + font-size: var(--text-xs); + } + + .posts-list article { + padding: var(--space-6); + margin-bottom: var(--space-12); + } +} + +@media (max-width: 480px) { + .container { + padding: var(--space-3) var(--space-2) var(--space-10); + } + + nav { + gap: var(--space-3); + font-size: var(--text-xs); + } + + nav a { + padding: var(--space-1) var(--space-2); + } + + .pagination { + flex-direction: column; + gap: var(--space-3); + } +} + +/* Print styles */ +@media print { + :root { + --primary-bg: #ffffff; + --text-primary: #000000; + --text-secondary: #333333; + --text-tertiary: #666666; + --text-muted: #999999; + --accent-primary: #000000; + --link-color: #000000; + } + + body { + background: none; + font-size: 12pt; + line-height: 1.4; + } + + .container { + width: 100%; + max-width: none; + padding: 0; + margin: 0; + } + + .theme-toggle, nav { + display: none; + } + + a[href^="http"]::after { + content: " (" attr(href) ")"; + font-size: 0.8em; + color: #666; + } + + h1, h2, h3, h4, h5, h6 { + page-break-after: avoid; + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + page-break-inside: avoid; + } + + blockquote, pre { + page-break-inside: avoid; + border: 1px solid #ccc; + background: #f9f9f9 !important; + } + + p, h2, h3 { + orphans: 3; + widows: 3; + } +} + +/* Accessibility enhancements */ +@media (prefers-contrast: high) { + :root { + --text-primary: #000000; + --text-secondary: #1a1a1a; + --link-color: #0000ff; + --link-hover: #000080; + --accent-primary: #8b0000; + --border-medium: #666666; + --shadow-light: rgba(0, 0, 0, 0.5); + --shadow-medium: rgba(0, 0, 0, 0.7); + } +} + +/* NO JAVASCRIPT CLASS: Better graceful degradation */ +.no-js .posts-list article { + /* Remove hover effects if no JS */ + transition: none; +} + +.no-js .site-title a::before, +.no-js nav a::before { + /* Remove complex pseudo-element animations if no JS */ + display: none; +} + +/* TEXT BROWSER SUPPORT: Ensure all content is accessible */ +@media (max-device-width: 0) { + /* This targets text browsers */ + .site-title::after, + header::after, + h1::after, + h2::before, + blockquote::before, + .pull-quote::before, + .pull-quote::after { + display: none; + } + + .theme-toggle { + display: none; + } +} + +/* PERFORMANCE: Use GPU acceleration only when beneficial */ +@media (min-width: 769px) { + .posts-list article { + will-change: box-shadow; + } +} + +/* Focus enhancements for keyboard navigation */ +:focus-visible { + outline: 3px solid var(--link-color); + outline-offset: 2px; + border-radius: var(--border-radius-sm); +} + +/* Skip to content link - ACCESSIBILITY ESSENTIAL */ +.skip-to-content { + position: absolute; + top: -40px; + left: 6px; + background: var(--accent-primary); + color: var(--surface-bg); + padding: 8px 12px; + text-decoration: none; + border-radius: var(--border-radius-sm); + font-family: var(--font-secondary); + font-size: var(--text-sm); + font-weight: 600; + z-index: 100; + transition: top var(--transition-fast); +} + +.skip-to-content:focus { + top: 6px; +} + +/* PRINT OPTIMIZATIONS: Better resource usage */ +@media print { + * { + /* Remove all shadows and transitions for print */ + box-shadow: none !important; + text-shadow: none !important; + transition: none !important; + animation: none !important; + } +} From f8f4e18be7fb70e08fe3599f3f43740bd568915a Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Thu, 17 Jul 2025 09:02:17 +0200 Subject: [PATCH 09/21] Added pre-compression, thoughtful theme, Cyber-Dark theme, removed some Google fonts leftovers --- config.sh | 2 +- scripts/bssg.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.sh b/config.sh index 8a32838..228c16d 100644 --- a/config.sh +++ b/config.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # BSSG - Configuration File -# Version 0.31 +# Version 0.32 # Contains all configurable parameters for the static site generator # Developed by Stefano Marinelli (stefano@dragas.it) # diff --git a/scripts/bssg.sh b/scripts/bssg.sh index 16089f8..cbc8ddb 100755 --- a/scripts/bssg.sh +++ b/scripts/bssg.sh @@ -109,7 +109,7 @@ chmod +x scripts/*.sh 2>/dev/null || true # Function to display help information show_help() { - echo "BSSG - Bash Static Site Generator (v0.31)" + echo "BSSG - Bash Static Site Generator (v0.32)" echo "=========================================" echo "" echo "Usage: $0 [--config ] command [options]" From b62b4d8b76e732fdcf40b263581faa4b8c5dba6b Mon Sep 17 00:00:00 2001 From: Stefan - ZipKid - Goethals Date: Wed, 10 Sep 2025 13:04:28 +0200 Subject: [PATCH 10/21] Dutch / nl translation file --- locales/nl.sh | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 locales/nl.sh diff --git a/locales/nl.sh b/locales/nl.sh new file mode 100644 index 0000000..aee094f --- /dev/null +++ b/locales/nl.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Dutch Locale for BSSG + +export MSG_HOME="Home" +export MSG_TAGS="Tags" +export MSG_AUTHORS="Autheurs" +export MSG_ARCHIVES="Archieven" +export MSG_RSS="RSS" +export MSG_PAGES="Paginas" +export MSG_SUBSCRIBE_RSS="Volg via RSS" +export MSG_PUBLISHED_ON="Gepubliceerd op" +export MSG_BY="door" +export MSG_POSTS_BY="Posts door" +export MSG_TAG_PAGE_TITLE="Posts getagd met" +export MSG_ALL_TAGS="All Tags" +export MSG_ALL_AUTHORS="Alle Authors" +export MSG_ALL_PAGES="All Paginas" +export MSG_ARCHIVES_FOR="Archiven voor" +export MSG_BACK_TO="Terug naar" +export MSG_POSTS_FROM="Posts van" +export MSG_OLDER_POSTS="Oudere Posts" +export MSG_NEWER_POSTS="Nieuwere Posts" +export MSG_PAGE_INFO_TEMPLATE="Pagina %d van %d" +export MSG_PAGE_TITLE_PREFIX="Pagina" +export MSG_RSS_FEED_TITLE="${SITE_TITLE} - RSS Feed" +export MSG_RSS_FEED_DESCRIPTION="${SITE_DESCRIPTION}" +export MSG_RSS_FEED="RSS Feed" +export MSG_ALL_RIGHTS_RESERVED="Alle rechten reserved." +export MSG_GENERATED_WITH="Deze site was gegenereerd met" +export MSG_LATEST_POSTS="Laatste Posts" +export MSG_GENERATOR_DESCRIPTION="." +export MSG_POSTS="posts" +export MSG_READ_MORE="Lees meer" +export MSG_MONTH_01="Januari" +export MSG_MONTH_02="Februari" +export MSG_MONTH_03="Maart" +export MSG_MONTH_04="April" +export MSG_MONTH_05="Mei" +export MSG_MONTH_06="Juni" +export MSG_MONTH_07="Juli" +export MSG_MONTH_08="Augustus" +export MSG_MONTH_09="September" +export MSG_MONTH_10="October" +export MSG_MONTH_11="November" +export MSG_MONTH_12="December" +export MSG_READING_TIME_TEMPLATE="%d min read" +export MSG_MINUTE="minuut" +export MSG_MINUTES="minuten" +export MSG_UPDATED_ON="Bijgewerkt op" +export MSG_BACK_TO_TOP="Terug naar Boven" +export MSG_RELATED_POSTS="Gerelateede Posts" \ No newline at end of file From fee45661de54abf1b9ac7584fb5c7bb2e779d31a Mon Sep 17 00:00:00 2001 From: Tj Date: Mon, 13 Oct 2025 11:59:01 +0000 Subject: [PATCH 11/21] Feature: expand config variables embedded in cmdline When editing multiple sites using BSSG_LCONF it is easy to forget which path one is currently working with and frustrating to need to type exact paths when editing files. E.g: export BSSF_LCONF=/a/very/deep/path/to/site/a bssg.sh page ... bssg.sh edit '/a/very/deep/path/to/site/a/pages/page-1.md' It is easier, more intutive, and friendlier to be able to do: bssg.sh edit '$PAGES_DIR/pages/page-1.md' Note the single quotes to prevent the interactive shell from doing parameter expansion. Signed-off-by: Tj --- scripts/bssg.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/bssg.sh b/scripts/bssg.sh index cbc8ddb..ecb9dd4 100755 --- a/scripts/bssg.sh +++ b/scripts/bssg.sh @@ -210,6 +210,9 @@ main() { command="$1" shift # Consume the command itself + # expand variables such as POSTS_DIR, PAGES_DIR embedded in the command-line + set -- $(eval echo "$@") + case "$command" in post) scripts/post.sh "$@" From c252f106c8177f0d931fc55b3510125c44f2b120 Mon Sep 17 00:00:00 2001 From: Tj Date: Mon, 13 Oct 2025 14:16:12 +0000 Subject: [PATCH 12/21] Fix parallel is not GNU BSSG assumes parallel is from GNU but moreutils also provides it. If the moreutils version is available BSSG fails due to parellel options not being supported: Found 1 posts needing processing out of 1 (Skipped: 0). Using GNU parallel to process 1 posts PARALLEL --jobs 16 --will-cite process_single_file_for_rebuild {} /usr/bin/parallel: invalid option -- '-' parallel [OPTIONS] command -- arguments for each argument, run command with argument, in parallel parallel [OPTIONS] -- commands run specified commands in parallel Parallel post processing failed. When parallel is detected ask it for its version to ensure it is GNU. --- scripts/build/deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 scripts/build/deps.sh diff --git a/scripts/build/deps.sh b/scripts/build/deps.sh old mode 100755 new mode 100644 index 7166ad7..4968a9f --- a/scripts/build/deps.sh +++ b/scripts/build/deps.sh @@ -96,7 +96,7 @@ check_dependencies() { if [[ "$(uname)" == "NetBSD" ]]; then echo -e "${YELLOW}Parallel processing is unreliable on NetBSD. Using sequential processing.${NC}" export HAS_PARALLEL=false - elif command -v parallel > /dev/null 2>&1; then + elif command -v parallel > /dev/null 2>&1 && { read -r _version < <(parallel -V 2>/dev/null ) && [[ "${_version:0:3}" = "GNU" ]]; }; then echo -e "${GREEN}GNU parallel found! Using parallel processing.${NC}" export HAS_PARALLEL=true else From b1c2397a935781edac9ee393dc55e37276211389 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Sun, 28 Dec 2025 14:47:56 +0100 Subject: [PATCH 13/21] Added INDEX_SHOW_FULL_CONTENT configuration option. If true, show full post content on homepage instead of just description/excerpt. --- README.md | 2 + config.sh | 1 + scripts/build/config_loader.sh | 7 +++- scripts/build/generate_index.sh | 67 +++++++++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2e08c31..18b6c8d 100644 --- a/README.md +++ b/README.md @@ -726,6 +726,7 @@ POSTS_PER_PAGE=10 RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". Include full post content in RSS feed. RSS_FILENAME="rss.xml" # The filename for the main RSS feed (e.g., feed.xml, rss.xml) +INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". Show full post content on homepage instead of just description/excerpt. ENABLE_ARCHIVES=true # Enable or disable archive pages ENABLE_AUTHOR_PAGES=false # Enable or disable author pages (default: false) ENABLE_AUTHOR_RSS=false # Enable or disable author-specific RSS feeds (default: false) @@ -1171,6 +1172,7 @@ ENABLE_ARCHIVES=true # Enable or disable archives by year/month URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be included in the RSS feed description instead of the excerpt. Useful for readers that consume entire posts via RSS. +INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be displayed on the homepage and paginated index pages instead of just the description/excerpt. ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags//rss.xml`. # Related Posts configuration diff --git a/config.sh b/config.sh index 228c16d..db504c0 100644 --- a/config.sh +++ b/config.sh @@ -47,6 +47,7 @@ POSTS_PER_PAGE=10 RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". Include full post content in RSS feed. RSS_FILENAME="rss.xml" # The filename for the main RSS feed (e.g., feed.xml, rss.xml) +INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". Show full post content on homepage instead of just description/excerpt. ENABLE_ARCHIVES=true # Enable or disable archive pages ENABLE_AUTHOR_PAGES=false # Enable or disable author pages (default: false) ENABLE_AUTHOR_RSS=false # Enable or disable author-specific RSS feeds (default: false) diff --git a/scripts/build/config_loader.sh b/scripts/build/config_loader.sh index 233741f..e09694e 100755 --- a/scripts/build/config_loader.sh +++ b/scripts/build/config_loader.sh @@ -31,6 +31,7 @@ POSTS_PER_PAGE="${POSTS_PER_PAGE:-10}" RSS_ITEM_LIMIT="${RSS_ITEM_LIMIT:-15}" # Default RSS item limit RSS_INCLUDE_FULL_CONTENT="${RSS_INCLUDE_FULL_CONTENT:-false}" # Default RSS full content RSS_FILENAME="${RSS_FILENAME:-rss.xml}" # Default RSS filename +INDEX_SHOW_FULL_CONTENT="${INDEX_SHOW_FULL_CONTENT:-false}" # Default: show excerpt on homepage CLEAN_OUTPUT="${CLEAN_OUTPUT:-false}" FORCE_REBUILD="${FORCE_REBUILD:-false}" SITE_LANG="${SITE_LANG:-en}" @@ -217,13 +218,14 @@ BSSG_CONFIG_VARS_ARRAY=( CONFIG_FILE SRC_DIR OUTPUT_DIR TEMPLATES_DIR THEMES_DIR STATIC_DIR THEME SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL DATE_FORMAT TIMEZONE SHOW_TIMEZONE POSTS_PER_PAGE RSS_ITEM_LIMIT RSS_INCLUDE_FULL_CONTENT RSS_FILENAME + INDEX_SHOW_FULL_CONTENT CLEAN_OUTPUT FORCE_REBUILD SITE_LANG LOCALE_DIR PAGES_DIR MARKDOWN_PROCESSOR MARKDOWN_PL_PATH ENABLE_ARCHIVES URL_SLUG_FORMAT PAGE_URL_FORMAT DRAFTS_DIR REBUILD_AFTER_POST REBUILD_AFTER_EDIT CUSTOM_CSS ENABLE_TAG_RSS ENABLE_AUTHOR_PAGES ENABLE_AUTHOR_RSS SHOW_AUTHORS_MENU_THRESHOLD - BACKUP_DIR CACHE_DIR - DEPLOY_AFTER_BUILD DEPLOY_SCRIPT + BACKUP_DIR CACHE_DIR + DEPLOY_AFTER_BUILD DEPLOY_SCRIPT ARCHIVES_LIST_ALL_POSTS ENABLE_RELATED_POSTS RELATED_POSTS_COUNT PRECOMPRESS_ASSETS @@ -256,6 +258,7 @@ export POSTS_PER_PAGE export RSS_ITEM_LIMIT export RSS_INCLUDE_FULL_CONTENT export RSS_FILENAME +export INDEX_SHOW_FULL_CONTENT export CLEAN_OUTPUT export FORCE_REBUILD export SITE_LANG diff --git a/scripts/build/generate_index.sh b/scripts/build/generate_index.sh index 455d1af..ea19c7d 100755 --- a/scripts/build/generate_index.sh +++ b/scripts/build/generate_index.sh @@ -229,7 +229,67 @@ EOF
    EOF fi - if [ -n "$description" ]; then + # Show either full content or just description based on config + if [ "${INDEX_SHOW_FULL_CONTENT:-false}" = "true" ]; then + # Show full post content + local post_content="" + local content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")" + + # Try to get content from cache first + if [ -f "$content_cache_file" ]; then + post_content=$(cat "$content_cache_file") + else + # Extract content from source file if cache doesn't exist + local in_frontmatter=false + local found_frontmatter=false + { + while IFS= read -r line; do + if [[ "$line" == "---" ]]; then + if ! $in_frontmatter && ! $found_frontmatter; then + in_frontmatter=true + found_frontmatter=true + continue + elif $in_frontmatter; then + in_frontmatter=false + continue + fi + fi + if ! $in_frontmatter && $found_frontmatter; then + post_content+="$line"$'\n' + fi + done + } < "$file" + + # If no frontmatter was found, use the whole file + if ! $found_frontmatter; then + post_content=$(cat "$file") + fi + fi + + # Convert to HTML if it's a markdown file + local html_content="" + if [[ "$file" == *.md ]]; then + html_content=$(convert_markdown_to_html "$post_content") + elif [[ "$file" == *.html ]]; then + # For HTML files, content is already HTML + html_content=$(sed -n '//,/<\/body>/p' "$file" | sed '1d;$d') + # If body extraction failed, use content as-is + if [ -z "$html_content" ]; then + html_content="$post_content" + fi + else + html_content="$post_content" + fi + + if [ -n "$html_content" ]; then + cat >> "$output_file" << EOF +
    + $html_content +
    +EOF + fi + elif [ -n "$description" ]; then + # Show just the description/excerpt (default behavior) cat >> "$output_file" << EOF
    $description @@ -305,10 +365,11 @@ EOF export OUTPUT_DIR URL_SLUG_FORMAT POSTS_PER_PAGE CACHE_DIR export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME DATE_FORMAT SITE_URL export FORCE_REBUILD HEADER_TEMPLATE FOOTER_TEMPLATE SHOW_TIMEZONE - export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY + export INDEX_SHOW_FULL_CONTENT + export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY export MSG_NEWER_POSTS MSG_OLDER_POSTS MSG_PAGE_INFO_TEMPLATE # Note: total_posts_orig is NOT exported, passed as argument now - export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url + export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url convert_markdown_to_html # Ensure templates are exported if [ -z "$HEADER_TEMPLATE" ] || [ -z "$FOOTER_TEMPLATE" ]; then From e91a1344b0eb90769f9c85fb9bd755c830b2bbed Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Sun, 28 Dec 2025 14:50:10 +0100 Subject: [PATCH 14/21] Version bump --- scripts/bssg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bssg.sh b/scripts/bssg.sh index ecb9dd4..62daa83 100755 --- a/scripts/bssg.sh +++ b/scripts/bssg.sh @@ -109,7 +109,7 @@ chmod +x scripts/*.sh 2>/dev/null || true # Function to display help information show_help() { - echo "BSSG - Bash Static Site Generator (v0.32)" + echo "BSSG - Bash Static Site Generator (v0.33)" echo "=========================================" echo "" echo "Usage: $0 [--config ] command [options]" From e2822ad6208004db009fa61c3c9336af1017f6fd Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Mon, 29 Dec 2025 11:22:44 +0100 Subject: [PATCH 15/21] Fixed header generation: the metadata should only contain the post title, not the site name. --- templates/header.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/header.html b/templates/header.html index c892f8d..daada4c 100644 --- a/templates/header.html +++ b/templates/header.html @@ -5,13 +5,13 @@ {{page_title}} | {{site_title}} - + {{og_image}} - + {{twitter_image}} From cbc08b06cc8b4e51886f0a4db14f3d99d5e5a5a7 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Tue, 10 Feb 2026 19:08:59 +0100 Subject: [PATCH 16/21] add RAM mode and overhaul the build pipeline - introduce RAM build mode with in-memory preload/index datasets and stage timing - refactor build orchestration, indexing, content processing, and template handling - improve cache/rebuild logic and parallel worker execution across generators - enhance posts/pages/tags/authors/archives/feed generation and related-post flow - update CLI/config/README for new build options and performance tuning - harden timing logic to handle locale-specific EPOCHREALTIME decimal separators --- README.md | 204 +++--- config.sh | 1 + scripts/bssg.sh | 32 +- scripts/build/config_loader.sh | 24 +- scripts/build/content.sh | 137 ++-- scripts/build/generate_archives.sh | 315 ++++++++- scripts/build/generate_authors.sh | 198 +++++- scripts/build/generate_feeds.sh | 800 +++++++++++++++------- scripts/build/generate_index.sh | 310 ++++++++- scripts/build/generate_pages.sh | 76 +- scripts/build/generate_posts.sh | 430 +++++++----- scripts/build/generate_secondary_pages.sh | 25 +- scripts/build/generate_tags.sh | 599 +++++++++++++++- scripts/build/indexing.sh | 155 ++++- scripts/build/main.sh | 398 +++++++++-- scripts/build/ram_mode.sh | 204 ++++++ scripts/build/related_posts.sh | 206 +++++- scripts/build/templates.sh | 158 +++-- scripts/build/utils.sh | 236 +++++-- 19 files changed, 3757 insertions(+), 751 deletions(-) create mode 100644 scripts/build/ram_mode.sh diff --git a/README.md b/README.md index 18b6c8d..be623f5 100644 --- a/README.md +++ b/README.md @@ -28,19 +28,21 @@ - Generates HTML from Markdown using pandoc, commonmark, or markdown.pl (configurable) - Supports post metadata (title, date, tags) -- Supports `lastmod` timestamp in frontmatter for tracking content updates (used in sitemap, RSS feed, and optionally displayed on posts). +- Supports `lastmod` timestamp in frontmatter for tracking content updates (used in sitemap, RSS feed, and optionally displayed on posts) - Full date and time support with timezone awareness - Post descriptions/summaries for previews, OpenGraph, and RSS - Admin interface for managing posts and scheduling publications (planned for future release) - Standalone post editor with modern Ghost-like interface for visual content creation -- Creates tag index pages -- Related Posts: Automatically suggests related posts based on shared tags at the end of each post -- Author index pages with conditional navigation menu +- Creates tag index pages with optional tag RSS feeds +- Related Posts: automatically suggests related posts based on shared tags at the end of each post +- Author index pages with conditional navigation menu and optional author RSS feeds - Archives by year and month for chronological browsing - Dynamic menu generation based on available pages - Support for primary and secondary pages with automatic menu organization -- Generates sitemap.xml and RSS feed with timezone support -- Asset pre-compression: Can automatically create gzipped versions of text-based files (`.html`, `.css`, `.xml`, `.js`) during the build for servers that support serving pre-compressed content. +- Generates `sitemap.xml` and RSS feeds with timezone support +- Two build modes: `normal` (incremental, cache-backed) and `ram` (memory-first) +- RAM mode stage timing summary printed at the end of each RAM build +- Asset pre-compression with incremental and parallel gzip processing (`.html`, `.css`, `.xml`, `.js`) - Clean design - No JavaScript required (except for admin interface) - Works well without images @@ -52,11 +54,9 @@ - Supports static files (images, CSS, JS, etc.) - Configurable clean output directory option - Draft posts support -- Post scheduling system - Backup and restore functionality -- Incremental builds with file caching for improved performance -- Smart metadata caching system -- Parallel processing support using GNU parallel (if available) +- Incremental builds with file and metadata caching for improved performance +- Parallel processing with GNU parallel (if available) plus shell-worker fallbacks - File locking for safe concurrent operations - Automatic handling of different operating systems (Linux/macOS/BSDs) - Custom URL slugs with SEO-friendly permalinks @@ -207,20 +207,25 @@ BSSG/ ├── scripts/ # Supporting scripts │ ├── build/ # Modular build scripts │ │ ├── main.sh # Main build orchestrator -│ │ ├── utils.sh # Utility functions (colors, formatting, etc.) -│ │ ├── cli.sh # Command-line argument parsing -│ │ ├── config_loader.sh # Loads default and user configuration -│ │ ├── deps.sh # Dependency checking -│ │ ├── cache.sh # Cache management functions -│ │ ├── content_discovery.sh # Finds posts, pages, drafts -│ │ ├── markdown_processor.sh # Markdown conversion logic -│ │ ├── process_posts.sh # Processes individual posts -│ │ ├── process_pages.sh # Processes individual pages -│ │ ├── generate_indexes.sh # Creates index, tag, and archive pages -│ │ ├── generate_feeds.sh # Creates RSS feed and sitemap +│ │ ├── config_loader.sh # Loads defaults and local overrides +│ │ ├── deps.sh # Dependency checks +│ │ ├── cache.sh # Cache/config hash helpers +│ │ ├── content.sh # Metadata/excerpt/markdown helpers +│ │ ├── indexing.sh # File/tags/authors/archive index builders +│ │ ├── templates.sh # Template preload/menu generation +│ │ ├── generate_posts.sh # Post rendering +│ │ ├── generate_pages.sh # Static page rendering +│ │ ├── generate_index.sh # Homepage/pagination generation +│ │ ├── generate_tags.sh # Tag pages (+ optional tag RSS) +│ │ ├── generate_authors.sh # Author pages (+ optional author RSS) +│ │ ├── generate_archives.sh # Archive pages (year/month) +│ │ ├── generate_feeds.sh # Main RSS + sitemap │ │ ├── generate_secondary_pages.sh # Creates pages.html index -│ │ ├── copy_static.sh # Copies static files and theme assets -│ │ └── theme_utils.sh # Theme-related utilities +│ │ ├── related_posts.sh # Related-post indexing/render helpers +│ │ ├── post_process.sh # URL rewrite + permissions fixes +│ │ ├── assets.sh # Static copy + CSS/theme handling +│ │ ├── ram_mode.sh # RAM-mode preload/in-memory datasets +│ │ └── utils.sh # Shared helpers (time, URLs, parallel) │ ├── post.sh # Handles post creation │ ├── page.sh # Handles page creation │ ├── edit.sh # Handles post/page editing (updates lastmod) @@ -228,6 +233,8 @@ BSSG/ │ ├── list.sh # Lists posts, pages, drafts, tags │ ├── backup.sh # Backup functionality │ ├── restore.sh # Restore functionality +│ ├── benchmark.sh # Build benchmarking helper +│ ├── server.sh # Local development server implementation │ ├── theme.sh # Theme management and processing (legacy helper) │ ├── template.sh # Template processing utilities (legacy helper) │ └── css.sh # CSS generation utilities (legacy helper) @@ -261,58 +268,33 @@ BSSG/ ```bash cd BSSG -./bssg.sh [command] [options] +./bssg.sh [--config ] [command] [options] ``` ### Available Commands ``` -Usage: ./bssg.sh command [options] +Usage: ./bssg.sh [--config ] command [options] Commands: - post [-html] [draft_file] # Interactive: Create/edit post/draft, prompt for title, open editor. - # Rebuilds site afterwards if REBUILD_AFTER_POST=true in config. - # Use -html for HTML format. + post [-html] [draft_file] + Interactive: create/edit post or continue a draft. post -t [-T <tags>] [-s <slug>] [--html] [-d] {-c <content> | -f <file> | --stdin} [--build] - # Command-line: Create post non-interactively. - # -t: Title (required) - # -T: Tags (comma-sep) - # -s: Slug (optional) - # --html: HTML format (default: MD) - # -d: Save as draft - # -c: Content string - # -f: Content file - # --stdin: Content from stdin - # --build: Force rebuild (overrides REBUILD_AFTER_POST=false) - page [-html] [-s] [draft_file] Create a new page (in $PAGES_DIR or $DRAFTS_DIR/pages) - or continue editing a draft (in $DRAFTS_DIR/pages) - Use -html to edit in HTML instead of Markdown - Use -s to mark page as secondary (for menu) - edit [-n] <file> Edit an existing post/page/draft (updates lastmod) - File path should point to $SRC_DIR, $PAGES_DIR, $DRAFTS_DIR etc. - Use -n to rename based on title (posts/drafts only currently) - delete [-f] <file> Delete a post/page/draft - File path should point to $SRC_DIR, $PAGES_DIR, $DRAFTS_DIR etc. - Use -f to skip confirmation - list {posts|pages|drafts|tags [-n]} - List posts ($SRC_DIR), pages ($PAGES_DIR), - drafts ($DRAFTS_DIR and $DRAFTS_DIR/pages), or tags. - For tags, use -n to sort by count. - backup Create a backup of all posts, pages, drafts, and config - restore [backup_file|ID] Restore from a backup (all content by default) - Options: --no-content, --no-config - backups List all available backups - build [opts] Build the site using the modular build system in scripts/build/ - Options: -c|--clean-output, -f|--force-rebuild, - --config FILE, --theme NAME, - --site-url URL, --output DIR - init <target_directory> Initialize a new, empty site structure in the specified directory. - This is useful for separating your site content from the BSSG core scripts. - The script will preserve the path format you provide (relative, absolute, or tilde-prefixed) - in the generated site 'config.sh.local' for portability. - Note: If using '~' for your home directory, quote the path (e.g., '~/mysite' or "~/mysite") - to ensure the tilde is preserved in the generated config. - help Show this help message + Command-line: create post non-interactively. + page [-html] [-s] [draft_file] + Create a page or continue a page draft. + edit [-n] <file> Edit an existing post/page/draft (updates lastmod). + delete [-f] <file> Delete a post/page/draft. + list List all posts. + tags [-n] List all tags. Use -n to sort by post count. + drafts List all draft posts. + backup Create a backup of posts, pages, drafts, and config. + restore [backup_file|ID] Restore from a backup (options: --no-content, --no-config). + backups List all available backups. + build [options] Build the site (run './bssg.sh build --help' for full options). + server [options] Build and run local server (run './bssg.sh server --help'). + init <target_directory> Initialize a new site in the specified directory. + help Show help. ``` ### Creating Posts and Pages @@ -469,23 +451,39 @@ You can use these options with restore to selectively restore content: Usage: ./bssg.sh build [options] Options: - -c, --clean-output Empty the output directory before building + --src DIR Override source directory (from config: SRC_DIR) + --pages DIR Override pages directory (from config: PAGES_DIR) + --drafts DIR Override drafts directory (from config: DRAFTS_DIR) + --output DIR Override output directory (from config: OUTPUT_DIR) + --templates DIR Override templates directory (from config: TEMPLATES_DIR) + --themes-dir DIR Override themes directory (from config: THEMES_DIR) + --theme NAME Override theme for this build + --static DIR Override static directory (from config: STATIC_DIR) + --clean-output [bool] Clean output directory before build (default from config) -f, --force-rebuild Ignore cache and rebuild all files - --config FILE Use a specific configuration file (e.g., my_config.sh) - instead of the default config.sh - --src DIR Override the SRC_DIR specified in the config file - --pages DIR Override the PAGES_DIR specified in the config file - --drafts DIR Override the DRAFTS_DIR specified in the config file - --output DIR Build the site to a specific output directory - --templates DIR Override the TEMPLATES_DIR specified in the config file - --themes-dir DIR Override the THEMES_DIR specified in the config file - --theme NAME Override the theme specified in the config file for this build - --static DIR Override the STATIC_DIR specified in the config file - --site-url URL Override the SITE_URL specified in the config file for this build + --build-mode MODE Build mode: normal or ram + --site-title TITLE Override site title + --site-url URL Override site URL + --site-description DESC Override site description + --author-name NAME Override author name + --author-email EMAIL Override author email + --posts-per-page NUM Override pagination size --deploy Force deployment after successful build (overrides config) - --no-deploy Prevent deployment after build (overrides config) + --no-deploy Skip deployment after build (overrides config) + --help Show build help ``` +`--config <path>` is a global option and can be passed with any command (including `build`) to load a specific configuration file. + +Examples: + +```bash +./bssg.sh --config /path/to/site/config.sh.local build --build-mode ram +./bssg.sh build --output ./public --clean-output true +``` + +The option list above reflects the current `build --help` output. + ### Internationalization (i18n) BSSG supports generating the site in different languages. @@ -707,6 +705,16 @@ CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh) REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh) PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build. +BUILD_MODE="normal" # Options: "normal", "ram". RAM mode preloads inputs and keeps build indexes/data in memory. + +# Optional performance tunables (not required): +# RAM_MODE_MAX_JOBS=6 # Cap parallel workers in RAM mode (defaults to 6) +# RAM_MODE_VERBOSE=false # Extra RAM-mode debug/timing logs +# PRECOMPRESS_GZIP_LEVEL=9 # gzip level for precompression (1-9) +# PRECOMPRESS_MAX_JOBS=0 # 0=auto based on CPU/RAM mode cap +# PRECOMPRESS_VERBOSE=false # Verbose logs for precompression +# RAM_RSS_PREFILL_MIN_HITS=2 # RAM tag-RSS cache prefill threshold +# RAM_RSS_PREFILL_MAX_POSTS=24 # RAM tag-RSS prefill upper bound # Customization CUSTOM_CSS="" # Optional: Path to custom CSS file relative to output root (e.g., "/css/custom.css"). File should be placed in STATIC_DIR. @@ -1130,13 +1138,25 @@ The system maintains a cache of extracted metadata from markdown files to reduce - File index information is stored in `.bssg_cache/file_index.txt` - Tags index information is stored in `.bssg_cache/tags_index.txt` +### RAM Build Mode + +BSSG supports a RAM-first build mode for faster full rebuilds and lower disk churn: + +- Set `BUILD_MODE="ram"` in `config.sh.local`, or run `./bssg.sh build --build-mode ram` +- Source/posts/pages/templates/locales are preloaded in memory +- Build indexes (file/tags/authors/archive, plus page lists) are kept in memory +- RAM mode intentionally skips cache persistence and always behaves like an in-memory full rebuild +- A stage timing summary is printed at the end of RAM-mode builds +- On low-end disk-bound hosts, RAM mode can significantly reduce build time by avoiding repeated disk reads + ### Parallel Processing -If GNU parallel is installed on your system, BSSG can process multiple files simultaneously: +BSSG uses multiple execution strategies to process files in parallel: - Automatically detects GNU parallel and enables it for builds with many files -- Uses 80% of available CPU cores for optimal performance -- Falls back to sequential processing if parallel is not available +- Falls back to internal shell workers when GNU parallel is unavailable or unsuitable for a stage +- Auto-detects CPU core count for worker sizing +- In RAM mode, worker count is capped by `RAM_MODE_MAX_JOBS` (default: `6`) to reduce memory pressure To take advantage of parallel processing, install GNU parallel: @@ -1151,6 +1171,10 @@ brew install parallel pkg install parallel ``` +### Real-World Result + +On a single-core OpenBSD server with spinning disks, the maintainer observed build time dropping to about one third of the previous release when building with `BUILD_MODE="ram"`. + ## Site Configuration Key configuration options: @@ -1168,6 +1192,7 @@ DATE_FORMAT="%Y-%m-%d %H:%M:%S %z" TIMEZONE="local" # Options: "local", "GMT", or a specific timezone SHOW_TIMEZONE="false" # Options: "true", "false". Determines if the timezone offset (e.g., +0200) is shown in displayed dates. POSTS_PER_PAGE=10 +BUILD_MODE="normal" # "normal" (incremental cache-backed) or "ram" (memory-first) ENABLE_ARCHIVES=true # Enable or disable archives by year/month URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed. @@ -1175,6 +1200,18 @@ RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", t INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be displayed on the homepage and paginated index pages instead of just the description/excerpt. ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags/<tag-slug>/rss.xml`. +# Precompression options +PRECOMPRESS_ASSETS="false" # Generate .gz siblings for changed text assets +# PRECOMPRESS_GZIP_LEVEL=9 +# PRECOMPRESS_MAX_JOBS=0 +# PRECOMPRESS_VERBOSE=false + +# RAM-mode tuning (optional) +# RAM_MODE_MAX_JOBS=6 +# RAM_MODE_VERBOSE=false +# RAM_RSS_PREFILL_MIN_HITS=2 +# RAM_RSS_PREFILL_MAX_POSTS=24 + # Related Posts configuration ENABLE_RELATED_POSTS=true # Options: "true", "false". If set to "true" (default), related posts based on shared tags will be shown at the end of each post. RELATED_POSTS_COUNT=3 # Number of related posts to display (default: 3, recommended maximum: 5). @@ -1284,4 +1321,3 @@ This project is licensed under the BSD 3-Clause License - see the LICENSE file f - **Themes**: Explore the available themes in the `themes` directory. - **Backup & Restore**: Use `./bssg.sh backup` and `./bssg.sh restore` to manage content backups. - **Development Blog**: Stay up-to-date with the latest release notes, development progress, and announcements on the official BSSG Dev Blog: [https://blog.bssg.dragas.net](https://blog.bssg.dragas.net) - diff --git a/config.sh b/config.sh index db504c0..77c26bf 100644 --- a/config.sh +++ b/config.sh @@ -28,6 +28,7 @@ CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh) REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh) PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build. +BUILD_MODE="ram" # Options: "normal", "ram". "ram" preloads inputs and keeps build state in memory (writes only output artifacts). # Customization CUSTOM_CSS="" # Optional: Path to custom CSS file relative to output root (e.g., "/css/custom.css"). File should be placed in STATIC_DIR. diff --git a/scripts/bssg.sh b/scripts/bssg.sh index 62daa83..4f38a52 100755 --- a/scripts/bssg.sh +++ b/scripts/bssg.sh @@ -99,10 +99,17 @@ fi # Terminal colors (still needed here if config_loader doesn't export them, though it should) # These are now primarily set and exported by config_loader.sh based on config files. # The ':-' syntax provides a fallback if they somehow aren't set, using tput. -RED="${RED:-$(tput setaf 1)}" -GREEN="${GREEN:-$(tput setaf 2)}" -YELLOW="${YELLOW:-$(tput setaf 3)}" -NC="${NC:-$(tput sgr0)}" # Reset color +if [[ -t 1 ]] && command -v tput > /dev/null 2>&1 && tput setaf 1 > /dev/null 2>&1; then + RED="${RED:-$(tput setaf 1)}" + GREEN="${GREEN:-$(tput setaf 2)}" + YELLOW="${YELLOW:-$(tput setaf 3)}" + NC="${NC:-$(tput sgr0)}" # Reset color +else + RED="${RED:-}" + GREEN="${GREEN:-}" + YELLOW="${YELLOW:-}" + NC="${NC:-}" +fi # Make sure all scripts are executable chmod +x scripts/*.sh 2>/dev/null || true @@ -163,6 +170,7 @@ show_build_help() { echo " --static DIR Override Static directory (from config: ${STATIC_DIR:-static})" echo " --clean-output [bool] Clean output directory before building (default from config: ${CLEAN_OUTPUT:-false})" echo " --force-rebuild, -f Force rebuild of all files regardless of modification time" + echo " --build-mode MODE Build mode: normal or ram (default from config: ${BUILD_MODE:-normal})" echo " --site-title TITLE Override Site title" echo " --site-url URL Override Site URL" echo " --site-description DESC Override Site description" @@ -301,6 +309,22 @@ main() { export FORCE_REBUILD=true shift 1 ;; + --build-mode) + if [[ -z "$2" || "$2" == -* ]]; then + echo -e "${RED}Error: --build-mode requires a value (normal|ram).${NC}" >&2 + exit 1 + fi + case "$2" in + normal|ram) + export BUILD_MODE="$2" + ;; + *) + echo -e "${RED}Error: Invalid --build-mode '$2'. Use 'normal' or 'ram'.${NC}" >&2 + exit 1 + ;; + esac + shift 2 + ;; --site-title) export SITE_TITLE="$2" shift 2 diff --git a/scripts/build/config_loader.sh b/scripts/build/config_loader.sh index e09694e..405d3fe 100755 --- a/scripts/build/config_loader.sh +++ b/scripts/build/config_loader.sh @@ -34,6 +34,7 @@ RSS_FILENAME="${RSS_FILENAME:-rss.xml}" # Default RSS filename INDEX_SHOW_FULL_CONTENT="${INDEX_SHOW_FULL_CONTENT:-false}" # Default: show excerpt on homepage CLEAN_OUTPUT="${CLEAN_OUTPUT:-false}" FORCE_REBUILD="${FORCE_REBUILD:-false}" +BUILD_MODE="${BUILD_MODE:-normal}" # Build mode: normal or ram SITE_LANG="${SITE_LANG:-en}" LOCALE_DIR="${LOCALE_DIR:-locales}" PAGES_DIR="${PAGES_DIR:-pages}" @@ -62,11 +63,19 @@ BSSG_SERVER_HOST_DEFAULT="${BSSG_SERVER_HOST_DEFAULT:-localhost}" CUSTOM_CSS="${CUSTOM_CSS:-}" # Default to empty string # Define default colors here so utils.sh can use them if not overridden by config -RED="${RED:-$(tput setaf 1)}" -GREEN="${GREEN:-$(tput setaf 2)}" -YELLOW="${YELLOW:-$(tput setaf 3)}" -BLUE="${BLUE:-$(tput setaf 4)}" # Added Blue for print_info, using tput -NC="${NC:-$(tput sgr0)}" # No Color, using tput +if [[ -t 1 ]] && command -v tput > /dev/null 2>&1 && tput setaf 1 > /dev/null 2>&1; then + RED="${RED:-$(tput setaf 1)}" + GREEN="${GREEN:-$(tput setaf 2)}" + YELLOW="${YELLOW:-$(tput setaf 3)}" + BLUE="${BLUE:-$(tput setaf 4)}" + NC="${NC:-$(tput sgr0)}" +else + RED="${RED:-}" + GREEN="${GREEN:-}" + YELLOW="${YELLOW:-}" + BLUE="${BLUE:-}" + NC="${NC:-}" +fi # --- Default Configuration Variables --- END --- @@ -219,7 +228,7 @@ BSSG_CONFIG_VARS_ARRAY=( SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL DATE_FORMAT TIMEZONE SHOW_TIMEZONE POSTS_PER_PAGE RSS_ITEM_LIMIT RSS_INCLUDE_FULL_CONTENT RSS_FILENAME INDEX_SHOW_FULL_CONTENT - CLEAN_OUTPUT FORCE_REBUILD SITE_LANG LOCALE_DIR PAGES_DIR MARKDOWN_PROCESSOR + CLEAN_OUTPUT FORCE_REBUILD BUILD_MODE SITE_LANG LOCALE_DIR PAGES_DIR MARKDOWN_PROCESSOR MARKDOWN_PL_PATH ENABLE_ARCHIVES URL_SLUG_FORMAT PAGE_URL_FORMAT DRAFTS_DIR REBUILD_AFTER_POST REBUILD_AFTER_EDIT CUSTOM_CSS @@ -261,6 +270,7 @@ export RSS_FILENAME export INDEX_SHOW_FULL_CONTENT export CLEAN_OUTPUT export FORCE_REBUILD +export BUILD_MODE export SITE_LANG export LOCALE_DIR export PAGES_DIR @@ -316,4 +326,4 @@ export MSG_MONTH_09 MSG_MONTH_10 MSG_MONTH_11 MSG_MONTH_12 # --- Final Path Adjustments (after all sourcing) --- START --- # Ensure relevant directory paths are exported if not already absolute. # ... existing code ... -# --- Final Path Adjustments (after all sourcing) --- END --- \ No newline at end of file +# --- Final Path Adjustments (after all sourcing) --- END --- diff --git a/scripts/build/content.sh b/scripts/build/content.sh index b2e0b12..f0ee63c 100755 --- a/scripts/build/content.sh +++ b/scripts/build/content.sh @@ -14,10 +14,29 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s parse_metadata() { local file="$1" local field="$2" + local value="" + + # RAM mode: parse directly from preloaded content to avoid disk/cache I/O. + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + local file_content frontmatter + file_content=$(ram_mode_get_content "$file") + frontmatter=$(printf '%s\n' "$file_content" | awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { exit; } + } + in_fm { print; } + ') + if [ -n "$frontmatter" ]; then + value=$(printf '%s\n' "$frontmatter" | grep -m 1 "^$field:[[:space:]]*" | cut -d ':' -f 2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + fi + echo "$value" + return 0 + fi # IMPORTANT: Assumes CACHE_DIR is exported/available local cache_file="${CACHE_DIR:-.bssg_cache}/meta/$(basename "$file")" - local value="" # Get locks for cache access # IMPORTANT: Assumes lock_file/unlock_file are sourced/available @@ -70,9 +89,13 @@ extract_metadata() { local file="$1" local metadata_cache_file="${CACHE_DIR:-.bssg_cache}/meta/$(basename "$file")" local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + ram_mode_active=true + fi # Check if file exists - if [ ! -f "$file" ]; then + if ! $ram_mode_active && [ ! -f "$file" ]; then echo "ERROR_FILE_NOT_FOUND" return 1 fi @@ -81,7 +104,7 @@ extract_metadata() { local frontmatter_changed=false # Check if cache exists and is newer than the source file - if [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$metadata_cache_file" ] && [ "$metadata_cache_file" -nt "$file" ]; then + if ! $ram_mode_active && [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$metadata_cache_file" ] && [ "$metadata_cache_file" -nt "$file" ]; then # Read from cache file (optimized - read once) echo "$(cat "$metadata_cache_file")" return 0 @@ -98,25 +121,39 @@ extract_metadata() { # Parse <meta> tags for HTML files # Use grep -m 1 for efficiency, handle missing tags gracefully # Note: This is basic parsing, assumes simple meta tag structure. - title=$(grep -m 1 -o '<title>[^<]*' "$file" 2>/dev/null | sed -e 's///' -e 's/<\/title>//') - date=$(grep -m 1 -o 'name="date" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - lastmod=$(grep -m 1 -o 'name="lastmod" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - tags=$(grep -m 1 -o 'name="tags" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - slug=$(grep -m 1 -o 'name="slug" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - image=$(grep -m 1 -o 'name="image" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - image_caption=$(grep -m 1 -o 'name="image_caption" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - description=$(grep -m 1 -o 'name="description" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - author_name=$(grep -m 1 -o 'name="author_name" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - author_email=$(grep -m 1 -o 'name="author_email" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + local html_source="" + if $ram_mode_active; then + html_source=$(ram_mode_get_content "$file") + title=$(printf '%s\n' "$html_source" | grep -m 1 -o '<title>[^<]*' 2>/dev/null | sed -e 's///' -e 's/<\/title>//') + date=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="date" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + lastmod=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="lastmod" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + tags=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="tags" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + slug=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="slug" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + image=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="image" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + image_caption=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="image_caption" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + description=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="description" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_name=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="author_name" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_email=$(printf '%s\n' "$html_source" | grep -m 1 -o 'name="author_email" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + else + title=$(grep -m 1 -o '<title>[^<]*' "$file" 2>/dev/null | sed -e 's///' -e 's/<\/title>//') + date=$(grep -m 1 -o 'name="date" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + lastmod=$(grep -m 1 -o 'name="lastmod" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + tags=$(grep -m 1 -o 'name="tags" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + slug=$(grep -m 1 -o 'name="slug" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + image=$(grep -m 1 -o 'name="image" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + image_caption=$(grep -m 1 -o 'name="image_caption" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + description=$(grep -m 1 -o 'name="description" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_name=$(grep -m 1 -o 'name="author_name" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + author_email=$(grep -m 1 -o 'name="author_email" content="[^"]*"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + fi # Note: Excerpt generation (fallback for description) might not work well for HTML elif [[ "$file" == *.md ]]; then # Parse YAML frontmatter for Markdown files - # Use awk with a here document for reliable script passing - - # Run awk and read results + # Use a shared awk parser for both disk and RAM paths. local parsed_data - parsed_data=$(awk -f - "$file" <<'EOF' + local awk_frontmatter_parser + awk_frontmatter_parser=$(cat <<'EOF' BEGIN { in_fm = 0; found_fm = 0; @@ -162,6 +199,12 @@ extract_metadata() { } EOF ) + + if $ram_mode_active; then + parsed_data=$(printf '%s\n' "$(ram_mode_get_content "$file")" | awk "$awk_frontmatter_parser") + else + parsed_data=$(awk "$awk_frontmatter_parser" "$file") + fi IFS='|' read -r title date lastmod tags slug image image_caption description author_name author_email <<< "$parsed_data" @@ -207,7 +250,7 @@ EOF local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description|$author_name|$author_email" # Check if there was a previous metadata file and compare - if [ -f "$metadata_cache_file" ]; then + if ! $ram_mode_active && [ -f "$metadata_cache_file" ]; then local old_metadata=$(cat "$metadata_cache_file") if [ "$old_metadata" != "$new_metadata" ]; then frontmatter_changed=true @@ -215,13 +258,15 @@ EOF fi # Store all metadata in one write operation - lock_file "$metadata_cache_file" - mkdir -p "$(dirname "$metadata_cache_file")" - echo "$new_metadata" > "$metadata_cache_file" - unlock_file "$metadata_cache_file" + if ! $ram_mode_active; then + lock_file "$metadata_cache_file" + mkdir -p "$(dirname "$metadata_cache_file")" + echo "$new_metadata" > "$metadata_cache_file" + unlock_file "$metadata_cache_file" + fi # If frontmatter has changed, update the marker file's timestamp - if $frontmatter_changed; then + if ! $ram_mode_active && $frontmatter_changed; then touch "$frontmatter_changes_marker" fi @@ -234,17 +279,30 @@ generate_excerpt() { local file="$1" local max_length="${2:-160}" # Default to 160 characters - # Extract content after frontmatter - local start_line=$(grep -n "^---$" "$file" | head -1 | cut -d: -f1) - local end_line=$(grep -n "^---$" "$file" | head -n 2 | tail -1 | cut -d: -f1) - local raw_content_stream - if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then - # Stream content after frontmatter - raw_content_stream=$(tail -n +$((end_line + 1)) "$file") + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + # Remove frontmatter directly from preloaded content + raw_content_stream=$(printf '%s\n' "$(ram_mode_get_content "$file")" | awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { in_fm = 0; next; } + } + { if (!in_fm) print; } + ') else - # No valid frontmatter, stream the whole file - raw_content_stream=$(cat "$file") + # Extract content after frontmatter + local start_line end_line + start_line=$(grep -n "^---$" "$file" | head -1 | cut -d: -f1) + end_line=$(grep -n "^---$" "$file" | head -n 2 | tail -1 | cut -d: -f1) + + if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then + # Stream content after frontmatter + raw_content_stream=$(tail -n +$((end_line + 1)) "$file") + else + # No valid frontmatter, stream the whole file + raw_content_stream=$(cat "$file") + fi fi # Sanitize and extract the first non-empty paragraph/line @@ -324,26 +382,19 @@ convert_markdown_to_html() { elif [ "$MARKDOWN_PROCESSOR" = "markdown.pl" ]; then # Preprocess content to handle fenced code blocks for markdown.pl local preprocessed_content="$content" - local temp_file - temp_file=$(mktemp) - # Use printf to avoid issues with content starting with - - printf '%s' "$preprocessed_content" > "$temp_file" - # Handle fenced code blocks (``` and ~~~) -> indented # Requires awk if command -v awk &> /dev/null; then - preprocessed_content=$(awk ' + preprocessed_content=$(printf '%s' "$preprocessed_content" | awk ' BEGIN { in_code = 0; } /^```[a-zA-Z0-9]*$/ || /^~~~[a-zA-Z0-9]*$/ { if (!in_code) { in_code = 1; print ""; next; } } /^```$/ || /^~~~$/ { if (in_code) { in_code = 0; print ""; next; } } { if (in_code) { print " " $0; } else { print $0; } } - ' "$temp_file") - rm "$temp_file" + ') else echo -e "${YELLOW}Warning: awk not found, markdown.pl fenced code block conversion skipped.${NC}" >&2 # Content remains as original if awk fails - preprocessed_content=$(cat "$temp_file") - rm "$temp_file" + preprocessed_content="$content" fi # Ensure MARKDOWN_PL_PATH is set and executable @@ -366,4 +417,4 @@ convert_markdown_to_html() { return 0 } -# --- Content Functions --- END --- \ No newline at end of file +# --- Content Functions --- END --- diff --git a/scripts/build/generate_archives.sh b/scripts/build/generate_archives.sh index 701ea41..137dcbe 100755 --- a/scripts/build/generate_archives.sh +++ b/scripts/build/generate_archives.sh @@ -14,6 +14,312 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s # Helper Functions for Archive Generation # ============================================================================== +_generate_ram_year_archive_page() { + local year="$1" + [ -z "$year" ] && return 0 + + local year_index_page="$OUTPUT_DIR/archives/$year/index.html" + mkdir -p "$(dirname "$year_index_page")" + + local year_header="$HEADER_TEMPLATE" + local year_footer="$FOOTER_TEMPLATE" + local year_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $year" + local year_archive_rel_url="/archives/$year/" + year_header=${year_header//\{\{site_title\}\}/"$SITE_TITLE"} + year_header=${year_header//\{\{page_title\}\}/"$year_page_title"} + year_header=${year_header//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + year_header=${year_header//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + year_header=${year_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + year_header=${year_header//\{\{og_type\}\}/"website"} + year_header=${year_header//\{\{page_url\}\}/"$year_archive_rel_url"} + year_header=${year_header//\{\{site_url\}\}/"$SITE_URL"} + year_header=${year_header//\{\{og_image\}\}/""} + year_header=${year_header//\{\{twitter_image\}\}/""} + local year_schema_json + year_schema_json='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$year_page_title"'","description": "Archive of posts from '"$year"'","url": "'"$SITE_URL$year_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' + year_header=${year_header//\{\{schema_json_ld\}\}/"$year_schema_json"} + year_footer=${year_footer//\{\{current_year\}\}/$(date +%Y)} + year_footer=${year_footer//\{\{author_name\}\}/"$AUTHOR_NAME"} + + { + echo "$year_header" + echo "<h1>$year_page_title</h1>" + echo "<ul class=\"month-list\">" + local month_key + for month_key in $(printf '%s\n' "${!month_posts[@]}" | awk -F'|' -v y="$year" '$1 == y { print $0 }' | sort -t'|' -k2,2nr); do + local month_num="${month_key#*|}" + local month_name="${month_name_map[$month_key]}" + local month_post_count + month_post_count=$(printf '%s\n' "${month_posts[$month_key]}" | awk 'NF { c++ } END { print c+0 }') + local month_idx_formatted + month_idx_formatted=$(printf "%02d" "$((10#$month_num))") + local month_var_name="MSG_MONTH_${month_idx_formatted}" + local current_month_name="${!month_var_name:-$month_name}" + local month_url + month_url=$(fix_url "/archives/$year/$month_idx_formatted/") + echo "<li><a href=\"$month_url\">$current_month_name ($month_post_count)</a></li>" + done + echo "</ul>" + echo "$year_footer" + } > "$year_index_page" +} + +_generate_ram_month_archive_page() { + local month_key="$1" + [ -z "$month_key" ] && return 0 + + local year="${month_key%|*}" + local month_num="${month_key#*|}" + local month_idx_formatted + month_idx_formatted=$(printf "%02d" "$((10#$month_num))") + local month_index_page="$OUTPUT_DIR/archives/$year/$month_idx_formatted/index.html" + mkdir -p "$(dirname "$month_index_page")" + + local month_name_var="MSG_MONTH_${month_idx_formatted}" + local month_name="${!month_name_var:-${month_name_map[$month_key]}}" + [ -z "$month_name" ] && month_name="Month $month_idx_formatted" + + local month_header="$HEADER_TEMPLATE" + local month_footer="$FOOTER_TEMPLATE" + local month_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $month_name $year" + local month_archive_rel_url="/archives/$year/$month_idx_formatted/" + month_header=${month_header//\{\{site_title\}\}/"$SITE_TITLE"} + month_header=${month_header//\{\{page_title\}\}/"$month_page_title"} + month_header=${month_header//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + month_header=${month_header//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + month_header=${month_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + month_header=${month_header//\{\{og_type\}\}/"website"} + month_header=${month_header//\{\{page_url\}\}/"$month_archive_rel_url"} + month_header=${month_header//\{\{site_url\}\}/"$SITE_URL"} + month_header=${month_header//\{\{og_image\}\}/""} + month_header=${month_header//\{\{twitter_image\}\}/""} + local month_schema_json + month_schema_json='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$month_page_title"'","description": "Archive of posts from '"$month_name $year"'","url": "'"$SITE_URL$month_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' + month_header=${month_header//\{\{schema_json_ld\}\}/"$month_schema_json"} + month_footer=${month_footer//\{\{current_year\}\}/$(date +%Y)} + month_footer=${month_footer//\{\{author_name\}\}/"$AUTHOR_NAME"} + + { + echo "$month_header" + echo "<h1>$month_page_title</h1>" + echo "<div class=\"posts-list\">" + while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description author_name author_email; do + [ -z "$title" ] && continue + local post_year post_month post_day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$post_year}" + url_path="${url_path//Month/$post_month}" + url_path="${url_path//Day/$post_day}" + url_path="${url_path//slug/$slug}" + local post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')" + post_url="${SITE_URL}${post_url}" + + local display_date_format="$DATE_FORMAT" + if [ "${SHOW_TIMEZONE:-false}" = false ]; then + display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//') + fi + local formatted_date + formatted_date=$(format_date "$date" "$display_date_format") + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + + cat << EOF + <article> + <h3><a href="${post_url}">$title</a></h3> + <div class="meta">${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date ${MSG_BY:-\"by\"} <strong>$display_author_name</strong></div> +EOF + if [ -n "$image" ]; then + local image_url + image_url=$(fix_url "$image") + local alt_text="${image_caption:-$title}" + local figcaption_content="${image_caption:-$title}" + cat << EOF + <figure class="featured-image tag-image"> + <a href="${post_url}"> + <img src="$image_url" alt="$alt_text" /> + </a> + <figcaption>$figcaption_content</figcaption> + </figure> +EOF + fi + if [ -n "$description" ]; then + cat << EOF + <div class="summary"> + $description + </div> +EOF + fi + cat << EOF + </article> +EOF + done < <(printf '%s\n' "${month_posts[$month_key]}" | awk 'NF' | sort -t'|' -k5,5r) + echo "</div>" + echo "$month_footer" + } > "$month_index_page" +} + +_generate_archive_pages_ram() { + echo -e "${YELLOW}Processing archive pages...${NC}" + + local archive_index_data + archive_index_data=$(ram_mode_get_dataset "archive_index") + if [ -z "$archive_index_data" ]; then + echo -e "${YELLOW}Warning: No archive index data in RAM. Skipping archive generation.${NC}" + return 0 + fi + + declare -A month_posts=() + declare -A month_name_map=() + declare -A year_map=() + + local line + while IFS= read -r line; do + [ -z "$line" ] && continue + local year month month_name + IFS='|' read -r year month month_name _ <<< "$line" + [ -z "$year" ] && continue + [ -z "$month" ] && continue + local month_key="${year}|${month}" + month_posts["$month_key"]+="$line"$'\n' + month_name_map["$month_key"]="$month_name" + year_map["$year"]=1 + done <<< "$archive_index_data" + + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"${MSG_ARCHIVES:-"Archives"}"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"archives/"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + header_content=${header_content//\{\{og_image\}\}/""} + header_content=${header_content//\{\{twitter_image\}\}/""} + local schema_json_ld + schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "Archives","description": "'"$SITE_DESCRIPTION"'","url": "'"$SITE_URL"'/archives/","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' + header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} + footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + + local archives_index_page="$OUTPUT_DIR/archives/index.html" + mkdir -p "$(dirname "$archives_index_page")" + { + echo "$header_content" + echo "<h1>${MSG_ARCHIVES:-"Archives"}</h1>" + echo "<div class=\"archives-list year-list\">" + + local year + for year in $(printf '%s\n' "${!year_map[@]}" | sort -nr); do + [ -z "$year" ] && continue + local year_url + year_url=$(fix_url "/archives/$year/") + echo " <h2><a href=\"$year_url\">$year</a></h2>" + echo " <ul class=\"month-list-detailed\">" + + local month_key + for month_key in $(printf '%s\n' "${!month_posts[@]}" | awk -F'|' -v y="$year" '$1 == y { print $0 }' | sort -t'|' -k2,2nr); do + local month_num="${month_key#*|}" + local month_name="${month_name_map[$month_key]}" + local month_idx_formatted + month_idx_formatted=$(printf "%02d" "$((10#$month_num))") + local month_var_name="MSG_MONTH_${month_idx_formatted}" + local current_month_name="${!month_var_name:-$month_name}" + local month_url + month_url=$(fix_url "/archives/$year/$month_idx_formatted/") + local month_post_count + month_post_count=$(printf '%s\n' "${month_posts[$month_key]}" | awk 'NF { c++ } END { print c+0 }') + + echo " <li>" + echo " <a href=\"$month_url\">$current_month_name ($month_post_count)</a>" + + if [ "${ARCHIVES_LIST_ALL_POSTS:-false}" = true ] && [ "$month_post_count" -gt 0 ]; then + echo " <ul class=\"post-list-condensed-inline\">" + while IFS='|' read -r _ _ _ title date _ filename slug _ _ _ author_name author_email; do + [ -z "$title" ] && continue + local post_year post_month post_day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$post_year}" + url_path="${url_path//Month/$post_month}" + url_path="${url_path//Day/$post_day}" + url_path="${url_path//slug/$slug}" + local post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')" + post_url=$(fix_url "$post_url") + local display_date + display_date=$(echo "$date" | cut -d' ' -f1) + echo " <li><a href=\"$post_url\">[$display_date] $title</a></li>" + done < <(printf '%s\n' "${month_posts[$month_key]}" | awk 'NF' | sort -t'|' -k5,5r) + echo " </ul>" + fi + echo " </li>" + done + echo " </ul>" + done + + echo "</div>" + echo "$footer_content" + } > "$archives_index_page" + + local year_count=${#year_map[@]} + local month_count=${#month_posts[@]} + local year_jobs month_jobs max_workers + max_workers=$(get_parallel_jobs) + year_jobs="$max_workers" + month_jobs="$max_workers" + if [ "$year_jobs" -gt "$year_count" ]; then + year_jobs="$year_count" + fi + if [ "$month_jobs" -gt "$month_count" ]; then + month_jobs="$month_count" + fi + + if [ "$year_jobs" -gt 1 ] && [ "$year_count" -gt 1 ]; then + echo -e "${GREEN}Using shell parallel workers for ${year_count} RAM-mode year archive pages${NC}" + run_parallel "$year_jobs" < <( + while IFS= read -r year; do + [ -z "$year" ] && continue + printf "_generate_ram_year_archive_page '%s'\n" "$year" + done < <(printf '%s\n' "${!year_map[@]}" | sort -nr) + ) || return 1 + else + local year + for year in $(printf '%s\n' "${!year_map[@]}" | sort -nr); do + _generate_ram_year_archive_page "$year" + done + fi + + if [ "$month_jobs" -gt 1 ] && [ "$month_count" -gt 1 ]; then + echo -e "${GREEN}Using shell parallel workers for ${month_count} RAM-mode monthly archive pages${NC}" + run_parallel "$month_jobs" < <( + while IFS= read -r month_key; do + [ -z "$month_key" ] && continue + printf "_generate_ram_month_archive_page '%s'\n" "$month_key" + done < <(printf '%s\n' "${!month_posts[@]}" | sort -t'|' -k1,1nr -k2,2nr) + ) || return 1 + else + local month_key + for month_key in $(printf '%s\n' "${!month_posts[@]}" | sort -t'|' -k1,1nr -k2,2nr); do + _generate_ram_month_archive_page "$month_key" + done + fi + + echo -e "${GREEN}Archive page processing complete.${NC}" +} + # Check if the main archive index page needs rebuilding _check_archive_index_rebuild_needed() { local archive_index_file="$CACHE_DIR/archive_index.txt" @@ -307,7 +613,7 @@ process_single_month() { # Generate header local header_content="$HEADER_TEMPLATE" - local month_page_title="${MSG_ARCHIVES_FOR:-\"Archives for\"} $month_name $year" + local month_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $month_name $year" local month_archive_rel_url="/archives/$year/$month_num/" header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} header_content=${header_content//\{\{page_title\}\}/"$month_page_title"} @@ -432,6 +738,11 @@ _process_single_month_parallel_wrapper() { # Main Archive Generation Orchestrator # ============================================================================== generate_archive_pages() { + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + _generate_archive_pages_ram + return $? + fi + echo -e "${YELLOW}Processing archive pages...${NC}" local archive_index_file="$CACHE_DIR/archive_index.txt" @@ -549,4 +860,4 @@ generate_archive_pages() { } # Make the function available for sourcing -export -f generate_archive_pages \ No newline at end of file +export -f generate_archive_pages diff --git a/scripts/build/generate_authors.sh b/scripts/build/generate_authors.sh index 9c1b649..4857d9c 100644 --- a/scripts/build/generate_authors.sh +++ b/scripts/build/generate_authors.sh @@ -13,8 +13,204 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s # shellcheck source=generate_feeds.sh disable=SC1091 source "$(dirname "$0")/generate_feeds.sh" || { echo >&2 "Error: Failed to source generate_feeds.sh from generate_authors.sh"; exit 1; } +_generate_author_pages_ram() { + echo -e "${YELLOW}Processing author pages${NC}${ENABLE_AUTHOR_RSS:+" and RSS feeds"}...${NC}" + + local authors_index_data + authors_index_data=$(ram_mode_get_dataset "authors_index") + local main_authors_index_output="$OUTPUT_DIR/authors/index.html" + + mkdir -p "$OUTPUT_DIR/authors" + + if [ -z "$authors_index_data" ]; then + echo -e "${YELLOW}No authors found in RAM index. Skipping author page generation.${NC}" + return 0 + fi + + declare -A author_posts_by_slug=() + declare -A author_name_by_slug=() + declare -A author_email_by_slug=() + local line author author_slug author_email + while IFS= read -r line; do + [ -z "$line" ] && continue + IFS='|' read -r author author_slug author_email _ <<< "$line" + [ -z "$author" ] && continue + [ -z "$author_slug" ] && continue + if [[ -z "${author_name_by_slug[$author_slug]+_}" ]]; then + author_name_by_slug["$author_slug"]="$author" + author_email_by_slug["$author_slug"]="$author_email" + fi + author_posts_by_slug["$author_slug"]+="$line"$'\n' + done <<< "$authors_index_data" + + local author_slug_key + for author_slug_key in $(printf '%s\n' "${!author_name_by_slug[@]}" | sort); do + author="${author_name_by_slug[$author_slug_key]}" + local author_data="${author_posts_by_slug[$author_slug_key]}" + local author_page_html_file="$OUTPUT_DIR/authors/$author_slug_key/index.html" + local author_rss_file="$OUTPUT_DIR/authors/$author_slug_key/${RSS_FILENAME:-rss.xml}" + local author_page_rel_url="authors/${author_slug_key}/" + local author_rss_rel_url="/authors/${author_slug_key}/${RSS_FILENAME:-rss.xml}" + local post_count + post_count=$(printf '%s\n' "$author_data" | awk 'NF { c++ } END { print c+0 }') + + mkdir -p "$(dirname "$author_page_html_file")" + + local author_page_content="" + author_page_content+="<h1>${MSG_POSTS_BY:-Posts by} $author</h1>"$'\n' + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + author_page_content+="<p><a href=\"$author_rss_rel_url\">${MSG_RSS_FEED:-RSS Feed}</a></p>"$'\n' + fi + author_page_content+="<div class=\"posts-list\">"$'\n' + + while IFS='|' read -r author_name_inner author_slug_inner author_email_inner post_title post_date post_lastmod post_filename post_slug post_image post_image_caption post_description; do + [ -z "$post_title" ] && continue + + local post_url + if [ -n "$post_date" ] && [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + local year month day url_path + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}" + url_path="${url_path//Month/$month}" + url_path="${url_path//Day/$day}" + url_path="${url_path//slug/$post_slug}" + post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')" + else + post_url="/$(echo "$post_slug" | sed 's|^/||; s|/*$|/|')" + fi + post_url="${BASE_URL}${post_url}" + local formatted_date + formatted_date=$(format_date "$post_date") + + author_page_content+="<article>"$'\n' + author_page_content+=" <h2><a href=\"$post_url\">$post_title</a></h2>"$'\n' + author_page_content+=" <div class=\"meta\">"$'\n' + author_page_content+=" <time datetime=\"$post_date\">$formatted_date</time>"$'\n' + author_page_content+=" </div>"$'\n' + if [ -n "$post_description" ]; then + author_page_content+=" <p class=\"summary\">$post_description</p>"$'\n' + fi + if [ -n "$post_image" ]; then + author_page_content+=" <div class=\"author-image\">"$'\n' + author_page_content+=" <img src=\"$post_image\" alt=\"$post_image_caption\" loading=\"lazy\">"$'\n' + author_page_content+=" </div>"$'\n' + fi + author_page_content+="</article>"$'\n' + done < <(printf '%s\n' "$author_data" | awk 'NF' | sort -t'|' -k5,5r) + + author_page_content+="</div>"$'\n' + + local page_title="${MSG_POSTS_BY:-Posts by} $author" + local page_description="${MSG_POSTS_BY:-Posts by} $author - $post_count ${MSG_POSTS:-posts}" + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"$page_title"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$page_description"} + header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"$author_page_rel_url"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + header_content=${header_content//\{\{og_image\}\}/} + header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//<!-- bssg:tag_rss_link -->/} + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + local author_rss_link="<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$author RSS Feed\" href=\"$SITE_URL$author_rss_rel_url\">" + header_content=${header_content//<!-- bssg:tag_rss_link -->/$author_rss_link} + fi + local schema_json + schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"$page_description\",\"url\": \"$SITE_URL$author_page_rel_url\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" + header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"} + + local current_year + current_year=$(date +%Y) + footer_content=${footer_content//\{\{current_year\}\}/"$current_year"} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"} + + { + echo "$header_content" + echo "$author_page_content" + echo "$footer_content" + } > "$author_page_html_file" + + if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then + local author_post_data + author_post_data=$(printf '%s\n' "$author_data" | awk 'NF' | sort -t'|' -k5,5r | awk -F'|' '{ + author_name = $1 + author_email = $3 + title = $4 + date = $5 + lastmod = $6 + filename = $7 + post_slug = $8 + image = $9 + image_caption = $10 + description = $11 + printf "%s|%s|%s|%s|%s||%s|%s|%s|%s|%s|%s\n", filename, filename, title, date, lastmod, post_slug, image, image_caption, description, author_name, author_email + }') + _generate_rss_feed "$author_rss_file" "$SITE_TITLE - ${MSG_POSTS_BY:-Posts by} $author" "${MSG_POSTS_BY:-Posts by} $author" "$author_page_rel_url" "$author_rss_rel_url" "$author_post_data" + fi + done + + local page_title="${MSG_ALL_AUTHORS:-All Authors}" + local page_description="${MSG_ALL_AUTHORS:-All Authors} - $SITE_DESCRIPTION" + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + local main_content="" + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"$page_title"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$page_description"} + header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"authors/"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + header_content=${header_content//\{\{og_image\}\}/} + header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//<!-- bssg:tag_rss_link -->/} + local schema_json + schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"List of all authors on $SITE_TITLE\",\"url\": \"$SITE_URL/authors/\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" + header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"} + local current_year + current_year=$(date +%Y) + footer_content=${footer_content//\{\{current_year\}\}/"$current_year"} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"} + + main_content+="<h1>${MSG_ALL_AUTHORS:-All Authors}</h1>"$'\n' + main_content+="<div class=\"tags-list\">"$'\n' + for author_slug_key in $(printf '%s\n' "${!author_name_by_slug[@]}" | sort); do + author="${author_name_by_slug[$author_slug_key]}" + local post_count + post_count=$(printf '%s\n' "${author_posts_by_slug[$author_slug_key]}" | awk 'NF { c++ } END { print c+0 }') + if [ "$post_count" -gt 0 ]; then + main_content+=" <a href=\"$BASE_URL/authors/$author_slug_key/\">$author <span class=\"tag-count\">($post_count)</span></a>"$'\n' + fi + done + main_content+="</div>"$'\n' + + { + echo "$header_content" + echo "$main_content" + echo "$footer_content" + } > "$main_authors_index_output" + + echo -e "${GREEN}Author pages processed!${NC}" + echo -e "${GREEN}Generated author list pages.${NC}" +} + # Generate author pages generate_author_pages() { + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + _generate_author_pages_ram + return $? + fi + echo -e "${YELLOW}Processing author pages${NC}${ENABLE_AUTHOR_RSS:+" and RSS feeds"}...${NC}" local authors_index_file="$CACHE_DIR/authors_index.txt" @@ -473,4 +669,4 @@ generate_author_pages() { echo -e "${GREEN}Author pages processed!${NC}" echo -e "${GREEN}Generated author list pages.${NC}" -} \ No newline at end of file +} diff --git a/scripts/build/generate_feeds.sh b/scripts/build/generate_feeds.sh index 9d13d7c..19035c8 100755 --- a/scripts/build/generate_feeds.sh +++ b/scripts/build/generate_feeds.sh @@ -14,6 +14,180 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s source "$(dirname "$0")/content.sh" || { echo >&2 "Error: Failed to source content.sh from generate_feeds.sh"; exit 1; } # Note: Needs access to primary_pages and SECONDARY_PAGES which should be exported by templates.sh +declare -gA BSSG_RAM_RSS_FULL_CONTENT_CACHE=() +declare -g BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=false +declare -gA BSSG_RAM_RSS_PUBDATE_CACHE=() +declare -gA BSSG_RAM_RSS_UPDATED_ISO_CACHE=() +declare -gA BSSG_RAM_RSS_URL_CACHE=() +declare -gA BSSG_RAM_RSS_ITEM_XML_CACHE=() +declare -g BSSG_RAM_RSS_METADATA_CACHE_READY=false + +_normalize_relative_url_path() { + local path="$1" + while [[ "$path" == */ ]]; do + path="${path%/}" + done + path="${path#/}" + if [ -z "$path" ]; then + printf '/' + else + printf '/%s/' "$path" + fi +} + +_ram_strip_frontmatter_for_rss() { + awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { in_fm = 0; next; } + } + { if (!in_fm) print; } + ' +} + +_ram_cache_full_content_for_file() { + local file="$1" + local resolved="$file" + + if declare -F ram_mode_resolve_key > /dev/null; then + resolved=$(ram_mode_resolve_key "$file") + fi + + if [[ -z "$resolved" ]]; then + return 1 + fi + + if [[ -n "${BSSG_RAM_RSS_FULL_CONTENT_CACHE[$resolved]+_}" ]]; then + return 0 + fi + + if ! declare -F ram_mode_has_file > /dev/null || ! ram_mode_has_file "$resolved"; then + return 1 + fi + + local raw_content + raw_content=$(ram_mode_get_content "$resolved") + + local stripped_content + stripped_content=$(printf '%s\n' "$raw_content" | _ram_strip_frontmatter_for_rss) + + local converted_html + converted_html=$(convert_markdown_to_html "$stripped_content" "$resolved") + local convert_status=$? + if [ $convert_status -ne 0 ] || [ -z "$converted_html" ]; then + return 1 + fi + + BSSG_RAM_RSS_FULL_CONTENT_CACHE["$resolved"]="$converted_html" + return 0 +} + +prepare_ram_rss_full_content_cache() { + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RSS_INCLUDE_FULL_CONTENT:-false}" != true ]; then + return 0 + fi + + if [ "$BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY" = true ]; then + return 0 + fi + + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=true + return 0 + fi + + local file filename title date lastmod tags slug image image_caption description author_name author_email + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + [ -z "$file" ] && continue + _ram_cache_full_content_for_file "$file" > /dev/null || true + done <<< "$file_index_data" + + BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=true +} + +_ram_prime_rss_metadata_entry() { + local date="$1" + local lastmod="$2" + local slug="$3" + local rss_date_fmt="$4" + local build_timestamp_iso="$5" + local source_file="$6" + + if [ -n "$date" ] && [[ -z "${BSSG_RAM_RSS_PUBDATE_CACHE[$date]+_}" ]]; then + BSSG_RAM_RSS_PUBDATE_CACHE["$date"]=$(format_date "$date" "$rss_date_fmt") + fi + + if [ -n "$lastmod" ] && [[ -z "${BSSG_RAM_RSS_UPDATED_ISO_CACHE[$lastmod]+_}" ]]; then + local updated_date_iso + updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z") + if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}" + fi + [ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso" + BSSG_RAM_RSS_UPDATED_ISO_CACHE["$lastmod"]="$updated_date_iso" + fi + + if [ -n "$date" ] && [ -n "$slug" ]; then + local url_key="${date}|${slug}" + if [[ -z "${BSSG_RAM_RSS_URL_CACHE[$url_key]+_}" ]]; then + local year month day formatted_path item_url + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo "Warning: Invalid date format '$date' for file $source_file, cannot precompute RSS URL." >&2 + fi + return 1 + fi + formatted_path="${URL_SLUG_FORMAT//Year/$year}" + formatted_path="${formatted_path//Month/$month}" + formatted_path="${formatted_path//Day/$day}" + formatted_path="${formatted_path//slug/$slug}" + item_url=$(_normalize_relative_url_path "$formatted_path") + BSSG_RAM_RSS_URL_CACHE["$url_key"]=$(fix_url "$item_url") + fi + fi + + return 0 +} + +prepare_ram_rss_metadata_cache() { + if [ "${BSSG_RAM_MODE:-false}" != true ]; then + return 0 + fi + + if [ "$BSSG_RAM_RSS_METADATA_CACHE_READY" = true ]; then + return 0 + fi + + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + BSSG_RAM_RSS_METADATA_CACHE_READY=true + return 0 + fi + + local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z" + local build_timestamp_iso + build_timestamp_iso=$(format_date "now" "%Y-%m-%dT%H:%M:%S%z") + if [[ "$build_timestamp_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + build_timestamp_iso="${build_timestamp_iso::${#build_timestamp_iso}-2}:${BASH_REMATCH[2]}" + fi + + local file filename title date lastmod tags slug image image_caption description author_name author_email + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + [ -z "$file" ] && continue + _ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" >/dev/null || true + done <<< "$file_index_data" + + BSSG_RAM_RSS_METADATA_CACHE_READY=true +} + # Function to get the latest lastmod date from a file index, optionally filtered # Usage: get_latest_mod_date <index_file> [field_index] [filter_pattern] [date_format] # Example: get_latest_mod_date "$file_index" 5 "" "%Y-%m-%d" # Latest overall post @@ -53,6 +227,212 @@ get_latest_mod_date() { fi } +# Fast path for RAM datasets: pick max YYYY-MM-DD from a given field without external sort/head. +_ram_latest_date_from_dataset() { + local dataset="$1" + local field_index="$2" + local date_format="${3:-%Y-%m-%d}" + + local latest_date_str + latest_date_str=$(printf '%s\n' "$dataset" | awk -F'|' -v field_index="$field_index" ' + NF { + value = substr($field_index, 1, 10) + if (value != "" && value > max_date) { + max_date = value + } + } + END { + if (max_date != "") { + print max_date + } + } + ') + + if [ -n "$latest_date_str" ]; then + printf '%s\n' "$latest_date_str" + else + format_date "now" "$date_format" + fi +} + +_generate_sitemap_with_awk_inputs() { + local sitemap="$1" + local file_index_input="$2" + local primary_pages_input="$3" + local secondary_pages_input="$4" + local tags_index_input="$5" + local authors_index_input="$6" + local latest_post_mod_date="$7" + local latest_tag_page_mod_date="$8" + local latest_author_page_mod_date="$9" + local sitemap_date_fmt="${10:-%Y-%m-%d}" + + # Determine the best awk command locally to avoid potential scoping issues with AWK_CMD. + local effective_awk_cmd="awk" + if command -v gawk > /dev/null 2>&1; then + effective_awk_cmd="gawk" + fi + + "$effective_awk_cmd" -v site_url="$SITE_URL" \ + -v url_slug_format="$URL_SLUG_FORMAT" \ + -v latest_post_mod_date="$latest_post_mod_date" \ + -v latest_tag_page_mod_date="$latest_tag_page_mod_date" \ + -v latest_author_page_mod_date="$latest_author_page_mod_date" \ + -v enable_author_pages="${ENABLE_AUTHOR_PAGES:-true}" \ + -v sitemap_date_fmt="$sitemap_date_fmt" \ + -F'|' \ + -f - \ + "$file_index_input" "$primary_pages_input" "$secondary_pages_input" "$tags_index_input" "$authors_index_input" <<'AWK_EOF' > "$sitemap" +# AWK script for sitemap generation. +BEGIN { + OFS = "" + print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + print "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">" + + # Homepage + print " <url>" + print " <loc>" fix_url_awk("/", site_url) "</loc>" + print " <lastmod>" latest_post_mod_date "</lastmod>" + print " <changefreq>daily</changefreq>" + print " <priority>1.0</priority>" + print " </url>" +} + +function fix_url_awk(path, base_url) { + if (substr(path, 1, 1) == "/") { + sub(/\/$/, "", base_url) + sub(/^\/+/, "/", path) + sub(/\/index\.html$/, "/", path) + if (substr(path, length(path), 1) != "/") { + path = path "/" + } + if (base_url == "" || base_url ~ /^http:\/\/localhost(:[0-9]+)?$/) { + return path + } else { + return base_url path + } + } else { + return path + } +} + +# Process file_index (posts). +FILENAME == ARGV[1] { + file = $1 + date = $4 + lastmod = $5 + slug = $7 + if (length(file) == 0 || length(date) == 0 || length(lastmod) == 0 || length(slug) == 0) next + + year = substr(date, 1, 4) + month = substr(date, 6, 2) + day = substr(date, 9, 2) + if (year ~ /^[0-9]{4}$/ && month ~ /^[0-9]{2}$/ && day ~ /^[0-9]{2}$/) { + formatted_path = url_slug_format + gsub(/Year/, year, formatted_path) + gsub(/Month/, month, formatted_path) + gsub(/Day/, day, formatted_path) + gsub(/slug/, slug, formatted_path) + item_url = "/" formatted_path + sub(/\/+$/, "/", item_url) + + mod_time = substr(lastmod, 1, 10) + if (mod_time == "") next + + print " <url>" + print " <loc>" fix_url_awk(item_url, site_url) "</loc>" + print " <lastmod>" mod_time "</lastmod>" + print " <changefreq>weekly</changefreq>" + print " <priority>0.8</priority>" + print " </url>" + } +} + +# Process primary pages. +FILENAME == ARGV[2] { + url = $2 + date = $3 + if (length(url) == 0 || length(date) == 0) next + sitemap_url = url + sub(/index\.html$/, "", sitemap_url) + sub(/\/+$/, "/", sitemap_url) + mod_time = substr(date, 1, 10) + if (mod_time == "") next + print " <url>" + print " <loc>" fix_url_awk(sitemap_url, site_url) "</loc>" + print " <lastmod>" mod_time "</lastmod>" + print " <changefreq>monthly</changefreq>" + print " <priority>0.7</priority>" + print " </url>" +} + +# Process secondary pages. +FILENAME == ARGV[3] { + url = $2 + date = $3 + if (length(url) == 0 || length(date) == 0) next + sitemap_url = url + sub(/index\.html$/, "", sitemap_url) + sub(/\/+$/, "/", sitemap_url) + mod_time = substr(date, 1, 10) + if (mod_time == "") next + print " <url>" + print " <loc>" fix_url_awk(sitemap_url, site_url) "</loc>" + print " <lastmod>" mod_time "</lastmod>" + print " <changefreq>monthly</changefreq>" + print " <priority>0.6</priority>" + print " </url>" +} + +# Process tags index. +FILENAME == ARGV[4] { + tag_slug = $2 + if (length(tag_slug) == 0) next + if (!(tag_slug in processed_tags)) { + processed_tags[tag_slug] = 1 + item_url = "/tags/" tag_slug "/" + print " <url>" + print " <loc>" fix_url_awk(item_url, site_url) "</loc>" + print " <lastmod>" latest_tag_page_mod_date "</lastmod>" + print " <changefreq>weekly</changefreq>" + print " <priority>0.5</priority>" + print " </url>" + } +} + +# Process authors index. +FILENAME == ARGV[5] && enable_author_pages == "true" { + author_slug = $2 + if (length(author_slug) == 0) next + if (!(author_slug in processed_authors)) { + processed_authors[author_slug] = 1 + + if (!authors_index_added) { + authors_index_added = 1 + print " <url>" + print " <loc>" fix_url_awk("/authors/", site_url) "</loc>" + print " <lastmod>" latest_author_page_mod_date "</lastmod>" + print " <changefreq>weekly</changefreq>" + print " <priority>0.6</priority>" + print " </url>" + } + + item_url = "/authors/" author_slug "/" + print " <url>" + print " <loc>" fix_url_awk(item_url, site_url) "</loc>" + print " <lastmod>" latest_author_page_mod_date "</lastmod>" + print " <changefreq>weekly</changefreq>" + print " <priority>0.5</priority>" + print " </url>" + } +} + +END { + print "</urlset>" +} +AWK_EOF +} + # Core RSS generation function # Usage: _generate_rss_feed <output_file> <feed_title> <feed_description> <feed_link_rel> <feed_atom_link_rel> <post_data_input> # <post_data_input> should be a string containing the filtered, sorted, and limited post data, @@ -80,67 +460,95 @@ _generate_rss_feed() { # Ensure output directory exists mkdir -p "$(dirname "$output_file")" - # Create the RSS feed header - cat > "$output_file" << EOF -<?xml version="1.0" encoding="UTF-8" ?> -<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"> -<channel> - <title>$(html_escape "$feed_title") - $(fix_url "$feed_link_rel") - $(html_escape "$feed_description") - ${SITE_LANG:-en} - $(format_date "now" "$rss_date_fmt") - -EOF + local escaped_feed_title escaped_feed_description feed_link feed_atom_link channel_last_build_date + escaped_feed_title=$(html_escape "$feed_title") + escaped_feed_description=$(html_escape "$feed_description") + feed_link=$(fix_url "$feed_link_rel") + feed_atom_link=$(fix_url "$feed_atom_link_rel") + channel_last_build_date=$(format_date "now" "$rss_date_fmt") + + exec 4> "$output_file" || return 1 + printf '%s\n' \ + '' \ + '' \ + '' \ + " ${escaped_feed_title}" \ + " ${feed_link}" \ + " ${escaped_feed_description}" \ + " ${SITE_LANG:-en}" \ + " ${channel_last_build_date}" \ + " " >&4 # Process the provided post data - echo "$post_data_input" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + # Ignore blank trailing lines from callers. + if [ -z "$file" ] && [ -z "$filename" ] && [ -z "$title" ] && [ -z "$date" ] && [ -z "$lastmod" ] && [ -z "$tags" ] && [ -z "$slug" ] && [ -z "$image" ] && [ -z "$image_caption" ] && [ -z "$description" ] && [ -z "$author_name" ] && [ -z "$author_email" ]; then + continue + fi # Skip if essential fields are missing (robustness) if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ] || [ -z "$lastmod" ] || [ -z "$slug" ]; then echo "Warning: Skipping RSS item due to missing fields in input line: file=$file, title=$title, date=$date, lastmod=$lastmod, slug=$slug" >&2 continue fi - # Format dates for RSS - local pub_date=$(format_date "$date" "$rss_date_fmt") - local updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z") - # Convert timezone format again if needed - if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then - updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}" + local rss_item_cache_key="" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + rss_item_cache_key="${RSS_INCLUDE_FULL_CONTENT:-false}|${file}|${date}|${lastmod}|${slug}|${title}" + if [[ -n "${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]+_}" ]]; then + printf '%s' "${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]}" >&4 + continue + fi fi - # Fallback for updated_date_iso - [ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso" - # Construct post URL based on URL_SLUG_FORMAT - local year month day formatted_path item_url - if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then - year="${BASH_REMATCH[1]}" - month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") - day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + # Format dates and URL (RAM mode caches repeated values across many tag feeds). + local pub_date updated_date_iso full_url + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + _ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" || { + echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2 + continue + } + pub_date="${BSSG_RAM_RSS_PUBDATE_CACHE[$date]}" + updated_date_iso="${BSSG_RAM_RSS_UPDATED_ISO_CACHE[$lastmod]}" + full_url="${BSSG_RAM_RSS_URL_CACHE[${date}|${slug}]}" else - echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2 - continue # Skip item if URL cannot be generated - fi - formatted_path="${URL_SLUG_FORMAT//Year/$year}" - formatted_path="${formatted_path//Month/$month}" - formatted_path="${formatted_path//Day/$day}" - formatted_path="${formatted_path//slug/$slug}" - item_url="/$(echo "$formatted_path" | sed 's|/*$|/|')" # Ensure trailing slash + pub_date=$(format_date "$date" "$rss_date_fmt") + updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z") + if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}" + fi + [ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso" - local full_url=$(fix_url "$item_url") # Use fix_url to prepend SITE_URL + local year month day formatted_path item_url + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2 + continue + fi + formatted_path="${URL_SLUG_FORMAT//Year/$year}" + formatted_path="${formatted_path//Month/$month}" + formatted_path="${formatted_path//Day/$day}" + formatted_path="${formatted_path//slug/$slug}" + item_url=$(_normalize_relative_url_path "$formatted_path") + full_url=$(fix_url "$item_url") + fi # --- RSS Item Description Enhancement --- local item_description_content="" local figure_part="" local caption_part="" local content_part="" + local escaped_title + escaped_title=$(html_escape "$title") # Build figure part if [ -n "$image" ]; then local img_src [[ "$image" =~ ^https?:// ]] && img_src="$image" || img_src=$(fix_url "$image") # Escape alt/title attributes safely using html_escape from utils.sh - local img_alt=$(html_escape "$title") + local img_alt="$escaped_title" local img_title=$(html_escape "$image_caption") [ -z "$img_title" ] && img_title="$img_alt" # Use alt if title is empty @@ -155,8 +563,24 @@ EOF # Build content part (excerpt or full) if [ "${RSS_INCLUDE_FULL_CONTENT:-false}" = true ]; then - local raw_content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")" - if [ -f "$raw_content_cache_file" ]; then + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local resolved_file="$file" + if declare -F ram_mode_resolve_key > /dev/null; then + resolved_file=$(ram_mode_resolve_key "$file") + fi + + if _ram_cache_full_content_for_file "$resolved_file"; then + content_part="${BSSG_RAM_RSS_FULL_CONTENT_CACHE[$resolved_file]}" + else + # RAM mode is memory-only: never fall back to disk cache reads. + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo "Warning: RAM content not available for RSS item ($file). Falling back to excerpt." >&2 + fi + content_part="$description" + fi + else + local raw_content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")" + if [ -f "$raw_content_cache_file" ]; then local raw_content=$(cat "$raw_content_cache_file") local converted_html=$(convert_markdown_to_html "$raw_content" "$file") local convert_status=$? @@ -166,9 +590,10 @@ EOF echo "Warning: Failed to convert markdown to HTML for RSS item ($file, status: $convert_status). Falling back to excerpt." >&2 content_part="$description" fi - else - echo "Warning: Cached raw markdown content file '$raw_content_cache_file' not found for RSS item ($file). Falling back to excerpt." >&2 - content_part="$description" + else + echo "Warning: Cached raw markdown content file '$raw_content_cache_file' not found for RSS item ($file). Falling back to excerpt." >&2 + content_part="$description" + fi fi else content_part="$description" @@ -194,26 +619,35 @@ EOF fi fi - cat >> "$output_file" << EOF - - $(html_escape "$title") + local rss_item_xml + rss_item_xml=" + ${escaped_title} ${full_url} - ${full_url} + ${full_url} ${pub_date} ${updated_date_iso} ${final_description} -${author_element} - -EOF - done +" + if [ -n "$author_element" ]; then + rss_item_xml+="${author_element}"$'\n' + fi + rss_item_xml+=" +" + + printf '%s' "$rss_item_xml" >&4 + + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + BSSG_RAM_RSS_ITEM_XML_CACHE["$rss_item_cache_key"]="$rss_item_xml" + fi + done <<< "$post_data_input" # Close the RSS feed - cat >> "$output_file" << EOF - - -EOF + printf '%s\n' '' '' >&4 + exec 4>&- - echo -e "${GREEN}RSS feed generated at $output_file${NC}" + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${GREEN}RSS feed generated at $output_file${NC}" + fi } export -f _generate_rss_feed # Export for potential parallel use or sourcing @@ -221,6 +655,28 @@ export -f _generate_rss_feed # Export for potential parallel use or sourcing generate_rss() { echo -e "${YELLOW}Generating main RSS feed...${NC}" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + echo -e "${YELLOW}No file index data in RAM. Skipping RSS generation.${NC}" + return 0 + fi + + prepare_ram_rss_metadata_cache >/dev/null || true + + local rss="$OUTPUT_DIR/${RSS_FILENAME:-rss.xml}" + local feed_title="${MSG_RSS_FEED_TITLE:-${SITE_TITLE} - RSS Feed}" + local feed_desc="${MSG_RSS_FEED_DESCRIPTION:-${SITE_DESCRIPTION}}" + local feed_link_rel="/" + local feed_atom_link_rel="/${RSS_FILENAME:-rss.xml}" + local rss_item_limit=${RSS_ITEM_LIMIT:-15} + local sorted_posts + sorted_posts=$(printf '%s\n' "$file_index_data" | awk 'NF' | sort -t'|' -k4,4r -k5,5r | head -n "$rss_item_limit") + _generate_rss_feed "$rss" "$feed_title" "$feed_desc" "$feed_link_rel" "$feed_atom_link_rel" "$sorted_posts" + return 0 + fi + # Ensure needed functions/vars are available if ! command -v convert_markdown_to_html &> /dev/null; then echo -e "${RED}Error: convert_markdown_to_html function not found.${NC}" >&2; return 1; fi @@ -296,6 +752,39 @@ export -f generate_rss generate_sitemap() { echo -e "${YELLOW}Generating sitemap.xml...${NC}" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local sitemap="$OUTPUT_DIR/sitemap.xml" + local file_index_data tags_index_data authors_index_data primary_pages_data secondary_pages_data + file_index_data=$(ram_mode_get_dataset "file_index") + tags_index_data=$(ram_mode_get_dataset "tags_index") + authors_index_data=$(ram_mode_get_dataset "authors_index") + primary_pages_data=$(ram_mode_get_dataset "primary_pages") + secondary_pages_data=$(ram_mode_get_dataset "secondary_pages") + + local latest_post_mod_date latest_tag_page_mod_date latest_author_page_mod_date + latest_post_mod_date=$(_ram_latest_date_from_dataset "$file_index_data" 5 "%Y-%m-%d") + latest_tag_page_mod_date=$(_ram_latest_date_from_dataset "$tags_index_data" 5 "%Y-%m-%d") + latest_author_page_mod_date=$(_ram_latest_date_from_dataset "$authors_index_data" 6 "%Y-%m-%d") + + [ -z "$latest_tag_page_mod_date" ] && latest_tag_page_mod_date="$latest_post_mod_date" + [ -z "$latest_author_page_mod_date" ] && latest_author_page_mod_date="$latest_post_mod_date" + + _generate_sitemap_with_awk_inputs \ + "$sitemap" \ + <(printf '%s\n' "$file_index_data") \ + <(printf '%s\n' "$primary_pages_data") \ + <(printf '%s\n' "$secondary_pages_data") \ + <(printf '%s\n' "$tags_index_data") \ + <(printf '%s\n' "$authors_index_data") \ + "$latest_post_mod_date" \ + "$latest_tag_page_mod_date" \ + "$latest_author_page_mod_date" \ + "%Y-%m-%d" + + echo -e "${GREEN}Sitemap generated!${NC}" + return 0 + fi + local sitemap="$OUTPUT_DIR/sitemap.xml" local file_index="$CACHE_DIR/file_index.txt" local tags_index="$CACHE_DIR/tags_index.txt" @@ -342,196 +831,23 @@ generate_sitemap() { local latest_tag_page_mod_date=$(get_latest_mod_date "$tags_index" 5 "" "$sitemap_date_fmt") # Assumes lastmod is relevant field in tags_index local latest_author_page_mod_date=$(get_latest_mod_date "$authors_index" 6 "" "$sitemap_date_fmt") # Field 6 is lastmod in authors_index - # --- Generate Sitemap using AWK --- START --- echo "Generating sitemap content using awk..." - - # Determine the best awk command locally to avoid potential scoping issues with AWK_CMD - local effective_awk_cmd="awk" # Default to standard awk - if command -v gawk > /dev/null 2>&1; then - effective_awk_cmd="gawk" # Prefer gawk if available - fi - - # Use awk with a here-doc for the script for cleaner quoting - # Use the locally determined effective_awk_cmd - "$effective_awk_cmd" -v site_url="$SITE_URL" \ - -v url_slug_format="$URL_SLUG_FORMAT" \ - -v latest_post_mod_date="$latest_post_mod_date" \ - -v latest_tag_page_mod_date="$latest_tag_page_mod_date" \ - -v latest_author_page_mod_date="$latest_author_page_mod_date" \ - -v enable_author_pages="${ENABLE_AUTHOR_PAGES:-true}" \ - -v sitemap_date_fmt="$sitemap_date_fmt" \ - -F'|' \ - -f - \ - "$file_index" "$primary_pages_cache" "$secondary_pages_cache" "$tags_index" "$authors_index" <<'AWK_EOF' > "$sitemap" -# AWK script for sitemap generation (fed via here-doc) -BEGIN { - OFS=""; # No output field separator needed for XML - print ""; - print ""; - - # Homepage - print " "; - print " " fix_url_awk("/", site_url) ""; - print " " latest_post_mod_date ""; - print " daily"; - print " 1.0"; - print " "; -} - -# Custom function to replicate fix_url shell function logic -function fix_url_awk(path, base_url) { - if (substr(path, 1, 1) == "/") { - # Remove trailing slash from base_url if present - sub(/\/$/, "", base_url); - # Ensure path doesnt start with // - sub(/^\/+/, "/", path); - # Remove index.html if present - sub(/\/index\.html$/, "/", path); - # Ensure trailing slash - if (substr(path, length(path), 1) != "/") { - path = path "/"; - } - # Handle case where base_url is empty or just http://localhost* - skip prepending - if (base_url == "" || base_url ~ /^http:\/\/localhost(:[0-9]+)?$/) { - return path - } else { - return base_url path; - } - } else { - return path; # Should not happen for sitemap paths? - } -} - -# Process file_index.txt (Posts) -FILENAME == ARGV[1] { - file=$1; filename=$2; title=$3; date=$4; lastmod=$5; tags=$6; slug=$7; - if (length(file) == 0 || length(date) == 0 || length(lastmod) == 0 || length(slug) == 0) next; - - year=substr(date, 1, 4); - month=substr(date, 6, 2); - day=substr(date, 9, 2); - # Ensure valid numbers? Basic check: - if (year ~ /^[0-9]{4}$/ && month ~ /^[0-9]{2}$/ && day ~ /^[0-9]{2}$/) { - formatted_path = url_slug_format; - gsub(/Year/, year, formatted_path); - gsub(/Month/, month, formatted_path); - gsub(/Day/, day, formatted_path); - gsub(/slug/, slug, formatted_path); - item_url = "/" formatted_path; - # Clean URL logic from shell script - sub(/\/+$/, "/", item_url); - - mod_time = substr(lastmod, 1, 10); # Extract YYYY-MM-DD from lastmod ($5) - if (mod_time == "") next; # Skip if date is invalid/empty - - print " "; - print " " fix_url_awk(item_url, site_url) ""; - print " " mod_time ""; - print " weekly"; - print " 0.8"; - print " "; - } -} - -# Process primary_pages.tmp -FILENAME == ARGV[2] { - url=$2; date=$3; # $1=_, $4=source_file - if (length(url) == 0 || length(date) == 0) next; - sitemap_url = url; - sub(/index\.html$/, "", sitemap_url); # Remove index.html - sub(/\/+$/, "/", sitemap_url); # Ensure trailing slash - mod_time = substr(date, 1, 10); # Extract YYYY-MM-DD from date ($3) - if (mod_time == "") next; # Skip if date is invalid/empty - print " "; - print " " fix_url_awk(sitemap_url, site_url) ""; - print " " mod_time ""; - print " monthly"; - print " 0.7"; - print " "; -} - -# Process secondary_pages.tmp -FILENAME == ARGV[3] { - url=$2; date=$3; # $1=_, $4=source_file - if (length(url) == 0 || length(date) == 0) next; - sitemap_url = url; - sub(/index\.html$/, "", sitemap_url); - sub(/\/+$/, "/", sitemap_url); - mod_time = substr(date, 1, 10); # Extract YYYY-MM-DD from date ($3) - if (mod_time == "") next; # Skip if date is invalid/empty - print " "; - print " " fix_url_awk(sitemap_url, site_url) ""; - print " " mod_time ""; - print " monthly"; - print " 0.6"; # Lower priority for secondary? - print " "; -} - -# Process tags_index.txt (Tag Pages) -FILENAME == ARGV[4] { - tag=$1; tag_slug=$2; # $5 = lastmod for posts with this tag - if (length(tag_slug) == 0) next; - # Check if tag slug already processed - if ( !(tag_slug in processed_tags) ) { - processed_tags[tag_slug] = 1; # Mark as processed - item_url = "/tags/" tag_slug "/"; - # Use the overall latest tag mod date for all tag pages? - mod_time = latest_tag_page_mod_date; - print " "; - print " " fix_url_awk(item_url, site_url) ""; - print " " mod_time ""; - print " weekly"; - print " 0.5"; - print " "; - } -} - -# Process authors_index.txt (Author Pages) - only if author pages are enabled -FILENAME == ARGV[5] && enable_author_pages == "true" { - author_name=$1; author_slug=$2; # $6 = lastmod for posts with this author - if (length(author_slug) == 0) next; - # Check if author slug already processed - if ( !(author_slug in processed_authors) ) { - processed_authors[author_slug] = 1; # Mark as processed - - # Add main authors index page (only once) - if (!authors_index_added) { - authors_index_added = 1; - print " "; - print " " fix_url_awk("/authors/", site_url) ""; - print " " latest_author_page_mod_date ""; - print " weekly"; - print " 0.6"; - print " "; - } - - # Add individual author page - item_url = "/authors/" author_slug "/"; - mod_time = latest_author_page_mod_date; - print " "; - print " " fix_url_awk(item_url, site_url) ""; - print " " mod_time ""; - print " weekly"; - print " 0.5"; - print " "; - } -} - -END { - print ""; -} -AWK_EOF - # awk exit status check - optional - # local awk_status=$? - # if [ $awk_status -ne 0 ]; then - # echo -e "${RED}Error: awk script for sitemap generation failed with status $awk_status${NC}" >&2 - # # Decide whether to return 1 or continue - # fi - - # --- Generate Sitemap using AWK --- END --- + _generate_sitemap_with_awk_inputs \ + "$sitemap" \ + "$file_index" \ + "$primary_pages_cache" \ + "$secondary_pages_cache" \ + "$tags_index" \ + "$authors_index" \ + "$latest_post_mod_date" \ + "$latest_tag_page_mod_date" \ + "$latest_author_page_mod_date" \ + "$sitemap_date_fmt" echo -e "${GREEN}Sitemap generated!${NC}" } # Export public functions -export -f generate_sitemap generate_rss \ No newline at end of file +export -f _normalize_relative_url_path +export -f _ram_strip_frontmatter_for_rss _ram_cache_full_content_for_file prepare_ram_rss_full_content_cache +export -f generate_sitemap generate_rss diff --git a/scripts/build/generate_index.sh b/scripts/build/generate_index.sh index ea19c7d..ee65ee0 100755 --- a/scripts/build/generate_index.sh +++ b/scripts/build/generate_index.sh @@ -10,8 +10,297 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s # shellcheck source=cache.sh disable=SC1091 source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_index.sh"; exit 1; } +_generate_index_ram() { + echo -e "${YELLOW}Generating index pages...${NC}" + + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + echo -e "${YELLOW}No posts found in RAM file index. Skipping index generation.${NC}" + return 0 + fi + + local total_posts_orig + total_posts_orig=$(printf '%s\n' "$file_index_data" | awk 'NF { c++ } END { print c+0 }') + local total_pages=$(( (total_posts_orig + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE )) + [ "$total_pages" -eq 0 ] && total_pages=1 + + mapfile -t file_index_lines < <(printf '%s\n' "$file_index_data" | awk 'NF') + echo -e "Generating ${GREEN}$total_pages${NC} index pages for ${GREEN}$total_posts_orig${NC} posts" + + local current_page + for (( current_page = 1; current_page <= total_pages; current_page++ )); do + local output_file + if [ "$current_page" -eq 1 ]; then + output_file="$OUTPUT_DIR/index.html" + else + output_file="$OUTPUT_DIR/page/$current_page/index.html" + mkdir -p "$(dirname "$output_file")" + fi + + local page_header="$HEADER_TEMPLATE" + if [ "$current_page" -eq 1 ]; then + page_header=${page_header//\{\{site_title\}\}/"$SITE_TITLE"} + page_header=${page_header//\{\{page_title\}\}/"${MSG_HOME:-"Home"}"} + page_header=${page_header//\{\{og_type\}\}/"website"} + page_header=${page_header//\{\{page_url\}\}/""} + page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"} + local home_schema + home_schema=$(cat < +{ + "@context": "https://schema.org", + "@type": "WebSite", + "name": "$SITE_TITLE", + "description": "$SITE_DESCRIPTION", + "url": "$SITE_URL/", + "potentialAction": { + "@type": "SearchAction", + "target": "$SITE_URL/search?q={search_term_string}", + "query-input": "required name=search_term_string" + }, + "publisher": { + "@type": "Organization", + "name": "$SITE_TITLE", + "url": "$SITE_URL" + } +} + +EOF +) + page_header=${page_header//\{\{schema_json_ld\}\}/"$home_schema"} + else + local pag_title + pag_title=$(printf "${MSG_PAGINATION_TITLE:-"%s - Page %d"}" "$SITE_TITLE" "$current_page") + page_header=${page_header//\{\{site_title\}\}/"$SITE_TITLE"} + page_header=${page_header//\{\{page_title\}\}/"$pag_title"} + page_header=${page_header//\{\{og_type\}\}/"website"} + local paginated_rel_url="/page/$current_page/" + page_header=${page_header//\{\{page_url\}\}/"$paginated_rel_url"} + page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"} + local collection_schema + collection_schema=$(cat < +{ + "@context": "https://schema.org", + "@type": "CollectionPage", + "name": "$pag_title", + "description": "$SITE_DESCRIPTION", + "url": "$SITE_URL${paginated_rel_url}", + "isPartOf": { + "@type": "WebSite", + "name": "$SITE_TITLE", + "url": "$SITE_URL" + } +} + +EOF +) + page_header=${page_header//\{\{schema_json_ld\}\}/"$collection_schema"} + fi + page_header=${page_header//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + page_header=${page_header//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + page_header=${page_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + page_header=${page_header//\{\{og_image\}\}/""} + page_header=${page_header//\{\{twitter_image\}\}/""} + + local page_footer="$FOOTER_TEMPLATE" + page_footer=${page_footer//\{\{current_year\}\}/$(date +%Y)} + page_footer=${page_footer//\{\{author_name\}\}/"$AUTHOR_NAME"} + + cat > "$output_file" < /dev/null && ram_mode_has_file "$index_file"; then + has_custom_index=true + elif [ -f "$index_file" ]; then + has_custom_index=true + fi + + if [ "$current_page" -eq 1 ] && [ "$has_custom_index" = true ]; then + local content="" html_content="" in_frontmatter=false found_frontmatter=false source_stream="" + if [ "${BSSG_RAM_MODE:-false}" = true ] && ram_mode_has_file "$index_file"; then + source_stream=$(ram_mode_get_content "$index_file") + else + source_stream=$(cat "$index_file") + fi + while IFS= read -r line; do + if [[ "$line" == "---" ]]; then + if ! $in_frontmatter && ! $found_frontmatter; then + in_frontmatter=true + found_frontmatter=true + continue + elif $in_frontmatter; then + in_frontmatter=false + continue + fi + fi + if ! $in_frontmatter && $found_frontmatter; then + content+="$line"$'\n' + fi + done <<< "$source_stream" + if ! $found_frontmatter; then + content="$source_stream" + fi + html_content=$(convert_markdown_to_html "$content") + echo "$html_content" >> "$output_file" + cat >> "$output_file" <> "$output_file" <${MSG_LATEST_POSTS:-"Latest Posts"} +
    +EOF + local start_index=$(( (current_page - 1) * POSTS_PER_PAGE )) + local end_index=$(( start_index + POSTS_PER_PAGE - 1 )) + local i + for (( i = start_index; i <= end_index && i < total_posts_orig; i++ )); do + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "${file_index_lines[$i]}" + [ -z "$file" ] && continue + [ -z "$title" ] && continue + [ -z "$date" ] && continue + + local post_year post_month post_day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d) + fi + local formatted_path="${URL_SLUG_FORMAT//Year/$post_year}" + formatted_path="${formatted_path//Month/$post_month}" + formatted_path="${formatted_path//Day/$post_day}" + formatted_path="${formatted_path//slug/$slug}" + local post_link="/$formatted_path/" + + local display_date_format="$DATE_FORMAT" + if [ "${SHOW_TIMEZONE:-false}" = false ]; then + display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//') + fi + local formatted_date + formatted_date=$(format_date "$date" "$display_date_format") + + cat >> "$output_file" < +

    $title

    +
    ${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${author_name:+" ${MSG_BY:-"by"} ${author_name:-$AUTHOR_NAME}"}
    +EOF + + if [ -n "$image" ]; then + local image_url="$image" + if [[ "$image" == /* ]]; then + image_url="${SITE_URL}${image}" + fi + cat >> "$output_file" < + + ${image_caption:-$title} + +
    +EOF + fi + + if [ "${INDEX_SHOW_FULL_CONTENT:-false}" = "true" ]; then + local post_content="" html_content="" + if [ "${BSSG_RAM_MODE:-false}" = true ] && ram_mode_has_file "$file"; then + local source_stream + source_stream=$(ram_mode_get_content "$file") + post_content=$(printf '%s\n' "$source_stream" | awk ' + BEGIN { in_fm = 0; found_fm = 0; } + /^---$/ { + if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; } + if (in_fm) { in_fm = 0; next; } + } + { if (!in_fm) print; } + ') + fi + if [ -n "$post_content" ]; then + if [[ "$file" == *.md ]]; then + html_content=$(convert_markdown_to_html "$post_content") + else + html_content="$post_content" + fi + fi + if [ -n "$html_content" ]; then + cat >> "$output_file" < + $html_content +
    +EOF + fi + elif [ -n "$description" ]; then + cat >> "$output_file" < + $description + +EOF + fi + + cat >> "$output_file" < +EOF + done + + cat >> "$output_file" < +EOF + + if [ "$total_pages" -gt 1 ]; then + cat >> "$output_file" < +" @@ -178,62 +248,16 @@ convert_markdown() { if [ -n "$date" ]; then local iso_date iso_lastmod_date - # Function to format date to ISO 8601 with corrected timezone - format_iso8601() { - local input_dt="$1" - local iso_dt="" - if [ -z "$input_dt" ]; then echo ""; return; fi - - # Handle "now" separately - if [ "$input_dt" = "now" ]; then - iso_dt=$(LC_ALL=C date +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - else - # Try parsing different formats based on OS - # Add LC_ALL=C for consistent parsing - if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == *"bsd"* ]]; then - # macOS/BSD: Try formats one by one with date -j -f - # Format 1: YYYY-MM-DD HH:MM:SS ZZZZ (e.g., +0200) - iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - # Format 2: YYYY-MM-DD HH:MM:SS - [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - # Format 3: YYYY-MM-DD (assume T00:00:00) - [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d" "$input_dt" +"%Y-%m-%dT00:00:00%z" 2>/dev/null) - # Format 4: RFC 2822 subset (e.g., 07 Sep 2023 08:10:00 +0200) - [ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%d %b %Y %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - else # Linux - # GNU date -d is more flexible and handles many formats automatically - iso_dt=$(LC_ALL=C date -d "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null) - fi - fi - - # If parsing succeeded, fix timezone format - if [ -n "$iso_dt" ]; then - # Fix timezone format from +0000 to +00:00 or Z - if [[ "$iso_dt" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then - local tz_offset="${BASH_REMATCH[0]}" - local tz_hh="${BASH_REMATCH[1]}" - local tz_mm="${BASH_REMATCH[2]}" - if [ "$tz_hh" == "+00" ] && [ "$tz_mm" == "00" ]; then - iso_dt="${iso_dt%$tz_offset}Z" - else - iso_dt="${iso_dt%$tz_offset}${tz_hh}:${tz_mm}" - fi - fi - echo "$iso_dt" - else - echo "" # Return empty if formatting failed - fi - } - - iso_date=$(format_iso8601 "$date") + iso_date=$(format_iso8601_post_date "$date") # Use date as fallback for lastmod, then format - iso_lastmod_date=$(format_iso8601 "${lastmod:-$date}") + iso_lastmod_date=$(format_iso8601_post_date "${lastmod:-$date}") # If lastmod still empty, use iso_date as fallback [ -z "$iso_lastmod_date" ] && iso_lastmod_date="$iso_date" # Fallback to build time if both are empty (should be rare) if [ -z "$iso_date" ]; then - local now_iso=$(format_iso8601 "now") + local now_iso + now_iso=$(format_iso8601_post_date "now") iso_date="$now_iso" iso_lastmod_date="$now_iso" fi @@ -319,10 +343,24 @@ convert_markdown() { # Generate related posts if enabled and tags exist local related_posts_html="" if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && [ -n "$tags" ]; then - echo -e "${BLUE}DEBUG: Generating related posts for $slug with tags: $tags${NC}" - related_posts_html=$(generate_related_posts "$slug" "$tags" "$date" "${RELATED_POSTS_COUNT:-3}") + # RAM fast path: direct map lookup avoids per-post command-substitution/function overhead. + if [ "${BSSG_RAM_MODE:-false}" = true ] && \ + [ "${BSSG_RAM_RELATED_POSTS_READY:-false}" = true ] && \ + [ "${BSSG_RAM_RELATED_POSTS_LIMIT:-}" = "${RELATED_POSTS_COUNT:-3}" ]; then + related_posts_html="${BSSG_RAM_RELATED_POSTS_HTML[$slug]-}" + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${BLUE}DEBUG: Generating related posts for $slug with tags: $tags${NC}" + fi + else + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${BLUE}DEBUG: Generating related posts for $slug with tags: $tags${NC}" + fi + related_posts_html=$(generate_related_posts "$slug" "$tags" "$date" "${RELATED_POSTS_COUNT:-3}") + fi else - echo -e "${BLUE}DEBUG: Skipping related posts for $slug - ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}, tags=$tags${NC}" + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${BLUE}DEBUG: Skipping related posts for $slug - ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}, tags=$tags${NC}" + fi fi # Construct article body @@ -368,13 +406,24 @@ process_all_markdown_files() { local modified_tags_list="${CACHE_DIR:-.bssg_cache}/modified_tags.list" # Define path for modified tags local modified_authors_list="${CACHE_DIR:-.bssg_cache}/modified_authors.list" # Define path for modified authors local file_index_prev="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" # Path to previous index + local ram_mode_active=false + local file_index_data="" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_active=true + file_index_data=$(ram_mode_get_dataset "file_index") + fi - if [ ! -f "$file_index" ]; then + if ! $ram_mode_active && [ ! -f "$file_index" ]; then echo -e "${RED}Error: File index not found at '$file_index'. Run indexing first.${NC}" >&2 return 1 fi - local total_file_count=$(wc -l < "$file_index") + local total_file_count=0 + if $ram_mode_active; then + total_file_count=$(printf '%s\n' "$file_index_data" | awk 'NF { c++ } END { print c+0 }') + else + total_file_count=$(wc -l < "$file_index") + fi if [ "$total_file_count" -eq 0 ]; then echo -e "${YELLOW}No posts found in file index. Skipping post generation.${NC}" return 0 @@ -386,7 +435,7 @@ process_all_markdown_files() { local posts_needing_rebuild=0 # Only do expensive Pass 1 if related posts are enabled AND posts might need rebuilding - if [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then + if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && ! $ram_mode_active; then echo -e "${BLUE}DEBUG: Related posts enabled, starting quick scan...${NC}" # Quick scan to see if ANY posts need rebuilding before doing expensive Pass 1 echo -e "${YELLOW}Quick scan: Checking if any posts need rebuilding...${NC}" @@ -435,13 +484,19 @@ process_all_markdown_files() { # Early exit optimization: if we find posts needing rebuild, we need Pass 1 break fi - done < "$file_index" + done < <( + if $ram_mode_active; then + printf '%s\n' "$file_index_data" | awk 'NF' + else + cat "$file_index" + fi + ) echo -e "Quick scan result: ${GREEN}$posts_needing_rebuild${NC} posts need rebuilding" fi # --- PASS 1: Only run if needed (posts need rebuilding AND related posts enabled) --- - if [ "$needs_pass1" = true ] && [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then + if [ "$needs_pass1" = true ] && [ "${ENABLE_RELATED_POSTS:-true}" = true ] && ! $ram_mode_active; then echo -e "${BLUE}DEBUG: Both needs_pass1=true and ENABLE_RELATED_POSTS=true, running Pass 1...${NC}" echo -e "${YELLOW}Pass 1: Identifying modified tags for related posts cache invalidation...${NC}" @@ -554,6 +609,8 @@ process_all_markdown_files() { # Export the list for use in pass 2 export RELATED_POSTS_INVALIDATED_LIST fi + elif $ram_mode_active; then + echo -e "${BLUE}DEBUG: RAM mode active, skipping Pass 1 related-posts invalidation (in-memory computation).${NC}" else echo -e "${BLUE}DEBUG: Pass 1 skipped - needs_pass1=$needs_pass1, ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}${NC}" fi @@ -566,63 +623,81 @@ process_all_markdown_files() { local files_to_process_count=0 local skipped_count=0 - while IFS= read -r line; do - local file filename title date lastmod tags slug image image_caption description author_name author_email - IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" - - # Basic check if it looks like a post - if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then - # echo -e "Skipping non-post file listed in index (pre-check): ${YELLOW}$file${NC}" >&2 # Too verbose - continue - fi - - # Calculate expected output path (logic copied from process_single_file) - local output_path - local year month day - if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then - year="${BASH_REMATCH[1]}" - month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") - day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") - else - year=$(date +%Y); month=$(date +%m); day=$(date +%d) - fi - local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" - url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; - url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" - local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" - - # Perform the rebuild check here - common_rebuild_check "$output_html_file" - local common_result=$? - local needs_rebuild=false - - if [ $common_result -eq 0 ]; then - needs_rebuild=true # Common checks failed (config changed, template newer, output missing) - else # common_result is 2 (output exists and newer than templates/locale) - local input_time=$(get_file_mtime "$file") - local output_time=$(get_file_mtime "$output_html_file") - if (( input_time > output_time )); then - needs_rebuild=true # Input file is newer + if $ram_mode_active && [ "${FORCE_REBUILD:-false}" = true ]; then + echo -e "RAM mode force rebuild: skipping per-post rebuild checks." + while IFS= read -r line; do + local file filename title date + IFS='|' read -r file filename _ date _ <<< "$line" + if [ -n "$date" ] && [[ "$file" == "$SRC_DIR"* ]]; then + files_to_process_list+=("$line") + files_to_process_count=$((files_to_process_count + 1)) fi - fi + done < <(printf '%s\n' "$file_index_data" | awk 'NF') + else + while IFS= read -r line; do + local file filename title date lastmod tags slug image image_caption description author_name author_email + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" - # Check if this post needs rebuilding due to related posts cache invalidation - if [ "$needs_rebuild" = false ] && [ -n "${RELATED_POSTS_INVALIDATED_LIST:-}" ] && [ -f "$RELATED_POSTS_INVALIDATED_LIST" ]; then - if grep -Fxq "$slug" "$RELATED_POSTS_INVALIDATED_LIST" 2>/dev/null; then - needs_rebuild=true # Related posts cache was invalidated - echo -e "Rebuilding ${GREEN}$(basename "$file")${NC} due to related posts cache invalidation" + # Basic check if it looks like a post + if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then + # echo -e "Skipping non-post file listed in index (pre-check): ${YELLOW}$file${NC}" >&2 # Too verbose + continue fi - fi - if $needs_rebuild; then - files_to_process_list+=("$line") - files_to_process_count=$((files_to_process_count + 1)) - else - # Only print skip message if not rebuilding - echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}" - skipped_count=$((skipped_count + 1)) - fi - done < "$file_index" + # Calculate expected output path (logic copied from process_single_file) + local output_path + local year month day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + year=$(date +%Y); month=$(date +%m); day=$(date +%d) + fi + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}"; + url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" + local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html" + + # Perform the rebuild check here + common_rebuild_check "$output_html_file" + local common_result=$? + local needs_rebuild=false + + if [ $common_result -eq 0 ]; then + needs_rebuild=true # Common checks failed (config changed, template newer, output missing) + else # common_result is 2 (output exists and newer than templates/locale) + local input_time=$(get_file_mtime "$file") + local output_time=$(get_file_mtime "$output_html_file") + if (( input_time > output_time )); then + needs_rebuild=true # Input file is newer + fi + fi + + # Check if this post needs rebuilding due to related posts cache invalidation + if ! $ram_mode_active && [ "$needs_rebuild" = false ] && [ -n "${RELATED_POSTS_INVALIDATED_LIST:-}" ] && [ -f "$RELATED_POSTS_INVALIDATED_LIST" ]; then + if grep -Fxq "$slug" "$RELATED_POSTS_INVALIDATED_LIST" 2>/dev/null; then + needs_rebuild=true # Related posts cache was invalidated + echo -e "Rebuilding ${GREEN}$(basename "$file")${NC} due to related posts cache invalidation" + fi + fi + + if $needs_rebuild; then + files_to_process_list+=("$line") + files_to_process_count=$((files_to_process_count + 1)) + else + # Only print skip message if not rebuilding + echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}" + skipped_count=$((skipped_count + 1)) + fi + done < <( + if $ram_mode_active; then + printf '%s\n' "$file_index_data" | awk 'NF' + else + cat "$file_index" + fi + ) + fi # Check if any files need processing if [ $files_to_process_count -eq 0 ]; then @@ -633,6 +708,10 @@ process_all_markdown_files() { echo -e "Found ${GREEN}$files_to_process_count${NC} posts needing processing out of $total_file_count (Skipped: $skipped_count)." + if $ram_mode_active && [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then + prepare_related_posts_ram_cache "${RELATED_POSTS_COUNT:-3}" + fi + # Define a function for processing a single file line from the *filtered* list process_single_file_for_rebuild() { local line="$1" @@ -658,21 +737,59 @@ process_all_markdown_files() { url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}" output_path="${OUTPUT_DIR:-output}/$url_path" - # Call the main conversion function - # We no longer rely on its internal file_needs_rebuild check - # TODO: Consider modifying convert_markdown to accept a force flag or skip its check - if ! convert_markdown "$file" "$output_path" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description" "$author_name" "$author_email"; then + # Call the conversion function, skipping internal rebuild checks because this + # function only receives files pre-selected for rebuild. + if ! convert_markdown "$file" "$output_path" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description" "$author_name" "$author_email" true; then local exit_code=$? echo -e "${RED}ERROR:${NC} convert_markdown failed for '$file' with exit code $exit_code. Output HTML may be missing or incomplete." >&2 fi } # Use GNU parallel if available - if [ "${HAS_PARALLEL:-false}" = true ]; then + if $ram_mode_active; then + local cores + cores=$(get_parallel_jobs) + if [ "$cores" -gt "$files_to_process_count" ]; then + cores="$files_to_process_count" + fi + + if [ "$files_to_process_count" -gt 1 ] && [ "$cores" -gt 1 ]; then + echo -e "${YELLOW}Using shell parallel workers for $files_to_process_count RAM-mode posts${NC}" + + local worker_pids=() + local worker_idx + for ((worker_idx = 0; worker_idx < cores; worker_idx++)); do + ( + local idx + for ((idx = worker_idx; idx < files_to_process_count; idx += cores)); do + process_single_file_for_rebuild "${files_to_process_list[$idx]}" + done + ) & + worker_pids+=("$!") + done + + local pid + local worker_failed=false + for pid in "${worker_pids[@]}"; do + if ! wait "$pid"; then + worker_failed=true + fi + done + if $worker_failed; then + echo -e "${RED}Parallel RAM-mode post processing failed.${NC}" + exit 1 + fi + else + echo -e "${YELLOW}Using sequential processing for $files_to_process_count RAM-mode posts${NC}" + local line + for line in "${files_to_process_list[@]}"; do + process_single_file_for_rebuild "$line" + done + fi + elif [ "${HAS_PARALLEL:-false}" = true ]; then echo -e "${GREEN}Using GNU parallel to process $files_to_process_count posts${NC}" - local cores=1 - if command -v nproc > /dev/null 2>&1; then cores=$(nproc); - elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi + local cores + cores=$(get_parallel_jobs) # Export functions and variables needed by parallel tasks # Note: We export the new process function @@ -680,6 +797,7 @@ process_all_markdown_files() { # Export dependencies of convert_markdown and its helpers export -f file_needs_rebuild get_file_mtime common_rebuild_check config_has_changed # Still needed by convert_markdown *internally* for now export -f calculate_reading_time generate_slug format_date fix_url parse_metadata extract_metadata convert_markdown_to_html + export -f format_iso8601_post_date export -f portable_md5sum # Used by cache funcs export CACHE_DIR FORCE_REBUILD OUTPUT_DIR SITE_URL URL_SLUG_FORMAT HEADER_TEMPLATE FOOTER_TEMPLATE export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME MARKDOWN_PROCESSOR MARKDOWN_PL_PATH DATE_FORMAT TIMEZONE SHOW_TIMEZONE diff --git a/scripts/build/generate_secondary_pages.sh b/scripts/build/generate_secondary_pages.sh index 7aefa4f..870bde6 100755 --- a/scripts/build/generate_secondary_pages.sh +++ b/scripts/build/generate_secondary_pages.sh @@ -14,6 +14,10 @@ generate_pages_index() { # --- Define Target File --- local pages_index="$OUTPUT_DIR/pages.html" local secondary_pages_list_file="${CACHE_DIR:-.bssg_cache}/secondary_pages.list" + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_active=true + fi # --- Cache Check --- START --- # Rebuild if force flag is set OR if list file exists and output is older than list file @@ -22,13 +26,13 @@ generate_pages_index() { if [[ "${FORCE_REBUILD:-false}" == true ]]; then should_rebuild=true echo -e "${YELLOW}Forcing pages index rebuild (--force-rebuild).${NC}" - elif [ ! -f "$secondary_pages_list_file" ]; then + elif ! $ram_mode_active && [ ! -f "$secondary_pages_list_file" ]; then # If list file doesn't exist, we need to generate pages.html (or handle absence) # This case might mean 0 secondary pages after a clean build. # Let the existing logic handle the case of 0 pages later. should_rebuild=true echo -e "${YELLOW}Secondary pages list file not found, rebuilding pages index.${NC}" - elif [ ! -f "$pages_index" ] || [ "$pages_index" -ot "$secondary_pages_list_file" ]; then + elif ! $ram_mode_active && { [ ! -f "$pages_index" ] || [ "$pages_index" -ot "$secondary_pages_list_file" ]; }; then should_rebuild=true echo -e "${YELLOW}Pages index is older than secondary pages list, rebuilding.${NC}" # Add checks for template file changes? More complex, rely on overall rebuild for now. @@ -47,7 +51,9 @@ generate_pages_index() { # --- Read secondary pages from cache file --- START --- local temp_secondary_pages=() - if [ -f "$secondary_pages_list_file" ]; then + if $ram_mode_active; then + mapfile -t temp_secondary_pages < <(printf '%s\n' "$(ram_mode_get_dataset "secondary_pages")" | awk 'NF') + elif [ -f "$secondary_pages_list_file" ]; then # Use mapfile (readarray) to read lines into the array mapfile -t temp_secondary_pages < "$secondary_pages_list_file" # Optional: Trim whitespace from each element if necessary (mapfile usually handles newlines) @@ -86,10 +92,8 @@ generate_pages_index() { # Generate CollectionPage schema local schema_json_ld="" - local tmp_schema=$(mktemp) - # Create CollectionPage schema - cat > "$tmp_schema" << EOF + schema_json_ld=$(cat << EOF EOF - - # Read the schema from the temporary file - schema_json_ld=$(cat "$tmp_schema") - - # Remove the temporary file - rm "$tmp_schema" +) # Add schema markup to header header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} @@ -150,4 +149,4 @@ EOF } # Make function available for sourcing -export -f generate_pages_index \ No newline at end of file +export -f generate_pages_index diff --git a/scripts/build/generate_tags.sh b/scripts/build/generate_tags.sh index 01e6012..76d47bf 100755 --- a/scripts/build/generate_tags.sh +++ b/scripts/build/generate_tags.sh @@ -13,8 +13,602 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s # shellcheck source=generate_feeds.sh disable=SC1091 source "$(dirname "$0")/generate_feeds.sh" || { echo >&2 "Error: Failed to source generate_feeds.sh from generate_tags.sh"; exit 1; } +declare -gA BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=() +declare -gA BSSG_RAM_TAG_POST_COUNT_BY_SLUG=() +declare -gA BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=() +declare -gA BSSG_RAM_RSS_TEMPLATE_BY_SLUG=() +declare -g BSSG_RAM_TAG_DISPLAY_DATE_FORMAT="" +declare -g BSSG_RAM_TAG_HEADER_BASE="" +declare -g BSSG_RAM_TAG_FOOTER_CONTENT="" + +_bssg_tags_now_ms() { + if declare -F _bssg_ram_timing_now_ms > /dev/null; then + _bssg_ram_timing_now_ms + return + fi + + if [ -n "${EPOCHREALTIME:-}" ]; then + local epoch_norm sec frac ms_part + # Some locales expose EPOCHREALTIME with ',' instead of '.' as decimal separator. + epoch_norm="${EPOCHREALTIME/,/.}" + if [[ "$epoch_norm" =~ ^([0-9]+)([.][0-9]+)?$ ]]; then + sec="${BASH_REMATCH[1]}" + frac="${BASH_REMATCH[2]#.}" + frac="${frac}000" + ms_part="${frac:0:3}" + printf '%s\n' $(( 10#$sec * 1000 + 10#$ms_part )) + return + fi + fi + + if command -v perl >/dev/null 2>&1; then + perl -MTime::HiRes=time -e 'printf("%.0f\n", time()*1000)' + else + printf '%s\n' $(( $(date +%s) * 1000 )) + fi +} + +_bssg_tags_format_ms() { + local ms="${1:-0}" + printf '%d.%03ds' $((ms / 1000)) $((ms % 1000)) +} + +_write_tag_rss_from_cached_items_ram() { + local output_file="$1" + local feed_link_rel="$2" + local feed_atom_link_rel="$3" + local tag="$4" + local rss_items_xml="$5" + + local feed_title="${SITE_TITLE} - ${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag" + local feed_description="${MSG_POSTS_TAGGED_WITH:-"Posts tagged with"}: $tag" + local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z" + + local escaped_feed_title escaped_feed_description feed_link feed_atom_link channel_last_build_date + escaped_feed_title=$(html_escape "$feed_title") + escaped_feed_description=$(html_escape "$feed_description") + feed_link=$(fix_url "$feed_link_rel") + feed_atom_link=$(fix_url "$feed_atom_link_rel") + channel_last_build_date=$(format_date "now" "$rss_date_fmt") + + exec 4> "$output_file" || return 1 + printf '%s\n' \ + '' \ + '' \ + '' \ + " ${escaped_feed_title}" \ + " ${feed_link}" \ + " ${escaped_feed_description}" \ + " ${SITE_LANG:-en}" \ + " ${channel_last_build_date}" \ + " " >&4 + + if [ -n "$rss_items_xml" ]; then + printf '%s' "$rss_items_xml" >&4 + fi + + printf '%s\n' '' '' >&4 + exec 4>&- + + if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + echo -e "${GREEN}RSS feed generated at $output_file${NC}" + fi +} + +_process_single_tag_page_ram() { + local tag_url="$1" + local tag="$2" + local tag_page_html_file="$OUTPUT_DIR/tags/$tag_url/index.html" + local tag_rss_file="$OUTPUT_DIR/tags/$tag_url/${RSS_FILENAME:-rss.xml}" + local tag_page_rel_url="/tags/${tag_url}/" + local tag_rss_rel_url="/tags/${tag_url}/${RSS_FILENAME:-rss.xml}" + mkdir -p "$(dirname "$tag_page_html_file")" + + local header_content="$BSSG_RAM_TAG_HEADER_BASE" + header_content=${header_content//\{\{page_title\}\}/"${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag"} + header_content=${header_content//\{\{page_url\}\}/"$tag_page_rel_url"} + if [ "${ENABLE_TAG_RSS:-false}" = true ]; then + header_content=${header_content///} + else + header_content=${header_content///} + fi + local schema_json_ld + schema_json_ld=$(cat < +{ + "@context": "https://schema.org", + "@type": "CollectionPage", + "name": "Posts tagged with: $tag", + "description": "Posts with tag: $tag", + "url": "$SITE_URL${tag_page_rel_url}", + "isPartOf": { + "@type": "WebSite", + "name": "$SITE_TITLE", + "url": "$SITE_URL" + } +} + +EOF +) + header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} + local footer_content="$BSSG_RAM_TAG_FOOTER_CONTENT" + + exec 3> "$tag_page_html_file" + printf '%s\n' "$header_content" >&3 + printf '

    %s: %s

    \n' "${MSG_TAG_PAGE_TITLE:-Posts tagged with}" "$tag" >&3 + printf '
    \n' >&3 + + local rss_item_limit=${RSS_ITEM_LIMIT:-15} + local rss_count=0 + local cached_rss_items="" + local rss_all_items_cached=true + local -a selected_rss_templates=() + local tag_post_slugs="" + if [[ -n "${BSSG_RAM_TAG_POST_SLUGS_BY_SLUG[$tag_url]+_}" ]]; then + tag_post_slugs="${BSSG_RAM_TAG_POST_SLUGS_BY_SLUG[$tag_url]}" + fi + + local slug cached_article_html rss_template + while IFS= read -r slug; do + [ -z "$slug" ] && continue + cached_article_html="${BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG[$slug]}" + if [ -n "$cached_article_html" ]; then + printf '%s' "$cached_article_html" >&3 + fi + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_count" -lt "$rss_item_limit" ]; then + rss_template="${BSSG_RAM_RSS_TEMPLATE_BY_SLUG[$slug]}" + if [ -n "$rss_template" ]; then + selected_rss_templates+=("$rss_template") + if $rss_all_items_cached; then + local rss_file rss_filename rss_title rss_date rss_lastmod rss_tags rss_slug rss_image rss_image_caption rss_description rss_author_name rss_author_email + IFS='|' read -r rss_file rss_filename rss_title rss_date rss_lastmod rss_tags rss_slug rss_image rss_image_caption rss_description rss_author_name rss_author_email <<< "$rss_template" + local rss_item_cache_key="${RSS_INCLUDE_FULL_CONTENT:-false}|${rss_file}|${rss_date}|${rss_lastmod}|${rss_slug}|${rss_title}" + local rss_item_xml="${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]-}" + if [ -n "$rss_item_xml" ]; then + cached_rss_items+="$rss_item_xml" + else + rss_all_items_cached=false + fi + fi + rss_count=$((rss_count + 1)) + fi + fi + done <<< "$tag_post_slugs" + + printf '
    \n' >&3 + printf '

    %s

    \n' "$SITE_URL" "${MSG_ALL_TAGS:-All Tags}" >&3 + printf '%s\n' "$footer_content" >&3 + exec 3>&- + + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "${#selected_rss_templates[@]}" -gt 0 ]; then + if $rss_all_items_cached; then + _write_tag_rss_from_cached_items_ram "$tag_rss_file" "$tag_page_rel_url" "$tag_rss_rel_url" "$tag" "$cached_rss_items" + else + local tag_post_data="" + local rss_template_entry + for rss_template_entry in "${selected_rss_templates[@]}"; do + tag_post_data+="${rss_template_entry//%TAG%/$tag}"$'\n' + done + _generate_rss_feed "$tag_rss_file" "${SITE_TITLE} - ${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag" "${MSG_POSTS_TAGGED_WITH:-"Posts tagged with"}: $tag" "$tag_page_rel_url" "$tag_rss_rel_url" "$tag_post_data" + fi + fi +} + +_generate_tag_pages_ram() { + echo -e "${YELLOW}Processing tag pages${NC}${ENABLE_TAG_RSS:+" and RSS feeds"}...${NC}" + local ram_tags_timing_enabled=false + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + ram_tags_timing_enabled=true + fi + local tags_total_start_ms=0 + local tags_phase_start_ms=0 + local tags_prep_ms=0 + local tags_render_ms=0 + local tags_index_ms=0 + local tags_total_ms=0 + if [ "$ram_tags_timing_enabled" = true ]; then + tags_total_start_ms="$(_bssg_tags_now_ms)" + tags_phase_start_ms="$tags_total_start_ms" + fi + + local tags_index_data + tags_index_data=$(ram_mode_get_dataset "tags_index") + local main_tags_index_output="$OUTPUT_DIR/tags/index.html" + + mkdir -p "$OUTPUT_DIR/tags" + + if [ -z "$tags_index_data" ]; then + echo -e "${YELLOW}No tags found in RAM index. Skipping tag page generation.${NC}" + return 0 + fi + + BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=() + BSSG_RAM_TAG_POST_COUNT_BY_SLUG=() + BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=() + BSSG_RAM_RSS_TEMPLATE_BY_SLUG=() + declare -A tag_name_by_slug=() + local sorted_tag_urls=() + declare -A rss_prefill_slug_set=() + declare -A rss_prefill_slug_hits=() + local rss_prefill_slugs=() + local rss_prefill_occurrences=0 + local rss_item_limit="${RSS_ITEM_LIMIT:-15}" + local rss_prefill_min_hits="${RAM_RSS_PREFILL_MIN_HITS:-2}" + local rss_prefill_max_posts="${RAM_RSS_PREFILL_MAX_POSTS:-24}" + if ! [[ "$rss_prefill_min_hits" =~ ^[0-9]+$ ]] || [ "$rss_prefill_min_hits" -lt 1 ]; then + rss_prefill_min_hits=1 + fi + if ! [[ "$rss_prefill_max_posts" =~ ^[0-9]+$ ]]; then + rss_prefill_max_posts=24 + fi + declare -A seen_post_slugs=() + local display_date_format="$DATE_FORMAT" + if [ "${SHOW_TIMEZONE:-false}" = false ]; then + display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//') + fi + BSSG_RAM_TAG_DISPLAY_DATE_FORMAT="$display_date_format" + + # Prime per-post caches once from file_index (one row per post), then build + # lightweight tag->post mappings from tags_index (many rows per post). + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + + local can_prime_rss_metadata=false + local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z" + local build_timestamp_iso="" + if [ "${ENABLE_TAG_RSS:-false}" = true ] && declare -F _ram_prime_rss_metadata_entry > /dev/null; then + can_prime_rss_metadata=true + build_timestamp_iso=$(format_date "now" "%Y-%m-%dT%H:%M:%S%z") + if [[ "$build_timestamp_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then + build_timestamp_iso="${build_timestamp_iso::${#build_timestamp_iso}-2}:${BASH_REMATCH[2]}" + fi + fi + + local file filename title date lastmod tags slug image image_caption description author_name author_email + while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do + [ -z "$file" ] && continue + [ -z "$slug" ] && continue + [[ -n "${seen_post_slugs[$slug]+_}" ]] && continue + seen_post_slugs["$slug"]=1 + + local post_year post_month post_day + if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d) + fi + + local formatted_path="${URL_SLUG_FORMAT//Year/$post_year}" + formatted_path="${formatted_path//Month/$post_month}" + formatted_path="${formatted_path//Day/$post_day}" + formatted_path="${formatted_path//slug/$slug}" + local post_link="/${formatted_path}/" + local formatted_date + formatted_date=$(format_date "$date" "$display_date_format") + + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local article_html="" + article_html+='
    '$'\n' + article_html+="

    ${title}

    "$'\n' + article_html+="
    ${MSG_PUBLISHED_ON:-Published on} ${formatted_date} ${MSG_BY:-by} ${display_author_name}
    "$'\n' + if [ -n "$image" ]; then + local image_url alt_text figcaption_content + image_url=$(fix_url "$image") + alt_text="${image_caption:-$title}" + figcaption_content="${image_caption:-$title}" + article_html+=' '$'\n' + fi + if [ -n "$description" ]; then + article_html+='
    '$'\n' + article_html+=" ${description}"$'\n' + article_html+='
    '$'\n' + fi + article_html+='
    '$'\n' + BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG["$slug"]="$article_html" + BSSG_RAM_RSS_TEMPLATE_BY_SLUG["$slug"]="${filename}|${filename}|${title}|${date}|${lastmod}|%TAG%|${slug}|${image}|${image_caption}|${description}|${author_name}|${author_email}" + + if $can_prime_rss_metadata; then + _ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" >/dev/null || true + fi + done <<< "$file_index_data" + + if $can_prime_rss_metadata; then + BSSG_RAM_RSS_METADATA_CACHE_READY=true + fi + + # Sort once globally by tag slug, then by publish date/lastmod descending. + # Aggregate per-tag rows in awk to reduce per-line bash map churn. + local aggregated_tags_data + aggregated_tags_data=$(printf '%s\n' "$tags_index_data" | awk 'NF' | LC_ALL=C sort -t'|' -k2,2 -k4,4r -k5,5r | awk -F'|' -v OFS='|' ' + { + tag = $1 + tag_slug = $2 + post_slug = $7 + if (tag == "" || tag_slug == "") next + + if (current_tag_slug != "" && tag_slug != current_tag_slug) { + print current_tag_slug, current_tag_name, current_count, current_post_slugs + current_count = 0 + current_post_slugs = "" + } + + if (tag_slug != current_tag_slug) { + current_tag_slug = tag_slug + current_tag_name = tag + } + + if (post_slug != "") { + if (current_post_slugs == "") { + current_post_slugs = post_slug + } else { + current_post_slugs = current_post_slugs "," post_slug + } + } + current_count++ + } + END { + if (current_tag_slug != "") { + print current_tag_slug, current_tag_name, current_count, current_post_slugs + } + }') + + local tag_slug tag_name tag_count_value tag_post_slugs_csv + while IFS='|' read -r tag_slug tag_name tag_count_value tag_post_slugs_csv; do + [ -z "$tag_slug" ] && continue + tag_name_by_slug["$tag_slug"]="$tag_name" + BSSG_RAM_TAG_POST_COUNT_BY_SLUG["$tag_slug"]="$tag_count_value" + local tag_post_slugs_newline="" + if [ -n "$tag_post_slugs_csv" ]; then + tag_post_slugs_newline="${tag_post_slugs_csv//,/$'\n'}" + fi + BSSG_RAM_TAG_POST_SLUGS_BY_SLUG["$tag_slug"]="$tag_post_slugs_newline" + sorted_tag_urls+=("$tag_slug") + + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ -n "$tag_post_slugs_newline" ]; then + local rss_prefill_count=0 + local rss_prefill_slug="" + while IFS= read -r rss_prefill_slug; do + [ -z "$rss_prefill_slug" ] && continue + rss_prefill_occurrences=$((rss_prefill_occurrences + 1)) + rss_prefill_slug_hits["$rss_prefill_slug"]=$(( ${rss_prefill_slug_hits[$rss_prefill_slug]:-0} + 1 )) + if [[ -z "${rss_prefill_slug_set[$rss_prefill_slug]+_}" ]]; then + rss_prefill_slug_set["$rss_prefill_slug"]=1 + rss_prefill_slugs+=("$rss_prefill_slug") + fi + rss_prefill_count=$((rss_prefill_count + 1)) + if [ "$rss_prefill_count" -ge "$rss_item_limit" ]; then + break + fi + done <<< "$tag_post_slugs_newline" + fi + done <<< "$aggregated_tags_data" + + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_prefill_min_hits" -gt 1 ] && [ "${#rss_prefill_slugs[@]}" -gt 0 ]; then + local -a rss_prefill_filtered_slugs=() + local rss_prefill_slug + for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do + if [ "${rss_prefill_slug_hits[$rss_prefill_slug]:-0}" -ge "$rss_prefill_min_hits" ]; then + rss_prefill_filtered_slugs+=("$rss_prefill_slug") + fi + done + if [ "${#rss_prefill_filtered_slugs[@]}" -gt 0 ]; then + rss_prefill_slugs=("${rss_prefill_filtered_slugs[@]}") + fi + fi + + local rss_prefill_pool_count="${#rss_prefill_slugs[@]}" + if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_prefill_max_posts" -gt 0 ] && [ "${#rss_prefill_slugs[@]}" -gt "$rss_prefill_max_posts" ]; then + local -a rss_prefill_ranked_lines=() + local rss_prefill_slug + for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do + rss_prefill_ranked_lines+=("${rss_prefill_slug_hits[$rss_prefill_slug]:-0}|$rss_prefill_slug") + done + + local -a rss_prefill_capped_slugs=() + local rss_prefill_rank_line + while IFS= read -r rss_prefill_rank_line; do + [ -z "$rss_prefill_rank_line" ] && continue + rss_prefill_capped_slugs+=("${rss_prefill_rank_line#*|}") + done < <( + printf '%s\n' "${rss_prefill_ranked_lines[@]}" \ + | LC_ALL=C sort -t'|' -k1,1nr -k2,2 \ + | head -n "$rss_prefill_max_posts" + ) + + if [ "${#rss_prefill_capped_slugs[@]}" -gt 0 ]; then + rss_prefill_slugs=("${rss_prefill_capped_slugs[@]}") + fi + fi + + local footer_base="$FOOTER_TEMPLATE" + footer_base=${footer_base//\{\{current_year\}\}/$(date +%Y)} + footer_base=${footer_base//\{\{author_name\}\}/"$AUTHOR_NAME"} + BSSG_RAM_TAG_FOOTER_CONTENT="$footer_base" + + local header_base="$HEADER_TEMPLATE" + header_base=${header_base//\{\{site_title\}\}/"$SITE_TITLE"} + header_base=${header_base//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_base=${header_base//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + header_base=${header_base//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + header_base=${header_base//\{\{og_type\}\}/"website"} + header_base=${header_base//\{\{site_url\}\}/"$SITE_URL"} + header_base=${header_base//\{\{og_image\}\}/""} + header_base=${header_base//\{\{twitter_image\}\}/""} + BSSG_RAM_TAG_HEADER_BASE="$header_base" + + local tag_count="${#sorted_tag_urls[@]}" + echo -e "Generating ${GREEN}$tag_count${NC} tag pages from RAM index." + + if [ "${ENABLE_TAG_RSS:-false}" = true ]; then + if declare -F prepare_ram_rss_metadata_cache > /dev/null; then + prepare_ram_rss_metadata_cache + fi + if [ "${RSS_INCLUDE_FULL_CONTENT:-false}" = true ] && declare -F prepare_ram_rss_full_content_cache > /dev/null; then + prepare_ram_rss_full_content_cache + fi + + # Pre-warm RAM RSS item XML cache once in parent process so worker + # subshells inherit it read-only and avoid rebuilding duplicate items. + if declare -F _generate_rss_feed > /dev/null; then + local rss_prefill_post_data="" + local rss_prefill_slug rss_template_entry + for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do + rss_template_entry="${BSSG_RAM_RSS_TEMPLATE_BY_SLUG[$rss_prefill_slug]}" + [ -z "$rss_template_entry" ] && continue + rss_prefill_post_data+="${rss_template_entry//%TAG%/__prefill__}"$'\n' + done + if [ -n "$rss_prefill_post_data" ]; then + if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then + local max_posts_label="unlimited" + if [ "$rss_prefill_max_posts" -gt 0 ]; then + max_posts_label="$rss_prefill_max_posts" + fi + echo -e "DEBUG: Pre-warming RAM RSS item cache for ${#rss_prefill_slugs[@]} posts (${rss_prefill_occurrences} tag-RSS slots, min hits: ${rss_prefill_min_hits}, max posts: ${max_posts_label}, pool: ${rss_prefill_pool_count})." + fi + _generate_rss_feed "/dev/null" "__prefill__" "__prefill__" "/" "/rss.xml" "$rss_prefill_post_data" >/dev/null || true + fi + fi + fi + + if [ "$ram_tags_timing_enabled" = true ]; then + local now_ms + now_ms="$(_bssg_tags_now_ms)" + tags_prep_ms=$((now_ms - tags_phase_start_ms)) + tags_phase_start_ms="$now_ms" + fi + + local tag_url + local cores + cores=$(get_parallel_jobs) + if [ "$cores" -gt "$tag_count" ]; then + cores="$tag_count" + fi + + if [ "$tag_count" -gt 1 ] && [ "$cores" -gt 1 ]; then + local worker_pids=() + local worker_idx + for ((worker_idx = 0; worker_idx < cores; worker_idx++)); do + ( + local idx local_tag_url local_tag + for ((idx = worker_idx; idx < tag_count; idx += cores)); do + local_tag_url="${sorted_tag_urls[$idx]}" + local_tag="${tag_name_by_slug[$local_tag_url]}" + _process_single_tag_page_ram "$local_tag_url" "$local_tag" + done + ) & + worker_pids+=("$!") + done + + local pid + local worker_failed=false + for pid in "${worker_pids[@]}"; do + if ! wait "$pid"; then + worker_failed=true + fi + done + if $worker_failed; then + echo -e "${RED}Parallel RAM-mode tag processing failed.${NC}" + exit 1 + fi + else + for tag_url in "${sorted_tag_urls[@]}"; do + tag="${tag_name_by_slug[$tag_url]}" + _process_single_tag_page_ram "$tag_url" "$tag" + done + fi + + if [ "$ram_tags_timing_enabled" = true ]; then + local now_ms + now_ms="$(_bssg_tags_now_ms)" + tags_render_ms=$((now_ms - tags_phase_start_ms)) + tags_phase_start_ms="$now_ms" + fi + + local header_content="$HEADER_TEMPLATE" + local footer_content="$FOOTER_TEMPLATE" + header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"} + header_content=${header_content//\{\{page_title\}\}/"${MSG_ALL_TAGS:-"All Tags"}"} + header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} + header_content=${header_content//\{\{og_type\}\}/"website"} + header_content=${header_content//\{\{page_url\}\}/"/tags/"} + header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} + header_content=${header_content///} + local tags_schema_json + tags_schema_json=$(cat < +{ + "@context": "https://schema.org", + "@type": "CollectionPage", + "name": "${MSG_ALL_TAGS:-"All Tags"}", + "description": "List of all tags on $SITE_TITLE", + "url": "$SITE_URL/tags/", + "isPartOf": { + "@type": "WebSite", + "name": "$SITE_TITLE", + "url": "$SITE_URL" + } +} + +EOF +) + header_content=${header_content//\{\{schema_json_ld\}\}/"$tags_schema_json"} + header_content=${header_content//\{\{og_image\}\}/""} + header_content=${header_content//\{\{twitter_image\}\}/""} + footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} + footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} + + exec 5> "$main_tags_index_output" + printf '%s\n' "$header_content" >&5 + printf '

    %s

    \n' "${MSG_ALL_TAGS:-All Tags}" >&5 + printf '
    \n' >&5 + for tag_url in "${sorted_tag_urls[@]}"; do + tag="${tag_name_by_slug[$tag_url]}" + local post_count="${BSSG_RAM_TAG_POST_COUNT_BY_SLUG[$tag_url]:-0}" + printf ' %s (%s)\n' "$SITE_URL" "$tag_url" "$tag" "$post_count" >&5 + done + printf '
    \n' >&5 + printf '%s\n' "$footer_content" >&5 + exec 5>&- + + if [ "$ram_tags_timing_enabled" = true ]; then + local now_ms + now_ms="$(_bssg_tags_now_ms)" + tags_index_ms=$((now_ms - tags_phase_start_ms)) + tags_total_ms=$((now_ms - tags_total_start_ms)) + echo -e "${BLUE}RAM tags sub-timing:${NC}" + echo -e " Prepare maps/cache: $(_bssg_tags_format_ms "$tags_prep_ms")" + echo -e " Tag pages+RSS: $(_bssg_tags_format_ms "$tags_render_ms")" + echo -e " tags/index.html: $(_bssg_tags_format_ms "$tags_index_ms")" + echo -e " Total tags stage: $(_bssg_tags_format_ms "$tags_total_ms")" + fi + + BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=() + BSSG_RAM_TAG_POST_COUNT_BY_SLUG=() + BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=() + BSSG_RAM_RSS_TEMPLATE_BY_SLUG=() + BSSG_RAM_TAG_HEADER_BASE="" + BSSG_RAM_TAG_FOOTER_CONTENT="" + BSSG_RAM_TAG_DISPLAY_DATE_FORMAT="" + + echo -e "${GREEN}Tag pages processed!${NC}" +} + # Generate tag pages generate_tag_pages() { + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + _generate_tag_pages_ram + return $? + fi + echo -e "${YELLOW}Processing tag pages${NC}${ENABLE_TAG_RSS:+" and RSS feeds"}...${NC}" local tags_index_file="$CACHE_DIR/tags_index.txt" @@ -493,9 +1087,8 @@ EOF # Use parallel if [ "${HAS_PARALLEL:-false}" = true ] ; then echo -e "${GREEN}Using GNU parallel to process tag pages${NC}${ENABLE_TAG_RSS:+/feeds}" - local cores=1 - if command -v nproc > /dev/null 2>&1; then cores=$(nproc); - elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi + local cores + cores=$(get_parallel_jobs) local jobs=$cores # Use all cores for tags by default if parallel # Export necessary functions and variables diff --git a/scripts/build/indexing.sh b/scripts/build/indexing.sh index 044f6aa..d5087bf 100755 --- a/scripts/build/indexing.sh +++ b/scripts/build/indexing.sh @@ -175,12 +175,33 @@ _process_raw_file_index() { } # Optimized file index building - orchestrates raw build and processing +_build_file_index_from_ram() { + while IFS= read -r file; do + [[ -z "$file" ]] && continue + local metadata + metadata=$(extract_metadata "$file") || continue + local filename + filename=$(basename "$file") + echo "$file|$filename|$metadata" + done < <(ram_mode_list_src_files) | sort -t '|' -k 4,4r -k 1,1 +} + optimized_build_file_index() { echo -e "${YELLOW}Building file index...${NC}" local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" local index_marker="${CACHE_DIR:-.bssg_cache}/index_marker" local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_list_src_files > /dev/null; then + local file_index_data + file_index_data=$(_build_file_index_from_ram) + ram_mode_set_dataset "file_index" "$file_index_data" + ram_mode_clear_dataset "file_index_prev" + ram_mode_set_dataset "frontmatter_changes_marker" "1" + echo -e "${GREEN}File index built from RAM preload with $(ram_mode_dataset_line_count "file_index") complete entries!${NC}" + return 0 + fi # Check if rebuild is needed if [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$file_index" ] && [ -f "$index_marker" ]; then @@ -293,6 +314,44 @@ build_tags_index() { local tags_index_file="${CACHE_DIR:-.bssg_cache}/tags_index.txt" local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local file_index_data tags_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + ram_mode_set_dataset "tags_index" "" + ram_mode_clear_dataset "has_tags" + echo -e "${GREEN}Tags index built!${NC}" + return 0 + fi + + tags_index_data=$(printf '%s\n' "$file_index_data" | awk -F'|' -v OFS='|' ' + { + if (length($6) > 0) { + split($6, tags_array, ","); + for (i in tags_array) { + tag = tags_array[i]; + gsub(/^[[:space:]]+|[[:space:]]+$/, "", tag); + if (length(tag) == 0) continue; + + tag_slug = tolower(tag); + gsub(/[^a-z0-9]+/, "-", tag_slug); + gsub(/^-+|-+$/, "", tag_slug); + if (length(tag_slug) == 0) tag_slug = "-"; + + print tag, tag_slug, $3, $4, $5, $2, $7, $8, $9, $10, $11, $12; + } + } + }') + ram_mode_set_dataset "tags_index" "$tags_index_data" + if [ -n "$tags_index_data" ]; then + ram_mode_set_dataset "has_tags" "1" + else + ram_mode_clear_dataset "has_tags" + fi + echo -e "${GREEN}Tags index built!${NC}" + return 0 + fi + # --- Optimized Rebuild Check --- START --- local rebuild_needed=false local reason="" @@ -376,6 +435,39 @@ build_authors_index() { local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local file_index_data authors_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + ram_mode_set_dataset "authors_index" "" + ram_mode_clear_dataset "has_authors" + echo -e "${GREEN}Authors index built!${NC}" + return 0 + fi + + authors_index_data=$(printf '%s\n' "$file_index_data" | awk -F'|' -v OFS='|' ' + { + author_name = $11; + author_email = $12; + if (length(author_name) == 0) next; + + author_slug = tolower(author_name); + gsub(/[^a-z0-9]+/, "-", author_slug); + gsub(/^-+|-+$/, "", author_slug); + if (length(author_slug) == 0) author_slug = "anonymous"; + + print author_name, author_slug, author_email, $3, $4, $5, $2, $7, $8, $9, $10; + }') + ram_mode_set_dataset "authors_index" "$authors_index_data" + if [ -n "$authors_index_data" ]; then + ram_mode_set_dataset "has_authors" "1" + else + ram_mode_clear_dataset "has_authors" + fi + echo -e "${GREEN}Authors index built!${NC}" + return 0 + fi + # Check if rebuild is needed: missing cache or input/dependencies changed local rebuild_needed=false if [ ! -f "$authors_index_file" ]; then @@ -443,6 +535,18 @@ identify_affected_authors() { export AFFECTED_AUTHORS="" export AUTHORS_INDEX_NEEDS_REBUILD="false" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local authors_index_data + authors_index_data=$(ram_mode_get_dataset "authors_index") + if [ -n "$authors_index_data" ]; then + AFFECTED_AUTHORS=$(printf '%s\n' "$authors_index_data" | awk -F'|' 'NF { print $1 }' | sort -u | tr '\n' ' ') + AUTHORS_INDEX_NEEDS_REBUILD="true" + fi + export AFFECTED_AUTHORS + export AUTHORS_INDEX_NEEDS_REBUILD + return 0 + fi + # If previous index doesn't exist, all authors in the current index are affected, # and the main index needs rebuilding. if [ ! -f "$authors_index_prev_file" ]; then @@ -519,6 +623,43 @@ build_archive_index() { local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" local archive_index_file="${CACHE_DIR:-.bssg_cache}/archive_index.txt" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local file_index_data archive_index_data="" + file_index_data=$(ram_mode_get_dataset "file_index") + if [ -z "$file_index_data" ]; then + ram_mode_set_dataset "archive_index" "" + echo -e "${GREEN}Archive index built!${NC}" + return 0 + fi + + local line file filename title date lastmod tags slug image image_caption description author_name author_email + while IFS= read -r line; do + [ -z "$line" ] && continue + IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line" + [ -z "$date" ] && continue + + local year month month_name + if [[ "$date" =~ ^([0-9]{4})[-/]([0-9]{1,2})[-/]([0-9]{1,2}) ]]; then + year="${BASH_REMATCH[1]}" + month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + else + continue + fi + + local month_name_var="MSG_MONTH_${month}" + month_name="${!month_name_var}" + if [[ -z "$month_name" ]]; then + month_name="$month" + fi + + archive_index_data+="$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description|$author_name|$author_email"$'\n' + done <<< "$file_index_data" + + ram_mode_set_dataset "archive_index" "$archive_index_data" + echo -e "${GREEN}Archive index built!${NC}" + return 0 + fi + # Check if rebuild is needed: missing cache or input/dependencies changed local rebuild_needed=false if [ ! -f "$archive_index_file" ]; then @@ -604,6 +745,18 @@ identify_affected_archive_months() { export AFFECTED_ARCHIVE_MONTHS="" export ARCHIVE_INDEX_NEEDS_REBUILD="false" + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local archive_index_data + archive_index_data=$(ram_mode_get_dataset "archive_index") + if [ -n "$archive_index_data" ]; then + AFFECTED_ARCHIVE_MONTHS=$(printf '%s\n' "$archive_index_data" | awk -F'|' 'NF { print $1 "|" $2 }' | sort -u | tr '\n' ' ') + ARCHIVE_INDEX_NEEDS_REBUILD="true" + fi + export AFFECTED_ARCHIVE_MONTHS + export ARCHIVE_INDEX_NEEDS_REBUILD + return 0 + fi + # If previous index doesn't exist, all months in the current index are affected, # and the main index needs rebuilding. if [ ! -f "$archive_index_prev_file" ]; then @@ -673,4 +826,4 @@ identify_affected_archive_months() { trap - RETURN # Remove trap upon successful completion } -# --- Indexing Functions --- END --- \ No newline at end of file +# --- Indexing Functions --- END --- diff --git a/scripts/build/main.sh b/scripts/build/main.sh index ec2db28..678c316 100755 --- a/scripts/build/main.sh +++ b/scripts/build/main.sh @@ -13,6 +13,7 @@ BUILD_START_TIME=$(date +%s) SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" # Determine the project root (one level up from the SCRIPT_DIR's parent) PROJECT_ROOT="$( dirname "$( dirname "$SCRIPT_DIR" )" )" +export BSSG_PROJECT_ROOT="$PROJECT_ROOT" # Check if PROJECT_ROOT is already the current directory to avoid unnecessary cd if [ "$PWD" != "$PROJECT_ROOT" ]; then echo "Changing directory to project root: $PROJECT_ROOT" @@ -81,25 +82,180 @@ fi # shellcheck source=utils.sh source "${SCRIPT_DIR}/utils.sh" || { echo -e "\033[0;31mError: Failed to source utils.sh\033[0m"; exit 1; } +# Build mode validation and setup +BUILD_MODE="${BUILD_MODE:-normal}" +case "$BUILD_MODE" in + normal|ram) ;; + *) + echo -e "${RED}Error: Invalid BUILD_MODE '$BUILD_MODE'. Use 'normal' or 'ram'.${NC}" >&2 + exit 1 + ;; +esac +export BUILD_MODE +export BSSG_RAM_MODE=false + # Print the theme being used for this build (final value after potential random selection) echo -e "${GREEN}Using theme: ${THEME}${NC}" echo "Loaded utilities." +# --- RAM Mode Stage Timing --- START --- +BSSG_RAM_TIMING_ENABLED=false +if [ "$BUILD_MODE" = "ram" ]; then + BSSG_RAM_TIMING_ENABLED=true +fi +declare -ga BSSG_RAM_TIMING_STAGE_KEYS=() +declare -ga BSSG_RAM_TIMING_STAGE_LABELS=() +declare -ga BSSG_RAM_TIMING_STAGE_MS=() +BSSG_RAM_TIMING_STAGE_ACTIVE=false +BSSG_RAM_TIMING_CURRENT_STAGE_KEY="" +BSSG_RAM_TIMING_CURRENT_STAGE_LABEL="" +BSSG_RAM_TIMING_CURRENT_STAGE_START_MS=0 + +_bssg_ram_timing_now_ms() { + if [ -n "${EPOCHREALTIME:-}" ]; then + local epoch_norm sec frac ms_part + # Some locales expose EPOCHREALTIME with ',' instead of '.' as decimal separator. + epoch_norm="${EPOCHREALTIME/,/.}" + if [[ "$epoch_norm" =~ ^([0-9]+)([.][0-9]+)?$ ]]; then + sec="${BASH_REMATCH[1]}" + frac="${BASH_REMATCH[2]#.}" + frac="${frac}000" + ms_part="${frac:0:3}" + printf '%s\n' $(( 10#$sec * 1000 + 10#$ms_part )) + return + fi + fi + + if command -v perl >/dev/null 2>&1; then + perl -MTime::HiRes=time -e 'printf("%.0f\n", time()*1000)' + else + printf '%s\n' $(( $(date +%s) * 1000 )) + fi +} + +_bssg_ram_timing_format_ms() { + local ms="$1" + printf '%d.%03ds' $((ms / 1000)) $((ms % 1000)) +} + +bssg_ram_timing_start() { + if [ "$BSSG_RAM_TIMING_ENABLED" != true ]; then + return + fi + + if [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" = true ]; then + bssg_ram_timing_end + fi + + BSSG_RAM_TIMING_CURRENT_STAGE_KEY="$1" + BSSG_RAM_TIMING_CURRENT_STAGE_LABEL="$2" + BSSG_RAM_TIMING_CURRENT_STAGE_START_MS="$(_bssg_ram_timing_now_ms)" + BSSG_RAM_TIMING_STAGE_ACTIVE=true +} + +bssg_ram_timing_end() { + if [ "$BSSG_RAM_TIMING_ENABLED" != true ] || [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" != true ]; then + return + fi + + local end_ms elapsed_ms + end_ms="$(_bssg_ram_timing_now_ms)" + elapsed_ms=$((end_ms - BSSG_RAM_TIMING_CURRENT_STAGE_START_MS)) + if [ "$elapsed_ms" -lt 0 ]; then + elapsed_ms=0 + fi + + BSSG_RAM_TIMING_STAGE_KEYS+=("$BSSG_RAM_TIMING_CURRENT_STAGE_KEY") + BSSG_RAM_TIMING_STAGE_LABELS+=("$BSSG_RAM_TIMING_CURRENT_STAGE_LABEL") + BSSG_RAM_TIMING_STAGE_MS+=("$elapsed_ms") + + BSSG_RAM_TIMING_STAGE_ACTIVE=false + BSSG_RAM_TIMING_CURRENT_STAGE_KEY="" + BSSG_RAM_TIMING_CURRENT_STAGE_LABEL="" + BSSG_RAM_TIMING_CURRENT_STAGE_START_MS=0 +} + +bssg_ram_timing_print_summary() { + if [ "$BSSG_RAM_TIMING_ENABLED" != true ]; then + return + fi + + # Close any open stage (defensive; build flow should end stages explicitly). + if [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" = true ]; then + bssg_ram_timing_end + fi + + local count="${#BSSG_RAM_TIMING_STAGE_MS[@]}" + if [ "$count" -eq 0 ]; then + return + fi + + local total_ms=0 + local max_ms=0 + local max_label="" + local i + for ((i = 0; i < count; i++)); do + local stage_ms="${BSSG_RAM_TIMING_STAGE_MS[$i]}" + total_ms=$((total_ms + stage_ms)) + if [ "$stage_ms" -gt "$max_ms" ]; then + max_ms="$stage_ms" + max_label="${BSSG_RAM_TIMING_STAGE_LABELS[$i]}" + fi + done + + echo "------------------------------------------------------" + echo -e "${GREEN}RAM mode timing summary:${NC}" + printf " %-26s %12s %10s\n" "Stage" "Duration" "Share" + for ((i = 0; i < count; i++)); do + local stage_label="${BSSG_RAM_TIMING_STAGE_LABELS[$i]}" + local stage_ms="${BSSG_RAM_TIMING_STAGE_MS[$i]}" + local pct_tenths=0 + if [ "$total_ms" -gt 0 ]; then + pct_tenths=$(( (stage_ms * 1000 + total_ms / 2) / total_ms )) + fi + local formatted_ms + formatted_ms="$(_bssg_ram_timing_format_ms "$stage_ms")" + printf " %-26s %12s %6d.%d%%\n" "$stage_label" "$formatted_ms" $((pct_tenths / 10)) $((pct_tenths % 10)) + done + echo -e " ${GREEN}Total (timed stages):$(_bssg_ram_timing_format_ms "$total_ms")${NC}" + if [ -n "$max_label" ]; then + echo -e " ${YELLOW}Slowest stage:${NC} ${max_label} ($(_bssg_ram_timing_format_ms "$max_ms"))" + fi +} +# --- RAM Mode Stage Timing --- END --- + # Check Dependencies # shellcheck source=deps.sh +bssg_ram_timing_start "dependencies" "Dependencies" source "${SCRIPT_DIR}/deps.sh" || { echo -e "${RED}Error: Failed to source deps.sh${NC}"; exit 1; } check_dependencies # Call the function to perform checks and export HAS_PARALLEL +bssg_ram_timing_end + +if [ "$BUILD_MODE" = "ram" ]; then + export BSSG_RAM_MODE=true + export FORCE_REBUILD=true + + # shellcheck source=ram_mode.sh + source "${SCRIPT_DIR}/ram_mode.sh" || { echo -e "${RED}Error: Failed to source ram_mode.sh${NC}"; exit 1; } + print_info "RAM mode enabled: source/template files and build indexes are held in memory." + print_info "RAM mode parallel worker cap: ${RAM_MODE_MAX_JOBS:-6} (set RAM_MODE_MAX_JOBS to tune)." +fi + echo "Checked dependencies. Parallel available: ${HAS_PARALLEL:-false}" # Source Cache Manager (defines cache functions) # shellcheck source=cache.sh +bssg_ram_timing_start "cache_setup" "Cache Setup/Clean" source "${SCRIPT_DIR}/cache.sh" || { echo -e "${RED}Error: Failed to source cache.sh${NC}"; exit 1; } echo "Loaded cache manager." # Check if config changed BEFORE updating the hash file, store status for later use BSSG_CONFIG_CHANGED_STATUS=1 # Default to 1 (not changed) -if config_has_changed; then +if [ "${BSSG_RAM_MODE:-false}" = true ]; then + # RAM mode is intentionally ephemeral, always rebuild from preloaded inputs. + BSSG_CONFIG_CHANGED_STATUS=0 +elif config_has_changed; then BSSG_CONFIG_CHANGED_STATUS=0 # Set to 0 (changed) fi export BSSG_CONFIG_CHANGED_STATUS @@ -118,17 +274,21 @@ fi # --- Add check for CLEAN_OUTPUT influencing FORCE_REBUILD --- END --- # Handle --force-rebuild first -if [ "${FORCE_REBUILD:-false}" = true ]; then +if [ "${BSSG_RAM_MODE:-false}" != true ] && [ "${FORCE_REBUILD:-false}" = true ]; then echo -e "${YELLOW}Force rebuild enabled, deleting entire cache directory (${CACHE_DIR:-.bssg_cache})...${NC}" rm -rf "${CACHE_DIR:-.bssg_cache}" echo -e "${GREEN}Cache deleted!${NC}" fi -echo "Ensuring cache directory structure exists... (${CACHE_DIR:-.bssg_cache})" -mkdir -p "${CACHE_DIR:-.bssg_cache}/meta" "${CACHE_DIR:-.bssg_cache}/content" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + echo "Ensuring cache directory structure exists... (${CACHE_DIR:-.bssg_cache})" + mkdir -p "${CACHE_DIR:-.bssg_cache}/meta" "${CACHE_DIR:-.bssg_cache}/content" -# Create initial config hash *after* ensuring cache dir exists -create_config_hash + # Create initial config hash *after* ensuring cache dir exists + create_config_hash +else + echo "RAM mode: skipping cache directory creation and config hash persistence." +fi # --- Initial Cache Setup & Cleaning --- END # Handle --clean-output flag (using logic moved from original main/clean_output_directory) @@ -148,10 +308,12 @@ if [ "${CLEAN_OUTPUT:-false}" = true ]; then echo -e "${YELLOW}Output directory (${OUTPUT_DIR:-output}) does not exist, no need to clean.${NC}" fi fi +bssg_ram_timing_end # Source Content Processor (defines functions like extract_metadata, convert_markdown_to_html) # Moved up before indexing as indexing uses some content functions (e.g., generate_slug) # shellcheck source=content.sh +bssg_ram_timing_start "index_build" "Index/Data Build" source "${SCRIPT_DIR}/content.sh" || { echo -e "${RED}Error: Failed to source content.sh${NC}"; exit 1; } echo "Loaded content processing functions." @@ -161,17 +323,23 @@ echo "Loaded content processing functions." source "${SCRIPT_DIR}/indexing.sh" || { echo -e "${RED}Error: Failed to source indexing.sh${NC}"; exit 1; } echo "Loaded indexing functions." +if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_preload_inputs || { echo -e "${RED}Error: RAM preload failed.${NC}"; exit 1; } +fi + # --- Build Intermediate Indexes --- # Moved up before preload_templates # --- Start Change: Snapshot previous file index --- file_index_file="${CACHE_DIR:-.bssg_cache}/file_index.txt" file_index_prev_file="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" -if [ -f "$file_index_file" ]; then - echo "Snapshotting previous file index to $file_index_prev_file" >&2 # Debug - cp "$file_index_file" "$file_index_prev_file" -else - # Ensure previous file doesn't exist if current doesn't - rm -f "$file_index_prev_file" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + if [ -f "$file_index_file" ]; then + echo "Snapshotting previous file index to $file_index_prev_file" >&2 # Debug + cp "$file_index_file" "$file_index_prev_file" + else + # Ensure previous file doesn't exist if current doesn't + rm -f "$file_index_prev_file" + fi fi # --- End Change --- optimized_build_file_index || { echo -e "${RED}Error: Failed to build file index.${NC}"; exit 1; } @@ -179,12 +347,14 @@ optimized_build_file_index || { echo -e "${RED}Error: Failed to build file index # --- Start Change: Snapshot previous tags index --- tags_index_file="${CACHE_DIR:-.bssg_cache}/tags_index.txt" tags_index_prev_file="${CACHE_DIR:-.bssg_cache}/tags_index_prev.txt" -if [ -f "$tags_index_file" ]; then - echo "Snapshotting previous tags index to $tags_index_prev_file" >&2 # Debug - cp "$tags_index_file" "$tags_index_prev_file" -else - # Ensure previous file doesn't exist if current doesn't - rm -f "$tags_index_prev_file" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + if [ -f "$tags_index_file" ]; then + echo "Snapshotting previous tags index to $tags_index_prev_file" >&2 # Debug + cp "$tags_index_file" "$tags_index_prev_file" + else + # Ensure previous file doesn't exist if current doesn't + rm -f "$tags_index_prev_file" + fi fi # --- End Change --- @@ -199,12 +369,14 @@ build_tags_index || { echo -e "${RED}Error: Failed to build tags index.${NC}"; e # --- Start Change: Snapshot previous authors index --- authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" authors_index_prev_file="${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" -if [ -f "$authors_index_file" ]; then - echo "Snapshotting previous authors index to $authors_index_prev_file" >&2 # Debug - cp "$authors_index_file" "$authors_index_prev_file" -else - # Ensure previous file doesn't exist if current doesn't - rm -f "$authors_index_prev_file" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + if [ -f "$authors_index_file" ]; then + echo "Snapshotting previous authors index to $authors_index_prev_file" >&2 # Debug + cp "$authors_index_file" "$authors_index_prev_file" + else + # Ensure previous file doesn't exist if current doesn't + rm -f "$authors_index_prev_file" + fi fi # --- End Change --- @@ -218,12 +390,14 @@ if [ "${ENABLE_ARCHIVES:-false}" = true ]; then # --- Start Change: Snapshot previous archive index --- archive_index_file="${CACHE_DIR:-.bssg_cache}/archive_index.txt" archive_index_prev_file="${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt" - if [ -f "$archive_index_file" ]; then - echo "Snapshotting previous archive index to $archive_index_prev_file" >&2 # Debug - cp "$archive_index_file" "$archive_index_prev_file" - else - # Ensure previous file doesn't exist if current doesn't - rm -f "$archive_index_prev_file" + if [ "${BSSG_RAM_MODE:-false}" != true ]; then + if [ -f "$archive_index_file" ]; then + echo "Snapshotting previous archive index to $archive_index_prev_file" >&2 # Debug + cp "$archive_index_file" "$archive_index_prev_file" + else + # Ensure previous file doesn't exist if current doesn't + rm -f "$archive_index_prev_file" + fi fi # --- End Change --- build_archive_index || { echo -e "${RED}Error: Failed to build archive index.${NC}"; exit 1; } @@ -232,10 +406,12 @@ if [ "${ENABLE_ARCHIVES:-false}" = true ]; then # --- End Change --- fi echo "Built intermediate cache indexes." +bssg_ram_timing_end # Load Templates (and generate dynamic menus, exports vars like HEADER_TEMPLATE) # Moved down after indexing # shellcheck source=templates.sh +bssg_ram_timing_start "templates" "Template Prep" source "${SCRIPT_DIR}/templates.sh" || { echo -e "${RED}Error: Failed to source templates.sh${NC}"; exit 1; } preload_templates # Call the function echo "Loaded and processed templates." @@ -279,6 +455,7 @@ fi export BSSG_MAX_TEMPLATE_LOCALE_TIME=$latest_template_locale_time echo "Latest template/locale time: $BSSG_MAX_TEMPLATE_LOCALE_TIME (Header: $header_time, Footer: $footer_time, Locale: $locale_time)" # --- Pre-calculate Max Template/Locale Time --- END --- +bssg_ram_timing_end # --- Prepare for Parallel Processing --- if [ "${HAS_PARALLEL:-false}" = true ]; then @@ -311,32 +488,39 @@ fi # --- Generate Content HTML --- # Source and run Post Generator # shellcheck source=generate_posts.sh +bssg_ram_timing_start "posts" "Posts" source "${SCRIPT_DIR}/generate_posts.sh" || { echo -e "${RED}Error: Failed to source generate_posts.sh${NC}"; exit 1; } process_all_markdown_files || { echo -e "${RED}Error: Post processing failed.${NC}"; exit 1; } echo "Generated post HTML files." +bssg_ram_timing_end # --- Post Generation --- END --- # --- Page Generation --- START -- # Source the page generation script # shellcheck source=generate_pages.sh disable=SC1091 +bssg_ram_timing_start "pages" "Static Pages" source "$SCRIPT_DIR/generate_pages.sh" || { echo -e "${RED}Error: Failed to source generate_pages.sh${NC}"; exit 1; } # Call the main page processing function process_all_pages || { echo -e "${RED}Error: Page processing failed.${NC}"; exit 1; } +bssg_ram_timing_end # --- Page Generation --- END --- # --- Tag Page Generation --- START --- # Source and run Tag Page Generator # shellcheck source=generate_tags.sh disable=SC1091 +bssg_ram_timing_start "tags" "Tags" source "$SCRIPT_DIR/generate_tags.sh" || { echo -e "${RED}Error: Failed to source generate_tags.sh${NC}"; exit 1; } # Call the main function from the sourced script generate_tag_pages || { echo -e "${RED}Error: Tag page generation failed.${NC}"; exit 1; } echo "Generated tag list pages." +bssg_ram_timing_end # --- Tag Page Generation --- END --- # --- Author Page Generation --- START --- # Source and run Author Page Generator (if enabled) if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then + bssg_ram_timing_start "authors" "Authors" # shellcheck source=generate_authors.sh disable=SC1091 source "$SCRIPT_DIR/generate_authors.sh" || { echo -e "${RED}Error: Failed to source generate_authors.sh${NC}"; exit 1; } @@ -344,12 +528,14 @@ if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then # It will internally use AFFECTED_AUTHORS and AUTHORS_INDEX_NEEDS_REBUILD generate_author_pages || { echo -e "${RED}Error: Author page generation failed.${NC}"; exit 1; } echo "Generated author pages." + bssg_ram_timing_end fi # --- Author Page Generation --- END --- # --- Archive Page Generation --- START --- # Source and run Archive Page Generator (if enabled) if [ "${ENABLE_ARCHIVES:-false}" = true ]; then + bssg_ram_timing_start "archives" "Archives" # Source the script (loads functions) # shellcheck source=generate_archives.sh disable=SC1091 source "$SCRIPT_DIR/generate_archives.sh" || { echo -e "${RED}Error: Failed to source generate_archives.sh${NC}"; exit 1; } @@ -358,28 +544,68 @@ if [ "${ENABLE_ARCHIVES:-false}" = true ]; then # It will internally use AFFECTED_ARCHIVE_MONTHS and ARCHIVE_INDEX_NEEDS_REBUILD generate_archive_pages || { echo -e "${RED}Error: Archive page generation failed.${NC}"; exit 1; } echo "Generated archive pages." + bssg_ram_timing_end fi # --- Archive Page Generation --- END --- # --- Main Index Page Generation --- START --- # Source and run Main Index Page Generator # shellcheck source=generate_index.sh disable=SC1091 +bssg_ram_timing_start "main_index" "Main Index" source "$SCRIPT_DIR/generate_index.sh" || { echo -e "${RED}Error: Failed to source generate_index.sh${NC}"; exit 1; } # Call the main function from the sourced script generate_index || { echo -e "${RED}Error: Index page generation failed.${NC}"; exit 1; } echo "Generated main index/pagination pages." +bssg_ram_timing_end # --- Main Index Page Generation --- END --- # --- Feed Generation --- START --- # Source and run Feed Generator # shellcheck source=generate_feeds.sh disable=SC1091 +bssg_ram_timing_start "feeds" "Sitemap/RSS" source "$SCRIPT_DIR/generate_feeds.sh" || { echo -e "${RED}Error: Failed to source generate_feeds.sh${NC}"; exit 1; } # Call the functions from the sourced script echo "Timing sitemap generation..." -generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure -echo "Timing RSS feed generation..." -generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure +if [ "${BSSG_RAM_MODE:-false}" = true ]; then + echo "Timing RSS feed generation..." + feed_jobs=0 + feed_jobs=$(get_parallel_jobs) + if [ "$feed_jobs" -gt 1 ]; then + echo "RAM mode: generating sitemap and RSS in parallel..." + + sitemap_failed=false + rss_failed=false + + generate_sitemap & + sitemap_pid=$! + + generate_rss & + rss_pid=$! + + if ! wait "$sitemap_pid"; then + sitemap_failed=true + fi + if ! wait "$rss_pid"; then + rss_failed=true + fi + + if $sitemap_failed; then + echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" + fi + if $rss_failed; then + echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" + fi + else + generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure + generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure + fi +else + generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure + echo "Timing RSS feed generation..." + generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure +fi echo "Generated RSS feed and sitemap." +bssg_ram_timing_end # --- Feed Generation --- END --- # --- Secondary Pages Index Generation --- START --- @@ -389,10 +615,12 @@ echo "Generated RSS feed and sitemap." # We attempt to reconstruct the array from the exported string. # shellcheck disable=SC2154 # SECONDARY_PAGES is exported by templates.sh if [ -n "$SECONDARY_PAGES" ] && [ "$SECONDARY_PAGES" != "()" ]; then + bssg_ram_timing_start "secondary_index" "Secondary Index" # shellcheck source=generate_secondary_pages.sh disable=SC1091 source "$SCRIPT_DIR/generate_secondary_pages.sh" || { echo -e "${RED}Error: Failed to source generate_secondary_pages.sh${NC}"; exit 1; } generate_pages_index || echo -e "${YELLOW}Secondary pages index generation failed, continuing build...${NC}" # Allow failure echo "Generated secondary pages index." + bssg_ram_timing_end else echo "No secondary pages defined, skipping secondary index generation." fi @@ -401,6 +629,7 @@ fi # --- Asset Handling --- START --- # Source the asset handling script # shellcheck source=assets.sh disable=SC1091 +bssg_ram_timing_start "assets" "Assets/CSS" source "$SCRIPT_DIR/assets.sh" || { echo -e "${RED}Error: Failed to source assets.sh${NC}"; exit 1; } # Copy static assets echo "Timing static files copy..." @@ -409,41 +638,60 @@ copy_static_files || { echo -e "${RED}Error: Failed to copy static assets.${NC}" echo "Timing CSS/Theme processing..." create_css "$OUTPUT_DIR" "$THEME" || { echo -e "${RED}Error: Failed to process CSS.${NC}"; exit 1; } # Pass OUTPUT_DIR and THEME echo "Handled static assets and CSS." +bssg_ram_timing_end # --- Asset Handling --- END --- # --- Post Processing --- START --- # Source and run Post Processor # shellcheck source=post_process.sh disable=SC1091 +bssg_ram_timing_start "post_process" "Post Processing" source "$SCRIPT_DIR/post_process.sh" || { echo -e "${RED}Error: Failed to source post_process.sh${NC}"; exit 1; } echo "Timing URL post-processing..." post_process_urls || echo -e "${YELLOW}URL post-processing failed, continuing...${NC}" # Allow failure echo "Timing output permissions fix..." fix_output_permissions || echo -e "${YELLOW}Fixing output permissions failed, continuing...${NC}" # Allow failure echo "Completed post-processing." +bssg_ram_timing_end # --- Post Processing --- END --- # --- Final Cache Update --- START --- -create_config_hash +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + create_config_hash +fi # --- Final Cache Update --- END --- # --- Final Cleanup --- START --- -echo "Cleaning up previous index files..." -rm -f "${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" -rm -f "${CACHE_DIR:-.bssg_cache}/tags_index_prev.txt" -rm -f "${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" -rm -f "${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt" +if [ "${BSSG_RAM_MODE:-false}" != true ]; then + echo "Cleaning up previous index files..." + rm -f "${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" + rm -f "${CACHE_DIR:-.bssg_cache}/tags_index_prev.txt" + rm -f "${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt" + rm -f "${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt" -# Remove the frontmatter changes marker if it exists -rm -f "${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" + # Remove the frontmatter changes marker if it exists + rm -f "${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" -# Clean up related posts temporary files to prevent unnecessary cache invalidation on next build -rm -f "${CACHE_DIR:-.bssg_cache}/modified_tags.list" -rm -f "${CACHE_DIR:-.bssg_cache}/modified_authors.list" -rm -f "${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list" + # Clean up related posts temporary files to prevent unnecessary cache invalidation on next build + rm -f "${CACHE_DIR:-.bssg_cache}/modified_tags.list" + rm -f "${CACHE_DIR:-.bssg_cache}/modified_authors.list" + rm -f "${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list" +fi # --- Final Cleanup --- END --- # --- Pre-compress Assets --- START --- +_precompress_single_file() { + local file="$1" + local gzfile="$2" + local compression_level="$3" + local verbose_logs="$4" + + if [ "$verbose_logs" = "true" ]; then + echo "Compressing: $file" + fi + gzip -c "-${compression_level}" -- "$file" > "$gzfile" +} + precompress_assets() { # Check if pre-compression is enabled in the config. if [ ! "${PRECOMPRESS_ASSETS:-false}" = "true" ]; then @@ -451,6 +699,11 @@ precompress_assets() { fi echo "Starting pre-compression of assets..." + local compression_level="${PRECOMPRESS_GZIP_LEVEL:-9}" + if ! [[ "$compression_level" =~ ^[1-9]$ ]]; then + compression_level=9 + fi + local verbose_logs="${PRECOMPRESS_VERBOSE:-${RAM_MODE_VERBOSE:-false}}" # 1. Cleanup: Remove any .gz file that does not have a corresponding original file. # This handles cases where original files were deleted. @@ -465,25 +718,63 @@ precompress_assets() { # 2. Compression: Compress text files if they are new or have been updated. # We target .html, .css, .xml and .js files. - find "${OUTPUT_DIR}" -type f \( -name "*.html" -o -name "*.css" -o -name "*.xml" -o -name "*.js" \) -print0 | while IFS= read -r -d '' file; do - gzfile="${file}.gz" + local changed_files=() + while IFS= read -r -d '' file; do + local gzfile="${file}.gz" # Compress if the .gz file doesn't exist, or if the original file is newer. if [ ! -f "$gzfile" ] || [ "$file" -nt "$gzfile" ]; then - echo "Compressing: $file" - # Use gzip with best compression (-9) and write to stdout, then redirect. - # This is a robust way to handle output and overwriting. - gzip -c -9 -- "$file" > "$gzfile" + changed_files+=("$file") fi - done + done < <(find "${OUTPUT_DIR}" -type f \( -name "*.html" -o -name "*.css" -o -name "*.xml" -o -name "*.js" \) -print0) + + if [ "${#changed_files[@]}" -eq 0 ]; then + echo "No changed assets to pre-compress." + echo "Asset pre-compression finished." + return + fi + + local compress_jobs + compress_jobs=$(get_parallel_jobs "${PRECOMPRESS_MAX_JOBS:-0}") + if [ "$compress_jobs" -gt "${#changed_files[@]}" ]; then + compress_jobs="${#changed_files[@]}" + fi + + if [ "$compress_jobs" -gt 1 ]; then + local file gzfile q_file q_gzfile q_level q_verbose + q_level=$(printf '%q' "$compression_level") + q_verbose=$(printf '%q' "$verbose_logs") + run_parallel "$compress_jobs" < <( + for file in "${changed_files[@]}"; do + gzfile="${file}.gz" + q_file=$(printf '%q' "$file") + q_gzfile=$(printf '%q' "$gzfile") + printf "_precompress_single_file %s %s %s %s\n" "$q_file" "$q_gzfile" "$q_level" "$q_verbose" + done + ) || { echo -e "${RED}Asset pre-compression failed.${NC}"; return 1; } + else + local file gzfile + for file in "${changed_files[@]}"; do + gzfile="${file}.gz" + _precompress_single_file "$file" "$gzfile" "$compression_level" "$verbose_logs" || { + echo -e "${RED}Asset pre-compression failed for ${file}.${NC}" + return 1 + } + done + fi + + echo "Pre-compressed ${#changed_files[@]} assets using ${compress_jobs} worker(s) (gzip -${compression_level})." echo "Asset pre-compression finished." } # Execute the asset compression. +bssg_ram_timing_start "precompress" "Pre-compress" precompress_assets +bssg_ram_timing_end # --- Pre-compress Assets --- END --- # --- Deployment --- START --- +bssg_ram_timing_start "deployment" "Deployment Decision/Run" deploy_now="false" if [[ "${CMD_DEPLOY_OVERRIDE:-unset}" == "true" ]]; then # Use default value for safety deploy_now="true" @@ -544,12 +835,15 @@ if [[ "$deploy_now" == "true" ]]; then echo -e "${YELLOW}Warning: Deployment was requested, but DEPLOY_SCRIPT is not set in configuration.${NC}" fi fi +bssg_ram_timing_end # --- Deployment --- END --- # --- End of execution --- BUILD_END_TIME=$(date +%s) BUILD_DURATION=$((BUILD_END_TIME - BUILD_START_TIME)) +bssg_ram_timing_print_summary echo "------------------------------------------------------" echo -e "${GREEN}Build process completed in ${BUILD_DURATION} seconds.${NC}" + exit 0 diff --git a/scripts/build/ram_mode.sh b/scripts/build/ram_mode.sh new file mode 100644 index 0000000..4d4e0a9 --- /dev/null +++ b/scripts/build/ram_mode.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# +# BSSG - RAM Build Helpers +# Preloads input content in memory and provides lookup helpers. +# + +# Guard against duplicate sourcing +if [[ -n "${BSSG_RAM_MODE_SCRIPT_LOADED:-}" ]]; then + return 0 +fi +export BSSG_RAM_MODE_SCRIPT_LOADED=1 + +# In-memory stores +declare -gA BSSG_RAM_FILE_CONTENT=() +declare -gA BSSG_RAM_FILE_MTIME=() +declare -gA BSSG_RAM_DATASET=() +declare -gA BSSG_RAM_BASENAME_KEY=() +declare -ga BSSG_RAM_SRC_FILES=() +declare -ga BSSG_RAM_PAGE_FILES=() +declare -ga BSSG_RAM_TEMPLATE_FILES=() + +ram_mode_enabled() { + [[ "${BSSG_RAM_MODE:-false}" == "true" ]] +} + +_ram_mode_disk_mtime() { + local file="$1" + local kernel_name + kernel_name=$(uname -s) + if [[ "$kernel_name" == "Darwin" ]] || [[ "$kernel_name" == *"BSD" ]]; then + stat -f "%m" "$file" 2>/dev/null || echo "0" + else + stat -c "%Y" "$file" 2>/dev/null || echo "0" + fi +} + +ram_mode_resolve_key() { + local file="$1" + if [[ -n "${BSSG_RAM_FILE_CONTENT[$file]+_}" || -n "${BSSG_RAM_FILE_MTIME[$file]+_}" ]]; then + echo "$file" + return 0 + fi + + if [[ "$file" == /* && -n "${BSSG_PROJECT_ROOT:-}" ]]; then + local prefix="${BSSG_PROJECT_ROOT%/}/" + if [[ "$file" == "$prefix"* ]]; then + local rel="${file#"$prefix"}" + if [[ -n "${BSSG_RAM_FILE_CONTENT[$rel]+_}" || -n "${BSSG_RAM_FILE_MTIME[$rel]+_}" ]]; then + echo "$rel" + return 0 + fi + fi + fi + + if [[ "$file" != */* && -n "${BSSG_RAM_BASENAME_KEY[$file]+_}" ]]; then + local mapped="${BSSG_RAM_BASENAME_KEY[$file]}" + if [[ "$mapped" != "__AMBIGUOUS__" ]]; then + echo "$mapped" + return 0 + fi + fi + + echo "$file" + return 0 +} + +ram_mode_has_file() { + local key + key=$(ram_mode_resolve_key "$1") + [[ -n "${BSSG_RAM_FILE_CONTENT[$key]+_}" || -n "${BSSG_RAM_FILE_MTIME[$key]+_}" ]] +} + +ram_mode_get_content() { + local key + key=$(ram_mode_resolve_key "$1") + if [[ -n "${BSSG_RAM_FILE_CONTENT[$key]+_}" ]]; then + printf '%s' "${BSSG_RAM_FILE_CONTENT[$key]}" + fi +} + +ram_mode_get_mtime() { + local key + key=$(ram_mode_resolve_key "$1") + if [[ -n "${BSSG_RAM_FILE_MTIME[$key]+_}" ]]; then + printf '%s\n' "${BSSG_RAM_FILE_MTIME[$key]}" + else + printf '0\n' + fi +} + +ram_mode_list_src_files() { + printf '%s\n' "${BSSG_RAM_SRC_FILES[@]}" +} + +ram_mode_list_page_files() { + printf '%s\n' "${BSSG_RAM_PAGE_FILES[@]}" +} + +ram_mode_set_dataset() { + local key="$1" + local value="$2" + BSSG_RAM_DATASET["$key"]="$value" +} + +ram_mode_get_dataset() { + local key="$1" + if [[ -n "${BSSG_RAM_DATASET[$key]+_}" ]]; then + printf '%s' "${BSSG_RAM_DATASET[$key]}" + fi +} + +ram_mode_clear_dataset() { + local key="$1" + unset 'BSSG_RAM_DATASET[$key]' +} + +ram_mode_dataset_line_count() { + local key="$1" + local data + data=$(ram_mode_get_dataset "$key") + if [[ -z "$data" ]]; then + echo "0" + return 0 + fi + printf '%s\n' "$data" | awk 'NF { c++ } END { print c+0 }' +} + +_ram_mode_store_file() { + local file="$1" + [[ -f "$file" ]] || return 0 + + local file_content + file_content=$(cat "$file") + BSSG_RAM_FILE_CONTENT["$file"]="$file_content" + BSSG_RAM_FILE_MTIME["$file"]="$(_ram_mode_disk_mtime "$file")" + + local base + base=$(basename "$file") + if [[ -z "${BSSG_RAM_BASENAME_KEY[$base]+_}" ]]; then + BSSG_RAM_BASENAME_KEY["$base"]="$file" + elif [[ "${BSSG_RAM_BASENAME_KEY[$base]}" != "$file" ]]; then + BSSG_RAM_BASENAME_KEY["$base"]="__AMBIGUOUS__" + fi +} + +_ram_mode_collect_content_files() { + local dir="$1" + [[ -d "$dir" ]] || return 0 + find "$dir" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort +} + +_ram_mode_collect_template_files() { + local dir="$1" + [[ -d "$dir" ]] || return 0 + find "$dir" -type f -name "*.html" -not -path "*/.*" | sort +} + +ram_mode_preload_inputs() { + if ! ram_mode_enabled; then + return 0 + fi + + BSSG_RAM_FILE_CONTENT=() + BSSG_RAM_FILE_MTIME=() + BSSG_RAM_DATASET=() + BSSG_RAM_BASENAME_KEY=() + BSSG_RAM_SRC_FILES=() + BSSG_RAM_PAGE_FILES=() + BSSG_RAM_TEMPLATE_FILES=() + + local file + while IFS= read -r file; do + [[ -z "$file" ]] && continue + BSSG_RAM_SRC_FILES+=("$file") + _ram_mode_store_file "$file" + done < <(_ram_mode_collect_content_files "${SRC_DIR:-src}") + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + BSSG_RAM_PAGE_FILES+=("$file") + _ram_mode_store_file "$file" + done < <(_ram_mode_collect_content_files "${PAGES_DIR:-pages}") + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + BSSG_RAM_TEMPLATE_FILES+=("$file") + _ram_mode_store_file "$file" + done < <(_ram_mode_collect_template_files "${TEMPLATES_DIR:-templates}") + + # Preload active locale (and fallback locale) so date/menu rendering avoids disk reads. + if [[ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]]; then + _ram_mode_store_file "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" + fi + if [[ -f "${LOCALE_DIR:-locales}/en.sh" ]]; then + _ram_mode_store_file "${LOCALE_DIR:-locales}/en.sh" + fi + + print_info "RAM mode preloaded ${#BSSG_RAM_FILE_CONTENT[@]} text files (${#BSSG_RAM_SRC_FILES[@]} posts, ${#BSSG_RAM_PAGE_FILES[@]} pages)." +} + +export -f ram_mode_enabled ram_mode_resolve_key ram_mode_has_file ram_mode_get_content ram_mode_get_mtime +export -f ram_mode_list_src_files ram_mode_list_page_files ram_mode_preload_inputs +export -f ram_mode_set_dataset ram_mode_get_dataset ram_mode_clear_dataset +export -f ram_mode_dataset_line_count diff --git a/scripts/build/related_posts.sh b/scripts/build/related_posts.sh index 8fe0b86..e071499 100644 --- a/scripts/build/related_posts.sh +++ b/scripts/build/related_posts.sh @@ -12,6 +12,169 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s # --- Related Posts Functions --- START --- +declare -gA BSSG_RAM_RELATED_POSTS_HTML=() +declare -g BSSG_RAM_RELATED_POSTS_READY=false +declare -g BSSG_RAM_RELATED_POSTS_LIMIT="" + +_build_post_url_from_date_slug() { + local post_date="$1" + local post_slug="$2" + local post_year post_month post_day + + if [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then + post_year="${BASH_REMATCH[1]}" + post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") + post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") + else + post_year=$(date +%Y) + post_month=$(date +%m) + post_day=$(date +%d) + fi + + local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}" + url_path="${url_path//Year/$post_year}" + url_path="${url_path//Month/$post_month}" + url_path="${url_path//Day/$post_day}" + url_path="${url_path//slug/$post_slug}" + printf '/%s/\n' "$url_path" +} + +_build_ram_related_posts_cache() { + local max_results="${1:-3}" + local file_index_data + file_index_data=$(ram_mode_get_dataset "file_index") + + BSSG_RAM_RELATED_POSTS_HTML=() + BSSG_RAM_RELATED_POSTS_READY=true + BSSG_RAM_RELATED_POSTS_LIMIT="$max_results" + + [ -z "$file_index_data" ] && return 0 + + local scored_results="" + scored_results=$(printf '%s\n' "$file_index_data" | awk -F'|' ' + function trim(s) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", s) + return s + } + + { + n++ + title[n] = $3 + date[n] = $4 + tags_raw[n] = $6 + slug[n] = $7 + desc[n] = $10 + + split(tags_raw[n], tag_arr, ",") + for (k in tag_arr) { + t = trim(tag_arr[k]) + if (t != "") { + tags[n SUBSEP t] = 1 + } + } + } + + END { + for (i = 1; i <= n; i++) { + if (slug[i] == "" || tags_raw[i] == "") { + continue + } + + split(tags_raw[i], i_tags, ",") + for (j = 1; j <= n; j++) { + if (i == j || slug[j] == "" || date[j] == "" || tags_raw[j] == "") { + continue + } + + score = 0 + delete seen + for (k in i_tags) { + t = trim(i_tags[k]) + if (t == "" || seen[t]) { + continue + } + seen[t] = 1 + if (tags[j SUBSEP t]) { + score++ + } + } + + if (score > 0) { + printf "%s|%d|%s|%s|%s|%s\n", slug[i], score, date[j], title[j], slug[j], desc[j] + } + } + } + } + ' | sort -t'|' -k1,1 -k2,2nr -k3,3r) + + [ -z "$scored_results" ] && return 0 + + local current_slug="" current_count=0 + local html_output="" + local slug score date title related_slug description + + while IFS='|' read -r slug score date title related_slug description; do + [ -z "$slug" ] && continue + + if [ "$slug" != "$current_slug" ]; then + if [ -n "$current_slug" ] && [ "$current_count" -gt 0 ]; then + html_output+=''$'\n' + html_output+=''$'\n' + BSSG_RAM_RELATED_POSTS_HTML["$current_slug"]="$html_output" + fi + current_slug="$slug" + current_count=0 + html_output="" + fi + + if [ "$current_count" -ge "$max_results" ]; then + continue + fi + + local post_url + post_url=$(_build_post_url_from_date_slug "$date" "$related_slug") + + local short_desc="$description" + if [[ ${#short_desc} -gt 120 ]]; then + short_desc="${short_desc:0:117}..." + fi + + if [ "$current_count" -eq 0 ]; then + html_output+=''$'\n' + BSSG_RAM_RELATED_POSTS_HTML["$current_slug"]="$html_output" + fi +} + +prepare_related_posts_ram_cache() { + local max_results="${1:-3}" + if [ "${BSSG_RAM_MODE:-false}" != true ]; then + return 0 + fi + + if [ "$BSSG_RAM_RELATED_POSTS_READY" = true ] && [ "$BSSG_RAM_RELATED_POSTS_LIMIT" = "$max_results" ]; then + return 0 + fi + + _build_ram_related_posts_cache "$max_results" +} + # Generate related posts for a given post based on shared tags # Args: $1=current_post_slug $2=current_post_tags $3=current_post_date $4=max_results (optional, default=3) # Returns: HTML snippet with related posts @@ -26,6 +189,17 @@ generate_related_posts() { return 0 # No related posts if missing essential data fi + # RAM mode uses a precomputed in-memory map to avoid repeated O(n^2) scans. + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + if [ "$BSSG_RAM_RELATED_POSTS_READY" != true ] || [ "$BSSG_RAM_RELATED_POSTS_LIMIT" != "$max_results" ]; then + _build_ram_related_posts_cache "$max_results" + fi + if [[ -n "${BSSG_RAM_RELATED_POSTS_HTML[$current_slug]+_}" ]]; then + printf '%s' "${BSSG_RAM_RELATED_POSTS_HTML[$current_slug]}" + fi + return 0 + fi + # Check cache first local cache_file="${CACHE_DIR:-.bssg_cache}/related_posts/${current_slug}.html" local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" @@ -60,8 +234,18 @@ compute_related_posts() { local max_results="$4" local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt" + local file_index_data="" + local ram_mode_active=false + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_active=true + file_index_data=$(ram_mode_get_dataset "file_index") + fi - if [[ ! -f "$file_index" ]]; then + if $ram_mode_active; then + if [[ -z "$file_index_data" ]]; then + return 0 + fi + elif [[ ! -f "$file_index" ]]; then return 0 # No posts to compare against fi @@ -81,7 +265,7 @@ compute_related_posts() { fi # Process all posts and calculate similarity scores - local temp_results=$(mktemp) + local temp_results="" while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do # Skip current post @@ -113,17 +297,22 @@ compute_related_posts() { # Only consider posts with at least one shared tag if [[ $score -gt 0 ]]; then # Store: score|date|title|slug|description - echo "${score}|${date}|${title}|${slug}|${description}" >> "$temp_results" + temp_results+="${score}|${date}|${title}|${slug}|${description}"$'\n' fi - done < "$file_index" + done < <( + if $ram_mode_active; then + printf '%s\n' "$file_index_data" | awk 'NF' + else + cat "$file_index" + fi + ) # Sort by score (descending), then by date (descending), limit results local sorted_results="" - if [[ -s "$temp_results" ]]; then - sorted_results=$(sort -t'|' -k1,1nr -k2,2r "$temp_results" | head -n "$max_results") + if [[ -n "$temp_results" ]]; then + sorted_results=$(printf '%s\n' "$temp_results" | awk 'NF' | sort -t'|' -k1,1nr -k2,2r | head -n "$max_results") fi - rm -f "$temp_results" # Generate HTML output if [[ -z "$sorted_results" ]]; then @@ -248,4 +437,5 @@ invalidate_related_posts_cache_for_tags() { # --- Related Posts Functions --- END --- # Export functions for use by other scripts -export -f generate_related_posts compute_related_posts clean_related_posts_cache invalidate_related_posts_cache_for_tags \ No newline at end of file +export -f generate_related_posts compute_related_posts clean_related_posts_cache invalidate_related_posts_cache_for_tags +export -f prepare_related_posts_ram_cache diff --git a/scripts/build/templates.sh b/scripts/build/templates.sh index d12f184..baef671 100755 --- a/scripts/build/templates.sh +++ b/scripts/build/templates.sh @@ -63,7 +63,9 @@ load_template() { # Function to pre-load all templates and process menus/placeholders preload_templates() { # Create template cache directory if it doesn't exist - mkdir -p "$TEMPLATE_CACHE_DIR" + if [ "${BSSG_RAM_MODE:-false}" != true ]; then + mkdir -p "$TEMPLATE_CACHE_DIR" + fi local template_dir local templates_to_load=("header.html" "footer.html" "post.html" "page.html" "index.html" "tag.html" "archive.html") @@ -131,8 +133,12 @@ preload_templates() { # Scan pages directory for markdown and HTML files if [ -d "${PAGES_DIR:-pages}" ]; then - local page_files - page_files=($(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) | sort)) + local page_files=() + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_list_page_files > /dev/null; then + mapfile -t page_files < <(ram_mode_list_page_files) + else + page_files=($(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) | sort)) + fi for file in "${page_files[@]}"; do # Skip if file is hidden @@ -144,10 +150,19 @@ preload_templates() { local title slug date secondary if [[ "$file" == *.html ]]; then # Crude HTML parsing - assumes specific meta tags exist - title=$(grep -m 1 '' "$file" 2>/dev/null | sed 's/<[^>]*>//g') - slug=$(grep -m 1 'meta name="slug"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') - date=$(grep -m 1 'meta name="date"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') # Extract date from meta - secondary=$(grep -m 1 'meta name="secondary"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + local html_source="" + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then + html_source=$(ram_mode_get_content "$file") + title=$(printf '%s\n' "$html_source" | grep -m 1 '<title>' 2>/dev/null | sed 's/<[^>]*>//g') + slug=$(printf '%s\n' "$html_source" | grep -m 1 'meta name="slug"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + date=$(printf '%s\n' "$html_source" | grep -m 1 'meta name="date"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + secondary=$(printf '%s\n' "$html_source" | grep -m 1 'meta name="secondary"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + else + title=$(grep -m 1 '<title>' "$file" 2>/dev/null | sed 's/<[^>]*>//g') + slug=$(grep -m 1 'meta name="slug"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + date=$(grep -m 1 'meta name="date"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') # Extract date from meta + secondary=$(grep -m 1 'meta name="secondary"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + fi else # Assumes parse_metadata is available title=$(parse_metadata "$file" "title") @@ -206,18 +221,33 @@ preload_templates() { # Add standard menu items local tags_flag_file="${CACHE_DIR:-.bssg_cache}/has_tags.flag" - # Add tags link only if the flag file exists (meaning tags were found in the last indexing run) - if [ -f "$tags_flag_file" ]; then + local has_tags=false + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + [ -n "$(ram_mode_get_dataset "has_tags")" ] && has_tags=true + elif [ -f "$tags_flag_file" ]; then + has_tags=true + fi + # Add tags link only if tags are present. + if [ "$has_tags" = true ]; then menu_items+=" <a href=\"${SITE_URL}/tags/\">${MSG_TAGS:-"Tags"}</a>" fi # Add Authors link if enabled and multiple authors exist local authors_flag_file="${CACHE_DIR:-.bssg_cache}/has_authors.flag" - if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ] && [ -f "$authors_flag_file" ]; then + if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then # Check if we have multiple authors (more than the threshold) local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt" - if [ -f "$authors_index_file" ]; then - local unique_author_count=$(awk -F'|' '{print $1}' "$authors_index_file" | sort -u | wc -l) + local unique_author_count=0 + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local authors_index_data + authors_index_data=$(ram_mode_get_dataset "authors_index") + if [ -n "$authors_index_data" ]; then + unique_author_count=$(printf '%s\n' "$authors_index_data" | awk -F'|' 'NF { print $1 }' | sort -u | wc -l | tr -d ' ') + fi + elif [ -f "$authors_index_file" ] && [ -f "$authors_flag_file" ]; then + unique_author_count=$(awk -F'|' '{print $1}' "$authors_index_file" | sort -u | wc -l) + fi + if [ "$unique_author_count" -gt 0 ]; then local threshold="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" if [ "$unique_author_count" -ge "$threshold" ]; then menu_items+=" <a href=\"${SITE_URL}/authors/\">${MSG_AUTHORS:-"Authors"}</a>" @@ -233,14 +263,23 @@ preload_templates() { menu_items+=" <a href=\"${SITE_URL}/${RSS_FILENAME:-rss.xml}\">${MSG_RSS:-"RSS"}</a>" # Add tags link to footer only if the flag file exists - if [ -f "$tags_flag_file" ]; then + if [ "$has_tags" = true ]; then footer_items+=" <a href=\"${SITE_URL}/tags/\">${MSG_TAGS:-"Tags"}</a> ·" fi # Add Authors link to footer if enabled and multiple authors exist - if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ] && [ -f "$authors_flag_file" ]; then - if [ -f "$authors_index_file" ]; then - local unique_author_count_footer=$(awk -F'|' '{print $1}' "$authors_index_file" | sort -u | wc -l) + if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then + local unique_author_count_footer=0 + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local authors_index_data_footer + authors_index_data_footer=$(ram_mode_get_dataset "authors_index") + if [ -n "$authors_index_data_footer" ]; then + unique_author_count_footer=$(printf '%s\n' "$authors_index_data_footer" | awk -F'|' 'NF { print $1 }' | sort -u | wc -l | tr -d ' ') + fi + elif [ -f "$authors_index_file" ] && [ -f "$authors_flag_file" ]; then + unique_author_count_footer=$(awk -F'|' '{print $1}' "$authors_index_file" | sort -u | wc -l) + fi + if [ "$unique_author_count_footer" -gt 0 ]; then local threshold_footer="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" if [ "$unique_author_count_footer" -ge "$threshold_footer" ]; then footer_items+=" <a href=\"${SITE_URL}/authors/\">${MSG_AUTHORS:-"Authors"}</a> ·" @@ -299,50 +338,55 @@ preload_templates() { HEADER_TEMPLATE=$(echo "$HEADER_TEMPLATE" | sed "s|{{[[:space:]]*custom_css_link[[:space:]]*}}|${custom_css_tag}|") # --- Handle Custom CSS --- END --- - # Write primary and secondary page lists to cache files only if changed - local primary_pages_cache="$CACHE_DIR/primary_pages.tmp" - local secondary_pages_cache="$CACHE_DIR/secondary_pages.tmp" - local secondary_pages_list_file="$CACHE_DIR/secondary_pages.list" # <-- Define list file path - - # Prepare content in temporary files - local primary_tmp=$(mktemp) - local secondary_tmp=$(mktemp) - local secondary_list_tmp=$(mktemp) # <-- Temp file for the list - - # Write current content to temporary files - # Use printf for safer writing - for page in "${primary_pages[@]}"; do - printf "%s\n" "$page" >> "$primary_tmp" - done - for page in "${SECONDARY_PAGES[@]}"; do - # Write to the temp file for comparison - printf "%s\n" "$page" >> "$secondary_tmp" - # Also write to the list temp file, one per line - printf "%s\n" "$page" >> "$secondary_list_tmp" - done + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + ram_mode_set_dataset "primary_pages" "$(printf '%s\n' "${primary_pages[@]}")" + ram_mode_set_dataset "secondary_pages" "$(printf '%s\n' "${SECONDARY_PAGES[@]}")" + else + # Write primary and secondary page lists to cache files only if changed + local primary_pages_cache="$CACHE_DIR/primary_pages.tmp" + local secondary_pages_cache="$CACHE_DIR/secondary_pages.tmp" + local secondary_pages_list_file="$CACHE_DIR/secondary_pages.list" # <-- Define list file path + + # Prepare content in temporary files + local primary_tmp=$(mktemp) + local secondary_tmp=$(mktemp) + local secondary_list_tmp=$(mktemp) # <-- Temp file for the list + + # Write current content to temporary files + # Use printf for safer writing + for page in "${primary_pages[@]}"; do + printf "%s\n" "$page" >> "$primary_tmp" + done + for page in "${SECONDARY_PAGES[@]}"; do + # Write to the temp file for comparison + printf "%s\n" "$page" >> "$secondary_tmp" + # Also write to the list temp file, one per line + printf "%s\n" "$page" >> "$secondary_list_tmp" + done - # Function to compare and update cache file - update_cache_if_changed() { - local temp_file="$1" - local cache_file="$2" - local file_desc="$3" + # Function to compare and update cache file + update_cache_if_changed() { + local temp_file="$1" + local cache_file="$2" + local file_desc="$3" - if [ ! -f "$cache_file" ] || ! cmp -s "$temp_file" "$cache_file"; then - mv "$temp_file" "$cache_file" - # echo "DEBUG: Updated $file_desc cache file." # Optional debug - else - rm "$temp_file" - # echo "DEBUG: $file_desc cache file unchanged." # Optional debug - fi - } + if [ ! -f "$cache_file" ] || ! cmp -s "$temp_file" "$cache_file"; then + mv "$temp_file" "$cache_file" + # echo "DEBUG: Updated $file_desc cache file." # Optional debug + else + rm "$temp_file" + # echo "DEBUG: $file_desc cache file unchanged." # Optional debug + fi + } - # Compare and update cache files - update_cache_if_changed "$primary_tmp" "$primary_pages_cache" - update_cache_if_changed "$secondary_tmp" "$secondary_pages_cache" - update_cache_if_changed "$secondary_list_tmp" "$secondary_pages_list_file" # <-- Update the list file + # Compare and update cache files + update_cache_if_changed "$primary_tmp" "$primary_pages_cache" + update_cache_if_changed "$secondary_tmp" "$secondary_pages_cache" + update_cache_if_changed "$secondary_list_tmp" "$secondary_pages_list_file" # <-- Update the list file - # Clean up temporary files - rm -f "$primary_tmp" "$secondary_tmp" "$secondary_list_tmp" # <-- Cleanup list temp file + # Clean up temporary files + rm -f "$primary_tmp" "$secondary_tmp" "$secondary_list_tmp" # <-- Cleanup list temp file + fi echo -e "${GREEN}Templates pre-processed (menus, locale placeholders).${NC}" } @@ -364,4 +408,4 @@ export FOOTER_TEMPLATE # Export functions - Do not export the SECONDARY_PAGES array itself anymore export -f preload_templates -# export SECONDARY_PAGES # <-- Remove this export \ No newline at end of file +# export SECONDARY_PAGES # <-- Remove this export diff --git a/scripts/build/utils.sh b/scripts/build/utils.sh index 80ad813..dd9f0a5 100755 --- a/scripts/build/utils.sh +++ b/scripts/build/utils.sh @@ -19,6 +19,31 @@ else NC="" fi +# Cache kernel name once to avoid repeated `uname` calls in hot paths. +if [ -z "${BSSG_KERNEL_NAME:-}" ]; then + BSSG_KERNEL_NAME="$(uname -s 2>/dev/null || echo "")" +fi + +# Cache repeated date formatting work across stages in the same process. +declare -gA BSSG_FORMAT_DATE_CACHE=() +declare -gA BSSG_FORMAT_DATE_TS_CACHE=() + +# GNU parallel workers import functions, but array declarations may not carry over. +# Keep date caches associative in every process to avoid bad-subscript errors. +_bssg_ensure_assoc_cache() { + local var_name="$1" + local var_decl + + var_decl=$(declare -p "$var_name" 2>/dev/null || true) + if [[ "$var_decl" == declare\ -A* ]]; then + return 0 + fi + + unset "$var_name" 2>/dev/null || true + declare -gA "$var_name" + eval "$var_name=()" +} + # --- Printing Functions --- START --- print_error() { # Print message in red to stderr @@ -69,7 +94,10 @@ format_date() { local format_override="$2" # Optional format string local target_format=${format_override:-"$DATE_FORMAT"} # Use override or global DATE_FORMAT local formatted_date - local kernel_name=$(uname -s) # Get kernel name (e.g., Linux, Darwin, FreeBSD) + local kernel_name="${BSSG_KERNEL_NAME:-}" + if [ -z "$kernel_name" ]; then + kernel_name="$(uname -s)" + fi # Skip formatting if date is empty if [ -z "$input_date" ]; then @@ -91,27 +119,46 @@ format_date() { return fi + _bssg_ensure_assoc_cache "BSSG_FORMAT_DATE_CACHE" + + # Use cached values for stable (non-"now") inputs. + local cache_tz="${TIMEZONE:-local}" + local cache_key="${cache_tz}|${target_format}|${input_date}" + if [[ -n "${BSSG_FORMAT_DATE_CACHE[$cache_key]+_}" ]]; then + echo "${BSSG_FORMAT_DATE_CACHE[$cache_key]}" + return + fi + # Try to format the date using the configured format # IMPORTANT: DATE_FORMAT must be exported or sourced *before* calling this if [[ "$kernel_name" == "Darwin" ]] || [[ "$kernel_name" == *"BSD" ]]; then # macOS/BSD date formatting (uses date -j -f) - # IMPORTANT: Using ISO 8601 format (YYYY-MM-DD HH:MM:SS) in source - # files is strongly recommended for portability. - - # Try parsing full ISO date-time first - formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date\" +\"$target_format\"" 2>/dev/null) - - # If failed, try RFC2822 format - if [ -z "$formatted_date" ]; then + # Fast-path common stable inputs to avoid multiple failed parse attempts. + if [[ "$input_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d\" \"$input_date\" +\"$target_format\"" 2>/dev/null) + elif [[ "$input_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]][0-9]{2}:[0-9]{2}:[0-9]{2}$ ]]; then + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date\" +\"$target_format\"" 2>/dev/null) + elif [[ "$input_date" =~ ^[A-Za-z]{3},[[:space:]][0-9]{2}[[:space:]][A-Za-z]{3}[[:space:]][0-9]{4}[[:space:]][0-9]{2}:[0-9]{2}:[0-9]{2}[[:space:]][+-][0-9]{4}$ ]]; then formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%a, %d %b %Y %H:%M:%S %z\" \"$input_date\" +\"$target_format\"" 2>/dev/null) fi - # If still failed, try parsing date-only (YYYY-MM-DD) and assume midnight + # Fallback parser chain for uncommon/legacy input variants. if [ -z "$formatted_date" ]; then - # Check if input looks like YYYY-MM-DD using shell pattern matching - if [[ "$input_date" == [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then - # Try parsing by appending midnight time - formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date 00:00:00\" +\"$target_format\"" 2>/dev/null) + # Try parsing full ISO date-time first + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date\" +\"$target_format\"" 2>/dev/null) + + # If failed, try RFC2822 format + if [ -z "$formatted_date" ]; then + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%a, %d %b %Y %H:%M:%S %z\" \"$input_date\" +\"$target_format\"" 2>/dev/null) + fi + + # If still failed, try parsing date-only (YYYY-MM-DD) and assume midnight + if [ -z "$formatted_date" ]; then + # Check if input looks like YYYY-MM-DD using shell pattern matching + if [[ "$input_date" == [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then + # Try parsing by appending midnight time + formatted_date=$(eval "${tz_prefix}LC_ALL=C date -j -f \"%Y-%m-%d %H:%M:%S\" \"$input_date 00:00:00\" +\"$target_format\"" 2>/dev/null) + fi fi fi @@ -125,6 +172,7 @@ format_date() { formatted_date=$(eval "${tz_prefix}LC_ALL=C date -d \"$input_date\" +\"$target_format\"" 2>/dev/null || echo "$input_date") fi + BSSG_FORMAT_DATE_CACHE["$cache_key"]="$formatted_date" echo "$formatted_date" } @@ -141,6 +189,16 @@ format_date_from_timestamp() { return fi + _bssg_ensure_assoc_cache "BSSG_FORMAT_DATE_TS_CACHE" + + # Cache by timestamp/format/timezone. + local cache_tz="${TIMEZONE:-local}" + local cache_key="${cache_tz}|${target_format}|${timestamp}" + if [[ -n "${BSSG_FORMAT_DATE_TS_CACHE[$cache_key]+_}" ]]; then + echo "${BSSG_FORMAT_DATE_TS_CACHE[$cache_key]}" + return + fi + # Set TZ environment variable if TIMEZONE is set and not "local" local tz_prefix="" if [ -n "${TIMEZONE:-}" ] && [ "${TIMEZONE:-local}" != "local" ]; then @@ -159,6 +217,7 @@ format_date_from_timestamp() { formatted_date=$(eval "${tz_prefix}LC_ALL=C date -d \"@$timestamp\" +\"$target_format\"" 2>/dev/null || echo "") fi + BSSG_FORMAT_DATE_TS_CACHE["$cache_key"]="$formatted_date" echo "$formatted_date" } @@ -226,7 +285,21 @@ unlock_file() { # Get file modification time in a portable way get_file_mtime() { local file="$1" - local kernel_name=$(uname -s) + local kernel_name="${BSSG_KERNEL_NAME:-}" + + # In RAM mode, prefer preloaded input timestamps. + if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_get_mtime > /dev/null; then + local ram_mtime + ram_mtime=$(ram_mode_get_mtime "$file") + if [ -n "$ram_mtime" ] && [ "$ram_mtime" != "0" ]; then + echo "$ram_mtime" + return 0 + fi + fi + + if [ -z "$kernel_name" ]; then + kernel_name="$(uname -s)" + fi # Use specific stat flags based on kernel name # %m for BSD/macOS (seconds since Epoch) @@ -242,58 +315,108 @@ get_file_mtime() { # Fallback parallel implementation using background processes # Used when GNU parallel is not available +detect_cpu_cores() { + if command -v nproc > /dev/null 2>&1; then + nproc + elif command -v sysctl > /dev/null 2>&1; then + sysctl -n hw.ncpu 2>/dev/null || echo 1 + else + echo 2 + fi +} + +# Determine worker count. +# In RAM mode we cap concurrency by default to reduce memory pressure from +# large inherited in-memory arrays in each worker process. +get_parallel_jobs() { + local requested_jobs="$1" + local jobs=0 + + if [[ "$requested_jobs" =~ ^[0-9]+$ ]] && [ "$requested_jobs" -gt 0 ]; then + jobs="$requested_jobs" + else + jobs=$(detect_cpu_cores) + fi + + if [ "${BSSG_RAM_MODE:-false}" = true ]; then + local ram_cap="${RAM_MODE_MAX_JOBS:-6}" + if ! [[ "$ram_cap" =~ ^[0-9]+$ ]] || [ "$ram_cap" -lt 1 ]; then + ram_cap=6 + fi + if [ "$jobs" -gt "$ram_cap" ]; then + jobs="$ram_cap" + fi + fi + + if [ "$jobs" -lt 1 ]; then + jobs=1 + fi + + echo "$jobs" +} + run_parallel() { local max_jobs="$1" shift - if [ -z "$max_jobs" ] || [ "$max_jobs" -lt 1 ]; then - # Determine number of CPU cores if not specified - if command -v nproc > /dev/null 2>&1; then - # Linux - max_jobs=$(nproc) - elif command -v sysctl > /dev/null 2>&1; then - # macOS, BSD - max_jobs=$(sysctl -n hw.ncpu 2>/dev/null || echo 1) - else - # Default to 2 jobs if we can't determine - max_jobs=2 - fi + max_jobs=$(get_parallel_jobs "$max_jobs") + + local had_error=0 + local wait_n_supported=0 + if [[ ${BASH_VERSINFO[0]:-0} -gt 4 ]] || { [[ ${BASH_VERSINFO[0]:-0} -eq 4 ]] && [[ ${BASH_VERSINFO[1]:-0} -ge 3 ]]; }; then + wait_n_supported=1 fi - local job_count=0 - local pids=() + if [ "$wait_n_supported" -eq 1 ]; then + local running_jobs=0 - # Read commands from stdin - while read -r cmd; do - # Skip empty lines - [ -z "$cmd" ] && continue + while read -r cmd; do + [ -z "$cmd" ] && continue - # If we've reached max jobs, wait for one to finish - if [ $job_count -ge $max_jobs ]; then - # Wait for any child process to finish - wait -n 2>/dev/null || true - - # Cleanup finished jobs from pids array - local new_pids=() - for pid in "${pids[@]}"; do - if kill -0 $pid 2>/dev/null; then - new_pids+=($pid) + while [ "$running_jobs" -ge "$max_jobs" ]; do + if ! wait -n 2>/dev/null; then + had_error=1 fi + running_jobs=$((running_jobs - 1)) done - pids=("${new_pids[@]}") - # Update job count - job_count=${#pids[@]} - fi + (eval "$cmd") & + running_jobs=$((running_jobs + 1)) + done - # Run the command in the background - (eval "$cmd") & - pids+=($!) - job_count=$((job_count + 1)) - done + while [ "$running_jobs" -gt 0 ]; do + if ! wait -n 2>/dev/null; then + had_error=1 + fi + running_jobs=$((running_jobs - 1)) + done + else + # Portable fallback for older bash without wait -n. + local pids=() + while read -r cmd; do + [ -z "$cmd" ] && continue - # Wait for all remaining jobs to finish - wait + while [ "${#pids[@]}" -ge "$max_jobs" ]; do + local oldest_pid="${pids[0]}" + if ! wait "$oldest_pid"; then + had_error=1 + fi + pids=("${pids[@]:1}") + done + + (eval "$cmd") & + pids+=($!) + done + + local pid + for pid in "${pids[@]}"; do + if ! wait "$pid"; then + had_error=1 + fi + done + fi + + return "$had_error" } # Add a reading time calculation function @@ -333,6 +456,8 @@ export -f generate_slug export -f lock_file export -f unlock_file export -f get_file_mtime +export -f detect_cpu_cores +export -f get_parallel_jobs export -f run_parallel export -f calculate_reading_time export -f html_escape @@ -340,4 +465,5 @@ export -f html_escape export -f print_error export -f print_warning export -f print_success -export -f print_info \ No newline at end of file +export -f print_info +export -f _bssg_ensure_assoc_cache From 0a5f9e20a3302f1da973fce3907611838d822ede Mon Sep 17 00:00:00 2001 From: Stefano Marinelli <stefano@dragas.it> Date: Sat, 14 Mar 2026 09:20:47 +0100 Subject: [PATCH 17/21] Added new themes - some somehow inspired by the BSDs (FreeBSD, NetBSD, OpenBSD), field-journal, microfiche, museum-label Optimized the "generate_theme_previews.sh" to build the site once, then copy the css files. This will increase the example directory build time. --- README.md | 8 + generate_theme_previews.sh | 118 ++++++- themes/field-journal/style.css | 462 +++++++++++++++++++++++++ themes/freebsd/style.css | 517 ++++++++++++++++++++++++++++ themes/liquid-glass/style.css | 611 +++++++++++++++++++++++++++++++++ themes/microfiche/style.css | 499 +++++++++++++++++++++++++++ themes/museum-label/style.css | 447 ++++++++++++++++++++++++ themes/mynotes/style.css | 574 +++++++++++++++++++++++++++++++ themes/netbsd/style.css | 531 ++++++++++++++++++++++++++++ themes/openbsd/style.css | 531 ++++++++++++++++++++++++++++ 10 files changed, 4297 insertions(+), 1 deletion(-) create mode 100644 themes/field-journal/style.css create mode 100644 themes/freebsd/style.css create mode 100644 themes/liquid-glass/style.css create mode 100644 themes/microfiche/style.css create mode 100644 themes/museum-label/style.css create mode 100644 themes/mynotes/style.css create mode 100644 themes/netbsd/style.css create mode 100644 themes/openbsd/style.css diff --git a/README.md b/README.md index be623f5..609b02d 100644 --- a/README.md +++ b/README.md @@ -894,6 +894,7 @@ BSSG includes a variety of themes to customize the look of your site. Themes are - `dark` - Dark mode theme - `flat` - Microsoft Metro/Modern UI inspired flat design - `glassmorphism` - Modern frosted glass effect with blue/teal gradient +- `liquid-glass` - Fluid translucent surfaces with refractive highlights and layered depth - `material` - Material Design inspired theme - `art-deco` - Inspired by 1920s-30s Art Deco style with geometric patterns, elegant fonts, and gold/black/silver/jewel color palettes - `bauhaus` - Inspired by the Bauhaus school, focusing on functionality, primary geometric shapes, primary colors plus black and white, and clean sans-serif typography @@ -918,9 +919,12 @@ BSSG includes a variety of themes to customize the look of your site. Themes are #### Operating System Themes - `beos` - BeOS inspired theme +- `freebsd` - FreeBSD-inspired theme with the iconic red/black visual language and orb motif - `macclassic` - Classic Mac OS inspired theme - `macos9` - Mac OS 9 inspired theme +- `netbsd` - NetBSD-inspired theme with a navy/orange flag aesthetic and engineering-focused layout - `nextstep` - NeXTSTEP inspired theme +- `openbsd` - OpenBSD-inspired yellow/black Puffy-style theme with bold security-flavored styling - `osx` - macOS inspired theme - `win311` - Windows 3.11 inspired theme - `win95` - Windows 95 inspired theme @@ -938,6 +942,9 @@ BSSG includes a variety of themes to customize the look of your site. Themes are - `docs` - A clean, structured theme ideal for technical documentation with excellent code formatting and clear navigation - `longform` - Optimized for reading long articles with highly readable typography, contained text width, and minimal distractions - `reader-mode` - Simulates browser reader mode with almost total emphasis on text, sepia background, very readable serif font, and minimal graphic elements +- `mynotes` - A warm, intimate, text-first journal theme designed for meditative long-form reading +- `museum-label` - Museum catalog style with refined serif typography, restrained metadata, and clean archival cards +- `field-journal` - Warm paper-inspired writing theme with natural tones and notebook-style presentation - `thoughtful` - A warm, accessible, and performant theme for personal reflection blogs and thoughtful writing - `text-only` - A step beyond minimalism using browser defaults with clean base typography for readability and lightning-fast loading @@ -945,6 +952,7 @@ BSSG includes a variety of themes to customize the look of your site. Themes are - `brutalist` - Raw, minimalist concrete-inspired design - `newspaper` - Classic newspaper layout - `diary` - Personal diary/journal style +- `microfiche` - Monochrome archival projection aesthetic with scanline and microfilm-inspired styling - `random` - Selects a random theme (from the available themes) for each build To use a theme, specify it in your config file: diff --git a/generate_theme_previews.sh b/generate_theme_previews.sh index 056956e..ba6c8f0 100755 --- a/generate_theme_previews.sh +++ b/generate_theme_previews.sh @@ -27,6 +27,8 @@ NC='\033[0m' # No Color # Default SITE_URL from config.sh if no other is specified by script's --site-url SITE_URL_BASE="http://localhost" +FULL_BUILD_MODE=false +SITE_URL_TOKEN="__BSSG_THEME_SITE_URL__" # --- Helper Functions --- info() { @@ -64,6 +66,7 @@ Options: -h, --help Display this help message and exit --site-url URL Set the base SITE_URL for theme previews (overrides config files) + --full-build Build each theme independently (slower fallback mode) Configuration: The script will use the SITE_URL from the following sources in order of precedence: @@ -76,6 +79,11 @@ Output: Theme previews will be generated in the '$EXAMPLE_ROOT_DIR_DYNAMIC' directory, with each theme in its own subdirectory. An index.html file will be created to navigate between themes. + +Performance: + By default, this script builds the site once and then clones it per theme, + replacing css/style.css and SITE_URL references. This is significantly faster. + Use --full-build to force one full BSSG build per theme. EOF exit 0 } @@ -97,6 +105,10 @@ parse_args() { error "--site-url requires a value for the base URL of previews" fi ;; + --full-build) + FULL_BUILD_MODE=true + shift + ;; *) warn "Unknown option: $1 (ignored)" shift @@ -276,6 +288,25 @@ find_themes() { } build_previews() { + prepare_example_directory + + if [ "$FULL_BUILD_MODE" = true ]; then + info "Using full-build mode (one BSSG build per theme)." + build_previews_full + return + fi + + if has_theme_specific_templates; then + warn "Theme-specific templates detected under templates/<theme>/. Falling back to full per-theme builds." + build_previews_full + return + fi + + info "Using fast preview mode: single build + clone + theme CSS swap." + build_previews_fast +} + +prepare_example_directory() { info "Clearing existing example directory: '$EXAMPLE_ROOT_DIR_DYNAMIC'" mkdir -p "$EXAMPLE_ROOT_DIR_DYNAMIC" # More robustly clear contents. Using find is safer for unusual filenames. @@ -289,7 +320,9 @@ build_previews() { # A safer alternative if `find` is available: # find "$EXAMPLE_ROOT_DIR_DYNAMIC" -mindepth 1 -delete success "Example directory cleared and ready." +} +build_previews_full() { info "Starting theme preview builds..." info "Previews will use content from the BSSG site configured by your standard config.sh/config.sh.local files." @@ -315,6 +348,89 @@ build_previews() { success "All theme previews built." } +has_theme_specific_templates() { + local template_root="./templates" + local theme + for theme in "${themes[@]}"; do + if [ -d "$template_root/$theme" ]; then + if [ -f "$template_root/$theme/header.html" ] || [ -f "$template_root/$theme/footer.html" ]; then + return 0 + fi + fi + done + return 1 +} + +replace_site_url_token_in_output() { + local output_dir="$1" + local replacement_url="$2" + local token="$3" + local escaped_replacement tmp_file file + + escaped_replacement=$(printf '%s' "$replacement_url" | sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g') + + while IFS= read -r -d '' file; do + if LC_ALL=C grep -Fq "$token" "$file"; then + tmp_file="${file}.tmp.$$" + sed "s|${token}|${escaped_replacement}|g" "$file" > "$tmp_file" + mv "$tmp_file" "$file" + fi + done < <(find "$output_dir" -type f \( -name "*.html" -o -name "*.xml" -o -name "*.txt" -o -name "*.css" -o -name "*.json" -o -name "*.js" \) -print0) +} + +clone_base_site_to_theme() { + local base_output_path="$1" + local theme_output_path="$2" + + mkdir -p "$theme_output_path" + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete --exclude='.DS_Store' "${base_output_path}/" "${theme_output_path}/" + else + cp -Rp "${base_output_path}/." "$theme_output_path/" + fi +} + +build_previews_fast() { + local base_theme="default" + local base_output_path="${EXAMPLE_ROOT_DIR_DYNAMIC}/.base-preview" + local theme theme_site_url theme_output_path + + if [ ! -f "${THEMES_DIR}/${base_theme}/style.css" ]; then + base_theme="${themes[0]}" + fi + + info "Building base preview once with theme '$base_theme' and SITE_URL token '$SITE_URL_TOKEN'..." + if ! "$BSSG_MAIN_SCRIPT" build -f --theme "$base_theme" --site-url "$SITE_URL_TOKEN" --output "$base_output_path"; then + error "Base build failed in fast preview mode." + fi + + for theme in "${themes[@]}"; do + theme_site_url="${SITE_URL_BASE%/}/${theme}" + theme_output_path="${EXAMPLE_ROOT_DIR_DYNAMIC}/${theme}" + + info "Preparing fast preview for theme '$theme'" + info "Theme Site URL: $theme_site_url" + info "Theme Output Path: $theme_output_path" + + clone_base_site_to_theme "$base_output_path" "$theme_output_path" + + if [ ! -f "${THEMES_DIR}/${theme}/style.css" ]; then + error "style.css not found for theme '$theme' in '${THEMES_DIR}/${theme}'." + fi + cp "${THEMES_DIR}/${theme}/style.css" "${theme_output_path}/css/style.css" + + replace_site_url_token_in_output "$theme_output_path" "$theme_site_url" "$SITE_URL_TOKEN" + + # If precompressed assets were generated in base build, they are now stale after token replacement. + find "$theme_output_path" -type f -name "*.gz" -delete 2>/dev/null || true + + success "Fast preview for theme '$theme' prepared successfully." + done + + rm -rf "$base_output_path" + success "All fast theme previews built." +} + create_index_page() { local index_file="$EXAMPLE_ROOT_DIR_DYNAMIC/index.html" info "Generating index file at '$index_file'..." @@ -518,4 +634,4 @@ main() { info "Open '$EXAMPLE_ROOT_DIR_DYNAMIC/index.html' in your browser to view them." } -main "$@" \ No newline at end of file +main "$@" diff --git a/themes/field-journal/style.css b/themes/field-journal/style.css new file mode 100644 index 0000000..f2d3ae6 --- /dev/null +++ b/themes/field-journal/style.css @@ -0,0 +1,462 @@ +/* + * Field Journal Theme for BSSG + * Warm paper, natural inks, and notebook-like details. + */ + +:root { + --bg-color: #f6f0e2; + --paper-color: #fffaf0; + --text-color: #2f3a2b; + --heading-color: #273221; + --muted-text: #68725f; + --link-color: #365d3b; + --link-hover: #234028; + --border-color: #d5c9a8; + --accent-color: #8d6f43; + --accent-soft: #ede2cb; + --tag-bg: #e8dcc2; + --tag-text: #3f5133; + --quote-bg: #f2e8d3; + --code-bg: #efe6d2; + --content-width: 860px; + --radius: 8px; + --shadow: 0 4px 12px rgba(68, 53, 24, 0.11); + --font-body: "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif; + --font-heading: "Hoefler Text", Baskerville, "Times New Roman", serif; + --font-ui: "Trebuchet MS", Verdana, Arial, sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 18px; +} + +body { + margin: 0; + color: var(--text-color); + background-color: var(--bg-color); + background-image: + radial-gradient(circle at 2px 2px, rgba(92, 77, 46, 0.06) 0.6px, transparent 0.6px), + linear-gradient(180deg, #f8f3e7 0%, #f2ead7 100%); + background-size: 4px 4px, 100% 100%; + font-family: var(--font-body); + line-height: 1.75; +} + +::selection { + background: #d7c49a; + color: #20261b; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 2rem 1.3rem 2.8rem; +} + +header { + margin-bottom: 2rem; + padding: 1.2rem 0 1rem; + border-top: 2px solid var(--border-color); + border-bottom: 2px solid var(--border-color); + background: linear-gradient(180deg, rgba(255, 252, 244, 0.82) 0%, rgba(246, 238, 221, 0.72) 100%); +} + +.site-title, +header h1 { + margin: 0; + color: var(--heading-color); + font-family: var(--font-heading); + font-size: clamp(2rem, 4.4vw, 2.9rem); + font-weight: 600; + letter-spacing: 0.02em; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +header p { + margin: 0.45rem 0 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.85rem; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +nav { + margin-top: 0.95rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +nav a { + display: inline-block; + text-decoration: none; + color: var(--link-color); + border: 1px solid #cfbf9b; + border-radius: 999px; + padding: 0.22rem 0.68rem; + font-family: var(--font-ui); + font-size: 0.74rem; + letter-spacing: 0.08em; + text-transform: uppercase; + background: rgba(255, 251, 239, 0.84); +} + +nav a:hover, +nav a:focus { + color: #1d3622; + background: #e5d9bd; +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.5rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--paper-color); + border: 1px solid #d3c4a0; + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 1.35rem 1.15rem 1.15rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.25rem 0 0.75rem; + color: var(--heading-color); + font-family: var(--font-heading); + line-height: 1.3; +} + +h1 { font-size: clamp(1.8rem, 3.7vw, 2.4rem); } +h2 { font-size: clamp(1.45rem, 3.1vw, 1.9rem); } +h3 { font-size: clamp(1.18rem, 2.3vw, 1.45rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 1rem; +} + +article.post > p:first-of-type::first-letter { + float: left; + font-size: 2.6em; + line-height: 0.88; + margin-right: 0.12em; + margin-top: 0.06em; + color: #4a633f; +} + +a { + color: var(--link-color); + text-underline-offset: 0.16em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.85rem; +} + +.meta { + margin: 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.77rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.reading-time { + margin-top: 0.28rem; +} + +.summary { + margin-top: 0.75rem; + color: #43533a; +} + +.post-content { + margin-top: 0.7rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 1.05rem 0; +} + +img { + max-width: 100%; + height: auto; + display: block; + border-radius: 4px; + border: 1px solid #bcae8c; +} + +.image-caption, +figcaption { + margin-top: 0.38rem; + color: var(--muted-text); + font-style: italic; + font-size: 0.86rem; +} + +blockquote { + margin: 1.25rem 0; + padding: 0.75rem 0.95rem; + background: var(--quote-bg); + border-left: 4px solid #9a8256; + color: #3d4d34; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.87rem; +} + +code { + background: var(--code-bg); + padding: 0.1rem 0.3rem; + border-radius: 3px; +} + +pre { + background: var(--code-bg); + border: 1px solid #cdbd99; + border-radius: 6px; + padding: 0.86rem; + overflow-x: auto; +} + +pre code { + background: none; + padding: 0; +} + +hr { + border: 0; + border-top: 2px dashed #c7b792; + margin: 1.3rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.42rem; + margin-top: 1rem; +} + +.tags a, +.tags-list a { + display: inline-block; + padding: 0.22rem 0.62rem; + border-radius: 999px; + border: 1px solid #bea97d; + background: var(--tag-bg); + color: var(--tag-text); + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.75rem; + letter-spacing: 0.05em; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #ddcea9; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tag-count { + color: #5e6b57; +} + +.posts-list article + article { + margin-top: 0.95rem; +} + +.posts-list h2, +.posts-list h3 { + margin-top: 0; +} + +.pagination { + margin-top: 1.4rem; + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + align-items: center; +} + +.pagination a { + display: inline-block; + padding: 0.26rem 0.7rem; + border-radius: 999px; + border: 1px solid #baa778; + background: #f0e5cd; + color: #32452b; + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.75rem; + letter-spacing: 0.05em; +} + +.pagination a:hover, +.pagination a:focus { + background: #e3d2ab; +} + +.page-info { + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.07em; +} + +.related-posts { + margin-top: 1.6rem; + padding-top: 1rem; + border-top: 2px dotted #bca779; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.1rem; +} + +.related-posts-list { + display: grid; + gap: 0.7rem; +} + +.related-post { + background: var(--accent-soft); + border: 1px solid #ccb993; + border-left: 4px solid #8f7448; + border-radius: 5px; + padding: 0.62rem 0.72rem; +} + +.related-post h4 { + margin: 0 0 0.3rem; +} + +.related-post p { + margin: 0; + color: #495b3f; +} + +footer { + margin-top: 2rem; + border-top: 2px solid var(--border-color); + padding-top: 0.95rem; + color: #62705d; + font-family: var(--font-ui); + font-size: 0.76rem; + letter-spacing: 0.04em; +} + +footer p { + margin: 0.38rem 0; +} + +footer a { + color: inherit; +} + +footer a:hover, +footer a:focus { + color: #2f4b35; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.95rem 0; +} + +th, +td { + border: 1px solid #c9ba96; + padding: 0.42rem 0.58rem; +} + +th { + background: #efe3c9; + font-family: var(--font-ui); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +@media (max-width: 780px) { + html { + font-size: 17px; + } + + .container { + padding: 1.3rem 0.9rem 2.2rem; + } + + article.post, + article.page, + .posts-list article { + padding: 1.05rem 0.9rem 0.95rem; + } + + nav { + gap: 0.38rem; + } + + nav a, + .meta, + .pagination a, + .page-info { + letter-spacing: 0.04em; + } +} diff --git a/themes/freebsd/style.css b/themes/freebsd/style.css new file mode 100644 index 0000000..0c90da4 --- /dev/null +++ b/themes/freebsd/style.css @@ -0,0 +1,517 @@ +/* + * FreeBSD Theme for BSSG + * Recognizable FreeBSD-inspired look using two reds + black and the orb motif. + */ + +:root { + --freebsd-red-1: #c61f2b; + --freebsd-red-2: #9b1a22; + --freebsd-black: #101112; + --freebsd-charcoal: #1a1d1f; + --paper: #fcfcfc; + --paper-2: #f4f5f6; + --text: #1d1f22; + --muted: #5a6169; + --border: #d5d9de; + --link: #a3151f; + --link-hover: #7f0f18; + --tag-bg: #f8e7e9; + --tag-text: #7f141d; + --quote-bg: #f6f7f8; + --code-bg: #f2f4f6; + --radius: 8px; + --shadow: 0 12px 28px rgba(0, 0, 0, 0.25); + --content-width: 920px; + --font-body: "Roboto", "Helvetica Neue", Arial, sans-serif; + --font-heading: "Montserrat", "Arial Narrow", "Trebuchet MS", sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 17px; +} + +body { + margin: 0; + color: var(--text); + font-family: var(--font-body); + line-height: 1.7; + background: + radial-gradient(circle at 80% 10%, rgba(198, 31, 43, 0.22) 0, transparent 32%), + linear-gradient(180deg, #1b1d20 0%, #0f1011 100%); +} + +::selection { + background: #f0c8cc; + color: #1a1a1a; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 0 1.25rem 2.7rem; + background: linear-gradient(180deg, #ffffff 0%, var(--paper) 100%); + border-left: 1px solid #3c4146; + border-right: 1px solid #3c4146; + box-shadow: var(--shadow); + min-height: 100vh; +} + +header { + margin: 0 -1.25rem 2rem; + padding: 1.1rem 1.25rem 1rem; + background: + linear-gradient(135deg, rgba(198, 31, 43, 0.12) 0 14%, transparent 15%), + linear-gradient(180deg, #fefefe 0%, #ebedf0 100%); + border-top: 6px solid var(--freebsd-red-1); + border-bottom: 1px solid #c8cfd6; + position: relative; +} + +header::before { + content: "FreeBSD: The Power To Serve"; + position: absolute; + top: 12px; + right: 18px; + background: linear-gradient(180deg, #c42935 0%, #981b24 100%); + color: #fff; + border: 1px solid #7f141d; + border-radius: 4px; + padding: 0.16rem 0.56rem; + font-family: var(--font-mono); + font-size: 0.58rem; + font-weight: 700; + letter-spacing: 0.01em; + text-transform: none; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.22); +} + +header::after { + content: ""; + position: absolute; + inset: auto 0 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--freebsd-red-1), transparent); +} + +.site-title, +header h1 { + margin: 0; + color: var(--freebsd-black); + font-family: var(--font-heading); + font-weight: 700; + font-size: clamp(1.9rem, 3.8vw, 2.75rem); + letter-spacing: 0.03em; + text-transform: uppercase; + line-height: 1.2; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: var(--freebsd-red-2); +} + +header p { + margin: 0.42rem 0 0; + color: #4f5660; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.1em; + font-weight: 600; +} + +nav { + margin-top: 0.95rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +nav a { + display: inline-block; + color: #eef1f4; + background: linear-gradient(180deg, #2c3035 0%, #1c1f23 100%); + border: 1px solid #4b5158; + border-bottom-color: #7a8796; + border-radius: 5px; + padding: 0.26rem 0.6rem; + text-decoration: none; + font-size: 0.73rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.07em; + transition: transform 0.16s ease, background-color 0.16s ease; +} + +nav a:hover, +nav a:focus { + background: linear-gradient(180deg, #b12430 0%, #8f1b24 100%); + border-color: #7f1a23; + color: #fff; + transform: translateY(-1px); +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.45rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--paper); + border: 1px solid var(--border); + border-left: 5px solid var(--freebsd-red-1); + border-radius: var(--radius); + padding: 1.2rem 1rem 1rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #141619; + font-family: var(--font-heading); + margin: 1.1rem 0 0.7rem; + line-height: 1.3; + letter-spacing: 0.01em; +} + +h1 { font-size: clamp(1.75rem, 3.2vw, 2.25rem); } +h2 { font-size: clamp(1.4rem, 2.6vw, 1.8rem); } +h3 { font-size: clamp(1.16rem, 2vw, 1.42rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.95rem; +} + +a { + color: var(--link); + text-decoration-thickness: 1px; + text-underline-offset: 0.15em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.75rem; +} + +.meta { + margin: 0; + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.reading-time { + margin-top: 0.26rem; +} + +.summary { + margin-top: 0.75rem; + color: #3c4249; +} + +.post-content { + margin-top: 0.65rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 0.95rem 0; +} + +img { + max-width: 100%; + height: auto; + display: block; + border-radius: 4px; + border: 1px solid #b9c1ca; +} + +.image-caption, +figcaption { + margin-top: 0.35rem; + color: #626a73; + font-size: 0.78rem; + font-style: italic; +} + +blockquote { + margin: 1rem 0; + padding: 0.7rem 0.85rem; + border-left: 4px solid var(--freebsd-red-1); + background: var(--quote-bg); + color: #2e343b; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +code { + background: var(--code-bg); + border: 1px solid #d8dde3; + padding: 0.08rem 0.28rem; + border-radius: 3px; +} + +pre { + background: var(--code-bg); + border: 1px solid #d3d9df; + border-radius: 6px; + padding: 0.8rem; + overflow-x: auto; +} + +pre code { + background: none; + border: 0; + padding: 0; +} + +hr { + border: 0; + border-top: 1px solid #d0d6dd; + margin: 1.15rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.9rem; +} + +.tags a, +.tags-list a { + display: inline-block; + text-decoration: none; + color: var(--tag-text); + background: var(--tag-bg); + border: 1px solid #debec2; + border-radius: 999px; + padding: 0.18rem 0.52rem; + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #f2d4d8; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.tag-count { + color: #6e5d60; +} + +.posts-list article + article { + margin-top: 0.85rem; +} + +.posts-list h2, +.posts-list h3 { + margin-top: 0; +} + +.pagination { + margin-top: 1.25rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.45rem; +} + +.pagination a { + display: inline-block; + background: #f0f3f6; + color: #1f252c; + border: 1px solid #c5cdd5; + border-radius: 5px; + text-decoration: none; + padding: 0.22rem 0.62rem; + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.pagination a:hover, +.pagination a:focus { + color: #fff; + background: linear-gradient(180deg, #ba2733 0%, #931b25 100%); + border-color: #801820; +} + +.page-info { + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.related-posts { + margin-top: 1.35rem; + padding-top: 0.85rem; + border-top: 1px dashed #bcc5ce; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.05rem; + color: #1f252b; +} + +.related-posts-list { + display: grid; + gap: 0.62rem; +} + +.related-post { + border: 1px solid #cfd6dd; + border-left: 4px solid var(--freebsd-red-1); + background: #f8fafc; + border-radius: 5px; + padding: 0.55rem 0.66rem; +} + +.related-post h4 { + margin: 0 0 0.22rem; + font-size: 0.95rem; +} + +.related-post p { + margin: 0; + color: #4b525a; + font-size: 0.9rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.92rem 0; +} + +th, +td { + border: 1px solid #d2d9e0; + padding: 0.4rem 0.52rem; +} + +th { + background: #e9edf2; + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +footer { + margin-top: 1.85rem; + padding: 0.9rem 0 0.1rem; + border-top: 2px solid #d1d7de; + color: #525861; + font-family: var(--font-mono); + font-size: 0.71rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +footer p { + margin: 0.33rem 0; +} + +footer a { + color: var(--link); +} + +footer a:hover, +footer a:focus { + color: var(--link-hover); +} + +@media (max-width: 840px) { + .container { + padding: 0 0.82rem 2rem; + } + + header { + margin: 0 -0.82rem 1.5rem; + padding: 0.92rem 0.82rem 0.82rem; + } + + nav { + padding-right: 0; + } + + header::before { + top: 10px; + right: 10px; + padding: 0.12rem 0.42rem; + font-size: 0.5rem; + letter-spacing: 0; + } + + article.post, + article.page, + .posts-list article { + padding: 0.95rem 0.8rem 0.82rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.02em; + } +} diff --git a/themes/liquid-glass/style.css b/themes/liquid-glass/style.css new file mode 100644 index 0000000..f7f59c4 --- /dev/null +++ b/themes/liquid-glass/style.css @@ -0,0 +1,611 @@ +/* + * Liquid Glass Theme for BSSG + * Accessibility-first translucent design with high-contrast reading surfaces. + */ + +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); + +:root { + --page-bg: #0b101a; + --page-bg-2: #15213a; + --glow-a: rgba(10, 132, 255, 0.28); + --glow-b: rgba(191, 90, 242, 0.18); + --glass-surface: rgba(18, 26, 42, 0.9); + --glass-surface-strong: rgba(21, 31, 50, 0.95); + --glass-panel: rgba(27, 39, 62, 0.9); + --glass-border: rgba(173, 202, 242, 0.26); + --glass-border-strong: rgba(199, 221, 250, 0.38); + --text: #f4f7ff; + --text-muted: #c9d4ea; + --heading: #ffffff; + --link: #9dcfff; + --link-hover: #cde7ff; + --accent: #64d2ff; + --accent-2: #0a84ff; + --shadow-lg: 0 20px 48px rgba(2, 8, 20, 0.5); + --shadow-sm: 0 10px 24px rgba(2, 8, 20, 0.35); + --quote-bg: rgba(17, 31, 52, 0.86); + --quote-border: rgba(100, 210, 255, 0.72); + --code-inline-bg: rgba(11, 18, 31, 0.84); + --code-inline-border: rgba(170, 199, 240, 0.35); + --code-inline-text: #e9f2ff; + --code-block-bg: #0f1b30; + --code-block-border: rgba(155, 189, 234, 0.36); + --code-block-text: #ecf3ff; + --content-width: 980px; + --radius-lg: 22px; + --radius-md: 14px; + --radius-sm: 10px; + --blur: 14px; + --blur-light: 8px; + --font-main: "Inter", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-headings: "Inter", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: light) { + :root { + --page-bg: #eef4ff; + --page-bg-2: #d8e5fb; + --glow-a: rgba(0, 122, 255, 0.17); + --glow-b: rgba(88, 86, 214, 0.12); + --glass-surface: rgba(255, 255, 255, 0.9); + --glass-surface-strong: rgba(255, 255, 255, 0.95); + --glass-panel: rgba(249, 252, 255, 0.93); + --glass-border: rgba(84, 117, 171, 0.24); + --glass-border-strong: rgba(63, 97, 157, 0.34); + --text: #1d2739; + --text-muted: #52617c; + --heading: #10192b; + --link: #005fcc; + --link-hover: #0047a3; + --accent: #007aff; + --accent-2: #5856d6; + --shadow-lg: 0 20px 45px rgba(58, 92, 149, 0.18); + --shadow-sm: 0 8px 22px rgba(58, 92, 149, 0.12); + --quote-bg: rgba(232, 240, 255, 0.88); + --quote-border: rgba(0, 122, 255, 0.62); + --code-inline-bg: rgba(227, 237, 255, 0.84); + --code-inline-border: rgba(79, 111, 170, 0.34); + --code-inline-text: #18345c; + --code-block-bg: #132342; + --code-block-border: rgba(56, 93, 149, 0.44); + --code-block-text: #ebf2ff; + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 18px; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + min-height: 100vh; + color: var(--text); + font-family: var(--font-main); + line-height: 1.72; + background: + radial-gradient(circle at 12% 10%, var(--glow-a) 0, transparent 36%), + radial-gradient(circle at 84% 6%, var(--glow-b) 0, transparent 32%), + linear-gradient(165deg, var(--page-bg) 0%, var(--page-bg-2) 55%, var(--page-bg) 100%); + padding: 22px; + position: relative; +} + +body::before, +body::after { + content: ""; + position: fixed; + pointer-events: none; + z-index: 0; +} + +body::before { + inset: -12% auto auto -10%; + width: 340px; + height: 340px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.22) 0, rgba(255, 255, 255, 0.04) 58%, transparent 74%); + filter: blur(9px); +} + +body::after { + inset: auto -8% -14% auto; + width: 380px; + height: 380px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.16) 0, rgba(255, 255, 255, 0.03) 60%, transparent 76%); + filter: blur(11px); +} + +::selection { + background: rgba(100, 210, 255, 0.35); + color: var(--heading); +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.container { + position: relative; + z-index: 1; + max-width: var(--content-width); + margin: 0 auto; + background: var(--glass-surface); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + overflow: hidden; + backdrop-filter: blur(var(--blur)); + -webkit-backdrop-filter: blur(var(--blur)); +} + +@supports not (backdrop-filter: blur(1px)) { + .container { + background: var(--glass-surface-strong); + } +} + +.container::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: + linear-gradient(120deg, rgba(255, 255, 255, 0.15), transparent 28%), + linear-gradient(330deg, rgba(255, 255, 255, 0.08), transparent 36%); + z-index: 0; +} + +header, +main, +footer { + position: relative; + z-index: 1; +} + +header { + padding: 30px 38px 17px; + border-bottom: 1px solid var(--glass-border); + background: var(--glass-surface-strong); +} + +.site-title, +header h1 { + margin: 0; + color: var(--heading); + font-family: var(--font-headings); + font-size: clamp(2rem, 3.7vw, 2.75rem); + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.15; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: var(--link-hover); +} + +header p { + margin: 0.56rem 0 0; + color: var(--text-muted); + font-size: 1rem; +} + +nav { + margin-top: 1rem; + display: flex; + flex-wrap: wrap; + gap: 0.46rem; +} + +nav a { + display: inline-flex; + align-items: center; + padding: 0.32rem 0.74rem; + border-radius: 999px; + border: 1px solid var(--glass-border-strong); + background: var(--glass-panel); + color: var(--text); + text-decoration: none; + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.01em; + backdrop-filter: blur(var(--blur-light)); + -webkit-backdrop-filter: blur(var(--blur-light)); + transition: background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease, transform 0.16s ease; +} + +nav a:hover, +nav a:focus { + transform: translateY(-1px); + background: rgba(100, 210, 255, 0.2); + border-color: rgba(100, 210, 255, 0.55); + color: var(--heading); +} + +main { + padding: 28px 38px 32px; + min-height: 62vh; +} + +article { + margin-bottom: 1.25rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--glass-panel); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + padding: 1.16rem 1.1rem; +} + +article.post + article.post, +.posts-list article + article { + margin-top: 0.95rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.08rem 0 0.72rem; + color: var(--heading); + font-family: var(--font-headings); + line-height: 1.3; + letter-spacing: -0.01em; +} + +h1 { font-size: clamp(1.72rem, 3vw, 2.24rem); } +h2 { font-size: clamp(1.4rem, 2.35vw, 1.78rem); } +h3 { font-size: clamp(1.18rem, 1.8vw, 1.4rem); } + +article.post > h1, +article.page > h1, +.posts-list h2, +.posts-list h3 { + margin-top: 0; +} + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.95rem; +} + +a { + color: var(--link); + text-decoration-thickness: 1px; + text-underline-offset: 0.14em; + text-decoration-color: rgba(157, 207, 255, 0.65); +} + +a:hover, +a:focus { + color: var(--link-hover); + text-decoration-color: currentColor; +} + +.meta, +.page-meta { + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 500; + letter-spacing: 0.02em; +} + +.page-meta { + margin-bottom: 0.68rem; +} + +.reading-time { + margin-top: 0.24rem; +} + +.summary { + color: var(--text-muted); +} + +.post-content { + margin-top: 0.65rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 0.92rem 0; +} + +img { + display: block; + max-width: 100%; + height: auto; + border-radius: 12px; + border: 1px solid var(--glass-border-strong); + box-shadow: 0 8px 20px rgba(2, 8, 20, 0.3); +} + +.image-caption, +figcaption { + margin-top: 0.42rem; + color: var(--text-muted); + font-size: 0.83rem; + font-style: italic; +} + +blockquote { + margin: 1rem 0; + padding: 0.72rem 0.92rem; + border-left: 4px solid var(--quote-border); + border-radius: var(--radius-sm); + background: var(--quote-bg); + color: var(--text); +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +code { + padding: 0.09rem 0.3rem; + border-radius: 6px; + border: 1px solid var(--code-inline-border); + background: var(--code-inline-bg); + color: var(--code-inline-text); +} + +pre { + margin: 1rem 0; + padding: 0.88rem; + border-radius: var(--radius-sm); + border: 1px solid var(--code-block-border); + background: var(--code-block-bg); + color: var(--code-block-text); + overflow-x: auto; +} + +pre code { + padding: 0; + border: 0; + background: none; + color: inherit; +} + +hr { + border: 0; + margin: 1.28rem 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(180, 206, 242, 0.7), transparent); +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.36rem; + margin-top: 0.84rem; +} + +.tags a, +.tags-list a { + display: inline-block; + color: var(--text); + text-decoration: none; + background: var(--glass-panel); + border: 1px solid var(--glass-border); + border-radius: 999px; + padding: 0.16rem 0.54rem; + font-size: 0.75rem; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + color: var(--heading); + border-color: rgba(100, 210, 255, 0.58); + background: rgba(100, 210, 255, 0.19); +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.tag-count { + color: var(--text-muted); +} + +.pagination { + margin-top: 1.12rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.42rem; +} + +.pagination a { + display: inline-block; + color: var(--text); + text-decoration: none; + background: var(--glass-panel); + border: 1px solid var(--glass-border); + border-radius: 999px; + padding: 0.18rem 0.62rem; + font-size: 0.75rem; +} + +.pagination a:hover, +.pagination a:focus { + color: var(--heading); + border-color: rgba(100, 210, 255, 0.58); + background: rgba(100, 210, 255, 0.19); +} + +.page-info { + color: var(--text-muted); + font-size: 0.77rem; +} + +.related-posts { + margin-top: 1.22rem; + padding-top: 0.8rem; + border-top: 1px dashed var(--glass-border-strong); +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.04rem; +} + +.related-posts-list { + display: grid; + gap: 0.58rem; +} + +.related-post { + border: 1px solid var(--glass-border); + border-radius: var(--radius-sm); + background: var(--glass-panel); + padding: 0.5rem 0.64rem; +} + +.related-post h4 { + margin: 0 0 0.18rem; + font-size: 0.93rem; +} + +.related-post p { + margin: 0; + color: var(--text-muted); + font-size: 0.88rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.92rem 0; +} + +th, +td { + border: 1px solid var(--glass-border); + padding: 0.38rem 0.52rem; +} + +th { + color: var(--heading); + background: rgba(100, 210, 255, 0.14); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +footer { + padding: 17px 38px 24px; + border-top: 1px solid var(--glass-border); + color: var(--text-muted); + font-size: 0.77rem; + background: var(--glass-surface-strong); +} + +footer p { + margin: 0.3rem 0; +} + +footer a { + color: var(--link); +} + +footer a:hover, +footer a:focus { + color: var(--link-hover); +} + +@media (max-width: 900px) { + body { + padding: 14px; + } + + header, + main, + footer { + padding-left: 22px; + padding-right: 22px; + } + + .site-title, + header h1 { + font-size: clamp(1.7rem, 7vw, 2.25rem); + } +} + +@media (max-width: 640px) { + html { + font-size: 16px; + } + + body { + padding: 10px; + } + + .container { + border-radius: 16px; + } + + header, + main, + footer { + padding-left: 14px; + padding-right: 14px; + } + + nav a { + min-height: 38px; + } + + article.post, + article.page, + .posts-list article { + padding: 0.95rem 0.85rem; + } + + .meta, + .page-info, + .pagination a { + font-size: 0.74rem; + } +} diff --git a/themes/microfiche/style.css b/themes/microfiche/style.css new file mode 100644 index 0000000..1d48936 --- /dev/null +++ b/themes/microfiche/style.css @@ -0,0 +1,499 @@ +/* + * Microfiche Theme for BSSG + * Monochrome projection look with scanlines and archival framing. + */ + +:root { + --bg-color: #0f1110; + --screen-color: #171a18; + --panel-color: #1d211e; + --text-color: #d8ddd7; + --muted-text: #9ca39b; + --heading-color: #edf2ea; + --link-color: #e3e8e0; + --link-hover: #ffffff; + --border-color: #4e564f; + --accent-color: #b8c1b5; + --tag-bg: #2a2f2b; + --tag-text: #d5dbd2; + --code-bg: #202522; + --quote-bg: #222723; + --overlay-line: rgba(232, 238, 230, 0.04); + --overlay-noise: rgba(240, 245, 236, 0.035); + --content-width: 900px; + --radius: 7px; + --shadow: 0 12px 24px rgba(0, 0, 0, 0.48); + --font-body: "IBM Plex Sans", "Lucida Grande", "Helvetica Neue", Arial, sans-serif; + --font-heading: "Franklin Gothic Medium", "Arial Narrow", "Trebuchet MS", sans-serif; + --font-ui: "IBM Plex Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; + --font-mono: "IBM Plex Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 17px; +} + +body { + margin: 0; + color: var(--text-color); + background: radial-gradient(circle at 50% 25%, #1a1f1b 0%, #0e110f 62%); + font-family: var(--font-body); + line-height: 1.7; + position: relative; +} + +body::before, +body::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; +} + +body::before { + background: repeating-linear-gradient( + to bottom, + transparent 0, + transparent 3px, + var(--overlay-line) 4px + ); + z-index: 1; +} + +body::after { + background-image: + radial-gradient(var(--overlay-noise) 0.65px, transparent 0.65px), + radial-gradient(var(--overlay-noise) 0.65px, transparent 0.65px); + background-size: 3px 3px, 4px 4px; + background-position: 0 0, 2px 1px; + z-index: 2; +} + +::selection { + background: #dce2d8; + color: #101311; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 1.9rem 1.1rem 2.6rem; + position: relative; + z-index: 3; + background: linear-gradient(180deg, rgba(27, 30, 28, 0.9) 0%, rgba(18, 21, 19, 0.92) 100%); + border-left: 1px solid var(--border-color); + border-right: 1px solid var(--border-color); + box-shadow: var(--shadow); +} + +header { + margin-bottom: 1.8rem; + padding: 1rem 0 0.95rem; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, #202521 0%, #171b18 100%); +} + +.site-title, +header h1 { + margin: 0; + color: var(--heading-color); + font-family: var(--font-heading); + font-weight: 600; + font-size: clamp(1.9rem, 3.8vw, 2.7rem); + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +header p { + margin: 0.42rem 0 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.74rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +header, +main, +footer { + padding-left: 0.85rem; + padding-right: 0.85rem; +} + +nav { + margin-top: 0.82rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem 0.5rem; +} + +nav a { + display: inline-block; + color: var(--tag-text); + background: #242926; + border: 1px solid #5b655d; + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 0.18rem 0.5rem; +} + +nav a:hover, +nav a:focus { + background: #303732; + color: #f4f8f2; +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.4rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--panel-color); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 1.1rem 1rem 0.95rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--heading-color); + font-family: var(--font-heading); + margin: 1.15rem 0 0.7rem; + line-height: 1.3; + letter-spacing: 0.03em; +} + +h1 { font-size: clamp(1.75rem, 3.4vw, 2.25rem); } +h2 { font-size: clamp(1.4rem, 2.7vw, 1.75rem); } +h3 { font-size: clamp(1.18rem, 2.2vw, 1.4rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.95rem; +} + +a { + color: var(--link-color); + text-decoration-thickness: 1px; + text-decoration-style: dotted; + text-underline-offset: 0.14em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.8rem; +} + +.meta { + margin: 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.72rem; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.reading-time { + margin-top: 0.25rem; +} + +.summary { + margin-top: 0.68rem; + color: #b8beb6; +} + +.post-content { + margin-top: 0.7rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 1rem 0; +} + +img { + display: block; + max-width: 100%; + height: auto; + border: 1px solid #5b635c; + filter: grayscale(100%) contrast(1.06) brightness(0.95); +} + +.image-caption, +figcaption { + margin-top: 0.35rem; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.71rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +blockquote { + margin: 1rem 0; + padding: 0.65rem 0.85rem; + border-left: 3px solid #7f887f; + background: var(--quote-bg); + color: #ced5cd; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +code { + background: var(--code-bg); + border: 1px solid #3f4741; + padding: 0.08rem 0.28rem; + border-radius: 3px; +} + +pre { + background: var(--code-bg); + border: 1px solid #3f4741; + border-radius: 4px; + padding: 0.8rem; + overflow-x: auto; +} + +pre code { + background: none; + border: 0; + padding: 0; +} + +hr { + border: 0; + border-top: 1px dashed #556058; + margin: 1.2rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.36rem; + margin-top: 0.9rem; +} + +.tags a, +.tags-list a { + display: inline-block; + text-decoration: none; + color: var(--tag-text); + background: var(--tag-bg); + border: 1px solid #606a62; + border-radius: 3px; + padding: 0.18rem 0.45rem; + font-family: var(--font-ui); + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #333a35; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.44rem; +} + +.tag-count { + color: var(--muted-text); +} + +.posts-list article + article { + margin-top: 0.9rem; +} + +.posts-list h2, +.posts-list h3 { + margin-top: 0; +} + +.pagination { + margin-top: 1.3rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.pagination a { + display: inline-block; + text-decoration: none; + color: var(--text-color); + border: 1px solid #68726a; + background: #2a2f2b; + padding: 0.2rem 0.55rem; + font-family: var(--font-ui); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.07em; +} + +.pagination a:hover, +.pagination a:focus { + background: #38403a; + color: #f0f5ee; +} + +.page-info { + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.73rem; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.related-posts { + margin-top: 1.45rem; + padding-top: 0.95rem; + border-top: 1px dashed #5e675f; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.05rem; +} + +.related-posts-list { + display: grid; + gap: 0.65rem; +} + +.related-post { + border: 1px solid #5a645c; + background: #242925; + padding: 0.55rem 0.66rem; +} + +.related-post h4 { + margin: 0 0 0.24rem; + font-size: 0.95rem; +} + +.related-post p { + margin: 0; + color: #b7beb5; + font-size: 0.9rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.95rem 0; +} + +th, +td { + border: 1px solid #505850; + padding: 0.4rem 0.52rem; +} + +th { + background: #2b302d; + font-family: var(--font-ui); + font-size: 0.73rem; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +footer { + margin-top: 1.8rem; + border-top: 1px solid var(--border-color); + padding-top: 0.8rem; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.71rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +footer p { + margin: 0.34rem 0; +} + +footer a { + color: inherit; +} + +footer a:hover, +footer a:focus { + color: #f5f9f1; +} + +@media (max-width: 820px) { + .container { + padding: 1.25rem 0.75rem 2rem; + } + + header, + main, + footer { + padding-left: 0.6rem; + padding-right: 0.6rem; + } + + article.post, + article.page, + .posts-list article { + padding: 0.92rem 0.8rem 0.82rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.04em; + } +} diff --git a/themes/museum-label/style.css b/themes/museum-label/style.css new file mode 100644 index 0000000..30d6a63 --- /dev/null +++ b/themes/museum-label/style.css @@ -0,0 +1,447 @@ +/* + * Museum Label Theme for BSSG + * Gallery-inspired typography: serif reading text with restrained sans metadata. + */ + +:root { + --bg-color: #fbfaf8; + --paper-color: #ffffff; + --text-color: #1d1d1b; + --muted-text: #5a5a56; + --heading-color: #111110; + --link-color: #314f68; + --link-hover: #1f3446; + --border-color: #d9d4cb; + --accent-color: #8d7754; + --tag-bg: #f4efe5; + --tag-text: #54452e; + --quote-border: #b6aa95; + --code-bg: #f5f5f3; + --shadow: 0 3px 10px rgba(0, 0, 0, 0.06); + --radius: 6px; + --content-width: 860px; + --font-body: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif; + --font-heading: Baskerville, "Times New Roman", Times, serif; + --font-ui: "Gill Sans", "Avenir Next", "Trebuchet MS", Arial, sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 18px; +} + +body { + margin: 0; + color: var(--text-color); + background-color: var(--bg-color); + background-image: linear-gradient(180deg, #fffdf9 0%, #f7f4ee 100%); + font-family: var(--font-body); + line-height: 1.7; +} + +::selection { + background-color: #e7dbc4; + color: #1a1a18; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 2.25rem 1.4rem 3rem; +} + +header { + border-top: 2px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + padding: 1.35rem 0 1.15rem; + margin-bottom: 2.1rem; +} + +.site-title, +header h1 { + margin: 0; + color: var(--heading-color); + font-family: var(--font-heading); + font-size: clamp(2rem, 4.2vw, 2.9rem); + font-weight: 600; + letter-spacing: 0.02em; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +header p { + margin: 0.45rem 0 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +nav { + display: flex; + flex-wrap: wrap; + gap: 0.8rem 1rem; + margin-top: 1rem; +} + +nav a { + color: var(--muted-text); + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.74rem; + letter-spacing: 0.13em; + text-transform: uppercase; + border-bottom: 1px solid transparent; + padding-bottom: 0.1rem; +} + +nav a:hover, +nav a:focus { + color: var(--heading-color); + border-bottom-color: var(--heading-color); +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.8rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--paper-color); + border: 1px solid var(--border-color); + box-shadow: var(--shadow); + border-radius: var(--radius); + padding: 1.45rem 1.25rem 1.2rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--heading-color); + font-family: var(--font-heading); + line-height: 1.3; + margin: 1.35rem 0 0.85rem; +} + +h1 { font-size: clamp(1.8rem, 3.8vw, 2.45rem); } +h2 { font-size: clamp(1.45rem, 3vw, 1.85rem); } +h3 { font-size: clamp(1.2rem, 2.2vw, 1.45rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 1rem; +} + +a { + color: var(--link-color); + text-decoration-thickness: 1px; + text-underline-offset: 0.15em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.9rem; +} + +.meta { + margin: 0; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.73rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.reading-time { + margin-top: 0.35rem; +} + +.summary { + margin-top: 0.85rem; + color: #3d3d39; +} + +.post-content { + margin-top: 0.8rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 1.1rem 0; +} + +img { + display: block; + max-width: 100%; + height: auto; + border: 1px solid var(--border-color); + border-radius: 2px; +} + +.image-caption, +figcaption { + margin-top: 0.45rem; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +blockquote { + margin: 1.2rem 0; + border-left: 3px solid var(--quote-border); + background: #faf7f1; + padding: 0.75rem 0.95rem; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.87rem; +} + +code { + background: var(--code-bg); + padding: 0.1rem 0.3rem; + border-radius: 2px; +} + +pre { + background: var(--code-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 0.9rem; + overflow-x: auto; +} + +pre code { + padding: 0; + background: none; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin-top: 1.1rem; +} + +.tags a, +.tags-list a { + display: inline-block; + color: var(--tag-text); + background: var(--tag-bg); + border: 1px solid #d8cfbe; + border-radius: 999px; + padding: 0.2rem 0.56rem; + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.73rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #e8dcc7; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tag-count { + color: var(--muted-text); +} + +.posts-list article + article { + margin-top: 1rem; +} + +.posts-list h2, +.posts-list h3 { + margin-top: 0; +} + +.pagination { + margin: 1.6rem 0 0.6rem; + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-items: center; +} + +.pagination a { + display: inline-block; + border: 1px solid var(--border-color); + border-radius: 999px; + padding: 0.3rem 0.75rem; + text-decoration: none; + color: var(--heading-color); + font-family: var(--font-ui); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.pagination a:hover, +.pagination a:focus { + background: #f1ece2; +} + +.page-info { + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.related-posts { + margin-top: 1.8rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.1rem; +} + +.related-posts-list { + display: grid; + gap: 0.7rem; +} + +.related-post { + border-left: 3px solid var(--accent-color); + background: var(--tag-bg); + padding: 0.65rem 0.75rem; +} + +.related-post h4 { + margin: 0 0 0.3rem; +} + +.related-post p { + margin: 0; + color: #4a4a44; + font-size: 0.95rem; +} + +footer { + margin-top: 2.2rem; + border-top: 1px solid var(--border-color); + padding-top: 1rem; + color: var(--muted-text); + font-family: var(--font-ui); + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +footer p { + margin: 0.4rem 0; +} + +footer a { + color: var(--muted-text); +} + +footer a:hover, +footer a:focus { + color: var(--heading-color); +} + +table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; +} + +th, +td { + border: 1px solid var(--border-color); + padding: 0.45rem 0.6rem; +} + +th { + font-family: var(--font-ui); + font-size: 0.78rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +hr { + border: 0; + border-top: 1px solid var(--border-color); + margin: 1.4rem 0; +} + +@media (max-width: 760px) { + html { + font-size: 17px; + } + + .container { + padding: 1.4rem 0.9rem 2.3rem; + } + + article.post, + article.page, + .posts-list article { + padding: 1.1rem 0.95rem; + } + + nav { + gap: 0.45rem 0.7rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.06em; + } +} diff --git a/themes/mynotes/style.css b/themes/mynotes/style.css new file mode 100644 index 0000000..4aee620 --- /dev/null +++ b/themes/mynotes/style.css @@ -0,0 +1,574 @@ +/* + * MyNotes Theme for BSSG + * Quiet, warm, text-first journal aesthetic. + */ + +@import url("https://fonts.googleapis.com/css2?family=Literata:opsz,wght@7..72,400;7..72,500;7..72,600&family=Source+Sans+3:wght@400;500;600&display=swap"); + +:root { + --bg: #1c1917; + --bg-soft: #221e1b; + --surface: #26211e; + --text: #e8e0d4; + --text-soft: #d2c7b5; + --muted: #afa08b; + --accent: #c89452; + --accent-strong: #ddb175; + --accent-subtle: rgba(200, 148, 82, 0.32); + --border: #4a4037; + --quote-bg: #24201d; + --quote-border: #a7773d; + --code-bg: #171411; + --code-text: #f1e8db; + --code-border: #3d342c; + --shadow-soft: 0 3px 16px rgba(0, 0, 0, 0.24); + --content-width: 700px; + --font-body: "Literata", Georgia, "Times New Roman", serif; + --font-ui: "Source Sans 3", system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: light) { + :root { + --bg: #f5f0e8; + --bg-soft: #f1e9de; + --surface: #fcf8f1; + --text: #2c2520; + --text-soft: #3b322c; + --muted: #726459; + --accent: #a96d2f; + --accent-strong: #8d5520; + --accent-subtle: rgba(169, 109, 47, 0.26); + --border: #d9cdbd; + --quote-bg: #f3ebdf; + --quote-border: #b67a3f; + --code-bg: #2a241f; + --code-text: #f5ecdd; + --code-border: #3a3129; + --shadow-soft: 0 2px 12px rgba(60, 42, 22, 0.12); + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 19px; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + color: var(--text); + background: + radial-gradient(circle at 15% 8%, rgba(201, 150, 91, 0.1), transparent 34%), + linear-gradient(180deg, var(--bg-soft) 0%, var(--bg) 100%); + font-family: var(--font-body); + line-height: 1.7; +} + +::selection { + background: var(--accent-subtle); + color: var(--text); +} + +.container { + max-width: calc(var(--content-width) + 2.8rem); + margin: 0 auto; + padding: 2.2rem 1.4rem 3.2rem; +} + +header { + border-bottom: 1px solid var(--border); + margin-bottom: 2.3rem; + padding-bottom: 1.15rem; +} + +.site-title, +header h1 { + margin: 0; + color: var(--text); + font-family: var(--font-body); + font-weight: 600; + font-size: clamp(2rem, 4vw, 2.75rem); + line-height: 1.2; + letter-spacing: 0.01em; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: var(--accent-strong); +} + +header p { + margin: 0.45rem 0 0; + color: var(--muted); + font-size: 0.95rem; + font-style: italic; + line-height: 1.5; +} + +nav { + margin-top: 1rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem 0.75rem; +} + +nav a { + display: inline-block; + padding: 0.28rem 0.1rem; + color: var(--text-soft); + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.82rem; + text-transform: uppercase; + letter-spacing: 0.09em; + border-bottom: 1px solid transparent; +} + +nav a:hover, +nav a:focus { + color: var(--accent-strong); + border-bottom-color: var(--accent-subtle); +} + +main { + max-width: var(--content-width); + margin: 0 auto; + min-height: 68vh; +} + +article { + margin-bottom: 2.2rem; +} + +article.post, +article.page { + padding-bottom: 1rem; +} + +.posts-list article { + margin: 0; + padding: 0 0 1.55rem; + border-bottom: 1px solid var(--border); +} + +.posts-list article + article { + margin-top: 1.55rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--text); + font-family: var(--font-body); + line-height: 1.3; + margin: 1.45rem 0 0.8rem; + letter-spacing: 0.01em; + font-weight: 600; +} + +h1 { + font-size: clamp(1.95rem, 3.3vw, 2.45rem); +} + +h2 { + font-size: clamp(1.55rem, 2.7vw, 1.92rem); +} + +h3 { + font-size: clamp(1.25rem, 2vw, 1.48rem); +} + +article.post > h1, +article.page > h1, +.posts-list h2, +.posts-list h3 { + margin-top: 0; +} + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 1.25rem; +} + +li + li { + margin-top: 0.38rem; +} + +a { + color: var(--accent); + text-decoration-line: underline; + text-decoration-thickness: 1px; + text-underline-offset: 0.16em; + text-decoration-color: rgba(200, 148, 82, 0.55); + transition: color 0.14s ease, text-decoration-color 0.14s ease; +} + +a:hover, +a:focus { + color: var(--accent-strong); + text-decoration-color: rgba(200, 148, 82, 0.95); +} + +.page-meta { + margin-bottom: 1rem; +} + +.meta { + margin: 0; + color: var(--muted); + font-family: var(--font-ui); + font-size: 0.83rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.reading-time { + margin-top: 0.28rem; +} + +.summary { + color: var(--text-soft); + margin-top: 0.6rem; +} + +.posts-list .summary { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.post-content { + margin-top: 0.75rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 1rem 0 1.05rem; +} + +.index-image { + display: none; +} + +img { + display: block; + max-width: 100%; + height: auto; + border-radius: 3px; + box-shadow: var(--shadow-soft); +} + +.image-caption, +figcaption { + margin-top: 0.45rem; + color: var(--muted); + font-size: 0.85rem; + font-style: italic; + line-height: 1.5; +} + +blockquote { + margin: 1.5rem 0; + padding: 0.9rem 1rem; + background: var(--quote-bg); + border-left: 3px solid var(--quote-border); + color: var(--text-soft); + font-style: italic; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.83rem; +} + +code { + color: var(--code-text); + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: 4px; + padding: 0.08rem 0.3rem; +} + +pre { + margin: 1.2rem 0; + color: var(--code-text); + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: 6px; + padding: 0.9rem 0.95rem; + overflow-x: auto; +} + +pre code { + border: 0; + background: transparent; + padding: 0; + color: inherit; +} + +hr { + border: 0; + margin: 2.2rem 0; + text-align: center; +} + +hr::before { + content: "\00b7\00b7\00b7"; + color: var(--muted); + letter-spacing: 0.6rem; + font-size: 1rem; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.36rem; + margin-top: 1.2rem; +} + +.tags a, +.tags-list a { + display: inline-block; + color: var(--text-soft); + background: color-mix(in srgb, var(--accent) 10%, transparent); + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.14rem 0.52rem; + text-decoration: none; + font-family: var(--font-ui); + font-size: 0.74rem; + letter-spacing: 0.04em; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + color: var(--accent-strong); + border-color: var(--accent-subtle); + background: color-mix(in srgb, var(--accent) 16%, transparent); +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.tag-count { + color: var(--muted); +} + +.pagination { + margin-top: 1.8rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.45rem; + font-family: var(--font-ui); +} + +.pagination a { + display: inline-block; + color: var(--text-soft); + background: color-mix(in srgb, var(--surface) 70%, transparent); + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.16rem 0.62rem; + text-decoration: none; + font-size: 0.78rem; +} + +.pagination a:hover, +.pagination a:focus { + color: var(--accent-strong); + border-color: var(--accent-subtle); +} + +.page-info { + color: var(--muted); + font-size: 0.78rem; + letter-spacing: 0.03em; +} + +.related-posts { + margin-top: 2rem; + padding-top: 1.1rem; + border-top: 1px dashed var(--border); +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.1rem; +} + +.related-posts-list { + display: grid; + gap: 0.8rem; +} + +.related-post { + border-left: 2px solid var(--quote-border); + background: color-mix(in srgb, var(--quote-bg) 85%, transparent); + padding: 0.55rem 0.72rem; + border-radius: 4px; +} + +.related-post h4 { + margin: 0 0 0.25rem; + font-size: 0.98rem; +} + +.related-post p { + margin: 0; + color: var(--muted); + font-size: 0.88rem; + line-height: 1.55; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 1.25rem 0; +} + +th, +td { + border: 1px solid var(--border); + padding: 0.46rem 0.56rem; +} + +th { + text-align: left; + font-family: var(--font-ui); + font-size: 0.79rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); +} + +footer { + max-width: var(--content-width); + margin: 2.5rem auto 0; + padding-top: 0.95rem; + border-top: 1px solid var(--border); + color: var(--muted); + font-family: var(--font-ui); + font-size: 0.77rem; + line-height: 1.5; +} + +footer p { + margin: 0.34rem 0; +} + +footer p:last-child { + display: none; +} + +footer a { + color: var(--accent); +} + +footer a:hover, +footer a:focus { + color: var(--accent-strong); +} + +@media (max-width: 760px) { + html { + font-size: 17px; + } + + .container { + padding: 1.4rem 1rem 2.4rem; + } + + header { + margin-bottom: 1.7rem; + padding-bottom: 0.95rem; + } + + nav { + gap: 0.24rem 0.6rem; + } + + nav a { + padding: 0.35rem 0.04rem; + font-size: 0.8rem; + letter-spacing: 0.06em; + } + + article { + margin-bottom: 1.8rem; + } + + .posts-list article { + padding-bottom: 1.2rem; + } +} + +@media (max-width: 520px) { + html { + font-size: 16px; + } + + .container { + padding: 1.1rem 0.82rem 2rem; + } + + nav { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.3rem 0.55rem; + } + + nav a { + min-height: 40px; + display: flex; + align-items: center; + justify-content: flex-start; + } + + .site-title, + header h1 { + font-size: clamp(1.72rem, 8.4vw, 2.2rem); + } + + .meta, + .pagination a, + .page-info { + font-size: 0.76rem; + letter-spacing: 0.02em; + } +} diff --git a/themes/netbsd/style.css b/themes/netbsd/style.css new file mode 100644 index 0000000..503b729 --- /dev/null +++ b/themes/netbsd/style.css @@ -0,0 +1,531 @@ +/* + * NetBSD Theme for BSSG + * NetBSD-recognizable navy/orange engineering look with flag-inspired geometry. + */ + +:root { + --netbsd-navy: #003c78; + --netbsd-deep: #002a54; + --netbsd-sky: #2d5d90; + --netbsd-orange: #f4821f; + --paper: #fbfdff; + --paper-2: #f1f5f9; + --text: #163046; + --muted: #506578; + --border: #cfdae4; + --link: #004a90; + --link-hover: #00366a; + --tag-bg: #edf4fb; + --tag-text: #184b78; + --quote-bg: #f2f7fc; + --code-bg: #edf3f8; + --radius: 8px; + --shadow: 0 12px 28px rgba(0, 28, 52, 0.2); + --content-width: 920px; + --font-body: "Source Sans 3", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-heading: "Avenir Next", "Segoe UI", "Trebuchet MS", sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 17px; +} + +body { + margin: 0; + color: var(--text); + font-family: var(--font-body); + line-height: 1.72; + background: + radial-gradient(circle at 0 0, rgba(244, 130, 31, 0.1) 0 18%, transparent 19%), + linear-gradient(180deg, #f3f8fc 0%, #e7eef5 100%); +} + +::selection { + background: #cfe2f3; + color: #0f2a3e; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 0 1.2rem 2.7rem; + background: linear-gradient(180deg, #ffffff 0%, var(--paper) 100%); + border-left: 1px solid #c6d2dd; + border-right: 1px solid #c6d2dd; + box-shadow: var(--shadow); + min-height: 100vh; +} + +header { + margin: 0 -1.2rem 1.95rem; + padding: 1rem 1.2rem 0.95rem; + color: #f2f7fb; + background: + linear-gradient(135deg, rgba(244, 130, 31, 0.2) 0 14%, transparent 15%), + linear-gradient(180deg, var(--netbsd-navy) 0%, var(--netbsd-deep) 100%); + border-top: 6px solid var(--netbsd-orange); + border-bottom: 1px solid #0e2942; + position: relative; + overflow: hidden; +} + +header::before { + content: "Of course it runs NetBSD!"; + position: absolute; + right: 18px; + top: 12px; + background: linear-gradient(180deg, #0f447a 0%, #06315a 100%); + color: #ffe1be; + border: 1px solid rgba(255, 255, 255, 0.4); + border-bottom-color: var(--netbsd-orange); + border-radius: 4px; + padding: 0.16rem 0.55rem; + font-family: var(--font-mono); + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: none; + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.25); +} + +header::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 3px; + background: linear-gradient(90deg, transparent, var(--netbsd-orange), transparent); +} + +.site-title, +header h1 { + margin: 0; + color: #f4f8fc; + font-family: var(--font-heading); + font-size: clamp(1.9rem, 3.6vw, 2.65rem); + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; + line-height: 1.2; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: #ffd7b3; +} + +header p { + margin: 0.42rem 0 0; + color: #c7d9e8; + font-family: var(--font-mono); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +nav { + margin-top: 0.92rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +nav a { + display: inline-block; + color: #e9f2f9; + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.22); + border-bottom: 2px solid rgba(244, 130, 31, 0.55); + border-radius: 5px; + padding: 0.24rem 0.6rem; + text-decoration: none; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +nav a:hover, +nav a:focus { + background: rgba(244, 130, 31, 0.18); + border-color: rgba(255, 255, 255, 0.45); + color: #fff; +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.4rem; +} + +article.post, +article.page, +.posts-list article { + background: #fff; + border: 1px solid var(--border); + border-top: 4px solid var(--netbsd-navy); + border-radius: var(--radius); + padding: 1.14rem 1rem 0.95rem; +} + +article.post::before, +article.page::before, +.posts-list article::before { + content: ""; + display: block; + height: 2px; + margin: -0.25rem -1rem 0.85rem; + background: linear-gradient(90deg, transparent, rgba(244, 130, 31, 0.62), transparent); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #102f48; + font-family: var(--font-heading); + margin: 1.08rem 0 0.68rem; + line-height: 1.3; + letter-spacing: 0.01em; +} + +h1 { font-size: clamp(1.7rem, 3.1vw, 2.22rem); } +h2 { font-size: clamp(1.38rem, 2.5vw, 1.78rem); } +h3 { font-size: clamp(1.14rem, 1.9vw, 1.38rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.92rem; +} + +a { + color: var(--link); + text-decoration-thickness: 1px; + text-underline-offset: 0.14em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.74rem; +} + +.meta { + margin: 0; + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.reading-time { + margin-top: 0.24rem; +} + +.summary { + margin-top: 0.72rem; + color: #39576f; +} + +.post-content { + margin-top: 0.64rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 0.94rem 0; +} + +img { + max-width: 100%; + height: auto; + display: block; + border: 1px solid #b9c8d6; + border-radius: 4px; +} + +.image-caption, +figcaption { + margin-top: 0.34rem; + color: #61778a; + font-size: 0.78rem; + font-style: italic; +} + +blockquote { + margin: 0.98rem 0; + padding: 0.68rem 0.85rem; + border-left: 4px solid var(--netbsd-sky); + background: var(--quote-bg); + color: #2d4a61; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.84rem; +} + +code { + background: var(--code-bg); + border: 1px solid #d3dfeb; + border-radius: 3px; + padding: 0.08rem 0.26rem; +} + +pre { + background: var(--code-bg); + border: 1px solid #d0dde8; + border-radius: 6px; + padding: 0.78rem; + overflow-x: auto; +} + +pre code { + background: none; + border: 0; + padding: 0; +} + +hr { + border: 0; + border-top: 1px solid #d2dee8; + margin: 1.1rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.34rem; + margin-top: 0.84rem; +} + +.tags a, +.tags-list a { + display: inline-block; + text-decoration: none; + color: var(--tag-text); + background: var(--tag-bg); + border: 1px solid #cadcec; + border-radius: 999px; + padding: 0.17rem 0.52rem; + font-family: var(--font-mono); + font-size: 0.71rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #deebf8; + border-color: #b8d0e7; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.42rem; +} + +.tag-count { + color: #61798f; +} + +.posts-list article + article { + margin-top: 0.82rem; +} + +.posts-list h2, +.posts-list h3 { + margin-top: 0; +} + +.pagination { + margin-top: 1.2rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.44rem; +} + +.pagination a { + display: inline-block; + color: #17364f; + text-decoration: none; + border: 1px solid #c0d2e1; + border-radius: 5px; + padding: 0.22rem 0.6rem; + background: #eff5fb; + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.pagination a:hover, +.pagination a:focus { + background: var(--netbsd-navy); + color: #fff; + border-color: #1f4e7e; +} + +.page-info { + color: #5f7488; + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.related-posts { + margin-top: 1.3rem; + padding-top: 0.8rem; + border-top: 1px dashed #c2d2df; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.05rem; +} + +.related-posts-list { + display: grid; + gap: 0.6rem; +} + +.related-post { + border: 1px solid #c5d5e4; + border-left: 4px solid var(--netbsd-orange); + background: #f6fafe; + border-radius: 5px; + padding: 0.52rem 0.64rem; +} + +.related-post h4 { + margin: 0 0 0.2rem; + font-size: 0.94rem; +} + +.related-post p { + margin: 0; + color: #4c6980; + font-size: 0.9rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.9rem 0; +} + +th, +td { + border: 1px solid #d0dde8; + padding: 0.38rem 0.5rem; +} + +th { + background: #edf4fb; + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +footer { + margin-top: 1.78rem; + padding: 0.84rem 0 0.08rem; + border-top: 2px solid #d1dbe5; + color: #577084; + font-family: var(--font-mono); + font-size: 0.71rem; + text-transform: uppercase; + letter-spacing: 0.05em; + position: relative; +} + +footer p { + margin: 0.32rem 0; +} + +footer a { + color: var(--link); +} + +footer a:hover, +footer a:focus { + color: var(--link-hover); +} + +@media (max-width: 840px) { + .container { + padding: 0 0.82rem 2rem; + } + + header { + margin: 0 -0.82rem 1.45rem; + padding: 0.88rem 0.82rem 0.8rem; + } + + header::before { + right: 10px; + top: 10px; + padding: 0.12rem 0.42rem; + font-size: 0.55rem; + letter-spacing: 0.01em; + } + + nav { + padding-right: 0; + } + + article.post, + article.page, + .posts-list article { + padding: 0.96rem 0.8rem 0.8rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.02em; + } +} diff --git a/themes/openbsd/style.css b/themes/openbsd/style.css new file mode 100644 index 0000000..aa1973c --- /dev/null +++ b/themes/openbsd/style.css @@ -0,0 +1,531 @@ +/* + * OpenBSD Theme for BSSG + * Puffy-inspired yellow/black visual language with bold outlines. + */ + +:root { + --obsd-yellow: #d7c06f; + --obsd-yellow-deep: #b79a4a; + --obsd-black: #121212; + --obsd-ink: #212121; + --obsd-paper: #f8f4e7; + --obsd-paper-2: #efe6c9; + --text: #1f2326; + --muted: #5f6468; + --border: #2a2a2a; + --link: #0d4f9e; + --link-hover: #08396f; + --tag-bg: #e9ddb5; + --tag-text: #2b2b2b; + --quote-bg: #f1e8cb; + --code-bg: #ece1bd; + --radius: 9px; + --shadow: 0 14px 30px rgba(0, 0, 0, 0.22); + --content-width: 900px; + --font-body: "Verdana", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-heading: "Trebuchet MS", "Verdana", "Segoe UI", sans-serif; + --font-mono: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 17px; +} + +body { + margin: 0; + color: var(--text); + font-family: var(--font-body); + line-height: 1.7; + background: + radial-gradient(circle at 2px 2px, rgba(18, 18, 18, 0.12) 1px, transparent 1px), + linear-gradient(180deg, #eee5c8 0%, var(--obsd-yellow) 52%, #c5af66 100%); + background-size: 12px 12px, 100% 100%; +} + +::selection { + background: #101010; + color: #f5edc9; +} + +.container { + max-width: var(--content-width); + margin: 0 auto; + padding: 0 1rem 2.5rem; + background: linear-gradient(180deg, #fbf8ef 0%, var(--obsd-paper) 100%); + border-left: 3px solid var(--obsd-black); + border-right: 3px solid var(--obsd-black); + box-shadow: var(--shadow); + min-height: 100vh; +} + +header { + margin: 0 -1rem 1.8rem; + padding: 1rem 1rem 0.92rem; + background: + repeating-linear-gradient( + -45deg, + #2c2c2c 0 8px, + #202020 8px 16px + ); + color: #fff5ce; + border-top: 8px solid #000; + border-bottom: 4px solid #000; + position: relative; + overflow: hidden; +} + +header::before { + content: "OpenBSD: Secure By Default"; + position: absolute; + right: 12px; + top: 12px; + background: linear-gradient(180deg, #e9d896 0%, #c8ad5f 100%); + color: #141414; + border: 2px solid #000; + border-radius: 6px; + padding: 0.16rem 0.54rem; + font-family: var(--font-mono); + font-size: 0.58rem; + font-weight: 700; + text-transform: none; + letter-spacing: 0.01em; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.28); +} + +header::after { + content: ""; + position: absolute; + inset: auto 0 0; + height: 3px; + background: linear-gradient(90deg, transparent, #e7d189, transparent); +} + +.site-title, +header h1 { + margin: 0; + color: #f2e4b8; + font-family: var(--font-heading); + font-size: clamp(1.9rem, 3.7vw, 2.6rem); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + line-height: 1.2; + text-shadow: + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; +} + +.site-title a { + color: inherit; + text-decoration: none; +} + +.site-title a:hover, +.site-title a:focus { + color: #fff; +} + +header p { + margin: 0.4rem 0 0; + color: #e4d093; + font-family: var(--font-mono); + font-size: 0.73rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +nav { + margin-top: 0.88rem; + display: flex; + flex-wrap: wrap; + gap: 0.44rem; +} + +nav a { + display: inline-block; + color: #1a1a1a; + background: linear-gradient(180deg, #e8d790 0%, #bfa357 100%); + border: 2px solid #000; + border-radius: 999px; + padding: 0.2rem 0.58rem; + text-decoration: none; + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 700; +} + +nav a:hover, +nav a:focus { + background: linear-gradient(180deg, #f2e5ba 0%, #cbb06a 100%); + transform: translateY(-1px); +} + +main { + min-height: 62vh; +} + +article { + margin-bottom: 1.35rem; +} + +article.post, +article.page, +.posts-list article { + background: var(--obsd-paper); + border: 2px solid var(--obsd-black); + border-radius: var(--radius); + box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.22); + padding: 1.06rem 0.95rem 0.9rem; + position: relative; +} + +article.post::before, +article.page::before, +.posts-list article::before { + content: none; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #111; + font-family: var(--font-heading); + margin: 1rem 0 0.64rem; + line-height: 1.3; +} + +h1 { font-size: clamp(1.7rem, 3vw, 2.22rem); } +h2 { font-size: clamp(1.36rem, 2.4vw, 1.76rem); } +h3 { font-size: clamp(1.12rem, 1.9vw, 1.34rem); } + +p, +ul, +ol { + margin-top: 0; + margin-bottom: 0.9rem; +} + +a { + color: var(--link); + text-decoration-thickness: 1px; + text-underline-offset: 0.13em; +} + +a:hover, +a:focus { + color: var(--link-hover); +} + +.page-meta { + margin-bottom: 0.72rem; +} + +.meta { + margin: 0; + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.reading-time { + margin-top: 0.22rem; +} + +.summary { + margin-top: 0.68rem; + color: #434a50; +} + +.post-content { + margin-top: 0.62rem; +} + +.featured-image, +.index-image, +.tag-image, +.archive-image, +.author-image { + margin: 0.9rem 0; +} + +img { + max-width: 100%; + height: auto; + display: block; + border: 2px solid #111; + border-radius: 6px; +} + +.image-caption, +figcaption { + margin-top: 0.32rem; + color: #5f676e; + font-size: 0.77rem; + font-style: italic; +} + +blockquote { + margin: 0.95rem 0; + padding: 0.65rem 0.8rem; + border-left: 5px solid #111; + background: var(--quote-bg); + color: #2e3438; +} + +pre, +code { + font-family: var(--font-mono); + font-size: 0.83rem; +} + +code { + background: var(--code-bg); + border: 1px solid #b89d55; + border-radius: 3px; + padding: 0.08rem 0.24rem; +} + +pre { + background: var(--code-bg); + border: 2px solid #111; + border-radius: 6px; + padding: 0.75rem; + overflow-x: auto; +} + +pre code { + background: none; + border: 0; + padding: 0; +} + +hr { + border: 0; + border-top: 2px dashed #3b3b3b; + margin: 1.08rem 0; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.34rem; + margin-top: 0.82rem; +} + +.tags a, +.tags-list a { + display: inline-block; + text-decoration: none; + color: var(--tag-text); + background: var(--tag-bg); + border: 2px solid #111; + border-radius: 999px; + padding: 0.16rem 0.5rem; + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 700; +} + +.tags a:hover, +.tags a:focus, +.tags-list a:hover, +.tags-list a:focus { + background: #e4d091; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.tag-count { + color: #3f3f3f; +} + +.posts-list article + article { + margin-top: 0.78rem; +} + +.posts-list h2, +.posts-list h3 { + margin-top: 0; +} + +.pagination { + margin-top: 1.12rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.42rem; +} + +.pagination a { + display: inline-block; + color: #131313; + text-decoration: none; + border: 2px solid #000; + border-radius: 999px; + padding: 0.2rem 0.56rem; + background: linear-gradient(180deg, #e8d994 0%, #c2a85e 100%); + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.03em; + font-weight: 700; +} + +.pagination a:hover, +.pagination a:focus { + background: linear-gradient(180deg, #f1e4bc 0%, #cfb471 100%); +} + +.page-info { + color: #52595f; + font-family: var(--font-mono); + font-size: 0.71rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.related-posts { + margin-top: 1.2rem; + padding-top: 0.74rem; + border-top: 2px dashed #222; +} + +.related-posts h3 { + margin-top: 0; + font-size: 1.02rem; +} + +.related-posts-list { + display: grid; + gap: 0.56rem; +} + +.related-post { + border: 2px solid #111; + border-left-width: 6px; + border-radius: 6px; + background: #f3ead1; + padding: 0.5rem 0.62rem; +} + +.related-post h4 { + margin: 0 0 0.18rem; + font-size: 0.93rem; +} + +.related-post p { + margin: 0; + color: #474e53; + font-size: 0.89rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 0.88rem 0; +} + +th, +td { + border: 2px solid #111; + padding: 0.36rem 0.48rem; +} + +th { + background: #ddca88; + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +footer { + margin-top: 1.72rem; + padding: 0.82rem 0 0.1rem; + border-top: 3px solid #111; + color: #4f565c; + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05em; + position: relative; +} + +footer p { + margin: 0.31rem 0; +} + +footer a { + color: var(--link); +} + +footer a:hover, +footer a:focus { + color: var(--link-hover); +} + +@media (max-width: 840px) { + .container { + padding: 0 0.78rem 1.95rem; + } + + header { + margin: 0 -0.78rem 1.42rem; + padding: 0.85rem 0.78rem 0.74rem; + } + + header::before { + right: 10px; + top: 10px; + padding: 0.12rem 0.42rem; + font-size: 0.5rem; + letter-spacing: 0; + } + + nav { + padding-right: 0; + } + + article.post, + article.page, + .posts-list article { + padding: 0.92rem 0.78rem 0.76rem; + } + + nav a, + .meta, + .page-info, + .pagination a { + letter-spacing: 0.02em; + } +} From d0ceef943cfd1ea8dded618d955cba909f48209e Mon Sep 17 00:00:00 2001 From: Stefano Marinelli <stefano@dragas.it> Date: Sat, 14 Mar 2026 09:25:19 +0100 Subject: [PATCH 18/21] Added mynotes theme --- themes/netbsd/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/netbsd/style.css b/themes/netbsd/style.css index 503b729..561043e 100644 --- a/themes/netbsd/style.css +++ b/themes/netbsd/style.css @@ -1,6 +1,6 @@ /* * NetBSD Theme for BSSG - * NetBSD-recognizable navy/orange engineering look with flag-inspired geometry. + * NetBSD-recognizable orange engineering look with flag-inspired geometry. */ :root { From d4b0d4d58abbd4290e9e3ade24977f9214c4e627 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli <stefano@dragas.it> Date: Sat, 14 Mar 2026 11:06:50 +0100 Subject: [PATCH 19/21] Fixes for RAM only build when there are no pages --- scripts/build/content.sh | 11 +++++++++++ scripts/build/generate_pages.sh | 6 +++++- scripts/build/ram_mode.sh | 23 ++++++++++++++++++++--- scripts/build/templates.sh | 2 ++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/scripts/build/content.sh b/scripts/build/content.sh index f0ee63c..458c6e9 100755 --- a/scripts/build/content.sh +++ b/scripts/build/content.sh @@ -16,6 +16,12 @@ parse_metadata() { local field="$2" local value="" + # Ignore empty or directory inputs so callers can safely scan optional lists. + if [[ -z "$file" || -d "$file" ]]; then + echo "$value" + return 0 + fi + # RAM mode: parse directly from preloaded content to avoid disk/cache I/O. if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then local file_content frontmatter @@ -87,6 +93,11 @@ parse_metadata() { # Extract metadata from markdown file (builds cache) extract_metadata() { local file="$1" + if [[ -z "$file" || -d "$file" ]]; then + echo "ERROR_FILE_NOT_FOUND" + return 1 + fi + local metadata_cache_file="${CACHE_DIR:-.bssg_cache}/meta/$(basename "$file")" local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker" local ram_mode_active=false diff --git a/scripts/build/generate_pages.sh b/scripts/build/generate_pages.sh index 7ac72d4..6f5714b 100755 --- a/scripts/build/generate_pages.sh +++ b/scripts/build/generate_pages.sh @@ -223,13 +223,16 @@ process_all_pages() { { local file quoted_file for file in "${page_files[@]}"; do + [[ -z "$file" ]] && continue printf -v quoted_file '%q' "$file" echo "process_single_page_file $quoted_file" done } | run_parallel "$cores" else echo -e "${YELLOW}Using sequential processing for RAM-mode pages${NC}" - process_single_page_file "${page_files[0]}" + if [[ -n "${page_files[0]}" ]]; then + process_single_page_file "${page_files[0]}" + fi fi # Use GNU parallel if available, otherwise fallback # IMPORTANT: Assumes HAS_PARALLEL is exported/available @@ -256,6 +259,7 @@ process_all_pages() { echo -e "${YELLOW}Using sequential processing for pages${NC}" local file for file in "${page_files[@]}"; do + [[ -z "$file" ]] && continue process_single_page_file "$file" done fi diff --git a/scripts/build/ram_mode.sh b/scripts/build/ram_mode.sh index 4d4e0a9..a659766 100644 --- a/scripts/build/ram_mode.sh +++ b/scripts/build/ram_mode.sh @@ -36,6 +36,10 @@ _ram_mode_disk_mtime() { ram_mode_resolve_key() { local file="$1" + if [[ -z "$file" ]]; then + return 1 + fi + if [[ -n "${BSSG_RAM_FILE_CONTENT[$file]+_}" || -n "${BSSG_RAM_FILE_MTIME[$file]+_}" ]]; then echo "$file" return 0 @@ -66,13 +70,17 @@ ram_mode_resolve_key() { ram_mode_has_file() { local key - key=$(ram_mode_resolve_key "$1") + if ! key=$(ram_mode_resolve_key "$1"); then + return 1 + fi [[ -n "${BSSG_RAM_FILE_CONTENT[$key]+_}" || -n "${BSSG_RAM_FILE_MTIME[$key]+_}" ]] } ram_mode_get_content() { local key - key=$(ram_mode_resolve_key "$1") + if ! key=$(ram_mode_resolve_key "$1"); then + return 0 + fi if [[ -n "${BSSG_RAM_FILE_CONTENT[$key]+_}" ]]; then printf '%s' "${BSSG_RAM_FILE_CONTENT[$key]}" fi @@ -80,7 +88,10 @@ ram_mode_get_content() { ram_mode_get_mtime() { local key - key=$(ram_mode_resolve_key "$1") + if ! key=$(ram_mode_resolve_key "$1"); then + printf '0\n' + return 0 + fi if [[ -n "${BSSG_RAM_FILE_MTIME[$key]+_}" ]]; then printf '%s\n' "${BSSG_RAM_FILE_MTIME[$key]}" else @@ -89,10 +100,16 @@ ram_mode_get_mtime() { } ram_mode_list_src_files() { + if [[ ${#BSSG_RAM_SRC_FILES[@]} -eq 0 ]]; then + return 0 + fi printf '%s\n' "${BSSG_RAM_SRC_FILES[@]}" } ram_mode_list_page_files() { + if [[ ${#BSSG_RAM_PAGE_FILES[@]} -eq 0 ]]; then + return 0 + fi printf '%s\n' "${BSSG_RAM_PAGE_FILES[@]}" } diff --git a/scripts/build/templates.sh b/scripts/build/templates.sh index baef671..cd88c8b 100755 --- a/scripts/build/templates.sh +++ b/scripts/build/templates.sh @@ -141,6 +141,8 @@ preload_templates() { fi for file in "${page_files[@]}"; do + [[ -z "$file" ]] && continue + # Skip if file is hidden if [[ $(basename "$file") == .* ]]; then continue From 37c5a2283e1338dfe1bc04a62d3074d1cb5d2ee2 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli <stefano@dragas.it> Date: Tue, 17 Mar 2026 11:39:33 +0100 Subject: [PATCH 20/21] Added Fediverse linking options --- README.md | 76 ++++++++++++++++++++ bssg-editor.html | 21 ++++++ config.sh | 13 ++++ scripts/build/config_loader.sh | 75 ++++++++++++++++++- scripts/build/generate_archives.sh | 6 ++ scripts/build/generate_authors.sh | 4 ++ scripts/build/generate_index.sh | 2 + scripts/build/generate_pages.sh | 1 + scripts/build/generate_posts.sh | 24 ++++++- scripts/build/generate_secondary_pages.sh | 1 + scripts/build/generate_tags.sh | 4 ++ scripts/build/templates.sh | 18 +++++ scripts/build/utils.sh | 45 ++++++++++++ scripts/post.sh | 4 ++ src/2025-04-01-bssg-features-and-examples.md | 3 + templates/header.html | 2 + 16 files changed, 297 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 609b02d..708559d 100644 --- a/README.md +++ b/README.md @@ -541,6 +541,7 @@ image_caption: Optional caption for the image description: A brief summary of your post that will appear in listings, social media shares, and RSS feeds. author_name: John Doe # Optional: Override default site author author_email: john@example.com # Optional: Override default site author email +fediverse_creator: @john@example.social # Optional: Override the fediverse:creator meta tag for this post --- Content goes here... @@ -584,6 +585,7 @@ BSSG supports multiple authors through optional frontmatter fields that can over - `author_name`: The name of the post author (optional) - `author_email`: The email address of the post author (optional) +- `fediverse_creator`: Explicit override for the post's `<meta name="fediverse:creator">` tag (optional) #### Fallback Behavior @@ -634,6 +636,41 @@ Author information is displayed and used in: - **Schema.org Metadata**: JSON-LD structured data for search engines - **Archive Pages**: Author information in post listings +### Fediverse Creator Tag + +BSSG can emit Mastodon's `fediverse:creator` metadata across generated pages so link previews can show and follow the author more easily. + +#### Fallback Order + +BSSG resolves the creator tag in this order: + +1. `fediverse_creator` in the post frontmatter +2. `AUTHOR_FEDIVERSE_CREATORS["Author Name"]` from config, matched against `author_name` +3. `FEDIVERSE_CREATOR` from config + +If none of those are set, no `fediverse:creator` meta tag is emitted. + +For non-post pages such as the homepage, tags, archives, authors, and static pages, BSSG uses the resolved site-level/default creator. Individual posts can still override that value with `fediverse_creator` in frontmatter. + +#### Configuration + +Add a site-wide default in `config.sh.local`: + +```bash +FEDIVERSE_CREATOR="@you@example.social" +``` + +For multi-author sites, you can optionally add exact-match per-author overrides: + +```bash +declare -A AUTHOR_FEDIVERSE_CREATORS=( + ["Jane Smith"]="@jane@example.social" + ["John Doe"]="@john@example.com" +) +``` + +If you customize `templates/header.html`, keep `{{fediverse_creator_meta}}` inside `<head>`. The bundled template already includes it, and BSSG also falls back to injecting the tag before `</head>` for older custom headers. + #### Examples **Post with custom author:** @@ -669,6 +706,34 @@ This feature is particularly useful for: - Maintaining author attribution when migrating content from other platforms - Creating author-focused content organization alongside tags and archives +### Fediverse Profile Verification + +BSSG can also emit one or more site-wide `<link rel="me">` tags in the document `<head>`, which is useful for Mastodon and compatible fediverse profile verification. + +Add this to `config.sh.local` for a single profile: + +```bash +REL_ME_URL="https://mastodon.example.com/@john" +``` + +Or use multiple links: + +```bash +REL_ME_URLS=( + "https://mastodon.example.com/@john" + "https://another-fedi.example/@john" +) +``` + +The default header.html now includes a `{{rel_me_link}}` placeholder, which expands to one or more tags such as: + +```html +<link rel="me" href="https://mastodon.example.com/@john"> +<link rel="me" href="https://another-fedi.example/@john"> +``` + +If both `REL_ME_URL` and `REL_ME_URLS` are set, BSSG emits all unique URLs from both. If neither is set, BSSG omits the tags. + ## Customization To customize the appearance of your site, you can edit: @@ -725,6 +790,12 @@ SITE_DESCRIPTION="A complete SSG - written in bash" SITE_URL="http://localhost:8000" AUTHOR_NAME="Anonymous" AUTHOR_EMAIL="anonymous@example.com" +REL_ME_URL="" # Optional fediverse profile URL for <link rel="me"> verification +# REL_ME_URLS=( +# "https://mastodon.example.com/@john" +# "https://another-fedi.example/@john" +# ) +FEDIVERSE_CREATOR="" # Optional default fediverse:creator value for posts # Content configuration DATE_FORMAT="%Y-%m-%d %H:%M:%S %z" @@ -742,6 +813,11 @@ SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2) URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs. Available: Year, Month, Day, slug ENABLE_TAG_RSS=true # Enable or disable tag-specific RSS feed generation (default: true) +# Optional exact-match per-author fediverse overrides +# declare -A AUTHOR_FEDIVERSE_CREATORS=( +# ["Jane Smith"]="@jane@example.social" +# ) + # Archive Page Configuration ARCHIVES_LIST_ALL_POSTS="false" # Options: "true", "false". If true, list all posts on the main archive page. diff --git a/bssg-editor.html b/bssg-editor.html index 7d1fb05..cfb871b 100644 --- a/bssg-editor.html +++ b/bssg-editor.html @@ -1136,6 +1136,12 @@ <div class="form-help">Author email for this post (optional)</div> </div> + <div class="form-group"> + <label class="form-label" for="fediverseCreator">Fediverse Creator</label> + <input type="text" id="fediverseCreator" class="form-input" placeholder="@you@example.social"> + <div class="form-help">Optional override for the fediverse:creator meta tag</div> + </div> + <hr style="margin: 1.5rem 0; border: none; border-top: 1px solid var(--border);"> <h3 style="font-size: 1rem; margin-bottom: 1rem; color: var(--text-secondary);">Settings</h3> @@ -1328,6 +1334,7 @@ imageCaption: '', authorName: '', authorEmail: '', + fediverseCreator: '', content: '' }, isDirty: false, @@ -1357,6 +1364,7 @@ imageCaption: document.getElementById('imageCaption'), authorName: document.getElementById('authorName'), authorEmail: document.getElementById('authorEmail'), + fediverseCreator: document.getElementById('fediverseCreator'), markdownEditor: document.getElementById('markdownEditor'), previewContent: document.getElementById('previewContent'), wordCount: document.getElementById('wordCount'), @@ -1519,6 +1527,10 @@ if (post.authorEmail) { frontmatter += `author_email: ${post.authorEmail}\n`; } + + if (post.fediverseCreator) { + frontmatter += `fediverse_creator: ${post.fediverseCreator}\n`; + } frontmatter += '---\n\n'; @@ -1818,6 +1830,7 @@ imageCaption: '', authorName: '', authorEmail: '', + fediverseCreator: '', content: '' }; state.currentArticleId = null; @@ -2025,6 +2038,7 @@ state.currentPost.imageCaption = elements.imageCaption.value; state.currentPost.authorName = elements.authorName.value; state.currentPost.authorEmail = elements.authorEmail.value; + state.currentPost.fediverseCreator = elements.fediverseCreator.value; state.currentPost.content = elements.markdownEditor.value; } @@ -2038,6 +2052,7 @@ elements.imageCaption.value = state.currentPost.imageCaption || ''; elements.authorName.value = state.currentPost.authorName || ''; elements.authorEmail.value = state.currentPost.authorEmail || ''; + elements.fediverseCreator.value = state.currentPost.fediverseCreator || ''; elements.markdownEditor.value = state.currentPost.content || ''; // Update tags @@ -2276,6 +2291,7 @@ imageCaption: parsed.image_caption || '', authorName: parsed.author_name || '', authorEmail: parsed.author_email || '', + fediverseCreator: parsed.fediverse_creator || '', content: markdownContent.trim() }; @@ -2502,6 +2518,11 @@ markDirty(); }); + elements.fediverseCreator.addEventListener('input', (e) => { + state.currentPost.fediverseCreator = e.target.value; + markDirty(); + }); + // Unsplash API key elements.unsplashKey.addEventListener('input', (e) => { const key = e.target.value.trim(); diff --git a/config.sh b/config.sh index 77c26bf..0a615f0 100644 --- a/config.sh +++ b/config.sh @@ -39,6 +39,19 @@ SITE_DESCRIPTION="A complete SSG - written in bash" SITE_URL="http://localhost:8000" AUTHOR_NAME="Anonymous" AUTHOR_EMAIL="anonymous@example.com" +REL_ME_URL="" # Optional fediverse profile URL for <link rel="me"> verification, e.g. "https://mastodon.example/@john" +# Optional additional rel="me" verification links. BSSG emits all unique values from +# REL_ME_URL and REL_ME_URLS. +# REL_ME_URLS=( +# "https://mastodon.example/@john" +# "https://another-fedi.example/@john" +# ) +FEDIVERSE_CREATOR="" # Optional default fediverse:creator value for posts, e.g. "@you@example.social" +# Optional per-author overrides matched against author_name exactly. +# declare -A AUTHOR_FEDIVERSE_CREATORS=( +# ["Jane Smith"]="@jane@example.social" +# ["John Doe"]="@john@example.com" +# ) # Content configuration DATE_FORMAT="%Y-%m-%d %H:%M:%S %z" diff --git a/scripts/build/config_loader.sh b/scripts/build/config_loader.sh index 405d3fe..244c6b2 100755 --- a/scripts/build/config_loader.sh +++ b/scripts/build/config_loader.sh @@ -24,6 +24,11 @@ SITE_DESCRIPTION="${SITE_DESCRIPTION:-A personal journal and introspective newsp SITE_URL="${SITE_URL:-http://localhost}" AUTHOR_NAME="${AUTHOR_NAME:-Anonymous}" AUTHOR_EMAIL="${AUTHOR_EMAIL:-anonymous@example.com}" +REL_ME_URL="${REL_ME_URL:-}" +REL_ME_URLS_SERIALIZED="${REL_ME_URLS_SERIALIZED:-}" +FEDIVERSE_CREATOR="${FEDIVERSE_CREATOR:-}" +AUTHOR_FEDIVERSE_CREATORS_SERIALIZED="${AUTHOR_FEDIVERSE_CREATORS_SERIALIZED:-}" +SITE_FEDIVERSE_CREATOR_META_TAG="${SITE_FEDIVERSE_CREATOR_META_TAG:-}" DATE_FORMAT="${DATE_FORMAT:-%Y-%m-%d %H:%M:%S}" TIMEZONE="${TIMEZONE:-local}" SHOW_TIMEZONE="${SHOW_TIMEZONE:-false}" @@ -218,6 +223,68 @@ done # --- Expand Tilde in Path Variables --- END --- +# --- Derived Configuration --- START --- +serialize_author_fediverse_creators() { + local serialized="" + local author_name + + if ! declare -p AUTHOR_FEDIVERSE_CREATORS >/dev/null 2>&1; then + printf '%s' "$serialized" + return 0 + fi + + if [[ "$(declare -p AUTHOR_FEDIVERSE_CREATORS 2>/dev/null)" != "declare -A"* ]]; then + print_warning "AUTHOR_FEDIVERSE_CREATORS is set but is not an associative array. Ignoring it." + printf '%s' "$serialized" + return 0 + fi + + while IFS= read -r author_name; do + serialized+="${author_name}"$'\t'"${AUTHOR_FEDIVERSE_CREATORS[$author_name]}"$'\n' + done < <(printf '%s\n' "${!AUTHOR_FEDIVERSE_CREATORS[@]}" | LC_ALL=C sort) + + printf '%s' "$serialized" +} + +serialize_rel_me_urls() { + local serialized="" + local rel_me_url="" + + rel_me_url=$(printf '%s' "$REL_ME_URL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [ -n "$rel_me_url" ]; then + serialized+="${rel_me_url}"$'\n' + fi + + if declare -p REL_ME_URLS >/dev/null 2>&1; then + local rel_me_decl + rel_me_decl="$(declare -p REL_ME_URLS 2>/dev/null)" + if [[ "$rel_me_decl" == "declare -a"* ]]; then + local rel_me_entry + for rel_me_entry in "${REL_ME_URLS[@]}"; do + rel_me_entry=$(printf '%s' "$rel_me_entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [ -n "$rel_me_entry" ]; then + serialized+="${rel_me_entry}"$'\n' + fi + done + else + print_warning "REL_ME_URLS is set but is not a standard array. Ignoring it." + fi + fi + + if [ -z "$serialized" ]; then + printf '%s' "" + return 0 + fi + + printf '%s' "$serialized" | awk 'NF && !seen[$0]++' +} + +REL_ME_URLS_SERIALIZED="$(serialize_rel_me_urls)" +AUTHOR_FEDIVERSE_CREATORS_SERIALIZED="$(serialize_author_fediverse_creators)" +SITE_FEDIVERSE_CREATOR_META_TAG="$(build_fediverse_creator_meta_tag "${AUTHOR_NAME:-Anonymous}" "")" +# --- Derived Configuration --- END --- + + # --- Export All Variables --- START --- # Define the list of configuration variables relevant for hashing/exporting @@ -225,7 +292,8 @@ done # and that should trigger a cache rebuild if changed. BSSG_CONFIG_VARS_ARRAY=( CONFIG_FILE SRC_DIR OUTPUT_DIR TEMPLATES_DIR THEMES_DIR STATIC_DIR THEME - SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL + SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL REL_ME_URL REL_ME_URLS_SERIALIZED + FEDIVERSE_CREATOR AUTHOR_FEDIVERSE_CREATORS_SERIALIZED SITE_FEDIVERSE_CREATOR_META_TAG DATE_FORMAT TIMEZONE SHOW_TIMEZONE POSTS_PER_PAGE RSS_ITEM_LIMIT RSS_INCLUDE_FULL_CONTENT RSS_FILENAME INDEX_SHOW_FULL_CONTENT CLEAN_OUTPUT FORCE_REBUILD BUILD_MODE SITE_LANG LOCALE_DIR PAGES_DIR MARKDOWN_PROCESSOR @@ -260,6 +328,11 @@ export SITE_DESCRIPTION export SITE_URL export AUTHOR_NAME export AUTHOR_EMAIL +export REL_ME_URL +export REL_ME_URLS_SERIALIZED +export FEDIVERSE_CREATOR +export AUTHOR_FEDIVERSE_CREATORS_SERIALIZED +export SITE_FEDIVERSE_CREATOR_META_TAG export DATE_FORMAT export TIMEZONE export SHOW_TIMEZONE diff --git a/scripts/build/generate_archives.sh b/scripts/build/generate_archives.sh index 137dcbe..2356626 100755 --- a/scripts/build/generate_archives.sh +++ b/scripts/build/generate_archives.sh @@ -35,6 +35,7 @@ _generate_ram_year_archive_page() { year_header=${year_header//\{\{site_url\}\}/"$SITE_URL"} year_header=${year_header//\{\{og_image\}\}/""} year_header=${year_header//\{\{twitter_image\}\}/""} + year_header=${year_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} local year_schema_json year_schema_json='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$year_page_title"'","description": "Archive of posts from '"$year"'","url": "'"$SITE_URL$year_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' year_header=${year_header//\{\{schema_json_ld\}\}/"$year_schema_json"} @@ -93,6 +94,7 @@ _generate_ram_month_archive_page() { month_header=${month_header//\{\{site_url\}\}/"$SITE_URL"} month_header=${month_header//\{\{og_image\}\}/""} month_header=${month_header//\{\{twitter_image\}\}/""} + month_header=${month_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} local month_schema_json month_schema_json='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$month_page_title"'","description": "Archive of posts from '"$month_name $year"'","url": "'"$SITE_URL$month_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' month_header=${month_header//\{\{schema_json_ld\}\}/"$month_schema_json"} @@ -203,6 +205,7 @@ _generate_archive_pages_ram() { header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} local schema_json_ld schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "Archives","description": "'"$SITE_DESCRIPTION"'","url": "'"$SITE_URL"'/archives/","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} @@ -403,6 +406,7 @@ _generate_main_archive_index() { header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Add schema local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "Archives","description": "'"$SITE_DESCRIPTION"'","url": "'"$SITE_URL"'/archives/","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} @@ -537,6 +541,7 @@ _generate_year_index() { header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Add schema local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$year_page_title"'","description": "Archive of posts from '"$year"'","url": "'"$SITE_URL$year_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} @@ -625,6 +630,7 @@ process_single_month() { header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Add schema local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$month_page_title"'","description": "Archive of posts from '"$month_name $year"'","url": "'"$SITE_URL$month_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>' header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} diff --git a/scripts/build/generate_authors.sh b/scripts/build/generate_authors.sh index 4857d9c..191b22a 100644 --- a/scripts/build/generate_authors.sh +++ b/scripts/build/generate_authors.sh @@ -117,6 +117,7 @@ _generate_author_pages_ram() { header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/} header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} header_content=${header_content//<!-- bssg:tag_rss_link -->/} if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then local author_rss_link="<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$author RSS Feed\" href=\"$SITE_URL$author_rss_rel_url\">" @@ -172,6 +173,7 @@ _generate_author_pages_ram() { header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/} header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} header_content=${header_content//<!-- bssg:tag_rss_link -->/} local schema_json schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"List of all authors on $SITE_TITLE\",\"url\": \"$SITE_URL/authors/\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}" @@ -496,6 +498,7 @@ generate_author_pages() { # Remove unprocessed image placeholders header_content=${header_content//\{\{og_image\}\}/} header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Remove the placeholder for the tag-specific RSS feed link header_content=${header_content//<!-- bssg:tag_rss_link -->/} @@ -636,6 +639,7 @@ generate_author_pages() { # Remove unprocessed image placeholders header_content=${header_content//\{\{og_image\}\}/} header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Remove the placeholder for the tag-specific RSS feed link in the main authors index header_content=${header_content//<!-- bssg:tag_rss_link -->/} diff --git a/scripts/build/generate_index.sh b/scripts/build/generate_index.sh index ee65ee0..2611cc5 100755 --- a/scripts/build/generate_index.sh +++ b/scripts/build/generate_index.sh @@ -103,6 +103,7 @@ EOF page_header=${page_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} page_header=${page_header//\{\{og_image\}\}/""} page_header=${page_header//\{\{twitter_image\}\}/""} + page_header=${page_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} local page_footer="$FOOTER_TEMPLATE" page_footer=${page_footer//\{\{current_year\}\}/$(date +%Y)} @@ -432,6 +433,7 @@ EOF page_header=${page_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} page_header=${page_header//\{\{og_image\}\}/""} page_header=${page_header//\{\{twitter_image\}\}/""} + page_header=${page_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Replace placeholders in the footer local page_footer="$FOOTER_TEMPLATE" diff --git a/scripts/build/generate_pages.sh b/scripts/build/generate_pages.sh index 6f5714b..2e4295f 100755 --- a/scripts/build/generate_pages.sh +++ b/scripts/build/generate_pages.sh @@ -125,6 +125,7 @@ convert_page() { # Handle image placeholders (remove for pages as they don't have featured images) header_content=${header_content//\{\{og_image\}\}/} header_content=${header_content//\{\{twitter_image\}\}/} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Assemble the final HTML local final_html="${header_content}" diff --git a/scripts/build/generate_posts.sh b/scripts/build/generate_posts.sh index 5be397e..2c962fb 100755 --- a/scripts/build/generate_posts.sh +++ b/scripts/build/generate_posts.sh @@ -119,11 +119,20 @@ convert_markdown() { # This is materially faster than line-by-line bash parsing on large markdown files. local content="" local source_stream="" + local fediverse_creator_override="" if $ram_mode_active; then source_stream=$(ram_mode_get_content "$input_file") else source_stream=$(cat "$input_file") fi + if [[ "$input_file" == *.html ]]; then + fediverse_creator_override=$(printf '%s\n' "$source_stream" | grep -m 1 -o 'name="fediverse_creator" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + if [ -z "$fediverse_creator_override" ]; then + fediverse_creator_override=$(printf '%s\n' "$source_stream" | grep -m 1 -o 'name="fediverse:creator" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/') + fi + else + fediverse_creator_override=$(parse_metadata "$input_file" "fediverse_creator") + fi content=$(printf '%s' "$source_stream" | awk ' NR == 1 { if ($0 == "---") { @@ -242,6 +251,18 @@ convert_markdown() { meta_desc=$(echo "${description:-$SITE_DESCRIPTION}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') header_content=${header_content//\{\{og_description\}\}/"$meta_desc"} header_content=${header_content//\{\{twitter_description\}\}/"$meta_desc"} + local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" + local fediverse_creator_meta_tag="" + fediverse_creator_meta_tag=$(build_fediverse_creator_meta_tag "$display_author_name" "$fediverse_creator_override") + if [[ "$header_content" == *"{{fediverse_creator_meta}}"* ]]; then + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"$fediverse_creator_meta_tag"} + elif [ -n "$fediverse_creator_meta_tag" ]; then + if [[ "$header_content" == *"</head>"* ]]; then + header_content=${header_content/<\/head>/$'\n'"$fediverse_creator_meta_tag"$'\n''</head>'} + else + header_content+=$'\n'"$fediverse_creator_meta_tag" + fi + fi # Generate Schema.org JSON-LD for articles local schema_json_ld="" @@ -319,7 +340,6 @@ convert_markdown() { local formatted_lastmod=$(format_date "$lastmod" "$display_date_format") local post_meta_reading_time post_meta_reading_time=$(printf "${MSG_READING_TIME_TEMPLATE:-%d min read}" "$reading_time") - local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" local post_meta="<div class=\"page-meta\">" post_meta+="<p class=\"meta\">" post_meta+="${MSG_PUBLISHED_ON:-Published on}: <time datetime=\"$date\">$formatted_date</time> ${MSG_BY:-by} <strong>$display_author_name</strong>" @@ -797,10 +817,12 @@ process_all_markdown_files() { # Export dependencies of convert_markdown and its helpers export -f file_needs_rebuild get_file_mtime common_rebuild_check config_has_changed # Still needed by convert_markdown *internally* for now export -f calculate_reading_time generate_slug format_date fix_url parse_metadata extract_metadata convert_markdown_to_html + export -f trim_whitespace resolve_fediverse_creator build_fediverse_creator_meta_tag export -f format_iso8601_post_date export -f portable_md5sum # Used by cache funcs export CACHE_DIR FORCE_REBUILD OUTPUT_DIR SITE_URL URL_SLUG_FORMAT HEADER_TEMPLATE FOOTER_TEMPLATE export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME MARKDOWN_PROCESSOR MARKDOWN_PL_PATH DATE_FORMAT TIMEZONE SHOW_TIMEZONE + export FEDIVERSE_CREATOR AUTHOR_FEDIVERSE_CREATORS_SERIALIZED export MSG_PUBLISHED_ON MSG_UPDATED_ON MSG_READING_TIME_TEMPLATE # Export needed locale messages export CONFIG_HASH_FILE BSSG_CONFIG_CHANGED_STATUS # Export status for common_rebuild_check export ENABLE_RELATED_POSTS RELATED_POSTS_COUNT # Export related posts configuration diff --git a/scripts/build/generate_secondary_pages.sh b/scripts/build/generate_secondary_pages.sh index 870bde6..10c282c 100755 --- a/scripts/build/generate_secondary_pages.sh +++ b/scripts/build/generate_secondary_pages.sh @@ -117,6 +117,7 @@ EOF # Remove image placeholders header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Replace placeholders in the footer footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} diff --git a/scripts/build/generate_tags.sh b/scripts/build/generate_tags.sh index 76d47bf..950c939 100755 --- a/scripts/build/generate_tags.sh +++ b/scripts/build/generate_tags.sh @@ -441,6 +441,7 @@ _generate_tag_pages_ram() { header_base=${header_base//\{\{site_url\}\}/"$SITE_URL"} header_base=${header_base//\{\{og_image\}\}/""} header_base=${header_base//\{\{twitter_image\}\}/""} + header_base=${header_base//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} BSSG_RAM_TAG_HEADER_BASE="$header_base" local tag_count="${#sorted_tag_urls[@]}" @@ -563,6 +564,7 @@ EOF header_content=${header_content//\{\{schema_json_ld\}\}/"$tags_schema_json"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"} @@ -879,6 +881,7 @@ EOF # Remove image placeholders header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Replace placeholders in the footer footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} @@ -1215,6 +1218,7 @@ EOF header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} + header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"} # Replace placeholders in the footer footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)} diff --git a/scripts/build/templates.sh b/scripts/build/templates.sh index cd88c8b..293cf45 100755 --- a/scripts/build/templates.sh +++ b/scripts/build/templates.sh @@ -320,6 +320,24 @@ preload_templates() { HEADER_TEMPLATE=$(echo "$HEADER_TEMPLATE" | sed "s|{{[[:space:]]*rss_filename[[:space:]]*}}|${RSS_FILENAME:-rss.xml}|g") # --- Add RSS Filename Placeholder --- END --- + # --- Handle rel="me" Verification Link --- START --- + local rel_me_tags="" + if [ -n "${REL_ME_URLS_SERIALIZED:-}" ]; then + local rel_me_link_url rel_me_href + while IFS= read -r rel_me_link_url; do + [ -n "$rel_me_link_url" ] || continue + rel_me_href=$(html_escape "$rel_me_link_url") + rel_me_tags+="<link rel=\"me\" href=\"${rel_me_href}\">"$'\n' + done <<< "$REL_ME_URLS_SERIALIZED" + rel_me_tags="${rel_me_tags%$'\n'}" + print_info "Adding rel=\"me\" verification links from REL_ME_URL/REL_ME_URLS." + else + print_info "No REL_ME_URL or REL_ME_URLS specified, skipping rel=\"me\" links." + fi + HEADER_TEMPLATE=$(echo "$HEADER_TEMPLATE" | sed "s|{{[[:space:]]*rel_me_link[[:space:]]*}}|__BSSG_REL_ME_LINK__|g") + HEADER_TEMPLATE=${HEADER_TEMPLATE//__BSSG_REL_ME_LINK__/$rel_me_tags} + # --- Handle rel="me" Verification Link --- END --- + # --- Handle Custom CSS --- START --- local custom_css_tag="" if [ -n "$CUSTOM_CSS" ]; then diff --git a/scripts/build/utils.sh b/scripts/build/utils.sh index dd9f0a5..b4c3b9d 100755 --- a/scripts/build/utils.sh +++ b/scripts/build/utils.sh @@ -450,6 +450,48 @@ html_escape() { fi } +trim_whitespace() { + printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} + +resolve_fediverse_creator() { + local author_name="$1" + local post_fediverse_creator="$2" + local resolved_creator="" + + resolved_creator=$(trim_whitespace "$post_fediverse_creator") + if [ -n "$resolved_creator" ]; then + printf '%s' "$resolved_creator" + return 0 + fi + + if [ -n "$author_name" ] && [ -n "${AUTHOR_FEDIVERSE_CREATORS_SERIALIZED:-}" ]; then + local configured_author configured_creator + while IFS=$'\t' read -r configured_author configured_creator; do + [ "$configured_author" = "$author_name" ] || continue + resolved_creator=$(trim_whitespace "$configured_creator") + if [ -n "$resolved_creator" ]; then + printf '%s' "$resolved_creator" + return 0 + fi + done <<< "${AUTHOR_FEDIVERSE_CREATORS_SERIALIZED}" + fi + + trim_whitespace "${FEDIVERSE_CREATOR:-}" +} + +build_fediverse_creator_meta_tag() { + local fediverse_creator + fediverse_creator=$(resolve_fediverse_creator "$1" "$2") + + if [ -z "$fediverse_creator" ]; then + printf '%s' "" + return 0 + fi + + printf '<meta name="fediverse:creator" content="%s">' "$(html_escape "$fediverse_creator")" +} + # Export the functions export -f format_date_from_timestamp export -f generate_slug @@ -461,6 +503,9 @@ export -f get_parallel_jobs export -f run_parallel export -f calculate_reading_time export -f html_escape +export -f trim_whitespace +export -f resolve_fediverse_creator +export -f build_fediverse_creator_meta_tag # Export the new print functions export -f print_error export -f print_warning diff --git a/scripts/post.sh b/scripts/post.sh index 9533ac5..f6cb964 100755 --- a/scripts/post.sh +++ b/scripts/post.sh @@ -224,6 +224,7 @@ EOM <meta name="slug" content="$slug"> <meta name="author_name" content=""> <meta name="author_email" content=""> + <meta name="fediverse_creator" content=""> </head> <body> <h1>$title</h1> @@ -249,6 +250,7 @@ image_caption: description: author_name: author_email: +fediverse_creator: --- $initial_content @@ -475,6 +477,7 @@ else <meta name="slug" content="$POST_SLUG"> <meta name="author_name" content=""> <meta name="author_email" content=""> + <meta name="fediverse_creator" content=""> </head> <body> <h1>$POST_TITLE</h1> @@ -496,6 +499,7 @@ image_caption: description: author_name: author_email: +fediverse_creator: --- $POST_CONTENT diff --git a/src/2025-04-01-bssg-features-and-examples.md b/src/2025-04-01-bssg-features-and-examples.md index f33b14e..853959e 100644 --- a/src/2025-04-01-bssg-features-and-examples.md +++ b/src/2025-04-01-bssg-features-and-examples.md @@ -6,6 +6,7 @@ slug: bssg-features-examples description: A detailed overview of BSSG's key features with practical examples showing how to get the most out of this Bash Static Site Generator. image: https://picsum.photos/537/354 image_caption: Sample, random pic from picsum +fediverse_creator: @author@example.social --- BSSG (Bash Static Site Generator) offers a powerful yet simple approach to creating static websites. This post demonstrates some of its key features with practical examples. @@ -23,6 +24,7 @@ slug: custom-url-slug image: /path/to/featured-image.jpg image_caption: A caption for your featured image description: A brief summary of your post for previews and SEO +fediverse_creator: @author@example.social --- ``` @@ -123,6 +125,7 @@ When you build your BSSG site, it generates clean HTML with excellent accessibil <meta property="og:title" content="Post Title"> <meta property="og:description" content="Post description"> <meta property="og:url" content="https://example.com/post-slug"> + <meta name="fediverse:creator" content="@author@example.social"> <link rel="stylesheet" href="/css/style.css"> </head> <body> diff --git a/templates/header.html b/templates/header.html index daada4c..db26a96 100644 --- a/templates/header.html +++ b/templates/header.html @@ -14,9 +14,11 @@ <meta name="twitter:title" content="{{page_title}}"> <meta name="twitter:description" content="{{twitter_description}}"> {{twitter_image}} + {{fediverse_creator_meta}} <meta name="theme-color" content="#808080"> <link rel="stylesheet" href="{{site_url}}/css/style.css"> <link rel="alternate" type="application/rss+xml" title="{{rss_feed}}" href="{{site_url}}/{{rss_filename}}"> + {{rel_me_link}} {{schema_json_ld}} {{custom_css_link}} </head> From 608a82aec464a2d2e2d18b16deda7475bcbb51f4 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli <stefano@dragas.it> Date: Wed, 18 Mar 2026 11:35:13 +0100 Subject: [PATCH 21/21] - Added support for --config in generate_theme_previews.sh, with config resolution aligned to the main build flow: command-line override first, then BSSG_LCONF, then local/default config files. - Improved theme preview generation so it now loads the effective BSSG config once, reuses configured SITE_URL, OUTPUT_DIR, THEMES_DIR, and TEMPLATES_DIR, and passes the selected config through to each preview build. - Improved preview output path detection for external site setups, so previews are generated under the correct site example directory instead of assuming the project root layout. - Standardized list-page article headings from <h3> to <h2> across homepage, archives, authors, tags, and secondary pages, improving heading hierarchy and semantic consistency. - Fixed Open Graph URL generation in templates/header.html by concatenating {{site_url}}{{page_url}} directly, and normalized page URL placeholders to leading-slash paths like /, /archives/, /authors/..., and /pages.html. - Updated theme stylesheets to match the new list markup, switching post-list selectors from .posts-list h3 to .posts-list h2 and preserving hover, focus, spacing, and responsive typography rules. - Adjusted themes with decorative h2 treatments so post listings do not inherit unwanted heading ornaments or pseudo-elements after the heading-level change. --- README.md | 21 +++- generate_theme_previews.sh | 140 ++++++++++++++-------- scripts/build/generate_archives.sh | 8 +- scripts/build/generate_authors.sh | 8 +- scripts/build/generate_index.sh | 8 +- scripts/build/generate_secondary_pages.sh | 4 +- scripts/build/generate_tags.sh | 4 +- templates/header.html | 2 +- themes/amiga500/style.css | 2 +- themes/apple2/style.css | 1 + themes/art-deco/style.css | 2 +- themes/atarist/style.css | 2 + themes/bauhaus/style.css | 2 +- themes/bbs/style.css | 6 +- themes/beos/style.css | 2 + themes/blackberry/style.css | 2 +- themes/braun/style.css | 2 +- themes/brutalist/style.css | 4 + themes/c64/style.css | 4 +- themes/cyber-dark/style.css | 3 +- themes/dark/style.css | 2 +- themes/default/style.css | 6 +- themes/diary/style.css | 8 ++ themes/field-journal/style.css | 3 +- themes/flat/style.css | 4 + themes/freebsd/style.css | 3 +- themes/glassmorphism/style.css | 1 + themes/ios/style.css | 6 +- themes/liquid-glass/style.css | 3 +- themes/longform/style.css | 6 +- themes/macclassic/style.css | 2 +- themes/macos9/style.css | 2 +- themes/mario/style.css | 1 + themes/material/style.css | 5 + themes/microfiche/style.css | 3 +- themes/mid-century/style.css | 2 +- themes/minimal/style.css | 6 +- themes/mondrian/style.css | 2 +- themes/msdos/style.css | 1 + themes/museum-label/style.css | 3 +- themes/mynotes/style.css | 3 +- themes/nes/style.css | 8 +- themes/netbsd/style.css | 3 +- themes/newspaper/style.css | 8 +- themes/nextstep/style.css | 1 + themes/nordic-clean/style.css | 2 +- themes/openbsd/style.css | 3 +- themes/osx/style.css | 6 +- themes/reader-mode/style.css | 8 +- themes/skeuomorphic/style.css | 1 + themes/stefano/style.css | 8 +- themes/swiss-design/style.css | 2 +- themes/terminal/style.css | 4 +- themes/text-only/style.css | 2 +- themes/thoughtful/style.css | 8 +- themes/tty/style.css | 6 +- themes/vaporwave/style.css | 3 + themes/web1/style.css | 1 + themes/web2/style.css | 1 + themes/win311/style.css | 2 +- themes/win7/style.css | 1 + themes/win95/style.css | 2 +- themes/winxp/style.css | 2 + themes/y2k/style.css | 2 + themes/zxspectrum/style.css | 7 +- 65 files changed, 240 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index 708559d..d0dd43b 100644 --- a/README.md +++ b/README.md @@ -1061,11 +1061,22 @@ You can also specify a custom SITE_URL for the previews: ./generate_theme_previews.sh --site-url "https://example.com/blog" ``` -The script will use the SITE_URL from the following sources in order of precedence: -1. Command line argument (--site-url) -2. Local config file (config.sh.local) -3. Main config file (config.sh) -4. Default value (http://localhost) +You can also point the preview generator at a site-specific config file, just like `bssg.sh build`: + +```bash +./generate_theme_previews.sh --config /path/to/site/config.sh.local +``` + +BSSG configuration is resolved in this order: +1. Command line argument (`--config`) +2. `BSSG_LCONF` environment variable +3. Local config file (`config.sh.local`) +4. Main config file (`config.sh`) + +The preview `SITE_URL` is then chosen from: +1. Command line argument (`--site-url`) +2. The selected BSSG configuration +3. Default value (`http://localhost`) Each theme preview will be accessible at `SITE_URL/theme` (e.g., `https://example.com/blog/dark`). diff --git a/generate_theme_previews.sh b/generate_theme_previews.sh index ba6c8f0..ef22659 100755 --- a/generate_theme_previews.sh +++ b/generate_theme_previews.sh @@ -11,9 +11,13 @@ set -euo pipefail # Ensure BSSG_MAIN_SCRIPT points to the main bssg.sh in the project root # Ensure this script (generate_theme_previews.sh) is run from the project root. readonly BSSG_MAIN_SCRIPT="./bssg.sh" -readonly THEMES_DIR="./themes" +THEMES_DIR="./themes" +TEMPLATES_DIR="./templates" CONFIG_FILE="config.sh" # For reading default SITE_URL if not overridden LOCAL_CONFIG_FILE="config.sh.local" # For reading default SITE_URL if not overridden +CMD_LINE_CONFIG_FILE="" +FINAL_CONFIG_OVERRIDE="" +site_url_from_cli="" # Global variable for the dynamic example root directory EXAMPLE_ROOT_DIR_DYNAMIC="./example" # Default value, will be updated @@ -64,16 +68,22 @@ Generate preview sites for all available BSSG themes. Options: -h, --help Display this help message and exit + --config PATH Use a custom BSSG configuration file --site-url URL Set the base SITE_URL for theme previews (overrides config files) --full-build Build each theme independently (slower fallback mode) Configuration: + BSSG configuration is selected in this order: + 1. Command line argument (--config) + 2. BSSG_LCONF environment variable + 3. Local config file ($LOCAL_CONFIG_FILE) + 4. Main config file ($CONFIG_FILE) + The script will use the SITE_URL from the following sources in order of precedence: 1. Command line argument (--site-url) - 2. Local config file ($LOCAL_CONFIG_FILE) - 3. Main config file ($CONFIG_FILE) - 4. Default value (http://localhost) + 2. Selected BSSG configuration + 3. Default value (http://localhost) Output: Theme previews will be generated in the '$EXAMPLE_ROOT_DIR_DYNAMIC' directory, @@ -90,13 +100,19 @@ EOF # --- Parse Command Line Arguments (for this script) --- parse_args() { - site_url_from_cli="" # Made global for load_config - while [[ $# -gt 0 ]]; do case "$1" in -h|--help) print_help ;; + --config) + if [[ -n "${2:-}" && "$2" != -* ]]; then + CMD_LINE_CONFIG_FILE="$2" + shift 2 + else + error "--config requires a path to a BSSG configuration file" + fi + ;; --site-url) if [[ -n "${2:-}" ]]; then site_url_from_cli="$2" @@ -117,41 +133,50 @@ parse_args() { done } +resolve_config_override() { + if [ -n "$CMD_LINE_CONFIG_FILE" ]; then + FINAL_CONFIG_OVERRIDE="$CMD_LINE_CONFIG_FILE" + info "Using configuration file specified via --config: $FINAL_CONFIG_OVERRIDE" + elif [ -v BSSG_LCONF ] && [ -n "${BSSG_LCONF}" ]; then + FINAL_CONFIG_OVERRIDE="$BSSG_LCONF" + info "Using configuration file specified via BSSG_LCONF: $FINAL_CONFIG_OVERRIDE" + fi +} + +load_effective_bssg_configuration() { + local project_root_abs config_dump + local config_separator=$'\037' + + project_root_abs=$(pwd -P) + config_dump=$( + export BSSG_SCRIPT_DIR="$project_root_abs" + bash -c ' + source "$BSSG_SCRIPT_DIR/scripts/build/config_loader.sh" "$1" >/dev/null 2>&1 + printf "%s\037%s\037%s\037%s" "$SITE_URL" "$OUTPUT_DIR" "$THEMES_DIR" "$TEMPLATES_DIR" + ' bash "$FINAL_CONFIG_OVERRIDE" + ) || { + if [ -n "$FINAL_CONFIG_OVERRIDE" ]; then + error "Failed to load BSSG configuration from '$FINAL_CONFIG_OVERRIDE'." + fi + error "Failed to load the default BSSG configuration." + } + + IFS="$config_separator" read -r SITE_URL OUTPUT_DIR THEMES_DIR TEMPLATES_DIR <<< "$config_dump" +} + # --- Load Configuration (for this script's SITE_URL_BASE) --- load_config() { info "Loading base SITE_URL configuration for previews..." - - if [ -f "$CONFIG_FILE" ]; then - # Portable way to extract SITE_URL="value" - local main_conf_site_url - main_conf_site_url=$(awk -F'"' '/^SITE_URL=/ {print $2; exit}' "$CONFIG_FILE") - if [ -n "$main_conf_site_url" ]; then - SITE_URL_BASE="$main_conf_site_url" - info "Using SITE_URL_BASE='$SITE_URL_BASE' from $CONFIG_FILE as default" - fi - else - warn "Main configuration file '$CONFIG_FILE' not found, using default SITE_URL_BASE='$SITE_URL_BASE'." - fi - - if [ -f "$LOCAL_CONFIG_FILE" ]; then - local local_conf_site_url - # Check if SITE_URL is actually defined in the local config - if grep -q "^SITE_URL=" "$LOCAL_CONFIG_FILE" 2>/dev/null; then - local_conf_site_url=$(awk -F'"' '/^SITE_URL=/ {print $2; exit}' "$LOCAL_CONFIG_FILE") - if [ -n "$local_conf_site_url" ]; then - SITE_URL_BASE="$local_conf_site_url" - info "Overridden SITE_URL_BASE='$SITE_URL_BASE' from $LOCAL_CONFIG_FILE" - else - warn "Found $LOCAL_CONFIG_FILE but failed to extract SITE_URL, using current SITE_URL_BASE='$SITE_URL_BASE'" - fi - fi - fi - + + load_effective_bssg_configuration + SITE_URL_BASE="$SITE_URL" + info "Using SITE_URL_BASE='$SITE_URL_BASE' from the effective BSSG configuration" + if [ -n "$site_url_from_cli" ]; then SITE_URL_BASE="$site_url_from_cli" info "Using SITE_URL_BASE='$SITE_URL_BASE' from command line argument for previews" fi - + success "Configuration loaded. Using SITE_URL_BASE='$SITE_URL_BASE' for theme previews." } @@ -287,6 +312,21 @@ find_themes() { info "Found ${#themes[@]} themes: ${themes[*]}" } +run_bssg_build() { + local -a cmd=("$BSSG_MAIN_SCRIPT") + local formatted_cmd + + if [ -n "$FINAL_CONFIG_OVERRIDE" ]; then + cmd+=(--config "$FINAL_CONFIG_OVERRIDE") + fi + + cmd+=(build "$@") + formatted_cmd=$(printf '%q ' "${cmd[@]}") + info "Executing: ${formatted_cmd% }" + + "${cmd[@]}" +} + build_previews() { prepare_example_directory @@ -324,7 +364,11 @@ prepare_example_directory() { build_previews_full() { info "Starting theme preview builds..." - info "Previews will use content from the BSSG site configured by your standard config.sh/config.sh.local files." + if [ -n "$FINAL_CONFIG_OVERRIDE" ]; then + info "Previews will use content from the BSSG site configured by '$FINAL_CONFIG_OVERRIDE'." + else + info "Previews will use content from the BSSG site configured by your standard config.sh/config.sh.local files." + fi for theme in "${themes[@]}"; do info "Building preview for theme: '$theme'" @@ -337,9 +381,7 @@ build_previews_full() { mkdir -p "$theme_output_path" - info "Executing: $BSSG_MAIN_SCRIPT build -f --theme \"$theme\" --site-url \"$theme_site_url\" --output \"$theme_output_path\"" - - if ! "$BSSG_MAIN_SCRIPT" build -f --theme "$theme" --site-url "$theme_site_url" --output "$theme_output_path"; then + if ! run_bssg_build -f --theme "$theme" --site-url "$theme_site_url" --output "$theme_output_path"; then error "Build failed for theme '$theme'. Check output above." fi success "Preview for theme '$theme' built successfully in '$theme_output_path'" @@ -349,7 +391,7 @@ build_previews_full() { } has_theme_specific_templates() { - local template_root="./templates" + local template_root="$TEMPLATES_DIR" local theme for theme in "${themes[@]}"; do if [ -d "$template_root/$theme" ]; then @@ -400,7 +442,7 @@ build_previews_fast() { fi info "Building base preview once with theme '$base_theme' and SITE_URL token '$SITE_URL_TOKEN'..." - if ! "$BSSG_MAIN_SCRIPT" build -f --theme "$base_theme" --site-url "$SITE_URL_TOKEN" --output "$base_output_path"; then + if ! run_bssg_build -f --theme "$base_theme" --site-url "$SITE_URL_TOKEN" --output "$base_output_path"; then error "Base build failed in fast preview mode." fi @@ -572,22 +614,17 @@ determine_example_root_dir() { # Portable way to get absolute path of current directory project_root_abs=$( (cd . && pwd -P) || { error "Could not determine project root."; exit 1; } ) - - local effective_output_dir - effective_output_dir=$(export BSSG_SCRIPT_DIR="$project_root_abs"; \ - bash -c 'source "$BSSG_SCRIPT_DIR/scripts/build/config_loader.sh" "" &>/dev/null; echo "$OUTPUT_DIR"') - - if [ -z "$effective_output_dir" ]; then + if [ -z "${OUTPUT_DIR:-}" ]; then warn "Could not determine effective OUTPUT_DIR from BSSG configuration. Defaulting EXAMPLE_ROOT_DIR_DYNAMIC to '$EXAMPLE_ROOT_DIR_DYNAMIC'." return fi - info "Effective OUTPUT_DIR from BSSG configuration: '$effective_output_dir'" + info "Effective OUTPUT_DIR from BSSG configuration: '$OUTPUT_DIR'" local effective_output_dir_abs_unnormalized - if [[ "$effective_output_dir" == /* ]]; then - effective_output_dir_abs_unnormalized="$effective_output_dir" + if [[ "$OUTPUT_DIR" == /* ]]; then + effective_output_dir_abs_unnormalized="$OUTPUT_DIR" else - effective_output_dir_abs_unnormalized="$project_root_abs/$effective_output_dir" + effective_output_dir_abs_unnormalized="$project_root_abs/$OUTPUT_DIR" fi # Normalize the path using our helper (handles ., .., and non-existent paths) @@ -605,7 +642,7 @@ determine_example_root_dir() { fi - if [[ "$site_root_candidate" != "$project_root_abs" && "$effective_output_dir" == /* ]]; then + if [[ "$site_root_candidate" != "$project_root_abs" && "$OUTPUT_DIR" == /* ]]; then info "Detected external site configuration. Previews will be generated in '$site_root_candidate/example'." EXAMPLE_ROOT_DIR_DYNAMIC="$site_root_candidate/example" else @@ -622,6 +659,7 @@ main() { declare -a themes parse_args "$@" + resolve_config_override load_config check_dependencies determine_example_root_dir diff --git a/scripts/build/generate_archives.sh b/scripts/build/generate_archives.sh index 2356626..7cfe8f0 100755 --- a/scripts/build/generate_archives.sh +++ b/scripts/build/generate_archives.sh @@ -133,7 +133,7 @@ _generate_ram_month_archive_page() { cat << EOF <article> - <h3><a href="${post_url}">$title</a></h3> + <h2><a href="${post_url}">$title</a></h2> <div class="meta">${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date ${MSG_BY:-\"by\"} <strong>$display_author_name</strong></div> EOF if [ -n "$image" ]; then @@ -201,7 +201,7 @@ _generate_archive_pages_ram() { header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"} header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} header_content=${header_content//\{\{og_type\}\}/"website"} - header_content=${header_content//\{\{page_url\}\}/"archives/"} + header_content=${header_content//\{\{page_url\}\}/"/archives/"} header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} @@ -402,7 +402,7 @@ _generate_main_archive_index() { header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"} header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"} header_content=${header_content//\{\{og_type\}\}/"website"} - header_content=${header_content//\{\{page_url\}\}/"archives/"} # Relative URL for header placeholder + header_content=${header_content//\{\{page_url\}\}/"/archives/"} # Relative URL for header placeholder header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/""} header_content=${header_content//\{\{twitter_image\}\}/""} @@ -685,7 +685,7 @@ process_single_month() { # Use cat heredoc for multi-line article structure cat << EOF <article> - <h3><a href="${post_url}">$title</a></h3> + <h2><a href="${post_url}">$title</a></h2> <div class="meta">${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date ${MSG_BY:-\"by\"} <strong>$display_author_name</strong></div> EOF diff --git a/scripts/build/generate_authors.sh b/scripts/build/generate_authors.sh index 191b22a..e8c87c8 100644 --- a/scripts/build/generate_authors.sh +++ b/scripts/build/generate_authors.sh @@ -49,7 +49,7 @@ _generate_author_pages_ram() { local author_data="${author_posts_by_slug[$author_slug_key]}" local author_page_html_file="$OUTPUT_DIR/authors/$author_slug_key/index.html" local author_rss_file="$OUTPUT_DIR/authors/$author_slug_key/${RSS_FILENAME:-rss.xml}" - local author_page_rel_url="authors/${author_slug_key}/" + local author_page_rel_url="/authors/${author_slug_key}/" local author_rss_rel_url="/authors/${author_slug_key}/${RSS_FILENAME:-rss.xml}" local post_count post_count=$(printf '%s\n' "$author_data" | awk 'NF { c++ } END { print c+0 }') @@ -169,7 +169,7 @@ _generate_author_pages_ram() { header_content=${header_content//\{\{og_description\}\}/"$page_description"} header_content=${header_content//\{\{twitter_description\}\}/"$page_description"} header_content=${header_content//\{\{og_type\}\}/"website"} - header_content=${header_content//\{\{page_url\}\}/"authors/"} + header_content=${header_content//\{\{page_url\}\}/"/authors/"} header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} header_content=${header_content//\{\{og_image\}\}/} header_content=${header_content//\{\{twitter_image\}\}/} @@ -364,7 +364,7 @@ generate_author_pages() { if [ -n "$author" ]; then local author_page_html_file="$OUTPUT_DIR/authors/$author_slug/index.html" local author_rss_file="$OUTPUT_DIR/authors/$author_slug/${RSS_FILENAME:-rss.xml}" - local author_page_rel_url="authors/${author_slug}/" + local author_page_rel_url="/authors/${author_slug}/" local author_rss_rel_url="/authors/${author_slug}/${RSS_FILENAME:-rss.xml}" local rebuild_html=false local rebuild_rss=false @@ -620,7 +620,7 @@ generate_author_pages() { # Generate full HTML page for main authors index local page_title="${MSG_ALL_AUTHORS:-All Authors}" local page_description="${MSG_ALL_AUTHORS:-All Authors} - $SITE_DESCRIPTION" - local authors_index_rel_url="authors/" + local authors_index_rel_url="/authors/" # Process templates with placeholder replacement (following tags generator pattern) local header_content="$HEADER_TEMPLATE" diff --git a/scripts/build/generate_index.sh b/scripts/build/generate_index.sh index 2611cc5..2886665 100755 --- a/scripts/build/generate_index.sh +++ b/scripts/build/generate_index.sh @@ -43,7 +43,7 @@ _generate_index_ram() { page_header=${page_header//\{\{site_title\}\}/"$SITE_TITLE"} page_header=${page_header//\{\{page_title\}\}/"${MSG_HOME:-"Home"}"} page_header=${page_header//\{\{og_type\}\}/"website"} - page_header=${page_header//\{\{page_url\}\}/""} + page_header=${page_header//\{\{page_url\}\}/"/"} page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"} local home_schema home_schema=$(cat <<EOF @@ -192,7 +192,7 @@ EOF cat >> "$output_file" <<EOF <article> - <h3><a href="$(fix_url "$post_link")">$title</a></h3> + <h2><a href="$(fix_url "$post_link")">$title</a></h2> <div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${author_name:+" ${MSG_BY:-"by"} ${author_name:-$AUTHOR_NAME}"}</div> EOF @@ -365,7 +365,7 @@ generate_index() { # For the homepage page_header=${page_header//\{\{page_title\}\}/"${MSG_HOME:-"Home"}"} page_header=${page_header//\{\{og_type\}\}/"website"} - page_header=${page_header//\{\{page_url\}\}/""} + page_header=${page_header//\{\{page_url\}\}/"/"} page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"} # Create WebSite schema for homepage @@ -506,7 +506,7 @@ EOF local post_link="/$formatted_path/" cat >> "$output_file" << EOF <article> - <h3><a href="$(fix_url "$post_link")">$title</a></h3> + <h2><a href="$(fix_url "$post_link")">$title</a></h2> <div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${author_name:+" ${MSG_BY:-"by"} ${author_name:-$AUTHOR_NAME}"}</div> EOF if [ -n "$image" ]; then diff --git a/scripts/build/generate_secondary_pages.sh b/scripts/build/generate_secondary_pages.sh index 10c282c..efffb6b 100755 --- a/scripts/build/generate_secondary_pages.sh +++ b/scripts/build/generate_secondary_pages.sh @@ -87,7 +87,7 @@ generate_pages_index() { header_content=${header_content//\{\{og_type\}\}/"website"} # Set proper URL in og:url - header_content=${header_content//\{\{page_url\}\}/"pages.html"} + header_content=${header_content//\{\{page_url\}\}/"/pages.html"} header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"} # Generate CollectionPage schema @@ -135,7 +135,7 @@ EOF IFS='|' read -r title url _ <<< "$page" # Ignore date for menu cat >> "$pages_index" << EOF <article> - <h3><a href="$url">$title</a></h3> + <h2><a href="$url">$title</a></h2> </article> EOF done diff --git a/scripts/build/generate_tags.sh b/scripts/build/generate_tags.sh index 950c939..798f9be 100755 --- a/scripts/build/generate_tags.sh +++ b/scripts/build/generate_tags.sh @@ -291,7 +291,7 @@ _generate_tag_pages_ram() { local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}" local article_html="" article_html+=' <article>'$'\n' - article_html+=" <h3><a href=\"${SITE_URL}${post_link}\">${title}</a></h3>"$'\n' + article_html+=" <h2><a href=\"${SITE_URL}${post_link}\">${title}</a></h2>"$'\n' article_html+=" <div class=\"meta\">${MSG_PUBLISHED_ON:-Published on} ${formatted_date} ${MSG_BY:-by} <strong>${display_author_name}</strong></div>"$'\n' if [ -n "$image" ]; then local image_url alt_text figcaption_content @@ -939,7 +939,7 @@ EOF cat >> "$tag_page_html_file" << EOF <article> - <h3><a href="${SITE_URL}${post_link}">$title</a></h3> + <h2><a href="${SITE_URL}${post_link}">$title</a></h2> <div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date ${MSG_BY:-"by"} <strong>$display_author_name</strong></div> EOF diff --git a/templates/header.html b/templates/header.html index db26a96..c15ae55 100644 --- a/templates/header.html +++ b/templates/header.html @@ -8,7 +8,7 @@ <meta property="og:title" content="{{page_title}}"> <meta property="og:description" content="{{og_description}}"> <meta property="og:type" content="{{og_type}}"> - <meta property="og:url" content="{{site_url}}/{{page_url}}"> + <meta property="og:url" content="{{site_url}}{{page_url}}"> {{og_image}} <meta name="twitter:card" content="summary"> <meta name="twitter:title" content="{{page_title}}"> diff --git a/themes/amiga500/style.css b/themes/amiga500/style.css index fe02c34..0d4d584 100644 --- a/themes/amiga500/style.css +++ b/themes/amiga500/style.css @@ -890,7 +890,7 @@ hr { margin-bottom: 20px; } -.posts-list h2, .posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } diff --git a/themes/apple2/style.css b/themes/apple2/style.css index 45d4d9e..09e2030 100644 --- a/themes/apple2/style.css +++ b/themes/apple2/style.css @@ -225,6 +225,7 @@ h2 { font-size: 1.3rem; } +.posts-list h2, h3 { font-size: 1.2rem; } diff --git a/themes/art-deco/style.css b/themes/art-deco/style.css index c343729..e1838d1 100644 --- a/themes/art-deco/style.css +++ b/themes/art-deco/style.css @@ -691,7 +691,7 @@ figcaption { margin-bottom: 3rem; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; text-align: center; diff --git a/themes/atarist/style.css b/themes/atarist/style.css index f8743d3..e6af47e 100644 --- a/themes/atarist/style.css +++ b/themes/atarist/style.css @@ -244,6 +244,7 @@ h2 { font-size: 1.4rem; } +.posts-list h2, h3 { font-size: 1.2rem; } @@ -593,6 +594,7 @@ footer a:focus { font-size: 1.2rem; } + .posts-list h2, h3 { font-size: 1.1rem; } diff --git a/themes/bauhaus/style.css b/themes/bauhaus/style.css index 729e4c1..7b29f97 100644 --- a/themes/bauhaus/style.css +++ b/themes/bauhaus/style.css @@ -486,7 +486,7 @@ footer a:focus { border-bottom: none; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; } diff --git a/themes/bbs/style.css b/themes/bbs/style.css index 9cad0c1..39925d2 100644 --- a/themes/bbs/style.css +++ b/themes/bbs/style.css @@ -315,11 +315,13 @@ h2::before { color: var(--accent-1); } +.posts-list h2, h3 { font-size: 1.3rem; color: var(--link-hover); } +.posts-list h2::before, h3::before { content: "> "; color: var(--accent-1); @@ -920,6 +922,7 @@ hr::before { font-size: 1.4rem; } + .posts-list h2, h3 { font-size: 1.2rem; } @@ -975,6 +978,7 @@ hr::before { font-size: 1.2rem; } + .posts-list h2, h3 { font-size: 1.1rem; } @@ -988,4 +992,4 @@ hr::before { font-size: 0.8rem; padding: 6px 8px; } -} \ No newline at end of file +} diff --git a/themes/beos/style.css b/themes/beos/style.css index b403a5a..edd9cd2 100644 --- a/themes/beos/style.css +++ b/themes/beos/style.css @@ -252,6 +252,7 @@ h2 { font-size: 1.6rem; } +.posts-list h2, h3 { font-size: 1.4rem; } @@ -717,6 +718,7 @@ tr:nth-child(even) { font-size: 1.4rem; } + .posts-list h2, h3 { font-size: 1.2rem; } diff --git a/themes/blackberry/style.css b/themes/blackberry/style.css index 6e7a676..0734f30 100644 --- a/themes/blackberry/style.css +++ b/themes/blackberry/style.css @@ -637,7 +637,7 @@ hr { margin-bottom: 2rem; } -.posts-list h2, .posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } diff --git a/themes/braun/style.css b/themes/braun/style.css index 563bfe6..9ae4abb 100644 --- a/themes/braun/style.css +++ b/themes/braun/style.css @@ -441,7 +441,7 @@ footer a:focus { padding-bottom: 0; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.5rem; margin-bottom: calc(var(--grid-size) * 1); diff --git a/themes/brutalist/style.css b/themes/brutalist/style.css index 72b1c4f..a77cfdc 100644 --- a/themes/brutalist/style.css +++ b/themes/brutalist/style.css @@ -266,6 +266,7 @@ h2 { letter-spacing: -1px; } +.posts-list h2, h3 { font-size: 1.5rem; } @@ -667,6 +668,7 @@ footer a:focus { font-size: 1.8rem; } + .posts-list h2, h3 { font-size: 1.5rem; } @@ -741,6 +743,7 @@ footer a:focus { font-size: 1.6rem; } + .posts-list h2, h3 { font-size: 1.4rem; } @@ -795,6 +798,7 @@ footer a:focus { font-size: 1.5rem; } + .posts-list h2, h3 { font-size: 1.3rem; } diff --git a/themes/c64/style.css b/themes/c64/style.css index 5ae1768..5c83d88 100644 --- a/themes/c64/style.css +++ b/themes/c64/style.css @@ -481,12 +481,12 @@ hr { margin-bottom: 2rem; } -.posts-list h2, .posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } -.posts-list h2::before, .posts-list h3::before { +.posts-list h2::before { content: ">"; margin-right: 0.5rem; color: var(--link-color); diff --git a/themes/cyber-dark/style.css b/themes/cyber-dark/style.css index 4b379a3..1b1f60b 100644 --- a/themes/cyber-dark/style.css +++ b/themes/cyber-dark/style.css @@ -18,6 +18,7 @@ a { font-family:sans-serif; color:var(--highlight2); text-decoration:none; } body { font-size:1.1em; color:var(--text); padding:0.2em; font-family:sans-serif; max-width:60em; margin:auto; line-height:1.5; } h1 { font-size:2em; color:var(--highlight1); text-shadow:0 0 20px; } h2 { font-size:1.7em; color:var(--highlight1); text-shadow:0 0 20px; } +.posts-list h2, h3 { font-size:1.4em; color:var(--highlight1); text-shadow:0 0 20px; } nav { display:block; text-align:center; padding-top:0.8em; padding-bottom:3.5em; } nav { a { padding-left:0.5em; padding-right:0.5em; text-decoration:underline var(--highlight1); text-shadow:0 0 9px var(--highlight1); } } @@ -26,7 +27,7 @@ p { padding-top:0.5em; padding-bottom:0.5em; } header { text-align:center; margin:auto; } header { p { text-shadow:0 0 10px var(--highlight2); } } img { display:block; max-width: 100%; margin: auto; padding-top: 20px; padding-bottom: 20px; } -.posts-list { h3 { a { color:var(--highlight1); text-decoration: underline var(--highlight2); text-shadow:0 0 15px var(--highlight2); } } } +.posts-list { h2 { a { color:var(--highlight1); text-decoration: underline var(--highlight2); text-shadow:0 0 15px var(--highlight2); } } } .featured-image.index-image { img { display:block; max-width: 100%; max-height:640px; margin: auto; padding-top: 20px; padding-bottom: 20px; } } .image-caption { color:var(--muted-text); text-align: center; } .site-title { a { text-shadow:0 0 30px var(--highlight2); text-decoration:underline 2px var(--highlight2); font-size:3em; font-weight:bold; color:var(--highlight1); }} diff --git a/themes/dark/style.css b/themes/dark/style.css index 764361b..e6fb210 100644 --- a/themes/dark/style.css +++ b/themes/dark/style.css @@ -525,7 +525,7 @@ footer::before { padding-bottom: 1.5rem; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } diff --git a/themes/default/style.css b/themes/default/style.css index bf8f1d9..725ada9 100644 --- a/themes/default/style.css +++ b/themes/default/style.css @@ -742,18 +742,18 @@ hr::before { padding-bottom: 1.5rem; } -.posts-list h2, .posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } -.posts-list h2 a, .posts-list h3 a { +.posts-list h2 a { outline: 2px solid transparent; outline-offset: 2px; border-radius: 2px; } -.posts-list h2 a:focus, .posts-list h3 a:focus { +.posts-list h2 a:focus { outline-color: var(--link-color); } diff --git a/themes/diary/style.css b/themes/diary/style.css index cb963f1..731473c 100644 --- a/themes/diary/style.css +++ b/themes/diary/style.css @@ -336,10 +336,15 @@ h2::after { background: linear-gradient(to right, var(--accent-primary), transparent); } +.posts-list h2, h3 { font-size: 1.5rem; } +.posts-list h2::after { + content: none; +} + h4 { font-size: 1.25rem; } @@ -822,6 +827,7 @@ article { font-size: 1.6rem; } + .posts-list h2, h3 { font-size: 1.3rem; } @@ -878,6 +884,7 @@ article { font-size: 1.5rem; } + .posts-list h2, h3 { font-size: 1.25rem; } @@ -1007,6 +1014,7 @@ article { font-size: 1.4rem; } + .posts-list h2, h3 { font-size: 1.2rem; } diff --git a/themes/field-journal/style.css b/themes/field-journal/style.css index f2d3ae6..e13a04d 100644 --- a/themes/field-journal/style.css +++ b/themes/field-journal/style.css @@ -319,8 +319,7 @@ hr { margin-top: 0.95rem; } -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; } diff --git a/themes/flat/style.css b/themes/flat/style.css index 55ece43..988748b 100644 --- a/themes/flat/style.css +++ b/themes/flat/style.css @@ -228,6 +228,7 @@ h2 { margin-top: 40px; } +.posts-list h2, h3 { font-size: 20px; margin-top: 30px; @@ -605,6 +606,7 @@ footer a:focus::after { font-size: 24px; } + .posts-list h2, h3 { font-size: 18px; } @@ -727,6 +729,7 @@ footer a:focus::after { margin-top: 30px; } + .posts-list h2, h3 { font-size: 17px; margin-top: 20px; @@ -794,6 +797,7 @@ footer a:focus::after { font-size: 18px; } + .posts-list h2, h3 { font-size: 16px; } diff --git a/themes/freebsd/style.css b/themes/freebsd/style.css index 0c90da4..4c64628 100644 --- a/themes/freebsd/style.css +++ b/themes/freebsd/style.css @@ -358,8 +358,7 @@ hr { margin-top: 0.85rem; } -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; } diff --git a/themes/glassmorphism/style.css b/themes/glassmorphism/style.css index b4b5833..67d30f7 100644 --- a/themes/glassmorphism/style.css +++ b/themes/glassmorphism/style.css @@ -262,6 +262,7 @@ h2 { font-size: 2rem; } +.posts-list h2, h3 { font-size: 1.5rem; } diff --git a/themes/ios/style.css b/themes/ios/style.css index f8144b2..982586a 100644 --- a/themes/ios/style.css +++ b/themes/ios/style.css @@ -632,16 +632,16 @@ footer a:focus { box-shadow: var(--floating-shadow); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: var(--spacing-xs); } -.posts-list h3 a { +.posts-list h2 a { color: var(--header-color); } -.posts-list h3 a:hover, .posts-list h3 a:active { +.posts-list h2 a:hover, .posts-list h2 a:active { color: var(--accent-color); } diff --git a/themes/liquid-glass/style.css b/themes/liquid-glass/style.css index f7f59c4..1ba5dd6 100644 --- a/themes/liquid-glass/style.css +++ b/themes/liquid-glass/style.css @@ -290,8 +290,7 @@ h3 { font-size: clamp(1.18rem, 1.8vw, 1.4rem); } article.post > h1, article.page > h1, -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; } diff --git a/themes/longform/style.css b/themes/longform/style.css index d6510ab..98a8448 100644 --- a/themes/longform/style.css +++ b/themes/longform/style.css @@ -562,17 +562,17 @@ main > h1 { border-bottom: none; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; } -.posts-list h3 a { +.posts-list h2 a { color: var(--heading-color); text-decoration: none; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--link-color); } diff --git a/themes/macclassic/style.css b/themes/macclassic/style.css index f89a5d1..bd03156 100644 --- a/themes/macclassic/style.css +++ b/themes/macclassic/style.css @@ -577,7 +577,7 @@ footer { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 5px; } diff --git a/themes/macos9/style.css b/themes/macos9/style.css index d012a20..c54110e 100644 --- a/themes/macos9/style.css +++ b/themes/macos9/style.css @@ -501,7 +501,7 @@ footer { border-bottom: 1px solid var(--accent-color); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.3em; } diff --git a/themes/mario/style.css b/themes/mario/style.css index e39ecde..5ccffcc 100644 --- a/themes/mario/style.css +++ b/themes/mario/style.css @@ -345,6 +345,7 @@ h2 { margin-top: var(--spacing-xl); } +.posts-list h2, h3 { font-size: 1.3rem; color: var(--mario-pipe-green); diff --git a/themes/material/style.css b/themes/material/style.css index 2a97731..e73f70f 100644 --- a/themes/material/style.css +++ b/themes/material/style.css @@ -312,6 +312,7 @@ nav a.active::after { font-size: 1.75rem; } + .posts-list h2, h3 { font-size: 1.35rem; } @@ -351,6 +352,7 @@ nav a.active::after { font-size: 1.5rem; } + .posts-list h2, h3 { font-size: 1.25rem; } @@ -426,6 +428,7 @@ nav a.active::after { font-size: 1.35rem; } + .posts-list h2, h3 { font-size: 1.15rem; } @@ -502,6 +505,7 @@ nav a.active::after { font-size: 1.2rem; } + .posts-list h2, h3 { font-size: 1.05rem; } @@ -583,6 +587,7 @@ h2 { letter-spacing: -0.01em; } +.posts-list h2, h3 { font-size: 1.5rem; } diff --git a/themes/microfiche/style.css b/themes/microfiche/style.css index 1d48936..31858a9 100644 --- a/themes/microfiche/style.css +++ b/themes/microfiche/style.css @@ -355,8 +355,7 @@ hr { margin-top: 0.9rem; } -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; } diff --git a/themes/mid-century/style.css b/themes/mid-century/style.css index 8e20f24..b2ea0fe 100644 --- a/themes/mid-century/style.css +++ b/themes/mid-century/style.css @@ -630,7 +630,7 @@ img { transform: translateX(5px); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; } diff --git a/themes/minimal/style.css b/themes/minimal/style.css index 31510c7..6598089 100644 --- a/themes/minimal/style.css +++ b/themes/minimal/style.css @@ -467,18 +467,18 @@ footer a:focus { margin-bottom: 1.75rem; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: 0.5rem; } -.posts-list h3 a { +.posts-list h2 a { outline: 2px solid transparent; outline-offset: 2px; transition: outline-color var(--transition); } -.posts-list h3 a:focus { +.posts-list h2 a:focus { outline-color: var(--link-color); } diff --git a/themes/mondrian/style.css b/themes/mondrian/style.css index 03a2aa4..3405132 100644 --- a/themes/mondrian/style.css +++ b/themes/mondrian/style.css @@ -579,7 +579,7 @@ figcaption, margin-bottom: 1rem; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; text-align: left; diff --git a/themes/msdos/style.css b/themes/msdos/style.css index df4e3be..be5f5bc 100644 --- a/themes/msdos/style.css +++ b/themes/msdos/style.css @@ -264,6 +264,7 @@ h2 { margin-top: calc(var(--spacing-unit) * 3); } +.posts-list h2, h3 { font-size: 1.1rem; margin-top: calc(var(--spacing-unit) * 2.5); diff --git a/themes/museum-label/style.css b/themes/museum-label/style.css index 30d6a63..e9145ec 100644 --- a/themes/museum-label/style.css +++ b/themes/museum-label/style.css @@ -299,8 +299,7 @@ pre code { margin-top: 1rem; } -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; } diff --git a/themes/mynotes/style.css b/themes/mynotes/style.css index 4aee620..38b37a1 100644 --- a/themes/mynotes/style.css +++ b/themes/mynotes/style.css @@ -203,8 +203,7 @@ h3 { article.post > h1, article.page > h1, -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; } diff --git a/themes/nes/style.css b/themes/nes/style.css index 0040caf..f279201 100644 --- a/themes/nes/style.css +++ b/themes/nes/style.css @@ -607,23 +607,23 @@ blockquote p:last-child { transform: scale(1.01); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.1rem; margin-bottom: var(--spacing-sm); } -.posts-list h3 a { +.posts-list h2 a { color: var(--nes-light-blue); text-decoration: none; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--nes-light-yellow); } /* ACCESSIBILITY: Focus states for post list links */ -.posts-list h3 a:focus { +.posts-list h2 a:focus { outline: 2px solid var(--nes-light-yellow); outline-offset: 2px; color: var(--nes-light-yellow); diff --git a/themes/netbsd/style.css b/themes/netbsd/style.css index 561043e..b58c1e1 100644 --- a/themes/netbsd/style.css +++ b/themes/netbsd/style.css @@ -372,8 +372,7 @@ hr { margin-top: 0.82rem; } -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; } diff --git a/themes/newspaper/style.css b/themes/newspaper/style.css index aa2fec4..fc64a97 100644 --- a/themes/newspaper/style.css +++ b/themes/newspaper/style.css @@ -529,7 +529,7 @@ footer::after { margin-bottom: var(--spacing-lg); } -.posts-list h3 { +.posts-list h2 { margin-top: var(--spacing-xs); font-size: 1.6rem; text-transform: uppercase; @@ -538,18 +538,18 @@ footer::after { text-align: center; } -.posts-list h3 a { +.posts-list h2 a { color: var(--header-color); text-decoration: none; transition: color 0.2s; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--link-color); } /* ACCESSIBILITY: Focus states for post list links */ -.posts-list h3 a:focus { +.posts-list h2 a:focus { outline: 2px solid var(--link-color); outline-offset: 2px; color: var(--link-color); diff --git a/themes/nextstep/style.css b/themes/nextstep/style.css index 830bc67..46411ef 100644 --- a/themes/nextstep/style.css +++ b/themes/nextstep/style.css @@ -275,6 +275,7 @@ h2 { font-size: 1.6rem; } +.posts-list h2, h3 { font-size: 1.4rem; } diff --git a/themes/nordic-clean/style.css b/themes/nordic-clean/style.css index c0214ed..b8bae61 100644 --- a/themes/nordic-clean/style.css +++ b/themes/nordic-clean/style.css @@ -544,7 +544,7 @@ footer a:focus { padding-bottom: 0; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: 1.8rem; margin-bottom: var(--spacing-sm); diff --git a/themes/openbsd/style.css b/themes/openbsd/style.css index aa1973c..9d3f3cf 100644 --- a/themes/openbsd/style.css +++ b/themes/openbsd/style.css @@ -373,8 +373,7 @@ hr { margin-top: 0.78rem; } -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; } diff --git a/themes/osx/style.css b/themes/osx/style.css index f04d772..fd1f2a5 100644 --- a/themes/osx/style.css +++ b/themes/osx/style.css @@ -502,16 +502,16 @@ footer { border-color: var(--border-color); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: var(--spacing-xs); } -.posts-list h3 a { +.posts-list h2 a { color: var(--header-color); } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--accent-color); text-decoration: none; } diff --git a/themes/reader-mode/style.css b/themes/reader-mode/style.css index 4415143..4d14d96 100644 --- a/themes/reader-mode/style.css +++ b/themes/reader-mode/style.css @@ -525,13 +525,13 @@ figcaption { border-bottom: none; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; text-align: center; margin-bottom: var(--spacing-sm); } -.posts-list h3 a { +.posts-list h2 a { color: var(--heading-color); text-decoration: none; transition: color var(--transition-base) ease; @@ -539,13 +539,13 @@ figcaption { outline: none; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--link-color); text-decoration: underline; } /* ACCESSIBILITY: Focus states for post list links */ -.posts-list h3 a:focus { +.posts-list h2 a:focus { outline: 2px solid var(--link-color); outline-offset: 2px; } diff --git a/themes/skeuomorphic/style.css b/themes/skeuomorphic/style.css index afd0efc..5192c52 100644 --- a/themes/skeuomorphic/style.css +++ b/themes/skeuomorphic/style.css @@ -408,6 +408,7 @@ h2 { text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); } +.posts-list h2, h3 { font-size: 1.4rem; color: var(--leather-medium); diff --git a/themes/stefano/style.css b/themes/stefano/style.css index a7b236f..2f0e252 100644 --- a/themes/stefano/style.css +++ b/themes/stefano/style.css @@ -522,21 +522,21 @@ footer { box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: var(--spacing-xs); } -.posts-list h3 a { +.posts-list h2 a { color: var(--header-color); } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--accent); } /* ACCESSIBILITY: Focus states for post list links */ -.posts-list h3 a:focus { +.posts-list h2 a:focus { outline: 2px solid var(--link-color); outline-offset: 2px; color: var(--accent); diff --git a/themes/swiss-design/style.css b/themes/swiss-design/style.css index 2e198fc..77ece6b 100644 --- a/themes/swiss-design/style.css +++ b/themes/swiss-design/style.css @@ -488,7 +488,7 @@ footer a:focus { padding-bottom: 0; } -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: var(--text-2xl); margin-bottom: var(--spacing-sm); diff --git a/themes/terminal/style.css b/themes/terminal/style.css index 3bd2787..0f74845 100644 --- a/themes/terminal/style.css +++ b/themes/terminal/style.css @@ -698,13 +698,13 @@ footer::before { color: var(--command-prompt); } -.posts-list h3 { +.posts-list h2 { margin-top: var(--spacing-xs); margin-bottom: var(--spacing-xs); text-transform: none; } -.posts-list h3::before { +.posts-list h2::before { content: ""; } diff --git a/themes/text-only/style.css b/themes/text-only/style.css index fbf8c3e..8027936 100644 --- a/themes/text-only/style.css +++ b/themes/text-only/style.css @@ -244,7 +244,7 @@ hr { border-bottom: 1px solid var(--color-border); } -.posts-list h3 { +.posts-list h2 { margin: 0; } diff --git a/themes/thoughtful/style.css b/themes/thoughtful/style.css index 8a61de6..7ed0eeb 100644 --- a/themes/thoughtful/style.css +++ b/themes/thoughtful/style.css @@ -719,25 +719,25 @@ figcaption { border-bottom: 1px solid var(--border-light); } -.posts-list h3 { +.posts-list h2 { margin: 0 0 var(--space-6); font-size: var(--text-2xl); text-align: center; line-height: var(--line-height-tight); } -.posts-list h3 a { +.posts-list h2 a { color: var(--text-primary); text-decoration: none; transition: color var(--transition-normal); display: block; } -.posts-list h3 a:hover { +.posts-list h2 a:hover { color: var(--accent-primary); } -.posts-list h3 a:focus { +.posts-list h2 a:focus { outline: 2px solid var(--link-color); outline-offset: 2px; } diff --git a/themes/tty/style.css b/themes/tty/style.css index a9df816..daf8f65 100644 --- a/themes/tty/style.css +++ b/themes/tty/style.css @@ -642,16 +642,14 @@ figcaption { margin-bottom: var(--spacing-xs); } -.posts-list h2, -.posts-list h3 { +.posts-list h2 { margin-top: 0; font-size: var(--text-md); padding-left: 1.5em; /* Increased to prevent overlap */ position: relative; } -.posts-list h2::before, -.posts-list h3::before { +.posts-list h2::before { position: absolute; left: 0; top: 0; diff --git a/themes/vaporwave/style.css b/themes/vaporwave/style.css index 056c457..ae7cb89 100644 --- a/themes/vaporwave/style.css +++ b/themes/vaporwave/style.css @@ -364,6 +364,7 @@ h2 { text-shadow: 0 0 5px var(--neon-blue); } +.posts-list h2, h3 { font-size: var(--text-xl); color: var(--neon-pink); @@ -704,6 +705,7 @@ footer a:hover { font-size: var(--text-xl); } + .posts-list h2, h3 { font-size: var(--text-lg); } @@ -743,6 +745,7 @@ footer a:hover { font-size: var(--text-lg); } + .posts-list h2, h3 { font-size: var(--text-md); } diff --git a/themes/web1/style.css b/themes/web1/style.css index 8d489d6..00dfc1f 100644 --- a/themes/web1/style.css +++ b/themes/web1/style.css @@ -352,6 +352,7 @@ h2 { text-shadow: 1px 1px 1px #ff00ff; } +.posts-list h2, h3 { font-size: var(--text-xl); color: #00cc00; diff --git a/themes/web2/style.css b/themes/web2/style.css index 0e8780d..9474aaa 100644 --- a/themes/web2/style.css +++ b/themes/web2/style.css @@ -283,6 +283,7 @@ h2 { color: #444; } +.posts-list h2, h3 { font-size: var(--text-xl); color: #555; diff --git a/themes/win311/style.css b/themes/win311/style.css index f446f49..9ccfe29 100644 --- a/themes/win311/style.css +++ b/themes/win311/style.css @@ -513,7 +513,7 @@ footer { margin-bottom: var(--spacing-md); } -.posts-list h3 { +.posts-list h2 { margin-top: 0; margin-bottom: var(--spacing-xs); color: var(--highlight-color); diff --git a/themes/win7/style.css b/themes/win7/style.css index a18186d..7b3f5ec 100644 --- a/themes/win7/style.css +++ b/themes/win7/style.css @@ -355,6 +355,7 @@ h2 { color: #1a73e8; } +.posts-list h2, h3 { font-size: var(--text-xl); } diff --git a/themes/win95/style.css b/themes/win95/style.css index 7e6cca6..01a705c 100644 --- a/themes/win95/style.css +++ b/themes/win95/style.css @@ -399,7 +399,7 @@ a:hover { border-bottom: 1px solid var(--border-color); } -.posts-list h3 { +.posts-list h2 { margin-top: var(--spacing-lg); margin-bottom: var(--spacing-sm); } diff --git a/themes/winxp/style.css b/themes/winxp/style.css index 6c182cf..d642ef1 100644 --- a/themes/winxp/style.css +++ b/themes/winxp/style.css @@ -280,6 +280,7 @@ h1, h2, h3, h4, h5, h6 { h1 { font-size: var(--text-2xl); } h2 { font-size: var(--text-xl); } +.posts-list h2, h3 { font-size: var(--text-lg); } h4 { font-size: var(--text-md); } @@ -731,6 +732,7 @@ footer a:focus { h1 { font-size: var(--text-3xl); } h2 { font-size: var(--text-2xl); } + .posts-list h2, h3 { font-size: var(--text-xl); } h4 { font-size: var(--text-lg); } diff --git a/themes/y2k/style.css b/themes/y2k/style.css index 6de4790..2a0d9c4 100644 --- a/themes/y2k/style.css +++ b/themes/y2k/style.css @@ -328,6 +328,7 @@ h2 { } } +.posts-list h2, h3 { font-size: var(--text-lg); color: var(--accent-blue); @@ -778,6 +779,7 @@ hr { font-size: var(--text-lg); } + .posts-list h2, h3 { font-size: var(--text-md); } diff --git a/themes/zxspectrum/style.css b/themes/zxspectrum/style.css index 2c87add..5168883 100644 --- a/themes/zxspectrum/style.css +++ b/themes/zxspectrum/style.css @@ -268,11 +268,16 @@ h2::before { color: var(--bright-yellow); } +.posts-list h2, h3 { font-size: var(--font-size-md); color: var(--bright-green); } +.posts-list h2::before { + content: none; +} + p { margin-bottom: var(--spacing-md); line-height: var(--line-height-normal); @@ -670,4 +675,4 @@ ul, ol { li { margin-bottom: var(--spacing-xs); -} \ No newline at end of file +}