Compare commits

..

No commits in common. "main" and "0.15.0" have entirely different histories.
main ... 0.15.0

101 changed files with 4158 additions and 24194 deletions

766
README.md

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
0.41.1

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
#
# BSSG - Configuration File
# Version controlled via root VERSION file
# Version 0.15
# Contains all configurable parameters for the static site generator
# Developed by Stefano Marinelli (stefano@dragas.it)
#
@ -21,16 +21,11 @@ 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 # 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).
RAM_MODE_INCREMENTAL=false # If true, RAM mode skips unchanged files (mtime check). Default false = full rebuild.
RAM_MODE_FREE_CONTENT_AFTER_INDEX=false # If true, free raw file content from RAM after index build. Reduces memory at cost of disk reads for content in later stages.
# 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.
@ -41,19 +36,6 @@ 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"
@ -62,26 +44,12 @@ SHOW_TIMEZONE="false" # Options: "true", "false". Whether to display the timezon
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)
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
URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs: Year/Month/Day/slug will create Year/Month/Day/slug/index.html
ENABLE_TAG_RSS=true # Enable or disable tag-specific RSS feed generation (default: true)
# Display configuration
SHOW_HEADER_MENU=true # Options: "true", "false". Show navigation menu in the header.
SHOW_INDEX_DESCRIPTIONS=true # Options: "true", "false". Show post descriptions/excerpts on the index page.
GENERATE_EXCERPT=true # Options: "true", "false". Generate excerpt from content when no description is provided.
SHOW_READING_TIME=true # Options: "true", "false". Show reading time on individual post pages.
# 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)
PAGE_URL_FORMAT="pages/slug" # Format for page URLs: pages/slug will create pages/slug/index.html
# Markdown processing configuration
MARKDOWN_PROCESSOR="commonmark" # Options: "pandoc", "commonmark", or "markdown.pl"
@ -89,23 +57,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
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.
# robots.txt configuration
ENABLE_ROBOTS="true" # Options: "true", "false". Generate robots.txt during build.
ROBOTS_CONTENT="" # Custom robots.txt content. If empty, a default robots.txt is generated.
# Terminal colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'

View file

@ -8,19 +8,12 @@
set -euo pipefail
# --- Configuration ---
# 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"
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
readonly BSSG_BUILD_SCRIPT="scripts/build/main.sh"
readonly THEMES_DIR="./themes"
readonly BSSG_DEFAULT_OUTPUT_DIR="./output" # Default output dir used by build.sh
readonly EXAMPLE_ROOT_DIR="./example"
readonly CONFIG_FILE="config.sh"
readonly LOCAL_CONFIG_FILE="config.sh.local"
# Terminal colors (optional, for better output)
RED='\033[0;31m'
@ -29,10 +22,8 @@ YELLOW='\033[0;33m'
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
SITE_URL_BASE="http://localhost"
FULL_BUILD_MODE=false
SITE_URL_TOKEN="__BSSG_THEME_SITE_URL__"
# Default SITE_URL from config.sh if no other is specified
SITE_URL="http://localhost"
# --- Helper Functions ---
info() {
@ -54,11 +45,20 @@ error() {
# --- Cleanup Functions ---
cleanup_directories() {
info "Cleanup function called on exit. No specific preview-script files to clean in this version."
local dirs=("$BSSG_DEFAULT_OUTPUT_DIR" ".bssg_cache")
for dir in "${dirs[@]}"; do
if [ -d "$dir" ]; then
info "Cleaning directory: '$dir'"
# Remove contents including hidden files, suppress errors for non-existent hidden files
rm -rf "$dir"/* "$dir"/.??* 2>/dev/null || true
success "Directory '$dir' cleaned successfully."
else
warn "Directory '$dir' does not exist, skipping cleanup."
fi
done
}
trap cleanup_directories EXIT
# --- Print Help ---
print_help() {
cat << EOF
@ -68,63 +68,43 @@ 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. Selected BSSG configuration
3. Default value (http://localhost)
2. Local config file ($LOCAL_CONFIG_FILE)
3. Main config file ($CONFIG_FILE)
4. Default value (http://localhost)
Output:
Theme previews will be generated in the '$EXAMPLE_ROOT_DIR_DYNAMIC' directory,
Theme previews will be generated in the '$EXAMPLE_ROOT_DIR' 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
}
# --- Parse Command Line Arguments (for this script) ---
# --- Parse Command Line Arguments ---
parse_args() {
# Initialize variable
site_url_from_cli=""
# Parse command line arguments
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"
shift 2
else
error "--site-url requires a value for the base URL of previews"
error "--site-url requires a value"
fi
;;
--full-build)
FULL_BUILD_MODE=true
shift
;;
*)
warn "Unknown option: $1 (ignored)"
shift
@ -133,355 +113,197 @@ 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 Configuration ---
load_config() {
info "Loading base SITE_URL configuration for previews..."
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"
info "Loading configuration..."
# Load main config if it exists
if [ -f "$CONFIG_FILE" ]; then
# Source with subshell to avoid polluting global namespace
# but extract SITE_URL
SITE_URL=$(grep -m 1 "^SITE_URL=" "$CONFIG_FILE" | cut -d'"' -f2 || echo "")
info "Loaded SITE_URL='$SITE_URL' from $CONFIG_FILE"
else
warn "Main configuration file '$CONFIG_FILE' not found, using default SITE_URL."
fi
success "Configuration loaded. Using SITE_URL_BASE='$SITE_URL_BASE' for theme previews."
# Load local config if it exists (overrides main config)
if [ -f "$LOCAL_CONFIG_FILE" ]; then
echo "Debug: Processing local config file: $LOCAL_CONFIG_FILE"
# Use a more resilient approach for FreeBSD
# First check if the file contains SITE_URL
if grep -q "^SITE_URL=" "$LOCAL_CONFIG_FILE" 2>/dev/null; then
# Now try to extract the value, protecting against pipeline failures
local_site_url=$(grep -m 1 "^SITE_URL=" "$LOCAL_CONFIG_FILE" | cut -d'"' -f2 || echo "")
echo "Debug: Found local_site_url='$local_site_url'"
if [ -n "$local_site_url" ]; then
SITE_URL="$local_site_url"
info "Loaded SITE_URL='$SITE_URL' from $LOCAL_CONFIG_FILE"
else
warn "Failed to extract SITE_URL from $LOCAL_CONFIG_FILE (empty result)"
fi
else
echo "Debug: No SITE_URL found in $LOCAL_CONFIG_FILE"
fi
fi
# Command line argument overrides all config files
if [ -n "$site_url_from_cli" ]; then
SITE_URL="$site_url_from_cli"
info "Using SITE_URL='$SITE_URL' from command line argument"
fi
success "Configuration loaded. Using SITE_URL='$SITE_URL'"
}
# --- Sanity Checks ---
check_dependencies() {
info "Checking requirements..."
if [ ! -f "$BSSG_MAIN_SCRIPT" ]; then
error "BSSG main script not found at '$BSSG_MAIN_SCRIPT'. Run this script from the BSSG project root."
if [ ! -f "$BSSG_BUILD_SCRIPT" ]; then
error "BSSG build script not found at '$BSSG_BUILD_SCRIPT'. Run this script from the BSSG project root."
fi
if [ ! -x "$BSSG_MAIN_SCRIPT" ]; then
error "BSSG main script '$BSSG_MAIN_SCRIPT' is not executable. Please run 'chmod +x $BSSG_MAIN_SCRIPT'."
if [ ! -x "$BSSG_BUILD_SCRIPT" ]; then
error "BSSG build script '$BSSG_BUILD_SCRIPT' is not executable. Please run 'chmod +x $BSSG_BUILD_SCRIPT'."
fi
if [ ! -d "$THEMES_DIR" ]; then
error "Themes directory not found at '$THEMES_DIR'. This script must be run from the BSSG project root."
error "Themes directory not found at '$THEMES_DIR'."
fi
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 [ ! -d "$BSSG_DEFAULT_OUTPUT_DIR" ]; then
warn "Default output directory '$BSSG_DEFAULT_OUTPUT_DIR' does not exist. Will be created by build script."
fi
# Check for essential commands
for cmd in find basename mkdir mv cat date rm ls grep cut; do # Added grep and cut for config parsing
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" # Keep for debugging if needed
ls -la "$THEMES_DIR"
local theme_names=()
echo "Debug: attempting BSD/FreeBSD compatible find"
# Use a more compatible approach for FreeBSD and other systems
themes=()
for d in "$THEMES_DIR"/*; do
if [ -d "$d" ]; then
theme_names+=("$(basename "$d")")
# Extract just the basename
theme_name=$(basename "$d")
themes+=("$theme_name")
fi
done
if [ ${#theme_names[@]} -eq 0 ]; then
if [ ${#themes[@]} -eq 0 ]; then
error "No valid theme directories found in '$THEMES_DIR'."
fi
# 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"
# 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
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[@]}"
}
# 2. Build preview for each theme
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.
# 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
info "Clearing existing example directory: '$EXAMPLE_ROOT_DIR'"
# Remove contents including hidden files, suppress errors for non-existent hidden files
rm -rf "$EXAMPLE_ROOT_DIR"/* "$EXAMPLE_ROOT_DIR"/.??* 2>/dev/null || true
# Ensure the directory exists after clearing
mkdir -p "$EXAMPLE_ROOT_DIR"
success "Example directory cleared and ready."
}
build_previews_full() {
info "Starting theme preview builds..."
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'"
local theme_site_url="${SITE_URL_BASE%/}/${theme}"
local theme_output_path="${EXAMPLE_ROOT_DIR_DYNAMIC}/${theme}"
# Set theme-specific SITE_URL
local theme_site_url="${SITE_URL}/${theme}"
info "Using theme-specific SITE_URL: $theme_site_url"
info "Theme Site URL: $theme_site_url"
info "Theme Output Path: $theme_output_path"
mkdir -p "$theme_output_path"
if ! run_bssg_build -f --theme "$theme" --site-url "$theme_site_url" --output "$theme_output_path"; then
# Run the main build script for this theme.
# Output goes to the default ./output directory.
if ! "$BSSG_BUILD_SCRIPT" --theme "$theme" --site-url "$theme_site_url" --output "$BSSG_DEFAULT_OUTPUT_DIR"; then
error "Build failed for theme '$theme'. Check output above."
fi
success "Preview for theme '$theme' built successfully in '$theme_output_path'"
done
success "Build completed for theme '$theme'."
success "All theme previews built."
}
# Define destination directory for this theme's preview
local theme_dest_dir="$EXAMPLE_ROOT_DIR/$theme"
has_theme_specific_templates() {
local template_root="$TEMPLATES_DIR"
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
info "Moving built site from '$BSSG_DEFAULT_OUTPUT_DIR' to '$theme_dest_dir'"
# Ensure destination directory itself exists
mkdir -p "$theme_dest_dir"
# --- Simplified Move Operation ---
# Check if there's anything to move first
if [ -n "$(ls -A "$BSSG_DEFAULT_OUTPUT_DIR")" ]; then # ls -A lists all except . and ..
# Move the visible contents of the output directory.
# Using /* ensures we move the contents, not the directory itself.
# Added '|| true' to handle potential errors if mv fails unexpectedly (e.g. perms)
# Added '2>/dev/null' to suppress errors if '*' matches nothing (less likely with ls -A check)
mv "$BSSG_DEFAULT_OUTPUT_DIR"/* "$theme_dest_dir/" 2>/dev/null || true
# Explicitly move hidden files if any (less common for build output, but safer)
# Using a loop to handle potential `mv` argument list too long errors and hidden files better
find "$BSSG_DEFAULT_OUTPUT_DIR" -maxdepth 1 -name '.*' -not -name '.' -not -name '..' -exec mv {} "$theme_dest_dir/" \; 2>/dev/null || true
# Check if destination directory is still empty after move attempt
if [ -z "$(ls -A "$theme_dest_dir")" ]; then
warn "Destination directory '$theme_dest_dir' is empty after move attempt for theme '$theme'. Check '$BSSG_DEFAULT_OUTPUT_DIR' content and permissions."
else
success "Preview for theme '$theme' moved to '$theme_dest_dir'."
fi
# Clean the source output dir after successful move
info "Cleaning source output directory '$BSSG_DEFAULT_OUTPUT_DIR'..."
rm -rf "$BSSG_DEFAULT_OUTPUT_DIR"/* "$BSSG_DEFAULT_OUTPUT_DIR"/.??* 2>/dev/null || true
else
warn "Default output directory '$BSSG_DEFAULT_OUTPUT_DIR' is empty after build for theme '$theme'. Nothing to move."
fi
# --- End Simplified Move ---
printf -- "----------------------------------------\n"
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 ! 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
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"
# 3. Generate the index.html in the example directory (No changes here)
generate_index() {
local index_file="$EXAMPLE_ROOT_DIR/index.html"
info "Generating index file at '$index_file'..."
# Get current date for the footer
local current_date
current_date=$(date)
current_date=$(date) # Use default date format
# Theme count for display
local theme_count=${#themes[@]}
# HTML content remains the same, heredoc is portable
# Use cat heredoc to create the HTML file
cat << EOF > "$index_file"
<!DOCTYPE html>
<html lang="en">
@ -491,82 +313,222 @@ create_index_page() {
<title>BSSG Theme Previews</title>
<style>
:root {
--bg-color: #fcfcfc; --text-color: #333333; --link-color: #3b82f6;
--link-hover-color: #1d4ed8; --header-color: #1e293b; --border-color: #e5e7eb;
--accent-color: #f0f9ff; --accent-secondary: #93c5fd; --tag-bg: #dbeafe;
--card-bg: #ffffff; --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05);
--radius: 10px; --transition: 0.2s ease;
/* Modern color scheme inspired by default BSSG theme */
--bg-color: #fcfcfc;
--text-color: #333333;
--link-color: #3b82f6;
--link-hover-color: #1d4ed8;
--header-color: #1e293b;
--border-color: #e5e7eb;
--accent-color: #f0f9ff;
--accent-secondary: #93c5fd;
--tag-bg: #dbeafe;
--card-bg: #ffffff;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.05);
--radius: 10px;
--transition: 0.2s ease;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #0f172a; --text-color: #e2e8f0; --link-color: #60a5fa;
--link-hover-color: #93c5fd; --header-color: #f8fafc; --border-color: #334155;
--accent-color: #1e3a8a; --accent-secondary: #3b82f6; --tag-bg: #1e3a8a;
--card-bg: #1e293b; --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.15);
--bg-color: #0f172a;
--text-color: #e2e8f0;
--link-color: #60a5fa;
--link-hover-color: #93c5fd;
--header-color: #f8fafc;
--border-color: #334155;
--accent-color: #1e3a8a;
--accent-secondary: #3b82f6;
--tag-bg: #1e3a8a;
--card-bg: #1e293b;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.15);
}
}
/* Base styles */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6; margin: 0; padding: 0; color: var(--text-color); background-color: var(--bg-color);
line-height: 1.6;
margin: 0;
padding: 0;
color: var(--text-color);
background-color: var(--bg-color);
}
.container { max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem; }
.container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
/* Header styles */
header {
text-align: center; margin-bottom: 2.5rem; position: relative;
padding-bottom: 1.5rem; border-bottom: 1px solid var(--border-color);
text-align: center;
margin-bottom: 2.5rem;
position: relative;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
header::after {
content: ""; position: absolute; bottom: -1px; left: 50%; transform: translateX(-50%);
width: 120px; height: 3px; background: linear-gradient(90deg, var(--link-color), var(--accent-secondary));
content: "";
position: absolute;
bottom: -1px;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 3px;
background: linear-gradient(90deg, var(--link-color), var(--accent-secondary));
border-radius: var(--radius);
}
h1 {
color: var(--header-color); font-size: 2.5rem; margin: 0; padding: 0;
color: var(--header-color);
font-size: 2.5rem;
margin: 0;
padding: 0;
background: linear-gradient(120deg, var(--header-color) 0%, var(--link-color) 100%);
background-clip: text; -webkit-background-clip: text; color: transparent;
background-clip: text;
-webkit-background-clip: text;
color: transparent;
text-shadow: 0 1px 1px rgba(0,0,0,0.05);
}
.theme-count {
display: inline-block; background-color: var(--accent-secondary); color: white;
font-weight: bold; padding: 0.4rem 1rem; border-radius: 2rem; margin: 1rem 0;
font-size: 1.1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: inline-block;
background-color: var(--accent-secondary);
color: white;
font-weight: bold;
padding: 0.4rem 1rem;
border-radius: 2rem;
margin: 1rem 0;
font-size: 1.1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.description { font-size: 1.1rem; max-width: 600px; margin: 1rem auto; opacity: 0.9; }
.description {
font-size: 1.1rem;
max-width: 600px;
margin: 1rem auto;
opacity: 0.9;
}
/* Grid layout */
.theme-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem; margin-bottom: 2rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.theme-card {
background-color: var(--card-bg); border-radius: var(--radius); overflow: hidden;
box-shadow: var(--card-shadow); transition: transform var(--transition), box-shadow var(--transition);
position: relative; border: 1px solid var(--border-color);
background-color: var(--card-bg);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--card-shadow);
transition: transform var(--transition), box-shadow var(--transition);
position: relative;
border: 1px solid var(--border-color);
}
.theme-card:hover, .theme-card:focus-within { transform: translateY(-5px); box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); }
.theme-card:hover, .theme-card:focus-within {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
.theme-card a {
display: block; padding: 1.5rem; text-decoration: none; color: var(--link-color);
font-weight: 500; font-size: 1.1rem; position: relative; z-index: 1;
display: block;
padding: 1.5rem;
text-decoration: none;
color: var(--link-color);
font-weight: 500;
font-size: 1.1rem;
position: relative;
z-index: 1;
}
.theme-card a::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; }
.theme-name { font-weight: 600; margin-bottom: 0.5rem; color: var(--header-color); transition: color var(--transition); }
.theme-card:hover .theme-name { color: var(--link-color); }
.theme-card a::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
.theme-name {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--header-color);
transition: color var(--transition);
}
.theme-card:hover .theme-name {
color: var(--link-color);
}
/* Hover indicator */
.theme-card::before {
content: "→"; position: absolute; right: 1.5rem; top: 50%; transform: translateY(-50%);
font-size: 1.25rem; opacity: 0; color: var(--link-color);
content: "→";
position: absolute;
right: 1.5rem;
top: 50%;
transform: translateY(-50%);
font-size: 1.25rem;
opacity: 0;
color: var(--link-color);
transition: opacity var(--transition), transform var(--transition);
}
.theme-card:hover::before { opacity: 1; transform: translate(5px, -50%); }
footer {
text-align: center; margin-top: 3rem; padding-top: 1.5rem; color: var(--text-color);
opacity: 0.8; font-size: 0.9rem; border-top: 1px solid var(--border-color); position: relative;
.theme-card:hover::before {
opacity: 1;
transform: translate(5px, -50%);
}
/* Footer styles */
footer {
text-align: center;
margin-top: 3rem;
padding-top: 1.5rem;
color: var(--text-color);
opacity: 0.8;
font-size: 0.9rem;
border-top: 1px solid var(--border-color);
position: relative;
}
footer::before {
content: ""; position: absolute; top: -1px; left: 50%; transform: translateX(-50%);
width: 100px; height: 3px; background: linear-gradient(90deg, var(--accent-secondary), var(--link-color));
content: "";
position: absolute;
top: -1px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 3px;
background: linear-gradient(90deg, var(--accent-secondary), var(--link-color));
border-radius: var(--radius);
}
@media (max-width: 768px) { .theme-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
/* Responsive adjustments */
@media (max-width: 768px) {
.theme-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
@media (max-width: 480px) {
.theme-grid { grid-template-columns: 1fr; }
h1 { font-size: 2rem; } .container { padding: 1.5rem 1rem; }
.theme-grid {
grid-template-columns: 1fr;
}
h1 {
font-size: 2rem;
}
.container {
padding: 1.5rem 1rem;
}
}
</style>
</head>
@ -577,13 +539,15 @@ create_index_page() {
<div class="theme-count">${theme_count} Themes Available</div>
<p class="description">Browse these theme previews using the current site content. Click on any theme to explore its design.</p>
</header>
<div class="theme-grid">
EOF
# Add grid items for each theme
for theme in "${themes[@]}"; do
local safe_theme_name="${theme//&/&}"
safe_theme_name="${safe_theme_name//</<}"
safe_theme_name="${safe_theme_name//>/>}"
local safe_theme_name="${theme//&/&amp;}"
safe_theme_name="${safe_theme_name//</&lt;}"
safe_theme_name="${safe_theme_name//>/&gt;}"
cat << EOF >> "$index_file"
<div class="theme-card">
@ -594,11 +558,13 @@ EOF
EOF
done
# Close the HTML structure
cat << EOF >> "$index_file"
</div>
<footer>
<p>Generated on ${current_date}</p>
<p>Base SITE_URL: ${SITE_URL_BASE}</p>
<p>Base SITE_URL: ${SITE_URL}</p>
</footer>
</div>
</body>
@ -608,68 +574,27 @@ EOF
success "Index file generated successfully with ${theme_count} themes."
}
determine_example_root_dir() {
info "Determining effective site root for EXAMPLE_ROOT_DIR_DYNAMIC..."
local project_root_abs
# Portable way to get absolute path of current directory
project_root_abs=$( (cd . && pwd -P) || { error "Could not determine project root."; exit 1; } )
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: '$OUTPUT_DIR'"
local effective_output_dir_abs_unnormalized
if [[ "$OUTPUT_DIR" == /* ]]; then
effective_output_dir_abs_unnormalized="$OUTPUT_DIR"
else
effective_output_dir_abs_unnormalized="$project_root_abs/$OUTPUT_DIR"
fi
# 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'"
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
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
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
# 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() {
# Ensure global 'themes' array is declared if not implicitly through find_themes
declare -a themes
parse_args "$@"
resolve_config_override
load_config
parse_args "$@" # Pass all script arguments to parser
check_dependencies
determine_example_root_dir
find_themes # Populates global 'themes' array
load_config
find_themes
# Clean output and cache directories before starting
info "Cleaning output and cache directories before starting..."
cleanup_directories
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."
generate_index
# Clean output and cache directories after finishing
info "Cleaning output and cache directories after finishing..."
cleanup_directories
info "All ${#themes[@]} theme previews have been generated in '$EXAMPLE_ROOT_DIR'."
success "Theme preview generation is complete. You can view them by opening '$EXAMPLE_ROOT_DIR/index.html' in your browser."
}
# Run the main function with all script arguments
main "$@"

View file

@ -3,17 +3,14 @@
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"
@ -47,5 +44,4 @@ 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"
export MSG_RELATED_POSTS="Ähnliche Beiträge"
export MSG_BACK_TO_TOP="Nach oben"

View file

@ -3,17 +3,14 @@
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"
@ -47,5 +44,4 @@ 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"
export MSG_RELATED_POSTS="Related Posts"
export MSG_BACK_TO_TOP="Back to Top"

View file

@ -3,17 +3,14 @@
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"
@ -47,5 +44,4 @@ 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"
export MSG_RELATED_POSTS="Artículos relacionados"
export MSG_BACK_TO_TOP="Volver arriba"

View file

@ -3,17 +3,14 @@
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 à"
@ -47,5 +44,4 @@ 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"
export MSG_RELATED_POSTS="Articles connexes"
export MSG_BACK_TO_TOP="Retour en haut"

View file

@ -3,17 +3,14 @@
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"
@ -47,5 +44,4 @@ 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 in cima"
export MSG_RELATED_POSTS="Articoli correlati"
export MSG_BACK_TO_TOP="Torna su"

View file

@ -3,17 +3,14 @@
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="に戻る"
@ -47,5 +44,4 @@ export MSG_READING_TIME_TEMPLATE="読了時間 %d分"
export MSG_MINUTE="分"
export MSG_MINUTES="分"
export MSG_UPDATED_ON="更新日"
export MSG_BACK_TO_TOP="トップに戻る"
export MSG_RELATED_POSTS="関連記事"
export MSG_BACK_TO_TOP="トップに戻る"

View file

@ -1,51 +0,0 @@
#!/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"

View file

@ -3,17 +3,14 @@
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"
@ -47,5 +44,4 @@ 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"
export MSG_RELATED_POSTS="Posts Relacionados"
export MSG_BACK_TO_TOP="Voltar ao topo"

View file

@ -3,17 +3,14 @@
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="返回"
@ -47,5 +44,4 @@ export MSG_READING_TIME_TEMPLATE="阅读时间 %d 分钟"
export MSG_MINUTE="分钟"
export MSG_MINUTES="分钟"
export MSG_UPDATED_ON="更新于"
export MSG_BACK_TO_TOP="返回顶部"
export MSG_RELATED_POSTS="相关文章"
export MSG_BACK_TO_TOP="返回顶部"

View file

@ -9,53 +9,6 @@
set -e
# --- Argument Parsing for --config --- START ---
# We need to parse arguments early to catch --config before loading the config.
# This allows the specified config file to override defaults.
CMD_LINE_CONFIG_FILE=""
declare -a OTHER_ARGS # Array to hold arguments not related to --config
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--config)
if [[ -z "$2" || "$2" == -* ]]; then # Check if value is missing or looks like another flag
echo -e "${RED}Error: --config option requires a path argument.${NC}" >&2
exit 1
fi
CMD_LINE_CONFIG_FILE="$2"
shift # past argument
shift # past value
;;
*) # unknown option or command
OTHER_ARGS+=("$1") # save it in an array for later
shift # past argument
;;
esac
done
# Restore positional parameters from the filtered arguments
set -- "${OTHER_ARGS[@]}"
# --- Argument Parsing for --config --- END ---
# --- Configuration Override Logic --- START ---
# Priority: --config > BSSG_LCONF > Default (config.sh.local)
FINAL_CONFIG_OVERRIDE=""
if [ -n "$CMD_LINE_CONFIG_FILE" ]; then
# --config flag was used, prioritize it
FINAL_CONFIG_OVERRIDE="$CMD_LINE_CONFIG_FILE"
echo "Info: Using configuration file specified via --config: $FINAL_CONFIG_OVERRIDE"
elif [ -v BSSG_LCONF ] && [ -n "${BSSG_LCONF}" ]; then
# --config not used, check BSSG_LCONF environment variable
FINAL_CONFIG_OVERRIDE="${BSSG_LCONF}"
echo "Info: Using configuration file specified via BSSG_LCONF environment variable: $FINAL_CONFIG_OVERRIDE"
# else
# Neither --config nor BSSG_LCONF is set, config_loader.sh will check for default config.sh.local
# No message needed here, config_loader will print messages.
fi
# --- Configuration Override Logic --- END ---
# Load configuration (DEPRECATED - Moved to config_loader.sh)
# CONFIG_FILE="config.sh"
# if [ -f "$CONFIG_FILE" ]; then
@ -76,7 +29,6 @@ fi
# Source the config loader script EARLY to set defaults, load configs, and expand paths.
# It handles config.sh, config.sh.local, and site-specific configs sourced via core local file.
# It also EXPORTS all necessary variables for subsequent scripts.
# NOW passes the command-line config path, if provided.
# Define path to config loader relative to this script
BSSG_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
@ -84,12 +36,9 @@ export BSSG_SCRIPT_DIR # Export the variable so sub-scripts inherit it
CONFIG_LOADER_SCRIPT="${BSSG_SCRIPT_DIR}/scripts/build/config_loader.sh"
if [ -f "$CONFIG_LOADER_SCRIPT" ]; then
# Pass the determined configuration override path (or empty string) to the loader script
# The loader script will handle sourcing it appropriately.
# shellcheck source=scripts/build/config_loader.sh
source "$CONFIG_LOADER_SCRIPT" "$FINAL_CONFIG_OVERRIDE"
source "$CONFIG_LOADER_SCRIPT"
echo "Central configuration loaded via config_loader.sh"
# Note: The echo message regarding the loaded local config is now handled within config_loader.sh
else
echo -e "${RED}Error: Config loader script not found at '$CONFIG_LOADER_SCRIPT'${NC}" >&2
exit 1
@ -97,36 +46,20 @@ fi
# --- Centralized Configuration Loading --- END ---
# 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.
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
RED='${RED:-\\033[0;31m}' # Default if not exported
GREEN='${GREEN:-\\033[0;32m}'
YELLOW='${YELLOW:-\\033[0;33m}'
NC='${NC:-\\033[0m}'
# Make sure all scripts are executable
chmod +x scripts/*.sh 2>/dev/null || true
# Function to display help information
show_help() {
echo "BSSG - Bash Static Site Generator (v${VERSION})"
echo "BSSG - Bash Static Site Generator (v0.15)"
echo "========================================="
echo ""
echo "Usage: $0 [--config <path>] command [options]"
echo ""
echo "Global Options:"
echo " --config <path> Specify a custom configuration file. Overrides BSSG_LCONF"
echo " and the default config.sh.local."
echo ""
echo "Environment Variables:"
echo " BSSG_LCONF Path to a configuration file to use if --config is not set."
echo "Usage: $0 command [options]"
echo ""
echo "Commands:"
echo " post [-html] [draft_file] Create a new post or continue editing a draft"
@ -145,83 +78,26 @@ show_help() {
echo " restore [backup_file|ID] Restore from a backup (all content by default)"
echo " Options: --no-content, --no-config"
echo " backups List all available backups"
echo " build [-f] [more...] Build the site (use 'build --help' for all options)"
echo " server [-h] [options] Build & run local server (use 'server --help' for options)"
echo " Options: --port <PORT> (default from config: ${BSSG_SERVER_PORT_DEFAULT:-8000})"
echo " --host <HOST> (default from config: ${BSSG_SERVER_HOST_DEFAULT:-localhost})"
echo " build Build the site"
echo " init <target_directory> Initialize a new site in the specified directory"
echo " help Show this help message"
echo ""
echo "For more information, refer to the README.md file."
}
# Function to display help specific to the build command
show_build_help() {
echo "Usage: $0 build [options]"
echo ""
echo "Build Options:"
echo " --src DIR Override Source directory (from config: ${SRC_DIR:-src})"
echo " --pages DIR Override Pages directory (from config: ${PAGES_DIR:-pages})"
echo " --drafts DIR Override Drafts directory (from config: ${DRAFTS_DIR:-drafts})"
echo " --output DIR Override Output directory (from config: ${OUTPUT_DIR:-output})"
echo " --templates DIR Override Templates directory (from config: ${TEMPLATES_DIR:-templates})"
echo " --themes-dir DIR Override Themes parent directory (from config: ${THEMES_DIR:-themes})"
echo " --theme NAME Override Theme to use (from config: ${THEME:-default})"
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"
echo " --author-name NAME Override Author name"
echo " --author-email EMAIL Override Author email"
echo " --posts-per-page NUM Override Posts per page (from config: ${POSTS_PER_PAGE:-10})"
echo " --deploy Force deployment after successful build (overrides config)"
echo " --no-deploy Prevent deployment after build (overrides config)"
echo " --help Display this build-specific help message and exit"
echo ""
echo "Note: These options override settings from configuration files for this build run."
}
# Function to display help specific to the server command
show_server_help() {
echo "Usage: $0 server [options]"
echo ""
echo "Builds the site and starts a local development server."
echo "The SITE_URL will be temporarily overridden to match the server's address during the build."
echo ""
echo "Server Options:"
echo " --port <PORT> Specify the port for the server to listen on."
echo " (Default from config: ${BSSG_SERVER_PORT_DEFAULT:-8000})"
echo " --host <HOST> Specify the host/IP address for the server."
echo " (Default from config: ${BSSG_SERVER_HOST_DEFAULT:-localhost})"
echo " --build-mode MODE Build mode: normal or ram (default from config: ${BUILD_MODE:-normal})"
echo " --no-build Skip the build step and start the server with existing"
echo " content in the output directory."
echo " -h, --help Display this help message and exit."
echo ""
}
# Main function
main() {
# Arguments are already parsed and filtered by the time main() is called.
# Positional parameters ($1, $2, etc.) now contain only the command and its specific options.
local command=""
# No arguments provided (after potential --config filtering)
# No arguments provided
if [ $# -eq 0 ]; then
show_help
exit 0
fi
command="$1"
shift # Consume the command itself
# expand variables such as POSTS_DIR, PAGES_DIR embedded in the command-line
set -- $(eval echo "$@")
shift
case "$command" in
post)
scripts/post.sh "$@"
@ -255,225 +131,9 @@ main() {
;;
build)
# Call the new build orchestrator script in the build/ directory
# Parse build-specific arguments first and export them as environment variables
echo "Parsing build-specific arguments..."
export CMD_DEPLOY_OVERRIDE="unset" # Reset deploy override for this build command
declare -a build_args=("$@") # Capture args passed to build
set -- "${build_args[@]}" # Set positional params for parsing
while [[ $# -gt 0 ]]; do
case "$1" in
--src)
export SRC_DIR="$2"
shift 2
;;
--pages)
export PAGES_DIR="$2"
shift 2
;;
--drafts)
export DRAFTS_DIR="$2"
shift 2
;;
--output)
export OUTPUT_DIR="$2"
shift 2
;;
--templates)
export TEMPLATES_DIR="$2"
shift 2
;;
--themes-dir)
export THEMES_DIR="$2"
shift 2
;;
--theme)
export THEME="$2"
shift 2
;;
--static)
export STATIC_DIR="$2"
shift 2
;;
--clean-output)
# Handle both flag style (--clean-output) and value style (--clean-output true/false)
if [[ "$2" == "true" || "$2" == "false" ]]; then
export CLEAN_OUTPUT="$2"
shift 2
else
export CLEAN_OUTPUT=true
shift 1
fi
;;
-f|--force-rebuild)
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
;;
--site-url)
export SITE_URL="$2"
shift 2
;;
--site-description)
export SITE_DESCRIPTION="$2"
shift 2
;;
--author-name)
export AUTHOR_NAME="$2"
shift 2
;;
--author-email)
export AUTHOR_EMAIL="$2"
shift 2
;;
--posts-per-page)
export POSTS_PER_PAGE="$2"
shift 2
;;
--deploy)
export CMD_DEPLOY_OVERRIDE="true"
shift 1
;;
--no-deploy)
export CMD_DEPLOY_OVERRIDE="false"
shift 1
;;
--help)
show_build_help
exit 0
;;
*)
echo -e "${RED}Error: Unknown build option: $1${NC}"
show_build_help
exit 1
;;
esac
done
echo "Invoking build process (scripts/build/main.sh)..."
# Execute the main build script. It will inherit the exported variables.
scripts/build/main.sh
;;
server)
# Use defaults from config (via exported BSSG_SERVER_PORT_DEFAULT, BSSG_SERVER_HOST_DEFAULT),
# which can be overridden by CLI options --port and --host for this specific run.
SERVER_CMD_PORT="${BSSG_SERVER_PORT_DEFAULT}"
SERVER_CMD_HOST="${BSSG_SERVER_HOST_DEFAULT}"
PERFORM_BUILD=true
# SERVER_SCRIPT_ARGS=() # Not currently used to pass to server.sh itself beyond port/doc_root
# Parse server-specific arguments
TEMP_ARGS=("$@") # Work with a copy of arguments
set -- "${TEMP_ARGS[@]}" # Set positional parameters for server command parsing
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
show_server_help
exit 0
;;
--port)
if [[ -z "$2" || "$2" == -* ]]; then
echo -e "${RED}Error: --port option requires a numeric argument.${NC}" >&2
exit 1
fi
SERVER_CMD_PORT="$2"
shift 2
;;
--host)
if [[ -z "$2" || "$2" == -* ]]; then
echo -e "${RED}Error: --host option requires a hostname or IP argument.${NC}" >&2
exit 1
fi
SERVER_CMD_HOST="$2"
shift 2
;;
--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
;;
--no-build)
PERFORM_BUILD=false
shift 1
;;
*)
# Collect unrecognized arguments if server.sh were to take more.
# For now, they are ignored or could be passed to build if we design it that way.
echo -e "${YELLOW}Warning: Unrecognized server option: $1${NC}"
shift # Consume unrecognized option
;;
esac
done
# Ensure OUTPUT_DIR is loaded (it should be by config_loader.sh)
if [ -z "${OUTPUT_DIR}" ]; then
echo -e "${RED}Error: OUTPUT_DIR is not set. Configuration issue? Ensure config is loaded.${NC}" >&2
exit 1
fi
# scripts/server.sh will resolve this path to absolute and check if it's a directory.
local effective_output_dir="${OUTPUT_DIR}"
if [ "$PERFORM_BUILD" = true ]; then
echo "Info: Server command will update SITE_URL to http://${SERVER_CMD_HOST}:${SERVER_CMD_PORT} for the build."
export SITE_URL="http://${SERVER_CMD_HOST}:${SERVER_CMD_PORT}" # Override SITE_URL for the build
echo "Info: Initiating build before starting server..."
if [ -f "${BSSG_SCRIPT_DIR}/scripts/build/main.sh" ]; then
# Call the main build script. It will pick up the exported SITE_URL and BUILD_MODE.
"${BSSG_SCRIPT_DIR}/scripts/build/main.sh"
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -ne 0 ]; then
echo -e "${RED}Error: Build failed with exit code $BUILD_EXIT_CODE. Server not started.${NC}" >&2
exit $BUILD_EXIT_CODE
fi
echo -e "${GREEN}Build complete.${NC}"
else
echo -e "${RED}Error: Build script (${BSSG_SCRIPT_DIR}/scripts/build/main.sh) not found.${NC}" >&2
exit 1
fi
else
echo "Info: Skipping build step due to --no-build flag."
fi
echo "Info: Starting server on http://${SERVER_CMD_HOST}:${SERVER_CMD_PORT}"
echo "Info: Serving files from ${effective_output_dir}"
# The server script (scripts/server.sh) takes PORT as $1 and WWW_ROOT as $2.
# WWW_ROOT should be the $OUTPUT_DIR from config.
# scripts/server.sh will perform its own validation for the output directory existence and type.
"${BSSG_SCRIPT_DIR}/scripts/server.sh" "$SERVER_CMD_PORT" "$effective_output_dir"
# Pass along any additional arguments (e.g., --force-rebuild)
echo "Invoking new build process..."
scripts/build/main.sh "$@"
;;
init)
# Check if directory argument is provided
@ -496,5 +156,4 @@ main() {
}
# Run the main function
# Pass the filtered arguments (command and its options) to main
main "$@"

View file

@ -5,17 +5,10 @@
#
# Define cache paths (should match exported config, but useful here too)
# CACHE_DIR=".bssg_cache" # Redundant: CACHE_DIR is now set and exported by config_loader.sh
CONFIG_HASH_FILE="${CACHE_DIR}/config_hash.md5" # Use variable directly
mkdir -p "${CACHE_DIR}/content_hash" 2>/dev/null || true
update_content_hash() {
local hash
hash=$(portable_md5sum < "$1" 2>/dev/null | awk '{print $1}')
if [ -n "$hash" ]; then
printf '%s\n' "$hash" > "${CACHE_DIR}/content_hash/$(basename "$1")"
fi
}
CACHE_DIR=".bssg_cache"
CONFIG_HASH_FILE="$CACHE_DIR/config_hash.md5"
# Add other cache-related paths if directly used by functions below
# (Example: $CACHE_DIR/theme.txt, $CACHE_DIR/file_index.txt, etc. are used)
# --- Cache Functions --- START ---
@ -27,9 +20,7 @@ create_config_hash() {
# IMPORTANT: Requires BSSG_CONFIG_VARS to be exported from config_loader.sh
local config_string=""
local var_name
local config_vars_array
# Read exported vars into an array
read -r -a config_vars_array <<< "$BSSG_CONFIG_VARS"
local config_vars_array=($BSSG_CONFIG_VARS)
for var_name in "${config_vars_array[@]}"; do
# Use printf -v to append safely, ensuring literal newlines
printf -v config_string '%s%s=%s\n' "$config_string" "$var_name" "${!var_name}"
@ -66,9 +57,7 @@ config_has_changed() {
# IMPORTANT: Requires BSSG_CONFIG_VARS to be exported from config_loader.sh
local config_string=""
local var_name
local config_vars_array
# Read exported vars into an array
read -r -a config_vars_array <<< "$BSSG_CONFIG_VARS"
local config_vars_array=($BSSG_CONFIG_VARS)
for var_name in "${config_vars_array[@]}"; do
# Use printf -v to append safely, ensuring literal newlines
printf -v config_string '%s%s=%s\n' "$config_string" "$var_name" "${!var_name}"
@ -98,22 +87,20 @@ config_has_changed() {
# other implicit theme-related changes that weren't previously tracked.
# For now, it assumes `config_has_changed` correctly reflects all non-theme changes.
only_theme_changed() {
local theme_cache_file="${CACHE_DIR}/theme.txt"
# If no hash file exists, more than just theme has changed
if [ ! -f "$CONFIG_HASH_FILE" ] || [ ! -f "$theme_cache_file" ]; then
if [ ! -f "$CONFIG_HASH_FILE" ] || [ ! -f "$CACHE_DIR/theme.txt" ]; then
return 1 # False, more than theme has changed
fi
# Read the stored theme
local stored_theme
stored_theme=$(cat "$theme_cache_file")
local stored_theme=$(cat "$CACHE_DIR/theme.txt")
# Compare current theme with stored theme
if [ "$THEME" != "$stored_theme" ]; then
echo -e "${YELLOW}Theme has changed from $stored_theme to $THEME${NC}"
# Store the current theme for next time
echo "$THEME" > "$theme_cache_file"
echo "$THEME" > "$CACHE_DIR/theme.txt"
# Check if any other config has changed
if ! config_has_changed; then
@ -131,6 +118,7 @@ clean_stale_cache() {
if [ "${FORCE_REBUILD:-false}" = true ]; then # Check exported FORCE_REBUILD
echo -e "${YELLOW}Force rebuild enabled, deleting entire cache...${NC}"
rm -rf "$CACHE_DIR"
mkdir -p "$CACHE_DIR"
mkdir -p "$CACHE_DIR/meta"
mkdir -p "$CACHE_DIR/content"
echo -e "${GREEN}Cache deleted!${NC}"
@ -178,25 +166,19 @@ clean_stale_cache() {
if [ "$posts_removed" = true ]; then
echo -e "${YELLOW}Posts were removed, forcing regeneration of index, tags, archives, sitemap, and RSS feed${NC}"
# Remove marker files to force regeneration
rm -f "${CACHE_DIR}/tags_index.txt"
rm -f "${CACHE_DIR}/archive_index.txt"
rm -f "${CACHE_DIR}/index_marker"
rm -f "$CACHE_DIR/tags_index.txt"
rm -f "$CACHE_DIR/archive_index.txt"
rm -f "$CACHE_DIR/index_marker"
# Remove the tags flag file as well
rm -f "${CACHE_DIR}/has_tags.flag"
rm -f "${CACHE_DIR:-.bssg_cache}/has_tags.flag"
# IMPORTANT: Requires OUTPUT_DIR to be exported/available
rm -f "${OUTPUT_DIR:-output}/sitemap.xml"
rm -f "${OUTPUT_DIR:-output}/${RSS_FILENAME:-rss.xml}"
rm -f "${OUTPUT_DIR:-output}/rss.xml"
rm -f "${OUTPUT_DIR:-output}/index.html"
# 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}"
@ -268,14 +250,8 @@ file_needs_rebuild() {
local input_time
input_time=$(get_file_mtime "$input_file")
if (( input_time > output_time )); then
# Mtime says rebuild, but verify content hash to avoid false positives
local current_hash stored_hash
current_hash=$(portable_md5sum < "$input_file" 2>/dev/null | awk '{print $1}')
stored_hash=$(cat "${CACHE_DIR}/content_hash/$(basename "$input_file")" 2>/dev/null)
if [ -n "$stored_hash" ] && [ "$current_hash" = "$stored_hash" ]; then
return 1
fi
return 0
# echo "DEBUG_CACHE: Input newer than output ($input_time > $output_time), returning 0" >&2
return 0 # Rebuild needed
fi
# echo "DEBUG_CACHE: file_needs_rebuild returning 1 (no rebuild) for '$output_file'" >&2
@ -342,7 +318,7 @@ indexes_need_rebuild() {
newest_meta_time=$(find "$meta_cache_dir" -type f -printf '%T@\n' 2>/dev/null | sort -nr | head -n 1)
newest_meta_time=${newest_meta_time:-0} # Handle empty dir
# Convert float timestamp to integer
newest_meta_time=${newest_meta_time%.*} # Truncate to integer
newest_meta_time=$(printf "%.0f" "$newest_meta_time")
else
# Fallback for non-GNU find (less efficient)
local meta_files

View file

@ -4,10 +4,6 @@
# Sets default variables, loads user config and locale files, and exports them.
#
# Capture the command-line config file path, if provided by the main script.
CMD_LINE_CONFIG_FILE="$1"
shift || true # Shift arguments even if only one was passed (or none)
# --- Default Configuration Variables --- START ---
# Use :- syntax to only set defaults if the variable is unset or null.
# This allows values set by CLI parsing (before this script is sourced) to persist.
@ -17,31 +13,20 @@ OUTPUT_DIR="${OUTPUT_DIR:-output}"
TEMPLATES_DIR="${TEMPLATES_DIR:-templates}"
THEMES_DIR="${THEMES_DIR:-themes}"
STATIC_DIR="${STATIC_DIR:-static}"
CACHE_DIR="${CACHE_DIR:-.bssg_cache}" # Default cache directory
THEME="${THEME:-default}"
SITE_TITLE="${SITE_TITLE:-My Journal}"
SITE_DESCRIPTION="${SITE_DESCRIPTION:-A personal journal and introspective newspaper}"
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}"
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}"
BUILD_MODE="${BUILD_MODE:-normal}" # Build mode: normal or ram
RAM_MODE_INCREMENTAL="${RAM_MODE_INCREMENTAL:-false}"
RAM_MODE_FREE_CONTENT_AFTER_INDEX="${RAM_MODE_FREE_CONTENT_AFTER_INDEX:-false}"
SITE_LANG="${SITE_LANG:-en}"
LOCALE_DIR="${LOCALE_DIR:-locales}"
PAGES_DIR="${PAGES_DIR:-pages}"
@ -51,44 +36,19 @@ 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
# 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
# Display Configuration Defaults
SHOW_HEADER_MENU="${SHOW_HEADER_MENU:-true}"
SHOW_INDEX_DESCRIPTIONS="${SHOW_INDEX_DESCRIPTIONS:-true}"
GENERATE_EXCERPT="${GENERATE_EXCERPT:-true}"
SHOW_READING_TIME="${SHOW_READING_TIME:-true}"
# --- Backup Directory --- Added ---
BACKUP_DIR="${BACKUP_DIR:-backup}" # Default backup location
# --- Server Defaults --- Added for 'bssg.sh server' ---
BSSG_SERVER_PORT_DEFAULT="${BSSG_SERVER_PORT_DEFAULT:-8000}"
BSSG_SERVER_HOST_DEFAULT="${BSSG_SERVER_HOST_DEFAULT:-localhost}"
# Customization Defaults
CUSTOM_CSS="${CUSTOM_CSS:-}" # Default to empty string
# Define default colors here so utils.sh can use them if not overridden by config
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
RED='${RED:-\\033[0;31m}'
GREEN='${GREEN:-\\033[0;32m}'
YELLOW='${YELLOW:-\\033[0;33m}'
BLUE='${BLUE:-\\033[0;34m}' # Added Blue for print_info
NC='${NC:-\\033[0m}' # No Color
# --- Default Configuration Variables --- END ---
@ -106,25 +66,8 @@ if [ -f "$UTILS_SCRIPT" ]; then
exit 1
fi
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.
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}"; }
print_info() { echo "Info: $1"; }
# Print the critical error and exit
print_error "Utilities script not found at '$UTILS_SCRIPT'. Required by config_loader.sh."
# Fallback to standard error echo if utils not found, but this is critical
echo "Error: Utilities script not found at '$UTILS_SCRIPT'. Required by config_loader.sh." >&2
exit 1
fi
# --- Source Utilities --- END ---
@ -136,32 +79,17 @@ fi
if [ -f "$CONFIG_FILE" ]; then
# shellcheck source=/dev/null disable=SC1090,SC1091
source "$CONFIG_FILE"
print_success "Default configuration loaded from $CONFIG_FILE"
echo -e "${GREEN}Configuration loaded from $CONFIG_FILE${NC}"
else
print_warning "Default configuration file '$CONFIG_FILE' not found, using defaults."
print_warning "Configuration file '$CONFIG_FILE' not found, using defaults."
fi
# Now, handle the override configuration file
# Prioritize the --config command line argument if provided
if [ -n "$CMD_LINE_CONFIG_FILE" ]; then
if [ -f "$CMD_LINE_CONFIG_FILE" ]; then
# shellcheck source=/dev/null disable=SC1090,SC1091
source "$CMD_LINE_CONFIG_FILE"
print_success "Command-line configuration loaded from ${CMD_LINE_CONFIG_FILE}"
else
print_error "Specified configuration file '${CMD_LINE_CONFIG_FILE}' not found."
exit 1
fi
else
# If --config was not used, check for the default local override file
LOCAL_CONFIG_OVERRIDE="${CONFIG_FILE}.local"
if [ -f "$LOCAL_CONFIG_OVERRIDE" ]; then
# shellcheck source=/dev/null disable=SC1090,SC1091
source "$LOCAL_CONFIG_OVERRIDE"
print_success "Local configuration loaded from ${LOCAL_CONFIG_OVERRIDE}"
# else
# No local config file found, which is normal. No message needed.
fi
# Check for local override config file (relative to the main config file)
LOCAL_CONFIG_OVERRIDE="${CONFIG_FILE}.local"
if [ -f "$LOCAL_CONFIG_OVERRIDE" ]; then
# shellcheck source=/dev/null disable=SC1090,SC1091
source "$LOCAL_CONFIG_OVERRIDE"
print_success "Local configuration loaded from ${LOCAL_CONFIG_OVERRIDE}"
fi
@ -208,7 +136,7 @@ export LOCAL_CONFIG_FILE # Export it for other scripts
# After all configs are sourced, expand ~ in relevant paths before exporting.
# This ensures scripts use the resolved paths, even if config stores portable '~'.
print_info "Expanding tilde (~) in configuration paths..."
PATHS_TO_EXPAND=("SRC_DIR" "PAGES_DIR" "DRAFTS_DIR" "OUTPUT_DIR" "TEMPLATES_DIR" "THEMES_DIR" "STATIC_DIR" "BACKUP_DIR" "CACHE_DIR") # Added CACHE_DIR
PATHS_TO_EXPAND=("SRC_DIR" "PAGES_DIR" "DRAFTS_DIR" "OUTPUT_DIR" "TEMPLATES_DIR" "THEMES_DIR" "STATIC_DIR" "BACKUP_DIR") # Added BACKUP_DIR
for var_name in "${PATHS_TO_EXPAND[@]}"; do
# Get the current value using indirect reference
current_value="${!var_name}"
@ -231,79 +159,6 @@ 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 ---
# --- Read Version from root VERSION file --- START ---
# BSSG root is two levels up from config_loader.sh (scripts/build -> scripts -> root)
BSSG_ROOT="$( cd "${CONFIG_LOADER_DIR}/../.." &> /dev/null && pwd )"
if [ -f "${BSSG_ROOT}/VERSION" ]; then
VERSION="$(cat "${BSSG_ROOT}/VERSION")"
else
VERSION="dev"
fi
export VERSION
# --- Read Version from root VERSION file --- END ---
# --- Export All Variables --- START ---
# Define the list of configuration variables relevant for hashing/exporting
@ -311,24 +166,15 @@ export VERSION
# 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 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 RAM_MODE_INCREMENTAL RAM_MODE_FREE_CONTENT_AFTER_INDEX SITE_LANG LOCALE_DIR PAGES_DIR MARKDOWN_PROCESSOR
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 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
ARCHIVES_LIST_ALL_POSTS
ENABLE_RELATED_POSTS RELATED_POSTS_COUNT
PRECOMPRESS_ASSETS
SHOW_HEADER_MENU SHOW_INDEX_DESCRIPTIONS GENERATE_EXCERPT SHOW_READING_TIME
ENABLE_ROBOTS ROBOTS_CONTENT
ENABLE_TAG_RSS
BACKUP_DIR
# Add any other custom config variables here if needed
BSSG_SERVER_PORT_DEFAULT BSSG_SERVER_HOST_DEFAULT # Server defaults
)
# Convert array to space-separated string for export
@ -349,24 +195,14 @@ 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
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 BUILD_MODE
export RAM_MODE_INCREMENTAL
export RAM_MODE_FREE_CONTENT_AFTER_INDEX
export SITE_LANG
export LOCALE_DIR
export PAGES_DIR
@ -380,30 +216,7 @@ 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
export DEPLOY_SCRIPT
export ARCHIVES_LIST_ALL_POSTS
export ENABLE_RELATED_POSTS
export RELATED_POSTS_COUNT
export PRECOMPRESS_ASSETS
export SHOW_HEADER_MENU
export SHOW_INDEX_DESCRIPTIONS
export GENERATE_EXCERPT
export SHOW_READING_TIME
export ENABLE_ROBOTS
export ROBOTS_CONTENT
# Server defaults export
export BSSG_SERVER_PORT_DEFAULT
export BSSG_SERVER_HOST_DEFAULT
# Export colors too, as they might be customized in config and needed by scripts
export RED GREEN YELLOW BLUE NC
# Export ALL MSG_* locale variables explicitly
# These are generally NOT included in BSSG_CONFIG_VARS as they don't affect the config hash directly,
@ -423,9 +236,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 ---
# --- 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 ---
# --- Export All Variables --- END ---

View file

@ -14,41 +14,10 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s
parse_metadata() {
local file="$1"
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 frontmatter
if [[ -n "${BSSG_RAM_PARSED_FM_CACHE[$file]+_}" ]]; then
frontmatter="${BSSG_RAM_PARSED_FM_CACHE[$file]}"
else
local file_content
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; }
')
BSSG_RAM_PARSED_FM_CACHE["$file"]="$frontmatter"
fi
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
@ -99,20 +68,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
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 ! $ram_mode_active && [ ! -f "$file" ]; then
if [ ! -f "$file" ]; then
echo "ERROR_FILE_NOT_FOUND"
return 1
fi
@ -121,7 +81,7 @@ extract_metadata() {
local frontmatter_changed=false
# Check if cache exists and is newer than the source file
if ! $ram_mode_active && [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$metadata_cache_file" ] && [ "$metadata_cache_file" -nt "$file" ]; then
if [ "${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
@ -131,46 +91,30 @@ extract_metadata() {
fi
# If we're here, we need to parse the file
local title="" date="" lastmod="" tags="" slug="" image="" image_caption="" description="" author_name="" author_email=""
local title="" date="" lastmod="" tags="" slug="" image="" image_caption="" description=""
# Check file type and parse accordingly
if [[ "$file" == *.html ]]; then
# 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.
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>[^<]*</title>' 2>/dev/null | sed -e 's/<title>//' -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>[^<]*</title>' "$file" 2>/dev/null | sed -e 's/<title>//' -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
title=$(grep -m 1 -o '<title>[^<]*</title>' "$file" 2>/dev/null | sed -e 's/<title>//' -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/')
# Note: Excerpt generation (fallback for description) might not work well for HTML
elif [[ "$file" == *.md ]]; then
# Parse YAML frontmatter for Markdown files
# Use a shared awk parser for both disk and RAM paths.
# Use awk with a here document for reliable script passing
# Run awk and read results
local parsed_data
local awk_frontmatter_parser
awk_frontmatter_parser=$(cat <<'EOF'
parsed_data=$(awk -f - "$file" <<'EOF'
BEGIN {
in_fm = 0;
found_fm = 0;
@ -178,7 +122,6 @@ 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; }
@ -211,19 +154,12 @@ 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["author_name"] "|" vars["author_email"];
vars["image_caption"] "|" vars["description"];
}
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"
IFS='|' read -r title date lastmod tags slug image image_caption description <<< "$parsed_data"
else
echo "Warning: Unknown file type '$file' for metadata extraction." >&2
@ -241,33 +177,21 @@ EOF
if [ -z "$lastmod" ]; then
lastmod="$date"
fi
if [ -z "$slug" ]; then
if [ -z "$slug" ]; then
slug=$(generate_slug "$title")
else
slug=$(generate_slug "$slug")
fi
if [ -z "$description" ] && [ "${GENERATE_EXCERPT:-true}" = "true" ]; then
if [ -z "$description" ]; then
# Generate excerpt only if description is missing
# The excerpt is already sanitized and HTML-escaped plain text
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|$author_name|$author_email"
local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description"
# Check if there was a previous metadata file and compare
if ! $ram_mode_active && [ -f "$metadata_cache_file" ]; then
if [ -f "$metadata_cache_file" ]; then
local old_metadata=$(cat "$metadata_cache_file")
if [ "$old_metadata" != "$new_metadata" ]; then
frontmatter_changed=true
@ -275,15 +199,13 @@ EOF
fi
# Store all metadata in one write operation
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
lock_file "$metadata_cache_file"
mkdir -p "$(dirname "$metadata_cache_file")"
echo "$new_metadata" > "$metadata_cache_file"
unlock_file "$metadata_cache_file"
# If frontmatter has changed, update the marker file's timestamp
if ! $ram_mode_active && $frontmatter_changed; then
if $frontmatter_changed; then
touch "$frontmatter_changes_marker"
fi
@ -296,30 +218,17 @@ generate_excerpt() {
local file="$1"
local max_length="${2:-160}" # Default to 160 characters
local raw_content_stream
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
# 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)
# 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)
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
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")
else
# No valid frontmatter, stream the whole file
raw_content_stream=$(cat "$file")
fi
# Sanitize and extract the first non-empty paragraph/line
@ -399,19 +308,26 @@ 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=$(printf '%s' "$preprocessed_content" | awk '
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="$content"
preprocessed_content=$(cat "$temp_file")
rm "$temp_file"
fi
# Ensure MARKDOWN_PL_PATH is set and executable
@ -434,4 +350,4 @@ convert_markdown_to_html() {
return 0
}
# --- Content Functions --- END ---
# --- Content Functions --- END ---

2
scripts/build/deps.sh Normal file → Executable file
View file

@ -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 && { read -r _version < <(parallel -V 2>/dev/null ) && [[ "${_version:0:3}" = "GNU" ]]; }; then
elif command -v parallel > /dev/null 2>&1; then
echo -e "${GREEN}GNU parallel found! Using parallel processing.${NC}"
export HAS_PARALLEL=true
else

View file

@ -14,336 +14,12 @@ 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\}\}/""}
year_header=${year_header//\{\{twitter_card\}\}/"summary"}
year_header=${year_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
year_header=${year_header//\{\{canonical\}\}/}
year_header=${year_header//\{\{featured_image_preload\}\}/}
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\}\}/""}
month_header=${month_header//\{\{twitter_card\}\}/"summary"}
month_header=${month_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
month_header=${month_header//\{\{canonical\}\}/}
month_header=${month_header//\{\{featured_image_preload\}\}/}
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>
<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
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" loading="lazy" />
</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() {
if [ "${RAM_MODE_INCREMENTAL:-false}" = true ] && [ "${BSSG_POSTS_PROCESSED_COUNT:-1}" -eq 0 ] && [ "${FORCE_REBUILD:-false}" != true ] && [ "${BSSG_CONFIG_CHANGED_STATUS:-0}" -eq 0 ]; then
echo -e "${GREEN}Archive pages appear up to date, skipping.${NC}"
echo -e "${GREEN}Archive pages processed!${NC}"
return 0
fi
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\}\}/""}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
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"
local archives_index_page="$OUTPUT_DIR/archives/index.html"
local rebuild_reason=""
# --- Core Rebuild Reasons ---
# 1. Force rebuild flag
if [ "$FORCE_REBUILD" = true ]; then
rebuild_reason="Force rebuild flag set."
@ -365,28 +41,15 @@ _check_archive_index_rebuild_needed() {
if (( header_time > output_time )) || (( footer_time > output_time )); then
rebuild_reason="Header or footer template changed."
fi
# 5. Explicit rebuild needed based on month count comparison
elif [ "${ARCHIVE_INDEX_NEEDS_REBUILD:-false}" = true ]; then
rebuild_reason="Archive month counts changed (detected by identify_affected_archive_months)."
fi
# --- Content-based Rebuild Reasons ---
# If no core reason found yet, check if content changed and if it affects the main index page.
if [ -z "$rebuild_reason" ]; then
if [ -n "$AFFECTED_ARCHIVE_MONTHS" ]; then # Check if any month had post changes
if [ "${ARCHIVES_LIST_ALL_POSTS:-false}" = true ]; then
# If listing all posts, ANY change in affected months requires main index rebuild
rebuild_reason="List all posts enabled and archive content changed."
elif [ "${ARCHIVE_INDEX_NEEDS_REBUILD:-false}" = true ]; then
# If *not* listing all posts, only rebuild main index if month *counts* changed
rebuild_reason="Archive month counts changed."
fi
fi
fi
# --- End Content-based Check ---
if [ -n "$rebuild_reason" ]; then
echo -e "${YELLOW}Main archive index rebuild needed: $rebuild_reason${NC}" >&2 # Debug
return 0 # Needs rebuild
else
# No message here - generate_archive_pages will print the skipping message if needed.
return 1 # No rebuild needed
fi
}
@ -416,14 +79,10 @@ _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\}\}/""}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# 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"}
@ -440,7 +99,7 @@ _generate_main_archive_index() {
echo "<h1>${MSG_ARCHIVES:-"Archives"}</h1>"
echo "<div class=\"archives-list year-list\">"
# Loop through years (Existing logic for Year/Month links)
# Loop through years
echo "$unique_years" | while IFS= read -r year; do
[ -z "$year" ] && continue
@ -448,8 +107,7 @@ _generate_main_archive_index() {
year_url=$(fix_url "/archives/$year/")
echo " <h2><a href=\"$year_url\">$year</a></h2>"
# Changed class to support potential block layout for month + posts
echo " <ul class=\"month-list-detailed\">"
echo " <ul class=\"month-list-inline\">"
# Get unique months for this year, sorted descending by month number
local months_in_year=""
@ -457,16 +115,14 @@ _generate_main_archive_index() {
months_in_year=$(grep "^$year|" "$archive_index_file" 2>/dev/null | cut -d'|' -f2,3 | sort -t'|' -k1,1nr | uniq)
fi
# Add month links and potentially post lists
# Add month links
echo "$months_in_year" | while IFS= read -r month_line; do
local month month_name
# IMPORTANT: month is the numeric month (1-12) from the index
IFS='|' read -r month month_name <<< "$month_line"
[ -z "$month" ] && continue
local month_post_count=0
if [ -f "$archive_index_file" ] && [ -s "$archive_index_file" ]; then
# Use the numeric month for grep
month_post_count=$(grep -c "^$year|$month|" "$archive_index_file" 2>/dev/null || echo 0)
fi
@ -476,56 +132,13 @@ _generate_main_archive_index() {
local month_url
month_url=$(fix_url "/archives/$year/$month_idx_formatted/")
# Start the list item for the month
echo " <li>"
# Print the month link itself
echo " <a href=\"$month_url\">$current_month_name ($month_post_count)</a>"
# --- START: Add nested post list if configured ---
if [ "${ARCHIVES_LIST_ALL_POSTS:-false}" = true ] && [ "$month_post_count" -gt 0 ]; then
echo " <ul class=\"post-list-condensed-inline\">" # Nested list for posts
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 _ _ _ 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]}"
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) # Fallback
fi
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}"
post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')"
post_url=$(fix_url "$post_url") # Use fix_url for BASE_URL prefixing if needed
# Extract just the date part (YYYY-MM-DD)
local display_date=$(echo "$date" | cut -d' ' -f1)
echo " <li><a href=\"$post_url\">[$display_date] $title</a></li>"
done
echo " </ul>" # Close nested post list
fi
# --- END: Add nested post list ---
# Close the list item for the month
echo " </li>"
echo " <li><a href=\"$month_url\">$current_month_name ($month_post_count)</a></li>"
done
echo " </ul>" # End of month-list-detailed
echo " </ul>"
done
echo "</div>" # End of year-list div
echo "</div>"
echo "$footer_content"
} > "$archives_index_page"
@ -558,10 +171,6 @@ _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//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# 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"}
@ -638,7 +247,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"}
@ -650,10 +259,6 @@ 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//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# 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"}
@ -670,7 +275,7 @@ process_single_month() {
echo "<div class=\"posts-list\">"
# 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 author_name author_email; do
grep "^$year|$month_num|" "$archive_index_file" 2>/dev/null | while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description; do
# --- Start: Card Generation Logic (copied from generate_tags.sh) ---
local post_url post_year post_month post_day url_path
@ -702,14 +307,11 @@ 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
<article>
<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>
<h3><a href="${post_url}">$title</a></h3>
<div class="meta">${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date</div>
EOF
if [ -n "$image" ]; then
@ -720,7 +322,7 @@ EOF
cat << EOF
<figure class="featured-image tag-image">
<a href="${post_url}">
<img src="$image_url" alt="$alt_text" loading="lazy" />
<img src="$image_url" alt="$alt_text" />
</a>
<figcaption>$figcaption_content</figcaption>
</figure>
@ -767,11 +369,6 @@ _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"
@ -779,7 +376,6 @@ generate_archive_pages() {
# Check if the archive index file exists (needed for any processing)
if [ ! -f "$archive_index_file" ]; then
echo -e "${YELLOW}Warning: Archive index file not found at '$archive_index_file'. Skipping archive generation.${NC}"
_clean_stale_archive_output_dirs
return 0
fi
@ -796,34 +392,8 @@ generate_archive_pages() {
AFFECTED_ARCHIVE_MONTHS=$(echo "$AFFECTED_ARCHIVE_MONTHS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$AFFECTED_ARCHIVE_MONTHS" ]; then
# If no per-file changes, check if config/templates changed — if so, rebuild ALL months
local global_rebuild=false
if [ "$FORCE_REBUILD" = true ]; then
global_rebuild=true
elif [ "$BSSG_CONFIG_CHANGED_STATUS" -eq 0 ]; then
global_rebuild=true
else
local archive_output_time
local first_monthly_page
first_monthly_page=$(awk -F'|' '!seen[$1"|"$2]++ {print $1"/"$2; exit}' "$archive_index_file" 2>/dev/null)
if [ -n "$first_monthly_page" ]; then
archive_output_time=$(get_file_mtime "${OUTPUT_DIR:-output}/archives/${first_monthly_page}/index.html" 2>/dev/null)
if (( ${BSSG_MAX_TEMPLATE_LOCALE_TIME:-0} > archive_output_time )); then
global_rebuild=true
fi
fi
fi
if $global_rebuild; then
local all_months
all_months=$(awk -F'|' '!seen[$1"|"$2]++ {printf "%s|%02d ", $1, $2}' "$archive_index_file" 2>/dev/null)
AFFECTED_ARCHIVE_MONTHS=$(echo "$all_months" | sed 's/[[:space:]]*$//')
echo -e "${YELLOW}Config or templates changed; regenerating all archive months.${NC}"
else
echo -e "${GREEN}No affected archive months found. Skipping monthly/yearly page generation.${NC}"
_clean_stale_archive_output_dirs
return 0
fi
echo -e "${GREEN}No affected archive months found. Skipping monthly/yearly page generation.${NC}"
return 0
fi
echo "Affected months needing update: $AFFECTED_ARCHIVE_MONTHS" >&2 # Debug
@ -913,56 +483,7 @@ generate_archive_pages() {
echo "Affected monthly index pages updated." >&2 # Debug
echo -e "${GREEN}Archive page processing complete.${NC}"
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
_clean_stale_archive_output_dirs
fi
}
# Clean orphaned archive month directories after archive page generation.
# Removes output/archives/<year>/<month> dirs not present in the current archive index.
_clean_stale_archive_output_dirs() {
local archive_index="$CACHE_DIR/archive_index.txt"
local archives_out_dir="$OUTPUT_DIR/archives"
[ -d "$archives_out_dir" ] || return 0
local active_months=""
if [ -f "$archive_index" ]; then
active_months=$(awk -F'|' '{print $1 "|" $2}' "$archive_index" | sort -u)
fi
local year_dir
for year_dir in "$archives_out_dir"/*/; do
local year
year=$(basename "$year_dir")
[ "$year" = "*" ] && continue
[ ! -d "$year_dir" ] && continue
local month_dir
for month_dir in "$year_dir"*/; do
local month_num
month_num=$(basename "$month_dir")
[ "$month_num" = "*" ] && continue
[ ! -d "$month_dir" ] && continue
local key="${year}|${month_num}"
if [ -z "$active_months" ] || ! echo "$active_months" | grep -qxF "$key"; then
echo -e "Removing stale archive output: ${YELLOW}$year/$month_num${NC}"
rm -rf "$archives_out_dir/$year/$month_num"
fi
done
# Remove year dir if it has no remaining month subdirs
local has_months=false
local d
for d in "$year_dir"*/; do
[ "$d" = "$year_dir*/" ] && continue
has_months=true
break
done
if ! $has_months; then
echo -e "Removing stale archive year output: ${YELLOW}$year${NC}"
rm -rf "$archives_out_dir/$year"
fi
done
}
# Make the function available for sourcing
export -f generate_archive_pages
export -f generate_archive_pages

View file

@ -1,784 +0,0 @@
#!/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_ram() {
if [ "${RAM_MODE_INCREMENTAL:-false}" = true ] && [ "${BSSG_POSTS_PROCESSED_COUNT:-1}" -eq 0 ] && [ "${FORCE_REBUILD:-false}" != true ] && [ "${BSSG_CONFIG_CHANGED_STATUS:-0}" -eq 0 ]; then
echo -e "${GREEN}Author pages appear up to date, skipping.${NC}"
echo -e "${GREEN}Author pages processed!${NC}"
return 0
fi
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"
# Collect sorted author slugs for parallel processing
local sorted_author_slugs=()
while IFS= read -r slug; do
sorted_author_slugs+=("$slug")
done < <(printf '%s\n' "${!author_name_by_slug[@]}" | sort)
local author_count="${#sorted_author_slugs[@]}"
if [ "$author_count" -eq 0 ]; then
echo -e "${YELLOW}No authors found. Skipping author page generation.${NC}"
return 0
fi
# Per-author incremental check: skip authors whose output is newer than all their posts
if [ "${RAM_MODE_INCREMENTAL:-false}" = true ] && [ "${BSSG_POSTS_PROCESSED_COUNT:-1}" -gt 0 ]; then
local -a _filtered_slugs=()
local _slug _auth_out _max_mtime _line _slug_from_auth
for _slug in "${sorted_author_slugs[@]}"; do
_auth_out="$OUTPUT_DIR/authors/$_slug/index.html"
if [ ! -f "$_auth_out" ]; then
_filtered_slugs+=("$_slug")
continue
fi
if [ -n "$BSSG_MAX_TEMPLATE_LOCALE_TIME" ] && [ "$BSSG_MAX_TEMPLATE_LOCALE_TIME" -gt "$(get_file_mtime "$_auth_out")" ]; then
_filtered_slugs+=("$_slug")
continue
fi
_max_mtime=0
while IFS= read -r _line; do
[ -z "$_line" ] && continue
_slug_from_auth=$(echo "$_line" | cut -d'|' -f8)
[ -z "$_slug_from_auth" ] && continue
local _src_path="${BSSG_RAM_SLUG_TO_SRC[$_slug_from_auth]:-}"
if [ -n "$_src_path" ]; then
local _file_mtime
_file_mtime=$(ram_mode_get_mtime "$_src_path")
[ "$_file_mtime" -gt "$_max_mtime" ] && _max_mtime="$_file_mtime"
fi
done <<< "${author_posts_by_slug[$_slug]:-}"
if [ "$_max_mtime" -gt "$(get_file_mtime "$_auth_out")" ]; then
_filtered_slugs+=("$_slug")
fi
done
local _filtered_count="${#_filtered_slugs[@]}"
local _skipped=$((author_count - _filtered_count))
if [ "$_skipped" -gt 0 ]; then
echo -e "Skipping ${YELLOW}$_skipped${NC} unchanged author pages."
fi
sorted_author_slugs=("${_filtered_slugs[@]}")
author_count="$_filtered_count"
if [ "$author_count" -eq 0 ]; then
echo -e "${GREEN}Author pages processed!${NC}"
return 0
fi
fi
# Define per-author page builder for parallel workers
_process_single_author_page_ram() {
local author_slug_key="$1"
local 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//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
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
}
# Round-robin parallel workers for author pages
local cores
cores=$(get_parallel_jobs)
if [ "$cores" -gt "$author_count" ]; then
cores="$author_count"
fi
if [ "$author_count" -gt 1 ] && [ "$cores" -gt 1 ]; then
echo -e "${YELLOW}Using shell parallel workers for $author_count RAM-mode author pages${NC}"
local worker_pids=()
local worker_idx
for ((worker_idx = 0; worker_idx < cores; worker_idx++)); do
(
local idx local_slug
for ((idx = worker_idx; idx < author_count; idx += cores)); do
local_slug="${sorted_author_slugs[$idx]}"
_process_single_author_page_ram "$local_slug"
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 author page processing failed.${NC}"
exit 1
fi
else
echo -e "${YELLOW}Using sequential processing for $author_count RAM-mode author pages${NC}"
local local_slug
for local_slug in "${sorted_author_slugs[@]}"; do
_process_single_author_page_ram "$local_slug"
done
fi
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//\{\{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\"}}"
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"
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+="<h1>${MSG_POSTS_BY:-Posts by} $author</h1>"$'\n'
# Add RSS link if enabled
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
# Add posts list
author_page_content+="<div class=\"posts-list\">"$'\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+="<article>"$'\n'
posts_html+=" <h2><a href=\"$post_url\">$post_title</a></h2>"$'\n'
posts_html+=" <div class=\"meta\">"$'\n'
posts_html+=" <time datetime=\"$post_date\">$formatted_date</time>"$'\n'
posts_html+=" </div>"$'\n'
if [ -n "$post_description" ]; then
posts_html+=" <p class=\"summary\">$post_description</p>"$'\n'
fi
if [ -n "$post_image" ]; then
posts_html+=" <div class=\"author-image\">"$'\n'
posts_html+=" <img src=\"$post_image\" alt=\"$post_image_caption\" loading=\"lazy\">"$'\n'
posts_html+=" </div>"$'\n'
fi
posts_html+="</article>"$'\n'
done < <(sort -t'|' -k5,5r "$author_data_file")
author_page_content+="$posts_html"
author_page_content+="</div>"$'\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\}\}/}
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 -->/}
# Add author RSS link if enabled
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
# 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\}\}/"<script type=\"application/ld+json\">$schema_json</script>"}
# 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+="<h1>${MSG_ALL_AUTHORS:-All Authors}</h1>"$'\n'
main_content+="<div class=\"tags-list\">"$'\n' # Reuse tags styling
while IFS='|' read -r author author_slug post_count; do
if [ -n "$author" ] && [ "$post_count" -gt 0 ]; then
main_content+=" <a href=\"$BASE_URL/authors/$author_slug/\">$author <span class=\"tag-count\">($post_count)</span></a>"$'\n'
fi
done < "${CACHE_DIR}/authors_with_counts.tmp"
main_content+="</div>"$'\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\}\}/}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# Remove the placeholder for the tag-specific RSS feed link in the main authors index
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
# 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\}\}/"<script type=\"application/ld+json\">$schema_json</script>"}
# 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}"
}

File diff suppressed because it is too large Load diff

View file

@ -10,354 +10,8 @@ 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() {
if [ "${RAM_MODE_INCREMENTAL:-false}" = true ] && [ "${BSSG_POSTS_PROCESSED_COUNT:-1}" -eq 0 ] && [ "${FORCE_REBUILD:-false}" != true ] && [ "${BSSG_CONFIG_CHANGED_STATUS:-0}" -eq 0 ]; then
echo -e "${GREEN}Index pages appear up to date, skipping.${NC}"
echo -e "${GREEN}Index pages generated!${NC}"
return 0
fi
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"
_process_single_index_page_ram() {
local current_page="$1"
local file_index_data
file_index_data=$(ram_mode_get_dataset "file_index")
local file_index_lines=()
mapfile -t file_index_lines < <(printf '%s\n' "$file_index_data" | awk 'NF')
local total_posts_orig="${#file_index_lines[@]}"
local total_pages=$(( (total_posts_orig + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE ))
[ "$total_pages" -eq 0 ] && total_pages=1
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 <<EOF
<script type="application/ld+json">
{
"@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"
}
}
</script>
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 <<EOF
<script type="application/ld+json">
{
"@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"
}
}
</script>
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\}\}/""}
page_header=${page_header//\{\{twitter_card\}\}/"summary"}
page_header=${page_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
page_header=${page_header//\{\{canonical\}\}/}
page_header=${page_header//\{\{featured_image_preload\}\}/}
local page_footer="$FOOTER_TEMPLATE"
page_footer=${page_footer//\{\{current_year\}\}/$(date +%Y)}
page_footer=${page_footer//\{\{author_name\}\}/"$AUTHOR_NAME"}
cat > "$output_file" <<EOF
$page_header
EOF
local index_file="${PAGES_DIR}/index.md"
local has_custom_index=false
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_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" <<EOF
$page_footer
EOF
return 0
fi
if [ "$total_posts_orig" -gt 0 ]; then
cat >> "$output_file" <<EOF
<h1>${MSG_LATEST_POSTS:-"Latest Posts"}</h1>
<div class="posts-list">
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" <<EOF
<article>
<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
local image_url="$image"
if [[ "$image" == /* ]]; then
image_url="${SITE_URL}${image}"
fi
cat >> "$output_file" <<EOF
<div class="featured-image index-image">
<a href="$(fix_url "$post_link")">
<img src="$image_url" alt="${image_caption:-$title}" title="${image_caption:-$title}" loading="lazy" />
</a>
</div>
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" <<EOF
<div class="post-content">
$html_content
</div>
EOF
fi
elif [ "${SHOW_INDEX_DESCRIPTIONS:-true}" = "true" ] && [ -n "$description" ]; then
cat >> "$output_file" <<EOF
<div class="summary">
$description
</div>
EOF
fi
cat >> "$output_file" <<EOF
</article>
EOF
done
cat >> "$output_file" <<EOF
</div> <!-- .posts-list -->
EOF
if [ "$total_pages" -gt 1 ]; then
cat >> "$output_file" <<EOF
<!-- Pagination -->
<div class="pagination">
EOF
if [ "$current_page" -gt 1 ]; then
local prev_page=$((current_page - 1))
local prev_url="/"
if [ $prev_page -ne 1 ]; then
prev_url="/page/$prev_page/"
fi
cat >> "$output_file" <<PAG_EOF
<a href="$(fix_url "$prev_url")" class="prev">&laquo; ${MSG_NEWER_POSTS:-Newer}</a>
PAG_EOF
fi
cat >> "$output_file" <<PAG_EOF
<span class="page-info">$(printf "${MSG_PAGE_INFO_TEMPLATE:-Page %d of %d}" "$current_page" "$total_pages")</span>
PAG_EOF
if [ "$current_page" -lt "$total_pages" ]; then
local next_page=$((current_page + 1))
cat >> "$output_file" <<PAG_EOF
<a href="$(fix_url "/page/$next_page/")" class="next">${MSG_OLDER_POSTS:-Older} &raquo;</a>
PAG_EOF
fi
cat >> "$output_file" <<EOF
</div>
EOF
fi
fi
cat >> "$output_file" <<EOF
$page_footer
EOF
}
local cores
cores=$(get_parallel_jobs)
if [ "$cores" -gt "$total_pages" ]; then
cores="$total_pages"
fi
if [ "$total_pages" -gt 1 ] && [ "$cores" -gt 1 ]; then
echo -e "${YELLOW}Using shell parallel workers for $total_pages RAM-mode index pages${NC}"
local worker_pids=()
local worker_idx
for ((worker_idx = 0; worker_idx < cores; worker_idx++)); do
(
local idx
for ((idx = worker_idx; idx < total_pages; idx += cores)); do
_process_single_index_page_ram "$((idx + 1))"
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 index page generation failed.${NC}"
exit 1
fi
else
echo -e "${YELLOW}Using sequential processing for $total_pages RAM-mode index pages${NC}"
local current_page
for ((current_page = 1; current_page <= total_pages; current_page++)); do
_process_single_index_page_ram "$current_page"
done
fi
echo -e "${GREEN}Index pages processed!${NC}"
}
# Generate main index page (homepage) and paginated pages
generate_index() {
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
_generate_index_ram
return $?
fi
echo -e "${YELLOW}Generating index pages...${NC}"
# Check if rebuild is needed (using function from cache.sh)
@ -421,7 +75,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
@ -489,10 +143,6 @@ 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//\{\{twitter_card\}\}/"summary"}
page_header=${page_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
page_header=${page_header//\{\{canonical\}\}/}
page_header=${page_header//\{\{featured_image_preload\}\}/}
# Replace placeholders in the footer
local page_footer="$FOOTER_TEMPLATE"
@ -540,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 author_name author_email; 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; do
# ... (rest of the post item generation logic remains the same) ...
if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ]; then
continue
@ -565,8 +215,8 @@ EOF
local post_link="/$formatted_path/"
cat >> "$output_file" << EOF
<article>
<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>
<h3><a href="$(fix_url "$post_link")">$title</a></h3>
<div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${AUTHOR_NAME:+" ${MSG_BY:-"by"} $AUTHOR_NAME"}</div>
EOF
if [ -n "$image" ]; then
local image_url="$image"
@ -579,79 +229,7 @@ EOF
</div>
EOF
fi
# 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 RAM preload first
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && 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; }
')
# Try to get content from cache first
elif [ -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.*>/,/<\/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
<div class="post-content">
$html_content
</div>
EOF
fi
elif [ "${SHOW_INDEX_DESCRIPTIONS:-true}" = "true" ] && [ -n "$description" ]; then
# Show just the description/excerpt (default behavior)
if [ -n "$description" ]; then
cat >> "$output_file" << EOF
<div class="summary">
$description
@ -716,8 +294,9 @@ EOF
# Use GNU parallel if available and beneficial
if [ "${HAS_PARALLEL:-false}" = true ] && [ "$total_pages" -gt 2 ] ; then
echo -e "${GREEN}Using GNU parallel to process index pages${NC}"
local cores
cores=$(get_parallel_jobs)
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
# Use all detected cores
local jobs=$cores
@ -726,12 +305,10 @@ 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 INDEX_SHOW_FULL_CONTENT
export SHOW_INDEX_DESCRIPTIONS
export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY
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 convert_markdown_to_html
export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url
# Ensure templates are exported
if [ -z "$HEADER_TEMPLATE" ] || [ -z "$FOOTER_TEMPLATE" ]; then
@ -755,4 +332,4 @@ EOF
}
# Make the function available for sourcing
export -f generate_index
export -f generate_index

View file

@ -24,24 +24,18 @@ convert_page() {
# IMPORTANT: Assumes CACHE_DIR, FORCE_REBUILD, PAGES_DIR, SITE_TITLE, SITE_DESCRIPTION, SITE_URL, AUTHOR_NAME are exported/available
local output_html_file="$output_base_path/index.html"
local ram_mode_active=false
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$input_file"; then
ram_mode_active=true
fi
# Check if the source file exists
if ! $ram_mode_active && [ ! -f "$input_file" ]; then
if [ ! -f "$input_file" ]; then
echo -e "${RED}Error: Source page '$input_file' not found${NC}" >&2
return 1
fi
# Skip if output file is newer than input file and no force rebuild
# Uses file_needs_rebuild from cache.sh
if ! $ram_mode_active || [ "${FORCE_REBUILD:-false}" != true ]; then
if ! file_needs_rebuild "$input_file" "$output_html_file"; then
echo -e "Skipping unchanged page: ${YELLOW}$(basename "$input_file")${NC}"
return 0
fi
if ! file_needs_rebuild "$input_file" "$output_html_file"; then
echo -e "Skipping unchanged page: ${YELLOW}$(basename "$input_file")${NC}"
return 0
fi
echo -e "Processing page: ${GREEN}$(basename "$input_file")${NC}"
@ -51,31 +45,21 @@ convert_page() {
if [[ "$input_file" == *.html ]]; then
# For HTML files, extract content between <body> tags (simple approach)
local html_source=""
if $ram_mode_active; then
html_source=$(ram_mode_get_content "$input_file")
else
html_source=$(cat "$input_file")
fi
html_content=$(printf '%s\n' "$html_source" | sed -n '/<body>/,/<\/body>/p' | sed '1d;$d')
html_content=$(sed -n '/<body>/,/<\/body>/p' "$input_file" | sed '1d;$d')
# We might not have raw content for reading time easily here
content=$(echo "$html_content" | sed 's/<[^>]*>//g') # Basic text extraction for reading time
else
# For markdown files, extract content after frontmatter
local source_stream=""
if $ram_mode_active; then
source_stream=$(ram_mode_get_content "$input_file")
local start_line=$(grep -n "^---$" "$input_file" | head -1 | cut -d: -f1)
local end_line=$(grep -n "^---$" "$input_file" | head -2 | tail -1 | cut -d: -f1)
if [[ -z "$start_line" || -z "$end_line" || ! $start_line -lt $end_line ]]; then
# No valid frontmatter found, use the whole file
content=$(cat "$input_file")
else
source_stream=$(cat "$input_file")
# Extract content after the second --- line
content=$(tail -n +$((end_line + 1)) "$input_file")
fi
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; }
')
# --- MODIFIED PART --- START ---
# Convert markdown content to HTML using the function from content.sh
@ -127,12 +111,6 @@ 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//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
# Add canonical link
local canonical_tag="<link rel=\"canonical\" href=\"${SITE_URL}${page_rel_url}\">"
header_content=${header_content//\{\{canonical\}\}/"$canonical_tag"}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# Assemble the final HTML
local final_html="${header_content}"
@ -184,7 +162,6 @@ process_single_page_file() {
# Call the modified convert_page function (defined above in this script)
convert_page "$file" "$output_path" "$title" "$date" "$slug"
update_content_hash "$file"
}
# --- Moved Function Definitions --- END ---
@ -201,13 +178,10 @@ process_all_pages() {
return 0
fi
echo -e "Checking ${GREEN}${#page_files[@]}${NC} pages for changes"
# Use mapfile -t to read sorted files into array (newline-separated, trailing newline stripped)
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
mapfile -t page_files < <(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort)
fi
mapfile -t page_files < <(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort)
local num_pages=${#page_files[@]}
if [ "$num_pages" -eq 0 ]; then
@ -216,47 +190,21 @@ process_all_pages() {
fi
echo -e "Found ${GREEN}$num_pages${NC} potential pages."
local ram_mode_active=false
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
ram_mode_active=true
fi
# RAM mode keeps source content only in-process (bash arrays).
# GNU parallel spawns fresh shells that cannot access those arrays.
if $ram_mode_active; then
if [ "$num_pages" -gt 1 ]; then
echo -e "${YELLOW}Using shell parallel workers for $num_pages RAM-mode pages${NC}"
local cores
cores=$(get_parallel_jobs)
{
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}"
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
elif [ "${HAS_PARALLEL:-false}" = true ]; then
if [ "${HAS_PARALLEL:-false}" = true ]; then
echo -e "${GREEN}Using GNU parallel to generate pages${NC}"
# Determine number of cores
local cores
cores=$(get_parallel_jobs)
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
# Export functions needed by the parallel process and its children
export -f convert_page process_single_page_file
# Export necessary dependencies from sourced scripts
export -f calculate_reading_time file_needs_rebuild convert_markdown_to_html parse_metadata generate_slug
export -f common_rebuild_check config_has_changed # from cache.sh
export -f portable_md5sum get_file_mtime format_date fix_url update_content_hash # from utils.sh and cache.sh
export -f portable_md5sum get_file_mtime format_date fix_url # from utils.sh
# Export necessary variables for cache checks and template paths
export OUTPUT_DIR CACHE_DIR TEMPLATES_DIR THEME LOCALE_DIR SITE_LANG FORCE_REBUILD HEADER_TEMPLATE FOOTER_TEMPLATE
export CONFIG_HASH_FILE # Export path to hash file
@ -268,7 +216,6 @@ 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
@ -276,4 +223,4 @@ process_all_pages() {
echo -e "${GREEN}Static page processing complete!${NC}"
}
# --- Page Generation Functions --- END ---
# --- Page Generation Functions --- END ---

View file

@ -11,68 +11,9 @@ 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 ---
declare -gA BSSG_POST_ISO8601_CACHE=()
format_iso8601_post_date() {
local input_dt="$1"
local iso_dt=""
if [ -z "$input_dt" ]; then
echo ""
return
fi
local cache_key="${TIMEZONE:-local}|${input_dt}"
if [[ "$(declare -p BSSG_POST_ISO8601_CACHE 2>/dev/null || true)" != "declare -A"* ]]; then
unset BSSG_POST_ISO8601_CACHE 2>/dev/null || true
declare -gA BSSG_POST_ISO8601_CACHE=()
fi
if [[ -n "${BSSG_POST_ISO8601_CACHE[$cache_key]+_}" ]]; then
echo "${BSSG_POST_ISO8601_CACHE[$cache_key]}"
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
if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == *"bsd"* ]]; then
# 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
# GNU date -d handles many formats.
iso_dt=$(LC_ALL=C date -d "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
fi
fi
# Normalize timezone from +0000 to Z and +hhmm to +hh:mm.
if [ -n "$iso_dt" ] && [[ "$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
BSSG_POST_ISO8601_CACHE["$cache_key"]="$iso_dt"
echo "$iso_dt"
}
# Convert markdown to HTML
convert_markdown() {
local input_file="$1"
@ -85,91 +26,65 @@ convert_markdown() {
local image="$8"
local image_caption="$9"
local description="${10}"
local author_name="${11}"
local author_email="${12}"
local skip_rebuild_check="${13:-false}"
local content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$input_file")"
local output_html_file="$output_base_path/index.html"
local ram_mode_active=false
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$input_file"; then
ram_mode_active=true
fi
# Check if the source file exists
if ! $ram_mode_active && [ ! -f "$input_file" ]; then
if [ ! -f "$input_file" ]; then
echo -e "${RED}Error: Source file '$input_file' not found${NC}" >&2
return 1
fi
# Skip if output file is newer than input file and no force rebuild.
# When callers already prefiltered rebuild candidates, this check can be skipped.
if [ "$skip_rebuild_check" != true ]; then
if ! file_needs_rebuild "$input_file" "$output_html_file"; then
echo -e "Skipping unchanged file: ${YELLOW}$(basename "$input_file")${NC}"
return 0
fi
# Skip if output file is newer than input file and no force rebuild
if ! file_needs_rebuild "$input_file" "$output_html_file"; then
echo -e "Skipping unchanged file: ${YELLOW}$(basename "$input_file")${NC}"
return 0
fi
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
echo -e "Processing post: ${GREEN}$(basename "$input_file")${NC}"
fi
echo -e "Processing post: ${GREEN}$(basename "$input_file")${NC}"
# Extract body content (without frontmatter) in one awk pass.
# This is materially faster than line-by-line bash parsing on large markdown files.
# IMPORTANT: Assumes lock_file/unlock_file are sourced/available
lock_file "$content_cache_file"
# Try to get content from cache or file
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")
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 # Skip the closing --- line itself
fi
fi
if ! $in_frontmatter && $found_frontmatter; then
content+="$line"$'\n'
fi
done
} < "$input_file"
# If no frontmatter was found, use the whole file as content
if ! $found_frontmatter; then
content=$(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 == "---") {
has_frontmatter = 1
in_frontmatter = 1
next
}
}
{
if (has_frontmatter) {
if (in_frontmatter) {
if ($0 == "---") {
in_frontmatter = 0
}
next
}
print
} else {
print
}
}
')
# Cache the markdown content *without frontmatter* for potential use in RSS full content
if ! $ram_mode_active && [ -n "$CACHE_DIR" ] && [ -d "${CACHE_DIR}/content" ]; then
if [ -n "$CACHE_DIR" ] && [ -d "${CACHE_DIR}/content" ]; then
# Write the $content variable (which has frontmatter removed) to the cache file
lock_file "$content_cache_file"
printf '%s' "$content" > "$content_cache_file"
unlock_file "$content_cache_file"
fi
unlock_file "$content_cache_file"
# Calculate reading time
local reading_time=0
if [ "${SHOW_READING_TIME:-true}" = "true" ]; then
reading_time=$(calculate_reading_time "$content")
fi
local reading_time
reading_time=$(calculate_reading_time "$content")
# Convert markdown content to HTML (No HTML caching here anymore)
local html_content
@ -203,7 +118,7 @@ convert_markdown() {
[[ -z "$tag" ]] && continue
local tag_slug=$(echo "$tag" | tr '[:upper:]' '[:lower:]' | sed -e 's/ /-/g' -e 's/[^a-z0-9-]//g')
if [[ -n "$tag_slug" ]]; then # Ensure tag slug is not empty
tags_html+=" <a href=\"${SITE_URL:-}/tags/${tag_slug}/\" class=\"tag\">${tag}</a>"
tags_html+=$(printf ' <a href="%s/tags/%s/" class="tag">%s</a>' "${SITE_URL:-}" "$tag_slug" "$tag")
fi
done
tags_html+="</div>"
@ -253,34 +168,68 @@ 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=""
local iso_date=""
local iso_lastmod_date=""
if [ -n "$date" ]; then
iso_date=$(format_iso8601_post_date "$date")
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")
# Use date as fallback for lastmod, then format
iso_lastmod_date=$(format_iso8601_post_date "${lastmod:-$date}")
iso_lastmod_date=$(format_iso8601 "${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
now_iso=$(format_iso8601_post_date "now")
local now_iso=$(format_iso8601 "now")
iso_date="$now_iso"
iso_lastmod_date="$now_iso"
fi
@ -290,17 +239,13 @@ convert_markdown() {
image_url=$(fix_url "$image")
fi
# Create JSON-LD using post-specific author info
local post_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}"
local author_json
author_json=$(printf '{\n "@type": "Person",\n "name": "%s"\n }' "$post_author_name")
schema_json_ld=$(printf '<script type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "BlogPosting",\n "headline": "%s",\n "datePublished": "%s",\n "dateModified": "%s",\n "author": %s,\n "publisher": {\n "@type": "Organization",\n "name": "%s",\n "logo": {\n "@type": "ImageObject",\n "url": "%s/logo.png"\n }\n },\n "description": "%s",\n "mainEntityOfPage": {\n "@type": "WebPage",\n "@id": "%s%s"\n }%s\n}\n</script>' \
# Create JSON-LD
schema_json_ld=$(printf '<script type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "Article",\n "headline": "%s",\n "datePublished": "%s",\n "dateModified": "%s",\n "author": {\n "@type": "Person",\n "name": "%s",\n "email": "%s"\n },\n "publisher": {\n "@type": "Organization",\n "name": "%s",\n "logo": {\n "@type": "ImageObject",\n "url": "%s/logo.png"\n }\n },\n "description": "%s",\n "mainEntityOfPage": {\n "@type": "WebPage",\n "@id": "%s%s"\n }%s\n}\n</script>' \
"$(echo "$title" | sed 's/"/\"/g')" \
"$iso_date" \
"$iso_lastmod_date" \
"$author_json" \
"${AUTHOR_NAME:-Anonymous}" \
"${AUTHOR_EMAIL:-anonymous@example.com}" \
"$SITE_TITLE" \
"$SITE_URL" \
"$(echo "$meta_desc" | sed 's/"/\"/g')" \
@ -319,22 +264,9 @@ convert_markdown() {
local twitter_image_tag="<meta name=\"twitter:image\" content=\"$image_url\">"
header_content=${header_content//\{\{og_image\}\}/"$og_image_tag"}
header_content=${header_content//\{\{twitter_image\}\}/"$twitter_image_tag"}
header_content=${header_content//\{\{twitter_card\}\}/"summary_large_image"}
else
header_content=${header_content//\{\{og_image\}\}/}
header_content=${header_content//\{\{twitter_image\}\}/}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
fi
# Handle canonical link
local canonical_tag="<link rel=\"canonical\" href=\"${SITE_URL}${page_url}\">"
header_content=${header_content//\{\{canonical\}\}/"$canonical_tag"}
# Handle featured image preload for LCP optimization
if [ -n "$image_url" ]; then
header_content=${header_content//\{\{featured_image_preload\}\}/"<link rel=\"preload\" as=\"image\" href=\"$image_url\">"}
else
header_content=${header_content//\{\{featured_image_preload\}\}/}
fi
# Construct meta div (date, reading time, lastmod)
@ -347,75 +279,29 @@ convert_markdown() {
local formatted_date=$(format_date "$date" "$display_date_format")
local formatted_lastmod=$(format_date "$lastmod" "$display_date_format")
local post_meta="<div class=\"page-meta\">"
post_meta+="<p class=\"meta\">"
post_meta+="${MSG_PUBLISHED_ON:-Published on}: <time datetime=\"$iso_date\">$formatted_date</time> ${MSG_BY:-by} <strong>$display_author_name</strong>"
post_meta+="</p>"
if [ "${SHOW_READING_TIME:-true}" = "true" ]; then
local post_meta_reading_time
post_meta_reading_time=$(printf "${MSG_READING_TIME_TEMPLATE:-%d min read}" "$reading_time")
if [ "$formatted_date" != "$formatted_lastmod" ]; then
post_meta+="<p class=\"meta reading-time\">"
post_meta+="${MSG_UPDATED_ON:-Updated on}: <time datetime=\"$iso_lastmod_date\">$formatted_lastmod</time> &bull; $post_meta_reading_time"
post_meta+="</p>"
else
post_meta+="<p class=\"meta reading-time\">$post_meta_reading_time</p>"
fi
elif [ "$formatted_date" != "$formatted_lastmod" ]; then
post_meta+="<p class=\"meta\">"
post_meta+="${MSG_UPDATED_ON:-Updated on}: <time datetime=\"$iso_lastmod_date\">$formatted_lastmod</time>"
post_meta+="</p>"
local post_meta_reading_time
post_meta_reading_time=$(printf "${MSG_READING_TIME_TEMPLATE:-%d min read}" "$reading_time")
local post_meta="<div class=\"page-meta\">${MSG_PUBLISHED_ON:-Published on}: $formatted_date"
if [ "$formatted_date" != "$formatted_lastmod" ]; then
post_meta+=" &bull; ${MSG_UPDATED_ON:-Updated on}: $formatted_lastmod"
fi
post_meta+="</div>"
post_meta+=" &bull; $post_meta_reading_time</div>"
# Construct featured image HTML
local image_html=""
if [ -n "$image" ]; then
local alt_text="${image_caption:-$title}"
image_html="<div class=\"featured-image\"><img src=\"$(fix_url "$image")\" alt=\"$alt_text\" fetchpriority=\"high\"><div class=\"image-caption\">${image_caption:-$title}</div></div>"
fi
# Generate related posts if enabled and tags exist
local related_posts_html=""
if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && [ -n "$tags" ]; then
# 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
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
image_html="<div class=\"featured-image\"><img src=\"$(fix_url "$image")\" alt=\"$alt_text\"><div class=\"image-caption\">${image_caption:-$title}</div></div>"
fi
# Construct article body
local final_html="${header_content}"
final_html+='<article class="post">'$'\n'
final_html+=" <h1>$title</h1>"$'\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+='</article>'$'\n'
final_html+=$(printf '<article class="post">\n <h1>%s</h1>\n%s\n%s\n%s\n%s\n</article>\n' "$title" "$post_meta" "$image_html" "$html_content" "$tags_html")
# 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\}\}/$post_author_name}
footer_content=${footer_content//\{\{author_name\}\}/${AUTHOR_NAME:-Anonymous}}
final_html+="${footer_content}"
@ -438,268 +324,150 @@ process_all_markdown_files() {
echo -e "${YELLOW}Processing markdown posts...${NC}"
local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt"
local modified_tags_list="${CACHE_DIR:-.bssg_cache}/modified_tags.list"
local modified_authors_list="${CACHE_DIR:-.bssg_cache}/modified_authors.list"
local file_index_prev="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt"
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
local modified_tags_list="${CACHE_DIR:-.bssg_cache}/modified_tags.list" # Define path for modified tags
local file_index_prev="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" # Path to previous index
if ! $ram_mode_active && [ ! -f "$file_index" ]; then
if [ ! -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=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
local total_file_count=$(wc -l < "$file_index")
if [ "$total_file_count" -eq 0 ]; then
BSSG_POSTS_PROCESSED_COUNT=0
export BSSG_POSTS_PROCESSED_COUNT
echo -e "${YELLOW}No posts found. Skipping post generation.${NC}"
echo -e "${YELLOW}No posts found in file index. Skipping post generation.${NC}"
return 0
fi
echo -e "Checking ${GREEN}$total_file_count${NC} potential posts listed in index."
local file_index_stream
if $ram_mode_active; then
file_index_stream=$(printf '%s\n' "$file_index_data" | awk 'NF')
else
file_index_stream=$(cat "$file_index")
fi
# --- Start Change: Clear previous modified tags list ---
echo "Clearing previous modified tags list: $modified_tags_list" >&2 # Debug message
rm -f "$modified_tags_list"
touch "$modified_tags_list" # Ensure file exists even if empty
# --- End Change ---
# --- Single pass: check rebuild, track tags, build processing list ---
# Pre-filter files that need rebuilding
local files_to_process_list=()
local files_to_process_count=0
local skipped_count=0
if $ram_mode_active; then
while IFS= read -r line; do
local file filename title date lastmod tags slug
IFS='|' read -r file filename title date lastmod tags slug _ <<< "$line"
if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then
continue
fi
if [ "${FORCE_REBUILD:-false}" = true ]; then
files_to_process_list+=("$line")
files_to_process_count=$((files_to_process_count + 1))
else
local year month day url_path output_html_file
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
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}"
output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html"
if [ -f "$output_html_file" ]; then
local input_mtime output_mtime
input_mtime=$(ram_mode_get_mtime "$file")
output_mtime=$(get_file_mtime "$output_html_file")
if [ "$input_mtime" -gt "$output_mtime" ]; then
files_to_process_list+=("$line")
files_to_process_count=$((files_to_process_count + 1))
else
echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}"
skipped_count=$((skipped_count + 1))
fi
else
files_to_process_list+=("$line")
files_to_process_count=$((files_to_process_count + 1))
fi
fi
done <<< "$file_index_stream"
else
local track_tags=false
local track_authors=false
local disk_prev_available=false
if [ -f "$file_index_prev" ]; then
disk_prev_available=true
fi
if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && ! $ram_mode_active; then
if $disk_prev_available; then
track_tags=true
rm -f "$modified_tags_list" "$modified_authors_list"
touch "$modified_tags_list"
touch "$modified_authors_list"
fi
fi
if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ] && $disk_prev_available; then
track_authors=true
if ! $track_tags; then
rm -f "$modified_authors_list"
touch "$modified_authors_list"
fi
fi
# Pre-pass: collect tags from deleted/changed/new posts to invalidate
# related-posts cache BEFORE the main scan so affected posts are found.
if $track_tags; then
local prev_data_for_diff=""
if [ -f "$file_index_prev" ]; then
prev_data_for_diff=$(cat "$file_index_prev")
fi
if [ -n "$prev_data_for_diff" ]; then
local rf rfilename rtags
while IFS='|' read -r rf rfilename _ _ _ rtags _; do
[ -z "$rf" ] && continue
if ! grep -q "^${rf}|" <<< "$file_index_stream"; then
if [ -n "$rtags" ]; then
echo "$rtags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . >> "$modified_tags_list"
fi
fi
done <<< "$prev_data_for_diff"
local cline cf ctags
while IFS= read -r cline; do
[ -z "$cline" ] && continue
cf=$(echo "$cline" | cut -d'|' -f1)
local prev_line
prev_line=$(grep "^${cf}|" <<< "$prev_data_for_diff")
if [ -z "$prev_line" ]; then
ctags=$(echo "$cline" | cut -d'|' -f6)
if [ -n "$ctags" ]; then
echo "$ctags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . >> "$modified_tags_list"
fi
elif [ "$cline" != "$prev_line" ]; then
ctags=$(echo "$cline" | cut -d'|' -f6)
local prev_tags
prev_tags=$(echo "$prev_line" | cut -d'|' -f6)
local combined="${prev_tags},${ctags}"
if [ -n "$combined" ]; then
echo "$combined" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . >> "$modified_tags_list"
fi
fi
done <<< "$file_index_stream"
local temp_sorted
temp_sorted=$(mktemp)
sort -u "$modified_tags_list" > "$temp_sorted" && mv "$temp_sorted" "$modified_tags_list"
if [ -s "$modified_tags_list" ]; then
if ! command -v invalidate_related_posts_cache_for_tags > /dev/null 2>&1; then
source "$(dirname "$0")/related_posts.sh" || { echo -e "${RED}Error: Failed to source related_posts.sh${NC}"; exit 1; }
fi
RELATED_POSTS_INVALIDATED_LIST="${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list"
> "$RELATED_POSTS_INVALIDATED_LIST"
invalidate_related_posts_cache_for_tags "$modified_tags_list" "$RELATED_POSTS_INVALIDATED_LIST"
export RELATED_POSTS_INVALIDATED_LIST
fi
if [ -f "$modified_authors_list" ]; then
temp_sorted=$(mktemp)
sort -u "$modified_authors_list" > "$temp_sorted" && mv "$temp_sorted" "$modified_authors_list"
fi
fi
fi
local line file filename title date lastmod tags slug image image_caption description author_name author_email
local year month day url_path output_html_file needs_rebuild
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"
if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then
continue
fi
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
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}"
output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html"
needs_rebuild=false
if $ram_mode_active && [ "${FORCE_REBUILD:-false}" = true ]; then
needs_rebuild=true
elif file_needs_rebuild "$file" "$output_html_file"; then
needs_rebuild=true
fi
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
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))
if $track_tags; then
local new_author="$author_name"
local old_author=""
if $use_disk_prev && [ -f "$file_index_prev" ]; then
old_author=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f11)
elif ! $use_disk_prev && [ -n "$file_index_prev_data" ]; then
old_author=$(grep "^${file}|" <<< "$file_index_prev_data" | cut -d'|' -f11)
fi
if [ -n "$old_author" ]; then
echo "$old_author" >> "$modified_authors_list"
fi
if [ -n "$new_author" ]; then
echo "$new_author" >> "$modified_authors_list"
fi
fi
else
echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}"
skipped_count=$((skipped_count + 1))
fi
done <<< "$file_index_stream"
if $track_tags && [ -f "$modified_authors_list" ]; then
local temp_sorted
temp_sorted=$(mktemp)
sort -u "$modified_authors_list" > "$temp_sorted" && mv "$temp_sorted" "$modified_authors_list"
fi
# 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
IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$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
# 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
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 $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) ---
# '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
# --- End Change ---
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"
# --- Start Change: Unique sort the modified tags list (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
# --- End Change ---
# Check if any files need processing
if [ $files_to_process_count -eq 0 ]; then
BSSG_POSTS_PROCESSED_COUNT=0
export BSSG_POSTS_PROCESSED_COUNT
echo -e "${GREEN}All $total_file_count posts are up to date.${NC}"
echo -e "${GREEN}Markdown posts processing complete!${NC}"
return 0
fi
if $ram_mode_active; then
echo -e "Building ${GREEN}$files_to_process_count${NC} posts (RAM mode full build)."
else
echo -e "Found ${GREEN}$files_to_process_count${NC} posts needing processing out of $total_file_count (Skipped: $skipped_count)."
fi
if $ram_mode_active && [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then
prepare_related_posts_ram_cache "${RELATED_POSTS_COUNT:-3}"
fi
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"
# Read the line from the argument variable
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"
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"
# No need for the basic check here, already done in pre-filter
@ -718,61 +486,21 @@ 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 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
# 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
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
else
update_content_hash "$file"
fi
}
# Use GNU parallel if available
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
if [ "${HAS_PARALLEL:-false}" = true ]; then
echo -e "${GREEN}Using GNU parallel to process $files_to_process_count posts${NC}"
local cores
cores=$(get_parallel_jobs)
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
# Export functions and variables needed by parallel tasks
# Note: We export the new process function
@ -780,15 +508,11 @@ 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 update_content_hash # Used by cache funcs
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 SHOW_READING_TIME
export FEDIVERSE_CREATOR AUTHOR_FEDIVERSE_CREATORS_SERIALIZED
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; }
@ -801,90 +525,9 @@ process_all_markdown_files() {
done
fi
BSSG_POSTS_PROCESSED_COUNT=$files_to_process_count
export BSSG_POSTS_PROCESSED_COUNT
echo -e "${GREEN}Markdown posts processing complete!${NC}"
}
cleanup_stale_post_output() {
local prev_index_data=""
local curr_index_data=""
local ram_mode_active=false
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
ram_mode_active=true
prev_index_data=$(ram_mode_get_dataset "file_index_prev")
curr_index_data=$(ram_mode_get_dataset "file_index")
else
local prev_file="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt"
local curr_file="${CACHE_DIR:-.bssg_cache}/file_index.txt"
if [ -f "$prev_file" ]; then
prev_index_data=$(cat "$prev_file")
fi
if [ -f "$curr_file" ]; then
curr_index_data=$(cat "$curr_file")
fi
fi
if [ -z "$prev_index_data" ]; then
echo -e "${YELLOW}No previous file index. Skipping stale output cleanup.${NC}"
return 0
fi
local curr_files=""
curr_files=$(printf '%s\n' "$curr_index_data" | awk -F'|' '{print $1}' | sort -u)
local deletions=0
while IFS= read -r prev_line; do
[ -z "$prev_line" ] && continue
local prev_file="${prev_line%%|*}"
[ -z "$prev_file" ] && continue
if ! printf '%s\n' "$curr_files" | grep -Fxq "$prev_file"; then
local rest="${prev_line#*|}"
local filename title date lastmod tags slug image
IFS='|' read -r filename title date lastmod tags slug image _discarded <<< "$rest"
if [ -z "$date" ] || [ -z "$slug" ]; then
continue
fi
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
continue
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 post_dir="${OUTPUT_DIR:-output}/$url_path"
if [ -d "$post_dir" ]; then
echo -e "${YELLOW}Removing stale output: $post_dir${NC}"
rm -rf "$post_dir"
# Prune empty parent dirs up to (but not including) OUTPUT_DIR.
# rmdir only succeeds on empty directories, so non-empty or
# important dirs are never touched regardless of URL format.
local _parent
_parent="$(dirname "$post_dir")"
while [ -n "$_parent" ] && [ "$_parent" != "${OUTPUT_DIR:-output}" ] && [ "$_parent" != "." ] && [ "$_parent" != "/" ]; do
rmdir "$_parent" 2>/dev/null || break
_parent="$(dirname "$_parent")"
done
deletions=$((deletions + 1))
fi
fi
done <<< "$prev_index_data"
if [ "$deletions" -gt 0 ]; then
echo -e "${GREEN}Cleaned up $deletions stale post output(s).${NC}"
fi
}
# --- Post Generation Functions --- END ---
# Make the main function available for sourcing

View file

@ -14,10 +14,6 @@ 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
@ -26,13 +22,13 @@ generate_pages_index() {
if [[ "${FORCE_REBUILD:-false}" == true ]]; then
should_rebuild=true
echo -e "${YELLOW}Forcing pages index rebuild (--force-rebuild).${NC}"
elif ! $ram_mode_active && [ ! -f "$secondary_pages_list_file" ]; then
elif [ ! -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 ! $ram_mode_active && { [ ! -f "$pages_index" ] || [ "$pages_index" -ot "$secondary_pages_list_file" ]; }; then
elif [ ! -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.
@ -51,9 +47,7 @@ generate_pages_index() {
# --- Read secondary pages from cache file --- START ---
local temp_secondary_pages=()
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
if [ -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)
@ -87,13 +81,15 @@ 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
local schema_json_ld=""
local tmp_schema=$(mktemp)
# Create CollectionPage schema
schema_json_ld=$(cat << EOF
cat > "$tmp_schema" << EOF
<script type="application/ld+json">
{
"@context": "https://schema.org",
@ -109,7 +105,12 @@ generate_pages_index() {
}
</script>
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"}
@ -117,10 +118,6 @@ EOF
# Remove image placeholders
header_content=${header_content//\{\{og_image\}\}/""}
header_content=${header_content//\{\{twitter_image\}\}/""}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# Replace placeholders in the footer
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
@ -138,7 +135,7 @@ EOF
IFS='|' read -r title url _ <<< "$page" # Ignore date for menu
cat >> "$pages_index" << EOF
<article>
<h2><a href="$url">$title</a></h2>
<h3><a href="$url">$title</a></h3>
</article>
EOF
done
@ -153,4 +150,4 @@ EOF
}
# Make function available for sourcing
export -f generate_pages_index
export -f generate_pages_index

View file

@ -13,659 +13,8 @@ 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 -gA BSSG_RAM_SLUG_TO_SRC=()
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 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=$(_rfc822_gmt_date "now")
exec 4> "$output_file" || return 1
printf '%s\n' \
'<?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>${escaped_feed_title}</title>" \
" <link>${feed_link}</link>" \
" <description>${escaped_feed_description}</description>" \
" <language>${SITE_LANG:-en}</language>" \
" <lastBuildDate>${channel_last_build_date}</lastBuildDate>" \
" <atom:link href=\"${feed_atom_link}\" rel=\"self\" type=\"application/rss+xml\" />" >&4
if [ -n "$rss_items_xml" ]; then
printf '%s' "$rss_items_xml" >&4
fi
printf '%s\n' '</channel>' '</rss>' >&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//<!-- bssg:tag_rss_link -->/<link rel="alternate" type="application/rss+xml" title="${SITE_TITLE} - Posts tagged with ${tag}" href="${SITE_URL}${tag_rss_rel_url}">}
else
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
fi
local schema_json_ld
schema_json_ld=$(cat <<EOF
<script type="application/ld+json">
{
"@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"
}
}
</script>
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 '<h1>%s: %s</h1>\n' "${MSG_TAG_PAGE_TITLE:-Posts tagged with}" "$tag" >&3
printf '<div class="posts-list">\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 '</div>\n' >&3
printf '<p><a href="%s/tags/">%s</a></p>\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() {
if [ "${RAM_MODE_INCREMENTAL:-false}" = true ] && [ "${BSSG_POSTS_PROCESSED_COUNT:-1}" -eq 0 ] && [ "${FORCE_REBUILD:-false}" != true ] && [ "${BSSG_CONFIG_CHANGED_STATUS:-0}" -eq 0 ]; then
echo -e "${GREEN}Tags index and tag pages appear up to date, skipping.${NC}"
echo -e "${GREEN}Tag pages processed!${NC}"
return 0
fi
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+=' <article>'$'\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
image_url=$(fix_url "$image")
alt_text="${image_caption:-$title}"
figcaption_content="${image_caption:-$title}"
article_html+=' <figure class="featured-image tag-image">'$'\n'
article_html+=" <a href=\"${SITE_URL}${post_link}\">"$'\n'
article_html+=" <img src=\"${image_url}\" alt=\"${alt_text}\" loading=\"lazy\" />"$'\n'
article_html+=' </a>'$'\n'
article_html+=" <figcaption>${figcaption_content}</figcaption>"$'\n'
article_html+=' </figure>'$'\n'
fi
if [ -n "$description" ]; then
article_html+=' <div class="summary">'$'\n'
article_html+=" ${description}"$'\n'
article_html+=' </div>'$'\n'
fi
article_html+=' </article>'$'\n'
BSSG_RAM_SLUG_TO_SRC["$slug"]="$file"
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\}\}/""}
header_base=${header_base//\{\{twitter_card\}\}/"summary"}
header_base=${header_base//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_base=${header_base//\{\{canonical\}\}/}
header_base=${header_base//\{\{featured_image_preload\}\}/}
BSSG_RAM_TAG_HEADER_BASE="$header_base"
local tag_count="${#sorted_tag_urls[@]}"
# Per-tag incremental check: skip tags whose output is newer than all their posts
if [ "${RAM_MODE_INCREMENTAL:-false}" = true ] && [ "${BSSG_POSTS_PROCESSED_COUNT:-1}" -gt 0 ]; then
local -a tags_to_process=()
local _tag_url _tag_out _max_mtime _slug
for _tag_url in "${sorted_tag_urls[@]}"; do
_tag_out="$OUTPUT_DIR/tags/$_tag_url/index.html"
if [ ! -f "$_tag_out" ]; then
tags_to_process+=("$_tag_url")
continue
fi
if [ -n "$BSSG_MAX_TEMPLATE_LOCALE_TIME" ] && [ "$BSSG_MAX_TEMPLATE_LOCALE_TIME" -gt "$(get_file_mtime "$_tag_out")" ]; then
tags_to_process+=("$_tag_url")
continue
fi
# Check if any post on this tag page has a newer source mtime than the output
_max_mtime=0
while IFS= read -r _slug; do
[ -z "$_slug" ] && continue
local _src_path="${BSSG_RAM_SLUG_TO_SRC[$_slug]:-}"
if [ -n "$_src_path" ]; then
local _file_mtime
_file_mtime=$(ram_mode_get_mtime "$_src_path")
[ "$_file_mtime" -gt "$_max_mtime" ] && _max_mtime="$_file_mtime"
fi
done <<< "${BSSG_RAM_TAG_POST_SLUGS_BY_SLUG[$_tag_url]:-}"
if [ "$_max_mtime" -gt "$(get_file_mtime "$_tag_out")" ]; then
tags_to_process+=("$_tag_url")
fi
done
local _filtered_count="${#tags_to_process[@]}"
local _skipped_tags=$((tag_count - _filtered_count))
if [ "$_skipped_tags" -gt 0 ]; then
echo -e "Skipping ${YELLOW}$_skipped_tags${NC} unchanged tag pages."
fi
sorted_tag_urls=("${tags_to_process[@]}")
tag_count="$_filtered_count"
if [ "$tag_count" -eq 0 ]; then
echo -e "${GREEN}Tag pages processed!${NC}"
return 0
fi
fi
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//<!-- bssg:tag_rss_link -->/}
local tags_schema_json
tags_schema_json=$(cat <<EOF
<script type="application/ld+json">
{
"@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"
}
}
</script>
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//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
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 '<h1>%s</h1>\n' "${MSG_ALL_TAGS:-All Tags}" >&5
printf '<div class="tags-list">\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 ' <a href="%s/tags/%s/">%s <span class="tag-count">(%s)</span></a>\n' "$SITE_URL" "$tag_url" "$tag" "$post_count" >&5
done
printf '</div>\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"
@ -681,7 +30,6 @@ generate_tag_pages() {
# Optionally create an empty index page? Or let it be absent? Let's ensure dir exists.
echo -e "${GREEN}Tag pages processed! (No tags found)${NC}"
echo -e "${GREEN}Generated tag list pages. (No tags found)${NC}"
_clean_stale_tag_output_dirs
return 0
fi
@ -723,9 +71,6 @@ generate_tag_pages() {
if [ "$force_rebuild_status" = true ]; then
proceed_with_generation=true
echo "Force rebuild enabled, proceeding with tag generation." >&2 # Debug
elif [ "${BSSG_CONFIG_CHANGED_STATUS:-1}" -eq 0 ]; then
proceed_with_generation=true
echo "Config changed, proceeding with tag generation." >&2 # Debug
elif [ "$latest_common_dep_time" -gt 0 ] && { [ ! -f "$main_tags_index_output" ] || (( $(get_file_mtime "$main_tags_index_output") < latest_common_dep_time )); }; then
# Common dependencies are newer than the main output (or main output missing)
proceed_with_generation=true
@ -744,7 +89,6 @@ generate_tag_pages() {
echo -e "${GREEN}Tags index, tag pages${NC}${ENABLE_TAG_RSS:+, and tag RSS feeds} appear up to date based on common dependencies and modified posts, skipping.${NC}"
echo -e "${GREEN}Tag pages processed!${NC}" # Keep consistent final message
echo -e "${GREEN}Generated tag list pages.${NC}" # Keep consistent final message
_clean_stale_tag_output_dirs
return 0
fi
# --- Simplified Global Check --- END ---
@ -840,9 +184,9 @@ generate_tag_pages() {
if [ -n "$tag" ]; then
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_rss_file="$OUTPUT_DIR/tags/$tag_url/rss.xml"
local tag_page_rel_url="/tags/${tag_url}/"
local tag_rss_rel_url="/tags/${tag_url}/${RSS_FILENAME:-rss.xml}"
local tag_rss_rel_url="/tags/${tag_url}/rss.xml"
local rebuild_html=false
local rebuild_rss=false
@ -941,10 +285,6 @@ EOF
# Remove image placeholders
header_content=${header_content//\{\{og_image\}\}/""}
header_content=${header_content//\{\{twitter_image\}\}/""}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# Replace placeholders in the footer
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
@ -967,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 author_name author_email
IFS='|' read -r _ _ title date lastmod filename slug image image_caption description author_name author_email <<< "$post_line"
local _ _ title date lastmod filename slug image image_caption description
IFS='|' read -r _ _ title date lastmod filename slug image image_caption description <<< "$post_line"
# Create slug-based URL path
local post_year post_month post_day
@ -993,17 +333,14 @@ 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 ---
cat >> "$tag_page_html_file" << EOF
<article>
<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>
<h3><a href="${SITE_URL}${post_link}">$title</a></h3>
<div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date</div>
EOF
if [ -n "$image" ]; then
@ -1013,7 +350,7 @@ EOF
cat >> "$tag_page_html_file" << EOF
<figure class="featured-image tag-image">
<a href="${SITE_URL}${post_link}">
<img src="$image_url" alt="$alt_text" loading="lazy" />
<img src="$image_url" alt="$alt_text" />
</a>
<figcaption>$figcaption_content</figcaption>
</figure>
@ -1056,9 +393,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|AuthorName|AuthorEmail
# IMPORTANT: tags_index.txt has format: Tag|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|Image|ImageCaption|PostDescription|OriginalFilePath
# We need to map this to the format expected by _generate_rss_feed:
# file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email
# file|filename|title|date|lastmod|tags|slug|image|image_caption|description
# We lack the original 'file' path and 'tags' string here. We can approximate.
local tag_post_data_tmp=$(mktemp)
@ -1067,8 +404,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 | author_name | author_email
print $6 "|" $6 "|" $3 "|" $4 "|" $5 "|" tag_val "|" $7 "|" $8 "|" $9 "|" $10 "|" $11 "|" $12
# file (placeholder) | filename | title | date | lastmod | tags | slug | image | image_caption | description
print $6 "|" $6 "|" $3 "|" $4 "|" $5 "|" tag_val "|" $7 "|" $8 "|" $9 "|" $10
}' > "$tag_post_data_tmp"
local tag_post_data=$(cat "$tag_post_data_tmp")
@ -1080,7 +417,6 @@ EOF
else
# Call the reusable function from generate_feeds.sh
# Ensure necessary vars like SITE_URL, SITE_LANG etc. are exported/available
# echo "DEBUG: In process_tag for '$tag', RSS_FILENAME='${RSS_FILENAME:-rss.xml}', tag_rss_file='${tag_rss_file}'" >&2 # DEBUG
_generate_rss_feed "$tag_rss_file" "$feed_title" "$feed_desc" "$feed_link_rel" "$feed_atom_link_rel" "$tag_post_data"
echo -e " Generated RSS feed for: ${GREEN}$tag${NC}"
fi
@ -1115,7 +451,7 @@ EOF
local tag tag_url
IFS='|' read -r tag tag_url <<< "$tag_line"
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_rss_file="$OUTPUT_DIR/tags/$tag_url/rss.xml"
local process_this_tag=false # Flag to decide if this tag needs processing
# --- Refined Check: Check if tag needs processing ---
@ -1153,8 +489,9 @@ 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
cores=$(get_parallel_jobs)
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 jobs=$cores # Use all cores for tags by default if parallel
# Export necessary functions and variables
@ -1281,10 +618,6 @@ 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//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# Replace placeholders in the footer
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
@ -1329,34 +662,6 @@ EOF
# --- Generate the main tags index page --- END ---
echo -e "${GREEN}Tag pages processed!${NC}"
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
_clean_stale_tag_output_dirs
fi
}
# Clean orphaned tag output directories after tag page generation.
# Removes output/tags/<slug> dirs not present in the current tags index.
_clean_stale_tag_output_dirs() {
local tags_index="$CACHE_DIR/tags_index.txt"
local tags_out_dir="$OUTPUT_DIR/tags"
[ -d "$tags_out_dir" ] || return 0
local active_slugs=""
if [ -f "$tags_index" ]; then
active_slugs=$(cut -d'|' -f2 "$tags_index" | sort -u)
fi
local tag_dir
for tag_dir in "$tags_out_dir"/*/; do
local slug
slug=$(basename "$tag_dir")
[ "$slug" = "*" ] && continue
if [ -z "$active_slugs" ] || ! echo "$active_slugs" | grep -qxF "$slug"; then
echo -e "Removing stale tag output: ${YELLOW}$slug${NC}"
rm -rf "$tags_out_dir/$slug"
fi
done
}
# Export the main function for the build script

View file

@ -15,79 +15,6 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s
# Global arrays (consider moving to main context if feasible)
declare -A file_index_data
# --- RAM-mode index computation helpers (output-only, no side effects) ---
# These echo their result to stdout. Used by main.sh for parallel index building.
_build_tags_index_ram_output() {
local file_index_data
file_index_data=$(ram_mode_get_dataset "file_index")
if [ -z "$file_index_data" ]; then
return 0
fi
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;
}
}
}'
}
_build_authors_index_ram_output() {
local file_index_data
file_index_data=$(ram_mode_get_dataset "file_index")
if [ -z "$file_index_data" ]; then
return 0
fi
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;
}'
}
_build_archive_index_ram_output() {
local file_index_data
file_index_data=$(ram_mode_get_dataset "file_index")
if [ -z "$file_index_data" ]; then
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}"
[[ -z "$month_name" ]] && month_name="$month"
echo "$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description|$author_name|$author_email"
done <<< "$file_index_data"
}
# --- Indexing Functions --- START ---
# Step 1: Build a raw index using centralized awk (fast extraction only)
@ -105,7 +32,6 @@ _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$/);
@ -114,8 +40,7 @@ _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["author_name"], vars["author_email"];
vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"];
}
reset_vars();
current_filename = FILENAME;
@ -176,8 +101,7 @@ _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["author_name"], vars["author_email"];
vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"];
}
}
EOF
@ -195,9 +119,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 author_name author_email
local file filename title date lastmod tags slug image image_caption description
local file_mtime
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email || [[ -n "$file" ]]; do
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description || [[ -n "$file" ]]; do
# Fallback for Title (use filename without extension)
if [ -z "$title" ]; then
title="${filename%.*}"
@ -214,264 +138,68 @@ _process_raw_file_index() {
lastmod="$date"
fi
if [ -n "$slug" ]; then
# Ensure slug is sanitized
slug=$(generate_slug "$slug")
fi
# Fallback for Slug (generate from title)
if [ -z "$slug" ]; then
# Ensure title is available for slug generation
if [ -z "$title" ]; then title="${filename%.*}"; fi
if [ -z "$title" ]; then title="${filename%.*}"; fi
slug=$(generate_slug "$title")
fi
# Fallback for Description (generate excerpt)
# Check if description is empty or contains only whitespace
if [[ -z "$description" || "$description" =~ ^[[:space:]]*$ ]]; then
if [ "${GENERATE_EXCERPT:-true}" = "true" ]; then
description=$(generate_excerpt "$file")
fi
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|$author_name|$author_email" >> "$output_processed_index"
echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description" >> "$output_processed_index"
done < "$input_raw_index"
wait # Ensure background processes from potential subshells (like generate_excerpt) finish
}
# Optimized file index building - orchestrates raw build and processing
_build_file_index_from_ram() {
# Phase 1: Bulk awk frontmatter extraction (one awk invocation for all files)
local raw_index_data
raw_index_data=$(
{
printf '\n---BSSG_EOF---\n'
while IFS= read -r file; do
[[ -z "$file" ]] && continue
printf 'BSSG_PATH:%s\n' "$file"
ram_mode_get_content "$file"
printf '\n---BSSG_EOF---\n'
done < <(ram_mode_list_src_files)
} | awk '
BEGIN {
RS = "\n---BSSG_EOF---\n"
FS = "\n"
}
NR > 1 {
file = $1
sub(/^BSSG_PATH:/, "", file)
basename = file
sub(/.*\//, "", basename)
title = ""; date = ""; lastmod = ""; tags = ""; slug = ""
image = ""; image_caption = ""; description = ""
author_name = ""; author_email = ""
if (file ~ /\.md$/) {
in_fm = 0; found_fm = 0
for (i = 2; i <= NF; i++) {
if ($i == "---") {
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; continue }
if (in_fm) break
}
if (in_fm) {
if (match($i, /^[^:]+:/)) {
key = $i
sub(/^[[:space:]]+/, "", key)
match(key, /^[^:]+/)
k = substr(key, RSTART, RLENGTH)
val = substr(key, RLENGTH + 2)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
k = tolower(k)
if (match(val, /^".*"$/) || match(val, /^\47.*\47$/)) {
val = substr(val, 2, length(val) - 2)
}
if (k == "title") title = val
else if (k == "date") date = val
else if (k == "lastmod") lastmod = val
else if (k == "tags") tags = val
else if (k == "slug") slug = val
else if (k == "image") image = val
else if (k == "image_caption") image_caption = val
else if (k == "description") description = val
else if (k == "author_name") author_name = val
else if (k == "author_email") author_email = val
}
}
}
} else if (file ~ /\.html$/) {
for (i = 2; i <= NF; i++) {
if ($i ~ /<title>[^<]*<\/title>/) {
match($i, /<title>[^<]*<\/title>/)
title = substr($i, RSTART + 7, RLENGTH - 15)
}
if ($i ~ /name="date" content="[^"]*"/) {
match($i, /name="date" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); date = s
}
if ($i ~ /name="lastmod" content="[^"]*"/) {
match($i, /name="lastmod" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); lastmod = s
}
if ($i ~ /name="tags" content="[^"]*"/) {
match($i, /name="tags" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); tags = s
}
if ($i ~ /name="slug" content="[^"]*"/) {
match($i, /name="slug" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); slug = s
}
if ($i ~ /name="image" content="[^"]*"/) {
match($i, /name="image" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); image = s
}
if ($i ~ /name="image_caption" content="[^"]*"/) {
match($i, /name="image_caption" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); image_caption = s
}
if ($i ~ /name="description" content="[^"]*"/) {
match($i, /name="description" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); description = s
}
if ($i ~ /name="author_name" content="[^"]*"/) {
match($i, /name="author_name" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); author_name = s
}
if ($i ~ /name="author_email" content="[^"]*"/) {
match($i, /name="author_email" content="[^"]*"/)
s = substr($i, RSTART, RLENGTH)
sub(/.*content="/, "", s); sub(/"$/, "", s); author_email = s
}
}
}
printf "%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n", file, basename, title, date, lastmod, tags, slug, image, image_caption, description, author_name, author_email
}
'
)
# Phase 2: Apply bash-level fallbacks in a single pass (title from filename,
# slug generation, excerpt, author defaults) then sort.
while IFS= read -r line; do
[[ -z "$line" ]] && continue
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"
if [ -z "$title" ]; then
title=$(echo "$filename" | sed 's/\.[^.]*$//')
fi
if [ -z "$date" ]; then
local file_mtime
file_mtime=$(ram_mode_get_mtime "$file")
date=$(format_date_from_timestamp "$file_mtime")
fi
if [ -z "$lastmod" ]; then
lastmod="$date"
fi
if [ -z "$slug" ]; then
slug=$(generate_slug "$title")
else
slug=$(generate_slug "$slug")
fi
if [ -z "$description" ] && [ "${GENERATE_EXCERPT:-true}" = "true" ]; then
description=$(generate_excerpt "$file")
fi
if [ -z "$author_name" ]; then
author_name="${AUTHOR_NAME:-Anonymous}"
fi
if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then
author_email="${AUTHOR_EMAIL:-}"
fi
printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$file" "$filename" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description" "$author_name" "$author_email"
done <<< "$raw_index_data" | 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_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 — single find for all metadata
local rebuild_needed=true
local find_output=""
# Check if rebuild is needed
if [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$file_index" ] && [ -f "$index_marker" ]; then
if find --version >/dev/null 2>&1 && grep -q GNU <<< "$(find --version)"; then
find_output=$(find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" -printf '%T@ %p\n' 2>/dev/null | sort -k1,1nr)
else
find_output=$(find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" -exec stat -f '%m %N' {} \; 2>/dev/null | sort -k1,1nr)
fi
local src_count=0
local newest_file_time=0
if [ -n "$find_output" ]; then
src_count=$(printf '%s\n' "$find_output" | wc -l | tr -d ' ')
local first_line
first_line=$(printf '%s\n' "$find_output" | head -n 1)
newest_file_time="${first_line%% *}"
newest_file_time=${newest_file_time%.*}
newest_file_time=${newest_file_time:-0}
# Use find -printf for efficiency if available (GNU find)
if find --version >/dev/null 2>&1 && grep -q GNU <<< "$(find --version)"; then
newest_file_time=$(find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" -printf '%T@\n' 2>/dev/null | sort -nr | head -n 1)
newest_file_time=${newest_file_time:-0} # Handle empty dir
newest_file_time=$(printf "%.0f" "$newest_file_time")
else # POSIX/BSD find
local src_files
# Use -exec stat for better portability than parsing ls
# This might still be slow on very large sites compared to GNU find -printf
newest_file_time=$(find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" -exec stat -f %m {} \; 2>/dev/null | sort -nr | head -n 1)
newest_file_time=${newest_file_time:-0} # Handle empty dir
fi
local marker_time=$(get_file_mtime "$index_marker")
# Defensive check: if marker_time is 0 or invalid, force rebuild
[[ -z "$marker_time" || "$marker_time" -eq 0 ]] && marker_time=0
local idx_count=$(wc -l < "$file_index" 2>/dev/null || echo 0)
if [ "$src_count" -ne "$idx_count" ]; then
echo -e "${YELLOW}Source file count mismatch (src=$src_count, index=$idx_count). Rebuilding index.${NC}"
rebuild_needed=true
elif [[ "$newest_file_time" -gt 0 && "$marker_time" -gt 0 && "$newest_file_time" -le "$marker_time" ]]; then
# Check if any source file is newer than the marker
if [[ "$newest_file_time" -gt 0 && "$marker_time" -gt 0 && "$newest_file_time" -le "$marker_time" ]]; then
echo -e "${GREEN}File index is up to date, skipping...${NC}"
rebuild_needed=false
return 0
else
echo -e "${YELLOW}File index rebuild needed (newest file: $newest_file_time, marker: $marker_time).${NC}"
rebuild_needed=true
fi
fi
if [ "$rebuild_needed" = false ]; then
return 0
fi
lock_file "$file_index"
# Find all markdown/html files
local all_files_list="${CACHE_DIR:-.bssg_cache}/all_files_list.$$"
if [ -n "$find_output" ]; then
printf '%s\n' "$find_output" | awk '{ sub(/^[0-9]+\.?[0-9]* /, ""); print }' | sort > "$all_files_list"
else
find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort > "$all_files_list"
fi
trap 'rm -f "$all_files_list"' EXIT
find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort > "$all_files_list"
trap 'rm -f "$all_files_list"' EXIT # Ensure cleanup
local total_files=$(wc -l < "$all_files_list")
if [ "$total_files" -eq 0 ]; then
@ -509,11 +237,30 @@ optimized_build_file_index() {
# Check if file_index content has changed
local index_content_changed=false
if [ -f "$file_index" ]; then
# Check if the file content differs using cmp (portable)