Compare commits
96 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5755e9fd0 | |||
| 0662983c11 | |||
| 48bc724d94 | |||
| 8250468f56 | |||
| 608a82aec4 | |||
| 37c5a2283e | |||
| d4b0d4d58a | |||
| d0ceef943c | |||
| 0a5f9e20a3 | |||
| cbc08b06cc | |||
| e2822ad620 | |||
| e91a1344b0 | |||
| b1c2397a93 | |||
| 41debaae5c | |||
| b5e1888d7a | |||
| 3fe84d322a | |||
| c252f106c8 | |||
| fee45661de | |||
|
|
b62b4d8b76 | ||
| f8f4e18be7 | |||
| 1f59e61879 | |||
| c43a6c3cb8 | |||
| 7ebd09eeeb | |||
| 1c7aab7b71 | |||
| 70760825b0 | |||
| 3c88fc7e69 | |||
| 6582872d70 | |||
| 47d0ea02b0 | |||
| ea2afcd6e9 | |||
| e8c98d10ab | |||
| d8e7e3f0ea | |||
|
|
7df772e509 | ||
| ce4269dd6e | |||
| 3a52a4b24a | |||
| 4dcec9f963 | |||
| 7eaf64ea4a | |||
| 03c83e5b76 | |||
| e494aed35b | |||
| 64b2c439c3 | |||
| 2474b8146d | |||
| ded254d171 | |||
| 95b5319b93 | |||
| 2636df570c | |||
| 88c2a98911 | |||
| afaaed81d1 | |||
| 12a39b5e62 | |||
| c5d1b48476 | |||
| 5872f2a5a6 | |||
| 220b82832a | |||
| 4d3ae7c92f | |||
| 81e8b8d5de | |||
| 7b3539de65 | |||
| a055615e79 | |||
| 54f8c66480 | |||
| 582d7970b8 | |||
| 48f332e1fc | |||
| 0d470e4f23 | |||
| a893411e78 | |||
| 68883053f0 | |||
| 31314e7328 | |||
| e3139d51f4 | |||
| 706cf9c154 | |||
| 46a5e8e496 | |||
| 4225940c2a | |||
| 9dd4b42c40 | |||
| c2cd5367f8 | |||
| 9976dfdf3f | |||
| 925e73900e | |||
| cc354c5dbd | |||
| a3201bbba7 | |||
| b43ea5ebbc | |||
| 5fc2bd74de | |||
| ebb42ab78e | |||
| 1876b199b3 | |||
| 96f708367f | |||
| 4e28bc3569 | |||
| 4144fe5c7b | |||
| e8538f872f | |||
| 66ea4b13cd | |||
| be4dff0b13 | |||
| f5e90ba1f0 | |||
| 852bba9f8d | |||
| e976b69e9a | |||
| 5c9c9a19e1 | |||
| b09a501b0c | |||
| e94d166f70 | |||
| f7024d80e2 | |||
| 9de9d95b7c | |||
| a15ae91fe1 | |||
| ae1312e2cb | |||
| b7f7ded3c4 | |||
| 23fc3874a2 | |||
| 25bfb2760f | |||
| 23eac6fbec | |||
| 17ab2e7e7a | |||
| 448559d41b |
110 changed files with 30121 additions and 8600 deletions
1
VERSION
Normal file
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.41
|
||||
2925
bssg-editor.html
Normal file
2925
bssg-editor.html
Normal file
File diff suppressed because it is too large
Load diff
63
config.sh
63
config.sh
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Configuration File
|
||||
# Version 0.9
|
||||
# Version controlled via root VERSION file
|
||||
# Contains all configurable parameters for the static site generator
|
||||
# Developed by Stefano Marinelli (stefano@dragas.it)
|
||||
#
|
||||
|
|
@ -19,10 +19,19 @@ OUTPUT_DIR="output"
|
|||
TEMPLATES_DIR="templates"
|
||||
THEMES_DIR="themes"
|
||||
STATIC_DIR="static"
|
||||
DRAFTS_DIR="drafts" # Directory for drafts
|
||||
THEME="default"
|
||||
CACHE_DIR=".bssg_cache" # Default cache directory location (relative to BSSG root)
|
||||
|
||||
# Build configuration
|
||||
CLEAN_OUTPUT=false
|
||||
CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild
|
||||
REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh)
|
||||
REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh)
|
||||
PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build.
|
||||
BUILD_MODE="ram" # Options: "normal", "ram". "ram" preloads inputs and keeps build state in memory (writes only output artifacts).
|
||||
|
||||
# Customization
|
||||
CUSTOM_CSS="" # Optional: Path to custom CSS file relative to output root (e.g., "/css/custom.css"). File should be placed in STATIC_DIR.
|
||||
|
||||
# Site information
|
||||
SITE_TITLE="My new BSSG site"
|
||||
|
|
@ -30,16 +39,47 @@ 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"
|
||||
DATE_FORMAT="%Y-%m-%d %H:%M:%S %z"
|
||||
TIMEZONE="local" # Options: "local", "GMT", or a specific timezone like "America/New_York"
|
||||
SHOW_TIMEZONE="false" # Options: "true", "false". Whether to display the timezone in rendered dates.
|
||||
POSTS_PER_PAGE=10
|
||||
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
|
||||
URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs: Year/Month/Day/slug will create Year/Month/Day/slug/index.html
|
||||
ENABLE_AUTHOR_PAGES=false # Enable or disable author pages (default: false)
|
||||
ENABLE_AUTHOR_RSS=false # Enable or disable author-specific RSS feeds (default: false)
|
||||
SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2)
|
||||
URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs. Available: Year, Month, Day, slug
|
||||
ENABLE_TAG_RSS=true # Enable or disable tag-specific RSS feed generation (default: true)
|
||||
|
||||
# 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="pages/slug" # Format for page URLs: pages/slug will create pages/slug/index.html
|
||||
PAGE_URL_FORMAT="slug" # Format for page URLs. Available: slug, filename (without ext)
|
||||
|
||||
# Markdown processing configuration
|
||||
MARKDOWN_PROCESSOR="commonmark" # Options: "pandoc", "commonmark", or "markdown.pl"
|
||||
|
|
@ -47,6 +87,19 @@ 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.
|
||||
|
||||
# Terminal colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,14 +3,17 @@
|
|||
|
||||
export MSG_HOME="Startseite"
|
||||
export MSG_TAGS="Tags"
|
||||
export MSG_AUTHORS="Autoren"
|
||||
export MSG_ARCHIVES="Archive"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="Seiten"
|
||||
export MSG_SUBSCRIBE_RSS="Per RSS abonnieren"
|
||||
export MSG_PUBLISHED_ON="Veröffentlicht am"
|
||||
export MSG_BY="von"
|
||||
export MSG_POSTS_BY="Beiträge von"
|
||||
export MSG_TAG_PAGE_TITLE="Beiträge mit dem Tag"
|
||||
export MSG_ALL_TAGS="Alle Tags"
|
||||
export MSG_ALL_AUTHORS="Alle Autoren"
|
||||
export MSG_ALL_PAGES="Alle Seiten"
|
||||
export MSG_ARCHIVES_FOR="Archive für"
|
||||
export MSG_BACK_TO="Zurück zu"
|
||||
|
|
@ -40,4 +43,9 @@ export MSG_MONTH_09="September"
|
|||
export MSG_MONTH_10="Oktober"
|
||||
export MSG_MONTH_11="November"
|
||||
export MSG_MONTH_12="Dezember"
|
||||
export MSG_READING_TIME_TEMPLATE="%d Min. Lesezeit"
|
||||
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"
|
||||
|
|
@ -3,14 +3,17 @@
|
|||
|
||||
export MSG_HOME="Home"
|
||||
export MSG_TAGS="Tags"
|
||||
export MSG_AUTHORS="Authors"
|
||||
export MSG_ARCHIVES="Archives"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="Pages"
|
||||
export MSG_SUBSCRIBE_RSS="Subscribe via RSS"
|
||||
export MSG_PUBLISHED_ON="Published on"
|
||||
export MSG_BY="by"
|
||||
export MSG_POSTS_BY="Posts by"
|
||||
export MSG_TAG_PAGE_TITLE="Posts tagged with"
|
||||
export MSG_ALL_TAGS="All Tags"
|
||||
export MSG_ALL_AUTHORS="All Authors"
|
||||
export MSG_ALL_PAGES="All Pages"
|
||||
export MSG_ARCHIVES_FOR="Archives for"
|
||||
export MSG_BACK_TO="Back to"
|
||||
|
|
@ -40,4 +43,9 @@ 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_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"
|
||||
|
|
@ -3,14 +3,17 @@
|
|||
|
||||
export MSG_HOME="Inicio"
|
||||
export MSG_TAGS="Etiquetas"
|
||||
export MSG_AUTHORS="Autores"
|
||||
export MSG_ARCHIVES="Archivos"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="Páginas"
|
||||
export MSG_SUBSCRIBE_RSS="Suscribirse vía RSS"
|
||||
export MSG_PUBLISHED_ON="Publicado el"
|
||||
export MSG_BY="por"
|
||||
export MSG_POSTS_BY="Entradas de"
|
||||
export MSG_TAG_PAGE_TITLE="Entradas etiquetadas con"
|
||||
export MSG_ALL_TAGS="Todas las etiquetas"
|
||||
export MSG_ALL_AUTHORS="Todos los autores"
|
||||
export MSG_ALL_PAGES="Todas las páginas"
|
||||
export MSG_ARCHIVES_FOR="Archivos de"
|
||||
export MSG_BACK_TO="Volver a"
|
||||
|
|
@ -40,4 +43,9 @@ export MSG_MONTH_09="Septiembre"
|
|||
export MSG_MONTH_10="Octubre"
|
||||
export MSG_MONTH_11="Noviembre"
|
||||
export MSG_MONTH_12="Diciembre"
|
||||
export MSG_READING_TIME_TEMPLATE="%d min de lectura"
|
||||
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"
|
||||
|
|
@ -3,14 +3,17 @@
|
|||
|
||||
export MSG_HOME="Accueil"
|
||||
export MSG_TAGS="Étiquettes"
|
||||
export MSG_AUTHORS="Auteurs"
|
||||
export MSG_ARCHIVES="Archives"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="Pages"
|
||||
export MSG_SUBSCRIBE_RSS="S'abonner via RSS"
|
||||
export MSG_PUBLISHED_ON="Publié le"
|
||||
export MSG_BY="par"
|
||||
export MSG_POSTS_BY="Articles de"
|
||||
export MSG_TAG_PAGE_TITLE="Articles étiquetés avec"
|
||||
export MSG_ALL_TAGS="Toutes les étiquettes"
|
||||
export MSG_ALL_AUTHORS="Tous les auteurs"
|
||||
export MSG_ALL_PAGES="Toutes les pages"
|
||||
export MSG_ARCHIVES_FOR="Archives pour"
|
||||
export MSG_BACK_TO="Retour à"
|
||||
|
|
@ -40,4 +43,9 @@ export MSG_MONTH_09="Septembre"
|
|||
export MSG_MONTH_10="Octobre"
|
||||
export MSG_MONTH_11="Novembre"
|
||||
export MSG_MONTH_12="Décembre"
|
||||
export MSG_READING_TIME_TEMPLATE="%d min de lecture"
|
||||
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"
|
||||
|
|
@ -3,14 +3,17 @@
|
|||
|
||||
export MSG_HOME="Home"
|
||||
export MSG_TAGS="Tag"
|
||||
export MSG_AUTHORS="Autori"
|
||||
export MSG_ARCHIVES="Archivi"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="Pagine"
|
||||
export MSG_SUBSCRIBE_RSS="Abbonati via RSS"
|
||||
export MSG_PUBLISHED_ON="Pubblicato il"
|
||||
export MSG_BY="da"
|
||||
export MSG_POSTS_BY="Articoli di"
|
||||
export MSG_TAG_PAGE_TITLE="Articoli taggati con"
|
||||
export MSG_ALL_TAGS="Tutti i tag"
|
||||
export MSG_ALL_AUTHORS="Tutti gli autori"
|
||||
export MSG_ALL_PAGES="Tutte le pagine"
|
||||
export MSG_ARCHIVES_FOR="Archivi per"
|
||||
export MSG_BACK_TO="Torna a"
|
||||
|
|
@ -40,4 +43,9 @@ export MSG_MONTH_09="Settembre"
|
|||
export MSG_MONTH_10="Ottobre"
|
||||
export MSG_MONTH_11="Novembre"
|
||||
export MSG_MONTH_12="Dicembre"
|
||||
export MSG_READING_TIME_TEMPLATE="%d min di lettura"
|
||||
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"
|
||||
|
|
@ -3,14 +3,17 @@
|
|||
|
||||
export MSG_HOME="ホーム"
|
||||
export MSG_TAGS="タグ"
|
||||
export MSG_AUTHORS="著者"
|
||||
export MSG_ARCHIVES="アーカイブ"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="ページ"
|
||||
export MSG_SUBSCRIBE_RSS="RSSで購読する"
|
||||
export MSG_PUBLISHED_ON="公開日"
|
||||
export MSG_BY="作成者"
|
||||
export MSG_POSTS_BY="の投稿"
|
||||
export MSG_TAG_PAGE_TITLE="タグ付きの投稿"
|
||||
export MSG_ALL_TAGS="すべてのタグ"
|
||||
export MSG_ALL_AUTHORS="すべての著者"
|
||||
export MSG_ALL_PAGES="すべてのページ"
|
||||
export MSG_ARCHIVES_FOR="のアーカイブ"
|
||||
export MSG_BACK_TO="に戻る"
|
||||
|
|
@ -40,4 +43,9 @@ export MSG_MONTH_09="9月"
|
|||
export MSG_MONTH_10="10月"
|
||||
export MSG_MONTH_11="11月"
|
||||
export MSG_MONTH_12="12月"
|
||||
export MSG_READING_TIME_TEMPLATE="読む時間: %d分"
|
||||
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="関連記事"
|
||||
51
locales/nl.sh
Normal file
51
locales/nl.sh
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env bash
|
||||
# Dutch Locale for BSSG
|
||||
|
||||
export MSG_HOME="Home"
|
||||
export MSG_TAGS="Tags"
|
||||
export MSG_AUTHORS="Autheurs"
|
||||
export MSG_ARCHIVES="Archieven"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="Paginas"
|
||||
export MSG_SUBSCRIBE_RSS="Volg via RSS"
|
||||
export MSG_PUBLISHED_ON="Gepubliceerd op"
|
||||
export MSG_BY="door"
|
||||
export MSG_POSTS_BY="Posts door"
|
||||
export MSG_TAG_PAGE_TITLE="Posts getagd met"
|
||||
export MSG_ALL_TAGS="All Tags"
|
||||
export MSG_ALL_AUTHORS="Alle Authors"
|
||||
export MSG_ALL_PAGES="All Paginas"
|
||||
export MSG_ARCHIVES_FOR="Archiven voor"
|
||||
export MSG_BACK_TO="Terug naar"
|
||||
export MSG_POSTS_FROM="Posts van"
|
||||
export MSG_OLDER_POSTS="Oudere Posts"
|
||||
export MSG_NEWER_POSTS="Nieuwere Posts"
|
||||
export MSG_PAGE_INFO_TEMPLATE="Pagina %d van %d"
|
||||
export MSG_PAGE_TITLE_PREFIX="Pagina"
|
||||
export MSG_RSS_FEED_TITLE="${SITE_TITLE} - RSS Feed"
|
||||
export MSG_RSS_FEED_DESCRIPTION="${SITE_DESCRIPTION}"
|
||||
export MSG_RSS_FEED="RSS Feed"
|
||||
export MSG_ALL_RIGHTS_RESERVED="Alle rechten reserved."
|
||||
export MSG_GENERATED_WITH="Deze site was gegenereerd met"
|
||||
export MSG_LATEST_POSTS="Laatste Posts"
|
||||
export MSG_GENERATOR_DESCRIPTION="."
|
||||
export MSG_POSTS="posts"
|
||||
export MSG_READ_MORE="Lees meer"
|
||||
export MSG_MONTH_01="Januari"
|
||||
export MSG_MONTH_02="Februari"
|
||||
export MSG_MONTH_03="Maart"
|
||||
export MSG_MONTH_04="April"
|
||||
export MSG_MONTH_05="Mei"
|
||||
export MSG_MONTH_06="Juni"
|
||||
export MSG_MONTH_07="Juli"
|
||||
export MSG_MONTH_08="Augustus"
|
||||
export MSG_MONTH_09="September"
|
||||
export MSG_MONTH_10="October"
|
||||
export MSG_MONTH_11="November"
|
||||
export MSG_MONTH_12="December"
|
||||
export MSG_READING_TIME_TEMPLATE="%d min read"
|
||||
export MSG_MINUTE="minuut"
|
||||
export MSG_MINUTES="minuten"
|
||||
export MSG_UPDATED_ON="Bijgewerkt op"
|
||||
export MSG_BACK_TO_TOP="Terug naar Boven"
|
||||
export MSG_RELATED_POSTS="Gerelateede Posts"
|
||||
|
|
@ -3,14 +3,17 @@
|
|||
|
||||
export MSG_HOME="Início"
|
||||
export MSG_TAGS="Etiquetas"
|
||||
export MSG_AUTHORS="Autores"
|
||||
export MSG_ARCHIVES="Arquivos"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="Páginas"
|
||||
export MSG_SUBSCRIBE_RSS="Subscrever via RSS"
|
||||
export MSG_PUBLISHED_ON="Publicado em"
|
||||
export MSG_BY="por"
|
||||
export MSG_POSTS_BY="Posts de"
|
||||
export MSG_TAG_PAGE_TITLE="Posts etiquetados com"
|
||||
export MSG_ALL_TAGS="Todas as Etiquetas"
|
||||
export MSG_ALL_AUTHORS="Todos os Autores"
|
||||
export MSG_ALL_PAGES="Todas as Páginas"
|
||||
export MSG_ARCHIVES_FOR="Arquivos de"
|
||||
export MSG_BACK_TO="Voltar para"
|
||||
|
|
@ -40,4 +43,9 @@ export MSG_MONTH_09="Setembro"
|
|||
export MSG_MONTH_10="Outubro"
|
||||
export MSG_MONTH_11="Novembro"
|
||||
export MSG_MONTH_12="Dezembro"
|
||||
export MSG_READING_TIME_TEMPLATE="%d min de leitura"
|
||||
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"
|
||||
|
|
@ -3,14 +3,17 @@
|
|||
|
||||
export MSG_HOME="首页"
|
||||
export MSG_TAGS="标签"
|
||||
export MSG_AUTHORS="作者"
|
||||
export MSG_ARCHIVES="归档"
|
||||
export MSG_RSS="RSS"
|
||||
export MSG_PAGES="页面"
|
||||
export MSG_SUBSCRIBE_RSS="通过 RSS 订阅"
|
||||
export MSG_PUBLISHED_ON="发布于"
|
||||
export MSG_BY="作者"
|
||||
export MSG_POSTS_BY="的文章"
|
||||
export MSG_TAG_PAGE_TITLE="标签为 的文章"
|
||||
export MSG_ALL_TAGS="所有标签"
|
||||
export MSG_ALL_AUTHORS="所有作者"
|
||||
export MSG_ALL_PAGES="所有页面"
|
||||
export MSG_ARCHIVES_FOR="的归档"
|
||||
export MSG_BACK_TO="返回"
|
||||
|
|
@ -40,4 +43,9 @@ export MSG_MONTH_09="九月"
|
|||
export MSG_MONTH_10="十月"
|
||||
export MSG_MONTH_11="十一月"
|
||||
export MSG_MONTH_12="十二月"
|
||||
export MSG_READING_TIME_TEMPLATE="阅读时间:%d 分钟"
|
||||
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="相关文章"
|
||||
|
|
@ -7,118 +7,207 @@
|
|||
# Project Homepage: https://bssg.dragas.net
|
||||
#
|
||||
|
||||
# This script RELIES on environment variables set by config_loader.sh
|
||||
# It should be called via the main bssg.sh script.
|
||||
|
||||
set -e
|
||||
|
||||
# Load configuration
|
||||
CONFIG_FILE="config.sh"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
source "$CONFIG_FILE"
|
||||
# Source utilities needed for logging (ensure they are available)
|
||||
# Determine the directory of the main bssg script if BSSG_SCRIPT_DIR is set
|
||||
SCRIPT_DIR="${BSSG_SCRIPT_DIR:-$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )/..}"
|
||||
UTILS_SCRIPT="${SCRIPT_DIR}/scripts/build/utils.sh"
|
||||
|
||||
if [ -f "$UTILS_SCRIPT" ]; then
|
||||
# shellcheck source=scripts/build/utils.sh
|
||||
source "$UTILS_SCRIPT"
|
||||
else
|
||||
echo "Error: Configuration file '$CONFIG_FILE' not found"
|
||||
exit 1
|
||||
# Minimal fallback if utils not found
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m'
|
||||
print_error() { echo -e "${RED}Error: $1${NC}" >&2; }
|
||||
print_warning() { echo -e "${YELLOW}Warning: $1${NC}"; }
|
||||
print_success() { echo -e "${GREEN}Success: $1${NC}"; }
|
||||
print_info() { echo -e "Info: $1"; }
|
||||
print_error "Utilities script not found at '$UTILS_SCRIPT'. Using fallback logging."
|
||||
fi
|
||||
|
||||
# Load local configuration overrides if they exist
|
||||
LOCAL_CONFIG_FILE="config.sh.local"
|
||||
if [ -f "$LOCAL_CONFIG_FILE" ]; then
|
||||
source "$LOCAL_CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Terminal colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
# Check essential variables are set (they should be exported by config_loader.sh)
|
||||
: "${CONFIG_FILE:?Error: CONFIG_FILE environment variable not set. Run via bssg.sh}"
|
||||
: "${SRC_DIR:?Error: SRC_DIR environment variable not set. Run via bssg.sh}"
|
||||
: "${BACKUP_DIR:?Error: BACKUP_DIR environment variable not set. Run via bssg.sh}"
|
||||
: "${LOCAL_CONFIG_FILE:?Error: LOCAL_CONFIG_FILE environment variable not set. Run via bssg.sh}"
|
||||
|
||||
# Create backup directory if it doesn't exist
|
||||
mkdir -p "backup"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Function to backup posts
|
||||
backup_posts() {
|
||||
local timestamp=$(date +%Y%m%d%H%M%S)
|
||||
local backup_file="backup/bssg_backup_$timestamp.tar.gz"
|
||||
|
||||
echo -e "${YELLOW}Creating backup of all posts...${NC}"
|
||||
|
||||
# Create tar archive with all .md and .html files in src directory
|
||||
tar -czf "$backup_file" -C "$SRC_DIR" .
|
||||
|
||||
# Also include the drafts directory if it exists
|
||||
if [ -d "drafts" ]; then
|
||||
echo -e "${YELLOW}Including drafts in backup...${NC}"
|
||||
tar -rf "${backup_file%.gz}" -C "drafts" .
|
||||
gzip -f "${backup_file%.gz}"
|
||||
# Function to backup site content and configuration
|
||||
create_backup() {
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d%H%M%S)
|
||||
local backup_filename="bssg_backup_$timestamp.tar.gz"
|
||||
local backup_filepath="${BACKUP_DIR}/${backup_filename}"
|
||||
|
||||
print_info "Creating backup..."
|
||||
|
||||
# Prepare list of tar options (-C dir file)
|
||||
local tar_opts=()
|
||||
|
||||
# Function to safely add item to tar options
|
||||
add_item_to_backup() {
|
||||
local item_path="$1"
|
||||
local item_type="$2" # 'file' or 'dir'
|
||||
|
||||
if [ "$item_type" == "file" ] && [ ! -f "$item_path" ]; then
|
||||
print_warning "File '$item_path' not found, skipping."
|
||||
return
|
||||
elif [ "$item_type" == "dir" ] && [ ! -d "$item_path" ]; then
|
||||
print_warning "Directory '$item_path' not found, skipping."
|
||||
return
|
||||
elif [ "$item_type" != "file" ] && [ "$item_type" != "dir" ]; then
|
||||
print_error "Internal error: Invalid item type '$item_type' for $item_path"
|
||||
return
|
||||
fi
|
||||
|
||||
local dir
|
||||
local base
|
||||
dir=$(dirname "$item_path")
|
||||
base=$(basename "$item_path")
|
||||
tar_opts+=("-C" "$dir" "$base")
|
||||
print_info "Adding $item_type '$base' from '$dir' to backup."
|
||||
}
|
||||
|
||||
# Add main config file
|
||||
add_item_to_backup "$CONFIG_FILE" "file"
|
||||
|
||||
# Add local config file
|
||||
add_item_to_backup "$LOCAL_CONFIG_FILE" "file"
|
||||
|
||||
# Add source directory
|
||||
add_item_to_backup "$SRC_DIR" "dir"
|
||||
|
||||
# Add drafts directory (if defined)
|
||||
if [ -n "${DRAFTS_DIR:-}" ]; then
|
||||
add_item_to_backup "$DRAFTS_DIR" "dir"
|
||||
fi
|
||||
|
||||
# Add pages directory (if defined)
|
||||
# Check if it's the same as SRC_DIR to avoid adding twice
|
||||
if [ -n "${PAGES_DIR:-}" ]; then
|
||||
if [[ "$(cd "$SRC_DIR" && pwd)" != "$(cd "$PAGES_DIR" && pwd)" ]]; then
|
||||
add_item_to_backup "$PAGES_DIR" "dir"
|
||||
else
|
||||
print_info "Pages directory '$PAGES_DIR' is same as source directory '$SRC_DIR', skipping duplicate add."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Also include the pages directory if it exists
|
||||
if [ -d "pages" ]; then
|
||||
echo -e "${YELLOW}Including pages in backup...${NC}"
|
||||
tar -rf "${backup_file%.gz}" -C "pages" .
|
||||
gzip -f "${backup_file%.gz}"
|
||||
# Check if there are items to back up
|
||||
# We check tar_opts length / 2 because each item adds two elements (-C and path)
|
||||
if [ ${#tar_opts[@]} -eq 0 ]; then
|
||||
print_error "No items found to back up. Please check configuration and file/directory existence."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create the tar archive
|
||||
print_info "Archiving items to $backup_filepath"
|
||||
# The items added via -C will be relative to the archive root
|
||||
tar -czf "$backup_filepath" "${tar_opts[@]}"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Backup created: $backup_filepath"
|
||||
else
|
||||
print_error "Failed to create backup archive."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Manage daily backup and cleanup
|
||||
manage_backup_rotation "$backup_filepath"
|
||||
|
||||
print_success "Backup process completed."
|
||||
}
|
||||
|
||||
# Function to manage daily backups and rotation
|
||||
manage_backup_rotation() {
|
||||
local latest_backup_filepath="$1"
|
||||
local today
|
||||
today=$(date +%Y%m%d)
|
||||
local daily_backup_filename="bssg_daily_$today.tar.gz"
|
||||
local daily_backup_filepath="${BACKUP_DIR}/${daily_backup_filename}"
|
||||
|
||||
# Create a daily backup if it doesn't exist or if the latest is newer
|
||||
if [ ! -f "$daily_backup_filepath" ] || [ "$latest_backup_filepath" -nt "$daily_backup_filepath" ]; then
|
||||
cp "$latest_backup_filepath" "$daily_backup_filepath"
|
||||
print_success "Daily backup created/updated: $daily_backup_filepath"
|
||||
fi
|
||||
|
||||
# Keep only latest 10 timestamped backups (excluding daily backups)
|
||||
print_info "Cleaning old timestamped backups (keeping latest 10)..."
|
||||
|
||||
# Portable way to list, sort by time, skip 10 newest, and delete the rest
|
||||
# Use null delimiter with ls/tail/xargs if available (safer), otherwise newline
|
||||
local file_list
|
||||
local count=0
|
||||
|
||||
# Count the number of backups first to avoid errors with tail if less than 11
|
||||
count=$(ls -1 "${BACKUP_DIR}"/bssg_backup_*.tar.gz 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$count" -gt 10 ]; then
|
||||
# List files sorted by modification time (newest first), get all except the first 10
|
||||
# Use process substitution to avoid issues with subshells and xargs
|
||||
# Use xargs -I {} rm -f "{}" for basic portability with spaces
|
||||
ls -t "${BACKUP_DIR}"/bssg_backup_*.tar.gz 2>/dev/null | tail -n +11 | xargs -I {} rm -f "{}"
|
||||
if [ $? -ne 0 ]; then
|
||||
print_warning "Potentially failed to clean some old backups. Please check manually."
|
||||
fi
|
||||
else
|
||||
print_info "Fewer than 11 timestamped backups found, no cleanup needed."
|
||||
fi
|
||||
|
||||
# Also include the config.sh.local file if it exists
|
||||
if [ -f "$LOCAL_CONFIG_FILE" ]; then
|
||||
echo -e "${YELLOW}Including local configuration in backup...${NC}"
|
||||
tar -rf "${backup_file%.gz}" "$LOCAL_CONFIG_FILE"
|
||||
gzip -f "${backup_file%.gz}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Backup created: $backup_file${NC}"
|
||||
|
||||
# Create a daily backup if it doesn't exist
|
||||
local today=$(date +%Y%m%d)
|
||||
local daily_backup="backup/bssg_daily_$today.tar.gz"
|
||||
|
||||
if [ ! -f "$daily_backup" ]; then
|
||||
cp "$backup_file" "$daily_backup"
|
||||
echo -e "${GREEN}Daily backup created: $daily_backup${NC}"
|
||||
fi
|
||||
|
||||
# Keep only latest 10 backups
|
||||
echo -e "${YELLOW}Cleaning old backups...${NC}"
|
||||
cd backup
|
||||
ls -t bssg_backup_*.tar.gz | tail -n +11 | xargs rm -f 2>/dev/null || true
|
||||
cd ..
|
||||
|
||||
echo -e "${GREEN}Backup process completed.${NC}"
|
||||
print_info "Backup cleanup finished."
|
||||
}
|
||||
|
||||
# Function to list available backups
|
||||
list_backups() {
|
||||
echo -e "${YELLOW}Available backups:${NC}"
|
||||
|
||||
if [ ! -d "backup" ] || [ -z "$(ls -A backup 2>/dev/null)" ]; then
|
||||
echo -e "${RED}No backups found.${NC}"
|
||||
exit 0
|
||||
print_info "Available backups in ${BACKUP_DIR}:${NC}"
|
||||
|
||||
if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]; then
|
||||
print_error "No backups found.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
||||
echo -e "ID\tDate\t\tTime\t\tSize\t\tFile"
|
||||
echo -e "--\t----\t\t----\t\t----\t\t----"
|
||||
|
||||
|
||||
local counter=1
|
||||
ls -t backup/bssg_*.tar.gz 2>/dev/null | while read -r file; do
|
||||
# Use find to handle filenames safely
|
||||
find "$BACKUP_DIR" -maxdepth 1 -name 'bssg_*.tar.gz' -printf '%T@ %p\n' | \
|
||||
sort -nr | \
|
||||
cut -d' ' -f2- | \
|
||||
while IFS= read -r file; do
|
||||
if [ -f "$file" ]; then
|
||||
local filename=$(basename "$file")
|
||||
local filename
|
||||
filename=$(basename "$file")
|
||||
local date_part=""
|
||||
local time_part=""
|
||||
|
||||
|
||||
# Match timestamped backup
|
||||
if [[ "$filename" =~ bssg_backup_([0-9]{8})([0-9]{6})\.tar\.gz ]]; then
|
||||
# Regular backup format
|
||||
date_part="${BASH_REMATCH[1]:0:4}-${BASH_REMATCH[1]:4:2}-${BASH_REMATCH[1]:6:2}"
|
||||
time_part="${BASH_REMATCH[2]:0:2}:${BASH_REMATCH[2]:2:2}:${BASH_REMATCH[2]:4:2}"
|
||||
# Match daily backup
|
||||
elif [[ "$filename" =~ bssg_daily_([0-9]{8})\.tar\.gz ]]; then
|
||||
# Daily backup format
|
||||
date_part="${BASH_REMATCH[1]:0:4}-${BASH_REMATCH[1]:4:2}-${BASH_REMATCH[1]:6:2}"
|
||||
time_part="00:00:00"
|
||||
time_part="Daily"
|
||||
else
|
||||
# Unknown format, show filename
|
||||
date_part="Unknown"
|
||||
time_part="Unknown"
|
||||
time_part="Format"
|
||||
fi
|
||||
|
||||
local size=$(du -h "$file" | cut -f1)
|
||||
echo -e "$counter\t$date_part\t$time_part\t$size\t\t$filename"
|
||||
|
||||
local size
|
||||
size=$(du -h "$file" | cut -f1)
|
||||
# Use printf for safer, more consistent formatting
|
||||
printf "%s\t%s\t%s\t%s\t%s\n" "$counter" "$date_part" "$time_part" "$size" "$filename"
|
||||
counter=$((counter + 1))
|
||||
fi
|
||||
done
|
||||
|
|
@ -127,22 +216,22 @@ list_backups() {
|
|||
# Main function
|
||||
main() {
|
||||
local command="backup"
|
||||
|
||||
|
||||
# Parse arguments
|
||||
if [ -n "$1" ]; then
|
||||
command="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
|
||||
case "$command" in
|
||||
backup|create)
|
||||
backup_posts
|
||||
create_backup
|
||||
;;
|
||||
list)
|
||||
list_backups
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unknown command '$command'${NC}"
|
||||
print_error "Unknown command '$command'"
|
||||
echo -e "Usage: $0 [backup|create|list]"
|
||||
exit 1
|
||||
;;
|
||||
|
|
|
|||
424
scripts/bssg.sh
424
scripts/bssg.sh
|
|
@ -9,75 +9,218 @@
|
|||
|
||||
set -e
|
||||
|
||||
# Load configuration
|
||||
CONFIG_FILE="config.sh"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
source "$CONFIG_FILE"
|
||||
# --- 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
|
||||
# source "$CONFIG_FILE"
|
||||
# else
|
||||
# echo "Error: Configuration file '$CONFIG_FILE' not found"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# Load local configuration overrides if they exist (DEPRECATED - Moved to config_loader.sh)
|
||||
# LOCAL_CONFIG_FILE="config.sh.local"
|
||||
# if [ -f "$LOCAL_CONFIG_FILE" ]; then
|
||||
# source "$LOCAL_CONFIG_FILE"
|
||||
# echo "Local configuration loaded from $LOCAL_CONFIG_FILE"
|
||||
# fi
|
||||
|
||||
# --- Centralized Configuration Loading --- START ---
|
||||
# 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 )"
|
||||
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"
|
||||
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 "Error: Configuration file '$CONFIG_FILE' not found"
|
||||
echo -e "${RED}Error: Config loader script not found at '$CONFIG_LOADER_SCRIPT'${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
# --- Centralized Configuration Loading --- END ---
|
||||
|
||||
# Load local configuration overrides if they exist
|
||||
LOCAL_CONFIG_FILE="config.sh.local"
|
||||
if [ -f "$LOCAL_CONFIG_FILE" ]; then
|
||||
source "$LOCAL_CONFIG_FILE"
|
||||
echo "Local configuration loaded from $LOCAL_CONFIG_FILE"
|
||||
# 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
|
||||
|
||||
# Terminal colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 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 (v0.9)"
|
||||
echo "=================================="
|
||||
echo "BSSG - Bash Static Site Generator (v${VERSION})"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "Usage: $0 command [options]"
|
||||
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 ""
|
||||
echo "Commands:"
|
||||
echo " post [-html] [draft_file] Create a new post or continue editing a draft"
|
||||
echo " Use -html to edit in HTML instead of Markdown"
|
||||
echo " page [-html] [draft_file] Create a new page or continue editing a draft"
|
||||
echo " Use -html to edit in HTML instead of Markdown"
|
||||
echo " edit [-n|-f] <post_file> Edit an existing post"
|
||||
echo " edit [-n] <post_file> Edit an existing post"
|
||||
echo " Use -n to give the post a new name if title changes"
|
||||
echo " Use -f to edit the full HTML file (advanced)"
|
||||
echo " delete [-f] <post_file> Delete a post"
|
||||
echo " Use -f to skip confirmation"
|
||||
echo " list List all posts"
|
||||
echo " tags [-n] List all tags"
|
||||
echo " Use -n to sort by number of posts"
|
||||
echo " drafts List all draft posts"
|
||||
echo " backup Create a backup of all posts, pages, and config"
|
||||
echo " backup Create a backup of all posts, pages, drafts, and config"
|
||||
echo " restore [backup_file|ID] Restore from a backup (all content by default)"
|
||||
echo " Options: --no-posts, --no-drafts, --no-pages, --no-config"
|
||||
echo " Options: --no-content, --no-config"
|
||||
echo " backups List all available backups"
|
||||
echo " build Build the site"
|
||||
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 " 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 " --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
|
||||
|
||||
# No arguments provided (after potential --config filtering)
|
||||
if [ $# -eq 0 ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
command="$1"
|
||||
shift
|
||||
|
||||
shift # Consume the command itself
|
||||
|
||||
# expand variables such as POSTS_DIR, PAGES_DIR embedded in the command-line
|
||||
set -- $(eval echo "$@")
|
||||
|
||||
case "$command" in
|
||||
post)
|
||||
scripts/post.sh "$@"
|
||||
|
|
@ -110,12 +253,222 @@ main() {
|
|||
scripts/backup.sh list
|
||||
;;
|
||||
build)
|
||||
# Instead of passing individual settings, pass the local config file path
|
||||
if [ -f "$LOCAL_CONFIG_FILE" ]; then
|
||||
scripts/build.sh --local-config "$LOCAL_CONFIG_FILE" "$@"
|
||||
else
|
||||
scripts/build.sh "$@"
|
||||
# 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
|
||||
;;
|
||||
--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.
|
||||
# We are not passing any server-specific arguments to the build script directly.
|
||||
# If build needs arguments, they should be passed via general bssg.sh build options
|
||||
# or configured in config files.
|
||||
"${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"
|
||||
;;
|
||||
init)
|
||||
# Check if directory argument is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo -e "${RED}Error: Target directory argument is required for the init command.${NC}"
|
||||
echo -e "Usage: $0 init <target_directory>"
|
||||
exit 1
|
||||
fi
|
||||
scripts/init.sh "$1"
|
||||
;;
|
||||
help)
|
||||
show_help
|
||||
|
|
@ -129,4 +482,5 @@ main() {
|
|||
}
|
||||
|
||||
# Run the main function
|
||||
main "$@"
|
||||
# Pass the filtered arguments (command and its options) to main
|
||||
main "$@"
|
||||
|
|
|
|||
4786
scripts/build.sh
4786
scripts/build.sh
File diff suppressed because it is too large
Load diff
64
scripts/build/assets.sh
Executable file
64
scripts/build/assets.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Asset Handling
|
||||
# Handles copying static assets and processing CSS.
|
||||
#
|
||||
|
||||
# Source dependencies (optional, but good practice if utils are needed)
|
||||
# shellcheck source=utils.sh disable=SC1091
|
||||
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from assets.sh"; exit 1; }
|
||||
|
||||
copy_static_files() {
|
||||
# Source utilities if needed for colors (already done above)
|
||||
|
||||
if [ -d "$STATIC_DIR" ]; then
|
||||
echo "Copying static files from $STATIC_DIR to $OUTPUT_DIR..."
|
||||
# Use rsync for efficiency if available, otherwise cp
|
||||
if command -v rsync > /dev/null 2>&1; then
|
||||
# Ensure source ends with / if copying contents
|
||||
rsync -a --checksum --exclude='.DS_Store' --exclude='._*' "${STATIC_DIR}/" "$OUTPUT_DIR/"
|
||||
else
|
||||
# Simple copy (less efficient, might overwrite newer)
|
||||
# Ensure target exists
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
# Using cp -R instead of -r for better compatibility, -p preserves timestamps
|
||||
cp -Rp "$STATIC_DIR/." "$OUTPUT_DIR/"
|
||||
fi
|
||||
echo -e "${GREEN}Static files copied.${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Static directory '$STATIC_DIR' not found, skipping copy.${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create CSS directory and copy theme CSS
|
||||
create_css() {
|
||||
local output_dir="$1"
|
||||
local theme="$2"
|
||||
local css_dir="${output_dir}/css"
|
||||
|
||||
mkdir -p "${css_dir}"
|
||||
|
||||
# Check if theme directory exists
|
||||
local theme_dir="${THEMES_DIR}/${theme}"
|
||||
if [ ! -d "$theme_dir" ]; then
|
||||
echo -e "${RED}Error: Theme directory '$theme_dir' (using THEMES_DIR='${THEMES_DIR}') not found.${NC}"
|
||||
# Decide if this is fatal. For now, just warn and skip CSS copy.
|
||||
return 1 # Return error code
|
||||
fi
|
||||
|
||||
# Check if style.css exists in the theme directory
|
||||
if [ ! -f "$theme_dir/style.css" ]; then
|
||||
echo -e "${RED}Error: style.css not found in theme directory '$theme_dir'.${NC}"
|
||||
# Decide if this is fatal. For now, just warn and skip CSS copy.
|
||||
return 1 # Return error code
|
||||
fi
|
||||
|
||||
# Copy the theme CSS file
|
||||
cp "$theme_dir/style.css" "${css_dir}/style.css"
|
||||
|
||||
echo "CSS file copied to ${css_dir}"
|
||||
}
|
||||
|
||||
# Export functions
|
||||
export -f copy_static_files
|
||||
export -f create_css
|
||||
370
scripts/build/cache.sh
Executable file
370
scripts/build/cache.sh
Executable file
|
|
@ -0,0 +1,370 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Cache Management Utilities
|
||||
# Functions for handling build cache and rebuild checks.
|
||||
#
|
||||
|
||||
# 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
|
||||
# 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 ---
|
||||
|
||||
# Create a hash of the current configuration
|
||||
create_config_hash() {
|
||||
echo "Generating configuration hash..."
|
||||
|
||||
# Dynamically build the config string from BSSG_CONFIG_VARS
|
||||
# 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"
|
||||
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}"
|
||||
done
|
||||
|
||||
# Calculate MD5 hash of the config string
|
||||
local current_hash
|
||||
current_hash=$(echo -n "$config_string" | portable_md5sum | awk '{print $1}')
|
||||
|
||||
# Check against stored hash before writing
|
||||
local stored_hash=""
|
||||
if [ -f "$CONFIG_HASH_FILE" ]; then
|
||||
stored_hash=$(cat "$CONFIG_HASH_FILE")
|
||||
fi
|
||||
|
||||
# Only write if hash changed or file doesn't exist
|
||||
if [ "$current_hash" != "$stored_hash" ]; then
|
||||
echo "$current_hash" > "$CONFIG_HASH_FILE"
|
||||
echo -e "Configuration hash created/updated: ${GREEN}$current_hash${NC}"
|
||||
else
|
||||
echo -e "Configuration hash is up to date: ${GREEN}$current_hash${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if configuration has changed since last build
|
||||
config_has_changed() {
|
||||
# If no hash file exists, configuration has effectively changed
|
||||
if [ ! -f "$CONFIG_HASH_FILE" ]; then
|
||||
# echo "DEBUG_CACHE: No stored config hash found." >&2
|
||||
return 0 # True, config has changed
|
||||
fi
|
||||
|
||||
# Dynamically build the config string from BSSG_CONFIG_VARS
|
||||
# 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"
|
||||
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}"
|
||||
done
|
||||
|
||||
# Create current full hash using the portable wrapper
|
||||
local current_hash
|
||||
current_hash=$(echo -n "$config_string" | portable_md5sum | awk '{print $1}')
|
||||
|
||||
# Read stored hash (which should also be a full hash)
|
||||
local stored_hash=$(cat "$CONFIG_HASH_FILE")
|
||||
|
||||
# Compare hashes
|
||||
if [ "$current_hash" != "$stored_hash" ]; then
|
||||
# echo "DEBUG_CACHE: Config hash mismatch. Current='$current_hash' Stored='$stored_hash'" >&2
|
||||
# DO NOT overwrite the stored hash here. Only check.
|
||||
# echo "$current_hash" > "$CONFIG_HASH_FILE"
|
||||
return 0 # True, config has changed
|
||||
fi
|
||||
|
||||
# echo "DEBUG_CACHE: Config hash matches." >&2 # Optional: Log match
|
||||
return 1 # False, config has not changed
|
||||
}
|
||||
|
||||
# Check if only the theme has changed (not any other config settings)
|
||||
# NOTE: This function might need adjustment if the dynamic hashing reveals
|
||||
# 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
|
||||
return 1 # False, more than theme has changed
|
||||
fi
|
||||
|
||||
# Read the stored theme
|
||||
local stored_theme
|
||||
stored_theme=$(cat "$theme_cache_file")
|
||||
|
||||
# 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"
|
||||
|
||||
# Check if any other config has changed
|
||||
if ! config_has_changed; then
|
||||
echo -e "${GREEN}Only theme has changed, will use cache where possible${NC}"
|
||||
return 0 # True, only theme has changed
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1 # False, more than theme has changed or theme hasn't changed
|
||||
}
|
||||
|
||||
# Clean stale cache entries
|
||||
clean_stale_cache() {
|
||||
# If FORCE_REBUILD is true, delete the entire cache directory and recreate it
|
||||
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/meta"
|
||||
mkdir -p "$CACHE_DIR/content"
|
||||
echo -e "${GREEN}Cache deleted!${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Cleaning stale cache entries...${NC}"
|
||||
|
||||
# Flag to track if any posts were removed
|
||||
local posts_removed=false
|
||||
|
||||
# Get list of all source files from both src and pages directories
|
||||
# IMPORTANT: Requires SRC_DIR, PAGES_DIR to be exported/available
|
||||
local md_files=$(find "${SRC_DIR:-src}" "${PAGES_DIR:-pages}" -type f -name "*.md" 2>/dev/null | sort)
|
||||
|
||||
# Get list of all cache meta files
|
||||
local cache_files=$(find "$CACHE_DIR/meta" -type f 2>/dev/null | sort)
|
||||
|
||||
# Convert markdown file paths to basenames for comparison
|
||||
local md_basenames=""
|
||||
for file in $md_files; do
|
||||
md_basenames="$md_basenames$(basename "$file")\n"
|
||||
done
|
||||
|
||||
# Check each cache file
|
||||
for cache_file in $cache_files; do
|
||||
local cache_basename=$(basename "$cache_file")
|
||||
|
||||
# Check if corresponding markdown file exists
|
||||
if ! echo -e "$md_basenames" | grep -q "^$cache_basename$"; then
|
||||
echo -e "Removing stale cache entry for: ${YELLOW}$cache_basename${NC}"
|
||||
rm -f "$cache_file"
|
||||
|
||||
# Also remove the content cache if it exists
|
||||
if [ -f "$CACHE_DIR/content/$cache_basename" ]; then
|
||||
rm -f "$CACHE_DIR/content/$cache_basename"
|
||||
fi
|
||||
|
||||
# Mark that posts were removed
|
||||
posts_removed=true
|
||||
fi
|
||||
done
|
||||
|
||||
# If any posts were removed, force regeneration of index, tags, archives, etc.
|
||||
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"
|
||||
# Remove the tags flag file as well
|
||||
rm -f "${CACHE_DIR}/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}/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}"
|
||||
}
|
||||
|
||||
# Check if a rebuild is needed based on common conditions
|
||||
common_rebuild_check() {
|
||||
local output_file_to_check="$1"
|
||||
# echo "DEBUG_CACHE: common_rebuild_check called for '$output_file_to_check'" >&2
|
||||
|
||||
# Force rebuild if flag is set
|
||||
if [ "${FORCE_REBUILD:-false}" = true ]; then
|
||||
# echo "DEBUG_CACHE: Force rebuild flag set, returning 0" >&2
|
||||
return 0 # Rebuild needed
|
||||
fi
|
||||
|
||||
# Check if configuration has changed using the pre-calculated status
|
||||
# IMPORTANT: Requires BSSG_CONFIG_CHANGED_STATUS to be exported from main.sh
|
||||
if [ "${BSSG_CONFIG_CHANGED_STATUS:-1}" -eq 0 ]; then # Default to 1 (not changed) if var unset
|
||||
# echo "DEBUG_CACHE: BSSG_CONFIG_CHANGED_STATUS=0, returning 0" >&2
|
||||
return 0 # Rebuild needed
|
||||
fi
|
||||
|
||||
# Check if output file exists. If not, rebuild needed.
|
||||
# Moved this basic check here for clarity.
|
||||
if [ ! -f "$output_file_to_check" ]; then
|
||||
# echo "DEBUG_CACHE: Output file '$output_file_to_check' missing, returning 0" >&2
|
||||
return 0 # Rebuild needed
|
||||
fi
|
||||
|
||||
# Removed template/locale checks here. They are done in file_needs_rebuild now.
|
||||
|
||||
# echo "DEBUG_CACHE: common_rebuild_check returning 1 (passed common checks)" >&2
|
||||
return 1 # Common checks passed (config ok, not forced, output exists)
|
||||
}
|
||||
|
||||
# Check if a rebuild is needed based on file timestamps and templates
|
||||
file_needs_rebuild() {
|
||||
local input_file="$1"
|
||||
local output_file="$2"
|
||||
# echo "DEBUG_CACHE: file_needs_rebuild check for Input='$input_file' Output='$output_file'" >&2
|
||||
|
||||
# Call the common rebuild check function (checks force, config, output existence)
|
||||
common_rebuild_check "$output_file"
|
||||
local common_result=$?
|
||||
# echo "DEBUG_CACHE: common_rebuild_check returned $common_result for '$output_file'" >&2
|
||||
|
||||
# If common conditions already determined we need to rebuild
|
||||
if [ $common_result -eq 0 ]; then
|
||||
return 0 # Rebuild needed
|
||||
fi
|
||||
|
||||
# At this point: Not forced, config OK, output file exists.
|
||||
# Now check against pre-calculated max template/locale time and input file time.
|
||||
|
||||
# IMPORTANT: Assumes get_file_mtime is sourced from utils.sh
|
||||
# IMPORTANT: Requires BSSG_MAX_TEMPLATE_LOCALE_TIME to be exported from main.sh
|
||||
local output_time
|
||||
output_time=$(get_file_mtime "$output_file")
|
||||
|
||||
# Check if templates/locale are newer than output
|
||||
# Default to 0 if variable is unset (should not happen if main.sh ran)
|
||||
if (( ${BSSG_MAX_TEMPLATE_LOCALE_TIME:-0} > output_time )); then
|
||||
# echo "DEBUG_CACHE: Templates/locale newer than output ($BSSG_MAX_TEMPLATE_LOCALE_TIME > $output_time), returning 0" >&2
|
||||
return 0 # Rebuild needed
|
||||
fi
|
||||
|
||||
# Check if input file is newer than output file
|
||||
local input_time
|
||||
input_time=$(get_file_mtime "$input_file")
|
||||
if (( input_time > output_time )); then
|
||||
# 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
|
||||
return 1 # No rebuild needed
|
||||
}
|
||||
|
||||
# Check if tags or indexes need rebuilding
|
||||
indexes_need_rebuild() {
|
||||
# Check common rebuild conditions for the main index file
|
||||
# IMPORTANT: Requires OUTPUT_DIR to be exported/available
|
||||
local main_index="${OUTPUT_DIR:-output}/index.html"
|
||||
|
||||
# Call the common rebuild check function
|
||||
common_rebuild_check "$main_index"
|
||||
local common_result=$?
|
||||
|
||||
# If common conditions already determined we need to rebuild
|
||||
if [ $common_result -eq 0 ]; then
|
||||
return 0 # Rebuild needed
|
||||
fi
|
||||
|
||||
# Check if any of the index files exist and are up to date
|
||||
local index_files=(
|
||||
"${OUTPUT_DIR:-output}/tags/index.html"
|
||||
"${OUTPUT_DIR:-output}/archives/index.html"
|
||||
"${OUTPUT_DIR:-output}/index.html"
|
||||
"${OUTPUT_DIR:-output}/rss.xml"
|
||||
"${OUTPUT_DIR:-output}/sitemap.xml"
|
||||
)
|
||||
|
||||
# Get the latest template/locale time (using main index as baseline)
|
||||
# IMPORTANT: Assumes get_file_mtime is sourced/available
|
||||
local latest_base_time
|
||||
latest_base_time=$(get_file_mtime "$main_index")
|
||||
|
||||
# Check if file_index.txt exists and is newer than the baseline
|
||||
local file_index="$CACHE_DIR/file_index.txt"
|
||||
if [ -f "$file_index" ]; then
|
||||
local file_index_time
|
||||
file_index_time=$(get_file_mtime "$file_index")
|
||||
if (( file_index_time > latest_base_time )); then
|
||||
latest_base_time=$file_index_time
|
||||
echo -e "${YELLOW}Source file list change detected, indexes need rebuild${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if frontmatter_changes_marker exists and is newer than baseline
|
||||
local frontmatter_changes_marker="$CACHE_DIR/frontmatter_changes_marker"
|
||||
if [ -f "$frontmatter_changes_marker" ]; then
|
||||
local marker_time
|
||||
marker_time=$(get_file_mtime "$frontmatter_changes_marker")
|
||||
if (( marker_time > latest_base_time )); then
|
||||
latest_base_time=$marker_time
|
||||
echo -e "${YELLOW}Frontmatter changes detected, indexes need rebuild${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Also check if metadata cache has changed (more robust than marker)
|
||||
local meta_cache_dir="$CACHE_DIR/meta"
|
||||
if [ -d "$meta_cache_dir" ]; then
|
||||
local newest_meta_time=0
|
||||
# Use find with -printf for efficiency if available
|
||||
if find --version >/dev/null 2>&1 && grep -q GNU <<< "$(find --version)"; then
|
||||
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
|
||||
else
|
||||
# Fallback for non-GNU find (less efficient)
|
||||
local meta_files
|
||||
meta_files=$(find "$meta_cache_dir" -type f 2>/dev/null)
|
||||
for meta_file in $meta_files; do
|
||||
local meta_time
|
||||
meta_time=$(get_file_mtime "$meta_file")
|
||||
if (( meta_time > newest_meta_time )); then
|
||||
newest_meta_time=$meta_time
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if (( newest_meta_time > latest_base_time )); then
|
||||
latest_base_time=$newest_meta_time
|
||||
echo -e "${YELLOW}Metadata cache change detected, indexes need rebuild${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if any index file is missing or older than the determined latest relevant time
|
||||
for index_file in "${index_files[@]}"; do
|
||||
# Skip check for archive index if archives disabled
|
||||
# IMPORTANT: Requires ENABLE_ARCHIVES to be exported/available
|
||||
if [[ "$index_file" == *"archives/index.html"* ]] && [ "${ENABLE_ARCHIVES:-true}" != true ]; then
|
||||
continue
|
||||
fi
|
||||
# IMPORTANT: Assumes get_file_mtime is sourced/available
|
||||
local index_file_time
|
||||
index_file_time=$(get_file_mtime "$index_file")
|
||||
if [ ! -f "$index_file" ] || (( index_file_time < latest_base_time )); then
|
||||
return 0 # Rebuild needed
|
||||
fi
|
||||
done
|
||||
|
||||
return 1 # No rebuild needed
|
||||
}
|
||||
|
||||
# --- Cache Functions --- END ---
|
||||
202
scripts/build/cli.sh
Executable file
202
scripts/build/cli.sh
Executable file
|
|
@ -0,0 +1,202 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Command Line Interface Handling
|
||||
# Functions for parsing arguments and showing help.
|
||||
#
|
||||
|
||||
# Parse command line arguments
|
||||
parse_args() {
|
||||
# Initialize deploy override flag
|
||||
CMD_DEPLOY_OVERRIDE="unset"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--config)
|
||||
# Save current settings that may have come from local config
|
||||
local saved_theme="$THEME"
|
||||
local saved_site_title="$SITE_TITLE"
|
||||
local saved_site_url="$SITE_URL"
|
||||
local saved_site_description="$SITE_DESCRIPTION"
|
||||
local saved_author_name="$AUTHOR_NAME"
|
||||
local saved_author_email="$AUTHOR_EMAIL"
|
||||
local saved_clean_output="$CLEAN_OUTPUT"
|
||||
|
||||
# Load the specified config file
|
||||
CONFIG_FILE="$2"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# Reset to defaults before loading new config
|
||||
THEME="default"
|
||||
SITE_TITLE="My Journal"
|
||||
SITE_DESCRIPTION="A personal journal and introspective newspaper"
|
||||
SITE_URL="http://localhost"
|
||||
AUTHOR_NAME="Anonymous"
|
||||
AUTHOR_EMAIL="anonymous@example.com"
|
||||
CLEAN_OUTPUT=false
|
||||
|
||||
# Load new config
|
||||
source "$CONFIG_FILE"
|
||||
echo -e "${GREEN}Configuration loaded from $CONFIG_FILE${NC}"
|
||||
|
||||
# Load local configuration if it exists
|
||||
local local_config="${CONFIG_FILE}.local"
|
||||
if [ -f "$local_config" ]; then
|
||||
source "$local_config"
|
||||
echo -e "${GREEN}Local configuration loaded from $local_config${NC}"
|
||||
else
|
||||
# If new local config doesn't exist, restore settings from previous local config
|
||||
# but only if they weren't set in the new config file
|
||||
if [ "$THEME" = "default" ] && [ "$saved_theme" != "default" ]; then
|
||||
THEME="$saved_theme"
|
||||
fi
|
||||
if [ "$SITE_TITLE" = "My Journal" ] && [ "$saved_site_title" != "My Journal" ]; then
|
||||
SITE_TITLE="$saved_site_title"
|
||||
fi
|
||||
if [ "$SITE_URL" = "http://localhost" ] && [ "$saved_site_url" != "http://localhost" ]; then
|
||||
SITE_URL="$saved_site_url"
|
||||
fi
|
||||
if [ "$AUTHOR_NAME" = "Anonymous" ] && [ "$saved_author_name" != "Anonymous" ]; then
|
||||
AUTHOR_NAME="$saved_author_name"
|
||||
fi
|
||||
if [ "$AUTHOR_EMAIL" = "anonymous@example.com" ] && [ "$saved_author_email" != "anonymous@example.com" ]; then
|
||||
AUTHOR_EMAIL="$saved_author_email"
|
||||
fi
|
||||
if [ "$CLEAN_OUTPUT" = false ] && [ "$saved_clean_output" != false ]; then
|
||||
CLEAN_OUTPUT="$saved_clean_output"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Error: Configuration file '$CONFIG_FILE' not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
--src)
|
||||
SRC_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--pages)
|
||||
PAGES_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--drafts)
|
||||
DRAFTS_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--templates)
|
||||
TEMPLATES_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--themes-dir)
|
||||
THEMES_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--theme)
|
||||
THEME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--static)
|
||||
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
|
||||
CLEAN_OUTPUT="$2"
|
||||
shift 2
|
||||
else
|
||||
CLEAN_OUTPUT=true
|
||||
shift 1
|
||||
fi
|
||||
;;
|
||||
--force-rebuild)
|
||||
FORCE_REBUILD=true
|
||||
shift 1
|
||||
;;
|
||||
--site-title)
|
||||
SITE_TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--site-url)
|
||||
SITE_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--site-description)
|
||||
SITE_DESCRIPTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--author-name)
|
||||
AUTHOR_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--author-email)
|
||||
AUTHOR_EMAIL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--posts-per-page)
|
||||
POSTS_PER_PAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--local-config)
|
||||
# Load the local config file directly
|
||||
if [ -f "$2" ]; then
|
||||
source "$2"
|
||||
echo -e "${GREEN}Local configuration loaded from $2${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Warning: Local config file $2 not found${NC}"
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
--deploy)
|
||||
CMD_DEPLOY_OVERRIDE="true"
|
||||
shift 1
|
||||
;;
|
||||
--no-deploy)
|
||||
CMD_DEPLOY_OVERRIDE="false"
|
||||
shift 1
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Display help information
|
||||
show_help() {
|
||||
echo "BSSG - Bash Static Site Generator"
|
||||
echo ""
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --config FILE Configuration file (default: config.sh)"
|
||||
echo " --src DIR Source directory containing markdown files (default: src)"
|
||||
echo " --pages DIR Pages directory containing markdown/html files (default: pages)"
|
||||
echo " --drafts DIR Drafts directory (default: drafts)"
|
||||
echo " --output DIR Output directory for the generated site (default: output)"
|
||||
echo " --templates DIR Templates directory (default: templates)"
|
||||
echo " --themes-dir DIR Themes parent directory (default: themes)"
|
||||
echo " --theme NAME Theme to use (default: default)"
|
||||
echo " --static DIR Static directory (default: static)"
|
||||
echo " --clean-output Clean output directory before building (default: false)"
|
||||
echo " --force-rebuild Force rebuild of all files regardless of modification time"
|
||||
echo " --site-title TITLE Site title (default: My Journal)"
|
||||
echo " --site-url URL Site URL (default: http://localhost)"
|
||||
echo " --site-description DESC Site description (default: A personal journal)"
|
||||
echo " --author-name NAME Author name (default: Anonymous)"
|
||||
echo " --author-email EMAIL Author email (default: anonymous@example.com)"
|
||||
echo " --posts-per-page NUM Posts per page (default: 10)"
|
||||
echo " --local-config FILE Load local configuration file directly"
|
||||
echo " --deploy Force deployment after successful build (overrides config)"
|
||||
echo " --no-deploy Prevent deployment after build (overrides config)"
|
||||
echo " --help Display this help message and exit"
|
||||
}
|
||||
424
scripts/build/config_loader.sh
Executable file
424
scripts/build/config_loader.sh
Executable file
|
|
@ -0,0 +1,424 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Configuration Loader
|
||||
# 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.
|
||||
CONFIG_FILE="${CONFIG_FILE:-config.sh}"
|
||||
SRC_DIR="${SRC_DIR:-src}"
|
||||
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
|
||||
SITE_LANG="${SITE_LANG:-en}"
|
||||
LOCALE_DIR="${LOCALE_DIR:-locales}"
|
||||
PAGES_DIR="${PAGES_DIR:-pages}"
|
||||
MARKDOWN_PROCESSOR="${MARKDOWN_PROCESSOR:-pandoc}"
|
||||
MARKDOWN_PL_PATH="${MARKDOWN_PL_PATH:-}"
|
||||
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
|
||||
# --- Default Configuration Variables --- END ---
|
||||
|
||||
|
||||
# --- Source Utilities --- START ---
|
||||
# Source utility functions (like print_info, print_error) needed by this script.
|
||||
# Determine the directory of this script
|
||||
CONFIG_LOADER_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
UTILS_SCRIPT="${CONFIG_LOADER_DIR}/utils.sh"
|
||||
|
||||
if [ -f "$UTILS_SCRIPT" ]; then
|
||||
# shellcheck source=utils.sh
|
||||
source "$UTILS_SCRIPT"
|
||||
if ! declare -F print_success > /dev/null; then
|
||||
echo "Error: Failed to source utils.sh correctly - 'print_success' function not found." >&2
|
||||
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."
|
||||
exit 1
|
||||
fi
|
||||
# --- Source Utilities --- END ---
|
||||
|
||||
|
||||
# --- Configuration and Locale Sourcing Logic --- START ---
|
||||
# Load main configuration file (using variable potentially set by CLI)
|
||||
# If CONFIG_FILE wasn't exported by main.sh before sourcing this, it will use the default set above.
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# shellcheck source=/dev/null disable=SC1090,SC1091
|
||||
source "$CONFIG_FILE"
|
||||
print_success "Default configuration loaded from $CONFIG_FILE"
|
||||
else
|
||||
print_warning "Default 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
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# ---- Start Locale Loading ----
|
||||
# Function to print error messages in red (specific to locale loading)
|
||||
# print_error() {
|
||||
# echo -e "${RED}Error: $1${NC}" >&2
|
||||
# }
|
||||
|
||||
# Set the path for the locale file based on SITE_LANG
|
||||
LOCALE_FILE="${LOCALE_DIR}/${SITE_LANG}.sh"
|
||||
DEFAULT_LOCALE_FILE="${LOCALE_DIR}/en.sh"
|
||||
|
||||
# Check if the specific locale file exists
|
||||
if [ -f "$LOCALE_FILE" ]; then
|
||||
print_info "Loading locale: ${SITE_LANG} from ${LOCALE_FILE}"
|
||||
# shellcheck source=/dev/null disable=SC1090
|
||||
. "$LOCALE_FILE"
|
||||
elif [ -f "$DEFAULT_LOCALE_FILE" ]; then
|
||||
print_warning "Locale file '${LOCALE_FILE}' not found. Defaulting to English."
|
||||
print_info "Loading locale: en from ${DEFAULT_LOCALE_FILE}"
|
||||
# shellcheck source=/dev/null disable=SC1090
|
||||
. "$DEFAULT_LOCALE_FILE"
|
||||
else
|
||||
print_error "Default locale file '${DEFAULT_LOCALE_FILE}' not found."
|
||||
print_error "Please ensure '${LOCALE_DIR}/en.sh' exists."
|
||||
exit 1
|
||||
fi
|
||||
# ---- End Locale Loading ----
|
||||
# --- Configuration and Locale Sourcing Logic --- END ---
|
||||
|
||||
|
||||
# --- Define Local Config File Path --- START ---
|
||||
# Define this *after* main config and local override sourcing, in case CONFIG_FILE was changed.
|
||||
# Note: LOCAL_CONFIG_OVERRIDE used during sourcing might differ if CONFIG_FILE changed mid-script,
|
||||
# but we export the path based on the *final* CONFIG_FILE value.
|
||||
LOCAL_CONFIG_FILE="${CONFIG_FILE}.local"
|
||||
export LOCAL_CONFIG_FILE # Export it for other scripts
|
||||
# --- Define Local Config File Path --- END ---
|
||||
|
||||
|
||||
# --- Expand Tilde in Path Variables --- START ---
|
||||
# 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
|
||||
for var_name in "${PATHS_TO_EXPAND[@]}"; do
|
||||
# Get the current value using indirect reference
|
||||
current_value="${!var_name}"
|
||||
expanded_value=""
|
||||
|
||||
# Check if it starts with ~ or ~/
|
||||
if [[ "$current_value" == "~" ]]; then
|
||||
expanded_value="$HOME"
|
||||
elif [[ "$current_value" == "~/"* ]]; then
|
||||
# Replace ~/ with $HOME/
|
||||
expanded_value="$HOME/${current_value#\~/}"
|
||||
fi
|
||||
|
||||
# If expansion occurred, update the variable in the current shell using printf -v
|
||||
if [ -n "$expanded_value" ]; then
|
||||
printf -v "$var_name" '%s' "$expanded_value"
|
||||
# echo "Expanded $var_name to: ${!var_name}" # Debugging
|
||||
fi
|
||||
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
|
||||
# Ensure this list includes ALL variables that could be set in config.sh or config.sh.local
|
||||
# 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 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
|
||||
# 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
|
||||
BSSG_CONFIG_VARS="${BSSG_CONFIG_VARS_ARRAY[@]}"
|
||||
export BSSG_CONFIG_VARS
|
||||
|
||||
# Export all config variables individually as well, for direct use by scripts
|
||||
# The values exported here will be the potentially tilde-expanded ones.
|
||||
export CONFIG_FILE
|
||||
export SRC_DIR
|
||||
export OUTPUT_DIR
|
||||
export TEMPLATES_DIR
|
||||
export THEMES_DIR
|
||||
export STATIC_DIR
|
||||
export THEME
|
||||
export SITE_TITLE
|
||||
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 SITE_LANG
|
||||
export LOCALE_DIR
|
||||
export PAGES_DIR
|
||||
export MARKDOWN_PROCESSOR
|
||||
export MARKDOWN_PL_PATH
|
||||
export ENABLE_ARCHIVES
|
||||
export URL_SLUG_FORMAT
|
||||
export PAGE_URL_FORMAT
|
||||
export DRAFTS_DIR
|
||||
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
|
||||
|
||||
# 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,
|
||||
# but changes to the locale *file* itself are checked by common_rebuild_check in cache.sh.
|
||||
export MSG_HOME MSG_TAGS MSG_ARCHIVES MSG_RSS MSG_PAGES
|
||||
export MSG_PUBLISHED_ON MSG_READING_TIME_TEMPLATE MSG_UPDATED_ON
|
||||
export MSG_PREVIOUS_POST MSG_NEXT_POST
|
||||
export MSG_TAG_PAGE_TITLE MSG_ARCHIVE_PAGE_TITLE
|
||||
export MSG_POSTS_TAGGED_WITH MSG_POSTS_IN_ARCHIVE
|
||||
export MSG_NO_POSTS_FOUND
|
||||
export MSG_MINUTE MSG_MINUTES
|
||||
# Exports needed by generate_index.sh (especially for parallel)
|
||||
export MSG_LATEST_POSTS MSG_BY MSG_PAGINATION_TITLE MSG_PAGE_INFO_TEMPLATE
|
||||
export MSG_MONTH_01 MSG_MONTH_02 MSG_MONTH_03 MSG_MONTH_04
|
||||
export MSG_MONTH_05 MSG_MONTH_06 MSG_MONTH_07 MSG_MONTH_08
|
||||
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 ---
|
||||
431
scripts/build/content.sh
Executable file
431
scripts/build/content.sh
Executable file
|
|
@ -0,0 +1,431 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Content Processing Utilities
|
||||
# Functions for parsing metadata, generating excerpts, and converting markdown.
|
||||
#
|
||||
|
||||
# Source Utilities if needed by functions below
|
||||
# shellcheck source=utils.sh disable=SC1091
|
||||
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from content.sh"; exit 1; }
|
||||
|
||||
# --- Content Functions --- START ---
|
||||
|
||||
# Parse metadata from a markdown file (uses cache)
|
||||
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 file_content frontmatter
|
||||
file_content=$(ram_mode_get_content "$file")
|
||||
frontmatter=$(printf '%s\n' "$file_content" | awk '
|
||||
BEGIN { in_fm = 0; found_fm = 0; }
|
||||
/^---$/ {
|
||||
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
|
||||
if (in_fm) { exit; }
|
||||
}
|
||||
in_fm { print; }
|
||||
')
|
||||
if [ -n "$frontmatter" ]; then
|
||||
value=$(printf '%s\n' "$frontmatter" | grep -m 1 "^$field:[[:space:]]*" | cut -d ':' -f 2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
fi
|
||||
echo "$value"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# IMPORTANT: Assumes CACHE_DIR is exported/available
|
||||
local cache_file="${CACHE_DIR:-.bssg_cache}/meta/$(basename "$file")"
|
||||
|
||||
# Get locks for cache access
|
||||
# IMPORTANT: Assumes lock_file/unlock_file are sourced/available
|
||||
lock_file "$cache_file"
|
||||
|
||||
# Create metadata cache if it doesn't exist or is older than source
|
||||
if [ ! -f "$cache_file" ] || [ "$file" -nt "$cache_file" ]; then
|
||||
# Use grep -n and sed to extract frontmatter block efficiently
|
||||
local frontmatter_lines
|
||||
frontmatter_lines=$(grep -n "^---$" "$file" | cut -d: -f1)
|
||||
local start_line=$(echo "$frontmatter_lines" | head -n 1)
|
||||
local end_line=$(echo "$frontmatter_lines" | head -n 2 | tail -n 1)
|
||||
|
||||
# Check if valid start and end lines were found
|
||||
if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then
|
||||
# Extract frontmatter, remove leading/trailing whitespace, and save to cache
|
||||
sed -n "$((start_line+1)),$((end_line-1))p" "$file" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$cache_file"
|
||||
else
|
||||
# No valid frontmatter found, create empty cache file
|
||||
> "$cache_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Read from cache if it exists
|
||||
if [ -f "$cache_file" ]; then
|
||||
# Use grep -m 1 for efficiency
|
||||
value=$(grep -m 1 "^$field:[[:space:]]*" "$cache_file" | cut -d ':' -f 2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
fi
|
||||
|
||||
# Release lock
|
||||
unlock_file "$cache_file"
|
||||
|
||||
# Fallback to direct file read ONLY if cache read failed (should be rare)
|
||||
if [ -z "$value" ]; then
|
||||
local frontmatter_lines
|
||||
frontmatter_lines=$(grep -n "^---$" "$file" | cut -d: -f1)
|
||||
local start_line=$(echo "$frontmatter_lines" | head -n 1)
|
||||
local end_line=$(echo "$frontmatter_lines" | head -n 2 | tail -n 1)
|
||||
|
||||
if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then
|
||||
value=$(sed -n "$((start_line+1)),$((end_line-1))p" "$file" | grep -m 1 "^$field:[[:space:]]*" | cut -d ':' -f 2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
# 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
|
||||
echo "ERROR_FILE_NOT_FOUND"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Flag to track whether frontmatter has changed
|
||||
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
|
||||
# Read from cache file (optimized - read once)
|
||||
echo "$(cat "$metadata_cache_file")"
|
||||
return 0
|
||||
else
|
||||
# If we're regenerating metadata, assume it changed for index rebuilding purposes
|
||||
frontmatter_changed=true
|
||||
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=""
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
local parsed_data
|
||||
local awk_frontmatter_parser
|
||||
awk_frontmatter_parser=$(cat <<'EOF'
|
||||
BEGIN {
|
||||
in_fm = 0;
|
||||
found_fm = 0;
|
||||
# Define default empty values
|
||||
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; }
|
||||
if (in_fm) { in_fm = 0; exit; } # Exit awk early after frontmatter
|
||||
}
|
||||
in_fm {
|
||||
# Match key: value, trim whitespace
|
||||
local key value
|
||||
if (match($0, /^([^:]+):[[:space:]]*(.*[^[:space:]])[[:space:]]*$/)) {
|
||||
key = substr($0, RSTART, RLENGTH);
|
||||
# Extract key part
|
||||
match(key, /^[^:]+/);
|
||||
key_str = substr(key, RSTART, RLENGTH);
|
||||
# Extract value part
|
||||
match(key, /:[[:space:]]*(.*)$/);
|
||||
value = substr(key, RSTART + 1, RLENGTH -1 ); # +1/-1 to skip the :
|
||||
# Trim spaces from key and value
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key_str);
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value);
|
||||
key = tolower(key_str);
|
||||
|
||||
# Handle quoted strings (optional, basic handling)
|
||||
if ( (match(value, /^"(.*)"$/) || match(value, /^\'(.*)\'$/)) && length(value) > 1 ) {
|
||||
value = substr(value, 2, length(value)-2);
|
||||
}
|
||||
vars[key] = value;
|
||||
}
|
||||
}
|
||||
END {
|
||||
# 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"];
|
||||
}
|
||||
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"
|
||||
|
||||
else
|
||||
echo "Warning: Unknown file type '$file' for metadata extraction." >&2
|
||||
fi
|
||||
|
||||
# Fallbacks for missing metadata
|
||||
if [ -z "$title" ]; then
|
||||
title=$(basename "$file" | sed 's/\\\\.[^.]*$//')
|
||||
fi
|
||||
if [ -z "$date" ]; then
|
||||
local file_mtime=$(get_file_mtime "$file")
|
||||
date=$(format_date_from_timestamp "$file_mtime")
|
||||
fi
|
||||
# Fallback for lastmod: use date if lastmod is empty
|
||||
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
|
||||
# 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"
|
||||
|
||||
# Check if there was a previous metadata file and compare
|
||||
if ! $ram_mode_active && [ -f "$metadata_cache_file" ]; then
|
||||
local old_metadata=$(cat "$metadata_cache_file")
|
||||
if [ "$old_metadata" != "$new_metadata" ]; then
|
||||
frontmatter_changed=true
|
||||
fi
|
||||
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
|
||||
|
||||
# If frontmatter has changed, update the marker file's timestamp
|
||||
if ! $ram_mode_active && $frontmatter_changed; then
|
||||
touch "$frontmatter_changes_marker"
|
||||
fi
|
||||
|
||||
# Return the metadata as pipe-separated values
|
||||
echo "$new_metadata"
|
||||
}
|
||||
|
||||
# Generate an excerpt from post content
|
||||
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)
|
||||
|
||||
if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then
|
||||
# Stream content after frontmatter
|
||||
raw_content_stream=$(tail -n +$((end_line + 1)) "$file")
|
||||
else
|
||||
# No valid frontmatter, stream the whole file
|
||||
raw_content_stream=$(cat "$file")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sanitize and extract the first non-empty paragraph/line
|
||||
# Apply sanitization steps sequentially
|
||||
local sanitized_content
|
||||
sanitized_content=$(echo "$raw_content_stream" | \
|
||||
# Remove code blocks (``` and indented)
|
||||
awk '/^```/{flag=!flag;next} !flag;' | grep -v '^```' | \
|
||||
grep -v '^ ' | \
|
||||
# Remove images, links, headings, hr, blockquotes
|
||||
sed -E 's/!\[([^]]*)\]\([^)]*\)//g' | \
|
||||
sed -E 's/\[([^]]+)\]\(([^)]+)\)/\1/g' | \
|
||||
sed 's/^#\{1,6\} //' | \
|
||||
grep -v '^---\+$' | \
|
||||
grep -v '^\*\*\*\+$' | \
|
||||
grep -v '^___\+$' | \
|
||||
sed 's/^> //' | \
|
||||
# Remove list markers
|
||||
sed -E 's/^\* |^- |^[0-9]+\. //' | \
|
||||
# Remove inline markdown: bold, italics, strikethrough, code
|
||||
sed -E 's/\*\*([^*]+)\*\*/\1/g; s/__([^_]+)__/\1/g' | \
|
||||
sed -E 's/\*([^*]+)\*/\1/g; s/_([^_]+)_/\1/g' | \
|
||||
sed -E 's/~~([^~]+)~~/\1/g' | \
|
||||
sed -E 's/`([^`]+)`/\1/g' | \
|
||||
# Remove HTML tags
|
||||
sed -E 's/<[^>]*>//g' | \
|
||||
# Escape basic HTML entities (ampersand, less than, greater than)
|
||||
sed 's/&/\&/g; s/</\</g; s/>/\>/g' | \
|
||||
# Remove extra blank lines
|
||||
awk 'NF {p=1} p' | \
|
||||
# Get the first non-empty line (first paragraph)
|
||||
awk 'NF {print; exit}'
|
||||
)
|
||||
|
||||
# Truncate to max length using dd for portability
|
||||
local excerpt
|
||||
if [ -z "$sanitized_content" ]; then
|
||||
excerpt=""
|
||||
else
|
||||
# Use dd: bs=1 reads byte by byte, count limits the total bytes
|
||||
# 2>/dev/null suppresses dd's status messages
|
||||
excerpt=$(echo "$sanitized_content" | dd bs=1 count="$max_length" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Add ellipsis if truncated
|
||||
if [ ${#sanitized_content} -gt $max_length ]; then
|
||||
excerpt+="..."
|
||||
fi
|
||||
|
||||
# Ensure description is not empty after all this
|
||||
if [ -z "$excerpt" ]; then
|
||||
# Fallback: use the filename if excerpt is still empty
|
||||
excerpt=$(basename "$file" | sed 's/\.[^.]*$//')
|
||||
fi
|
||||
|
||||
echo "$excerpt"
|
||||
}
|
||||
|
||||
# Convert provided markdown content string to HTML
|
||||
convert_markdown_to_html() {
|
||||
local content="$1" # Expect markdown content as the first argument
|
||||
local html_content=""
|
||||
|
||||
# IMPORTANT: Assumes MARKDOWN_PROCESSOR, MARKDOWN_PL_PATH are exported/available
|
||||
# IMPORTANT: Assumes required processor (pandoc, cmark, perl) is installed
|
||||
|
||||
if [ "${MARKDOWN_PROCESSOR:-pandoc}" = "pandoc" ]; then
|
||||
if ! html_content=$(echo "$content" | pandoc -f markdown -t html); then
|
||||
echo -e "${RED}Error: Markdown conversion failed using pandoc.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
elif [ "$MARKDOWN_PROCESSOR" = "commonmark" ]; then
|
||||
if ! html_content=$(echo "$content" | cmark); then
|
||||
echo -e "${RED}Error: Markdown conversion failed using cmark.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
elif [ "$MARKDOWN_PROCESSOR" = "markdown.pl" ]; then
|
||||
# Preprocess content to handle fenced code blocks for markdown.pl
|
||||
local preprocessed_content="$content"
|
||||
# Handle fenced code blocks (``` and ~~~) -> indented
|
||||
# Requires awk
|
||||
if command -v awk &> /dev/null; then
|
||||
preprocessed_content=$(printf '%s' "$preprocessed_content" | awk '
|
||||
BEGIN { in_code = 0; }
|
||||
/^```[a-zA-Z0-9]*$/ || /^~~~[a-zA-Z0-9]*$/ { if (!in_code) { in_code = 1; print ""; next; } }
|
||||
/^```$/ || /^~~~$/ { if (in_code) { in_code = 0; print ""; next; } }
|
||||
{ if (in_code) { print " " $0; } else { print $0; } }
|
||||
')
|
||||
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"
|
||||
fi
|
||||
|
||||
# Ensure MARKDOWN_PL_PATH is set and executable
|
||||
if [ -z "$MARKDOWN_PL_PATH" ] || [ ! -x "$MARKDOWN_PL_PATH" ]; then
|
||||
echo -e "${RED}Error: MARKDOWN_PL_PATH ('$MARKDOWN_PL_PATH') not set or not executable.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Use printf to pipe content to avoid issues with content starting with -
|
||||
if ! html_content=$(printf '%s' "$preprocessed_content" | perl "$MARKDOWN_PL_PATH"); then
|
||||
echo -e "${RED}Error: Markdown conversion failed using markdown.pl.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Error: Unknown MARKDOWN_PROCESSOR ('$MARKDOWN_PROCESSOR'). Cannot convert content.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$html_content" # Output the result
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Content Functions --- END ---
|
||||
149
scripts/build/deps.sh
Normal file
149
scripts/build/deps.sh
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Dependency Checking
|
||||
# Checks for required tools and sets up environment variables.
|
||||
#
|
||||
|
||||
# Portable md5sum wrapper
|
||||
portable_md5sum() {
|
||||
if command -v md5sum > /dev/null 2>&1; then
|
||||
# Linux: md5sum command exists
|
||||
md5sum "$@"
|
||||
elif [[ "$(uname)" == "OpenBSD" ]] || [[ "$(uname)" == "NetBSD" ]]; then
|
||||
# OpenBSD/NetBSD: md5 command without -r or -q
|
||||
# Output format: "MD5 (filename) = hash" -> Need "hash filename"
|
||||
if [ $# -eq 0 ] || [ "$1" = "-" ]; then
|
||||
# Handle stdin: OpenBSD md5 outputs just the hash directly.
|
||||
# Read the hash (field 1) and append " -" to match md5sum format.
|
||||
md5 | awk '{print $1 " -"}'
|
||||
else
|
||||
# Handle files: MD5 (file) = hash -> hash file
|
||||
md5 "$@" | awk '{print $4 " " $2}' | sed 's/[()]//g'
|
||||
fi
|
||||
elif command -v md5 > /dev/null 2>&1; then
|
||||
# macOS / FreeBSD: Use md5 -r which outputs "hash filename"
|
||||
# This matches the old script's alias logic for macOS
|
||||
md5 -r "$@"
|
||||
else
|
||||
echo -e "${RED}Error: Neither md5sum nor md5 command found.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for required tools
|
||||
check_dependencies() {
|
||||
local missing_deps=0
|
||||
|
||||
# Array of required commands
|
||||
local deps=("awk" "sed" "grep" "find" "date")
|
||||
|
||||
# Check if a usable MD5 command exists (md5sum or md5)
|
||||
if ! command -v md5sum &> /dev/null && ! command -v md5 &> /dev/null; then
|
||||
echo -e "${RED}Error: Neither 'md5sum' nor 'md5' command found. Cannot calculate checksums.${NC}"
|
||||
missing_deps=1
|
||||
# No need to add md5sum/md5 to the deps array, as we've already verified one exists
|
||||
fi
|
||||
|
||||
# Add markdown processor dependency based on configuration
|
||||
# IMPORTANT: Config variables like MARKDOWN_PROCESSOR must be exported/available
|
||||
if [ "${MARKDOWN_PROCESSOR:-pandoc}" = "pandoc" ]; then # Default to pandoc if unset
|
||||
deps+=("pandoc")
|
||||
elif [ "$MARKDOWN_PROCESSOR" = "commonmark" ]; then
|
||||
# Check if cmark (commonmark implementation) is installed
|
||||
if ! command -v cmark &> /dev/null; then
|
||||
echo -e "${RED}Error: commonmark (cmark) is not installed${NC}"
|
||||
echo -e "${YELLOW}Tip: Install commonmark/cmark from https://github.com/commonmark/cmark${NC}"
|
||||
missing_deps=1
|
||||
fi
|
||||
# Add cmark even if missing, so the main loop reports it
|
||||
deps+=("cmark")
|
||||
elif [ "$MARKDOWN_PROCESSOR" = "markdown.pl" ]; then
|
||||
# Check if markdown.pl or Markdown.pl exists in PATH or current directory
|
||||
if command -v markdown.pl &> /dev/null; then
|
||||
MARKDOWN_PL_PATH="markdown.pl"
|
||||
elif command -v Markdown.pl &> /dev/null; then
|
||||
MARKDOWN_PL_PATH="Markdown.pl"
|
||||
elif [ -f "./markdown.pl" ] && [ -x "./markdown.pl" ]; then
|
||||
MARKDOWN_PL_PATH="./markdown.pl"
|
||||
elif [ -f "./Markdown.pl" ] && [ -x "./Markdown.pl" ]; then
|
||||
MARKDOWN_PL_PATH="./Markdown.pl"
|
||||
else
|
||||
echo -e "${RED}Error: markdown.pl is not installed or not in PATH${NC}"
|
||||
echo -e "${YELLOW}Tip: You can place markdown.pl in the BSSG directory and make it executable${NC}"
|
||||
missing_deps=1
|
||||
fi
|
||||
# Add the specific path if found, otherwise add the generic name for error reporting
|
||||
deps+=("${MARKDOWN_PL_PATH:-markdown.pl}")
|
||||
else
|
||||
echo -e "${RED}Error: Invalid MARKDOWN_PROCESSOR value ('$MARKDOWN_PROCESSOR'). Use 'pandoc', 'commonmark', or 'markdown.pl'.${NC}"
|
||||
# No dependency to add, but set missing_deps
|
||||
missing_deps=1
|
||||
fi
|
||||
|
||||
echo "Checking dependencies..."
|
||||
|
||||
for dep in "${deps[@]}"; do
|
||||
if ! command -v "$dep" &> /dev/null; then
|
||||
# Avoid redundant error for cmark/markdown.pl already printed
|
||||
if [[ "$dep" != "cmark" && "$dep" != "markdown.pl" && "$dep" != "./markdown.pl" && "$dep" != "./Markdown.pl" ]]; then
|
||||
echo -e "${RED}Error: Required command '$dep' is not installed${NC}"
|
||||
missing_deps=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for GNU parallel
|
||||
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
|
||||
echo -e "${GREEN}GNU parallel found! Using parallel processing.${NC}"
|
||||
export HAS_PARALLEL=true
|
||||
else
|
||||
echo -e "${YELLOW}GNU parallel not found. Using sequential processing.${NC}"
|
||||
export HAS_PARALLEL=false
|
||||
fi
|
||||
|
||||
if [ $missing_deps -eq 1 ]; then
|
||||
echo -e "${RED}Please install the missing dependencies and try again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}All dependencies satisfied!${NC}"
|
||||
|
||||
# Call directory check after dependency check
|
||||
check_directories || { echo -e "${RED}Error: Directory check failed.${NC}"; exit 1; }
|
||||
}
|
||||
|
||||
# Check if src, templates directories exist and create output directory
|
||||
check_directories() {
|
||||
echo "Checking required directories..."
|
||||
if [ ! -d "$SRC_DIR" ]; then
|
||||
echo -e "${RED}Error: Source directory '$SRC_DIR' does not exist${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$TEMPLATES_DIR" ]; then
|
||||
echo -e "${RED}Error: Templates directory '$TEMPLATES_DIR' does not exist${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$THEMES_DIR" ]; then
|
||||
echo -e "${RED}Error: Themes directory '$THEMES_DIR' does not exist${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Note: Output directory and cache directory creation is handled in main.sh initial setup.
|
||||
# We just check the source/template dirs here.
|
||||
|
||||
echo -e "${GREEN}Source/Template/Theme directories verified!${NC}"
|
||||
}
|
||||
|
||||
# Export functions
|
||||
export -f check_dependencies
|
||||
export -f check_directories
|
||||
export -f portable_md5sum
|
||||
|
||||
# Define and export the MD5 command variable to use the portable function
|
||||
MD5_CMD="portable_md5sum"
|
||||
export MD5_CMD
|
||||
887
scripts/build/generate_archives.sh
Executable file
887
scripts/build/generate_archives.sh
Executable file
|
|
@ -0,0 +1,887 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Archive Page Generation
|
||||
# Handles the creation of yearly and monthly archive pages and the main archive index.
|
||||
#
|
||||
|
||||
# Source dependencies
|
||||
# shellcheck source=utils.sh disable=SC1091
|
||||
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_archives.sh"; exit 1; }
|
||||
# shellcheck source=cache.sh disable=SC1091
|
||||
source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_archives.sh"; exit 1; }
|
||||
|
||||
# ==============================================================================
|
||||
# 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() {
|
||||
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."
|
||||
# 2. Config changed
|
||||
elif [ "$BSSG_CONFIG_CHANGED_STATUS" -eq 0 ]; then
|
||||
rebuild_reason="Global config changed."
|
||||
# 3. Output index page missing
|
||||
elif [ ! -f "$archives_index_page" ]; then
|
||||
rebuild_reason="Archive index page '$archives_index_page' missing."
|
||||
# 4. Templates changed (header/footer are global dependencies)
|
||||
elif [ -f "$archives_index_page" ]; then
|
||||
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 output_time=$(get_file_mtime "$archives_index_page")
|
||||
local header_time=$(get_file_mtime "$header_template")
|
||||
local footer_time=$(get_file_mtime "$footer_template")
|
||||
if (( header_time > output_time )) || (( footer_time > output_time )); then
|
||||
rebuild_reason="Header or footer template changed."
|
||||
fi
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
# Generate the main archives index page (archives/index.html)
|
||||
_generate_main_archive_index() {
|
||||
local archive_index_file="$CACHE_DIR/archive_index.txt"
|
||||
local archives_index_page="$OUTPUT_DIR/archives/index.html"
|
||||
|
||||
echo "Generating main archive index page: $archives_index_page" >&2 # Debug
|
||||
|
||||
# Create archives directory if it doesn't exist
|
||||
mkdir -p "$(dirname "$archives_index_page")"
|
||||
|
||||
# Get unique years sorted descending
|
||||
local unique_years=""
|
||||
if [ -f "$archive_index_file" ] && [ -s "$archive_index_file" ]; then
|
||||
unique_years=$(cut -d'|' -f1 "$archive_index_file" | sort -nr | uniq)
|
||||
fi
|
||||
|
||||
# Generate header
|
||||
local header_content="$HEADER_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/"} # 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"}
|
||||
|
||||
|
||||
# Generate footer
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
|
||||
footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"}
|
||||
|
||||
# Create the archives index page content
|
||||
{
|
||||
echo "$header_content"
|
||||
echo "<h1>${MSG_ARCHIVES:-"Archives"}</h1>"
|
||||
echo "<div class=\"archives-list year-list\">"
|
||||
|
||||
# Loop through years (Existing logic for Year/Month links)
|
||||
echo "$unique_years" | while IFS= read -r year; do
|
||||
[ -z "$year" ] && continue
|
||||
|
||||
local year_url
|
||||
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\">"
|
||||
|
||||
# Get unique months for this year, sorted descending by month number
|
||||
local months_in_year=""
|
||||
if [ -f "$archive_index_file" ] && [ -s "$archive_index_file" ]; then
|
||||
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
|
||||
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
|
||||
|
||||
local month_idx_formatted=$(printf "%02d" "$((10#$month))")
|
||||
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/")
|
||||
|
||||
# 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>"
|
||||
done
|
||||
|
||||
echo " </ul>" # End of month-list-detailed
|
||||
done
|
||||
|
||||
echo "</div>" # End of year-list div
|
||||
|
||||
echo "$footer_content"
|
||||
|
||||
} > "$archives_index_page"
|
||||
|
||||
echo -e "Generated ${GREEN}$archives_index_page${NC}"
|
||||
}
|
||||
|
||||
# Generate the index page for a specific year (archives/YYYY/index.html)
|
||||
# This is called only if at least one month within the year needs updating.
|
||||
_generate_year_index() {
|
||||
local year="$1"
|
||||
local archive_index_file="$CACHE_DIR/archive_index.txt"
|
||||
local year_index_page="$OUTPUT_DIR/archives/$year/index.html"
|
||||
|
||||
echo "Generating year index page: $year_index_page" >&2 # Debug
|
||||
|
||||
mkdir -p "$(dirname "$year_index_page")"
|
||||
|
||||
# Generate header
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
local year_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $year"
|
||||
local year_archive_rel_url="/archives/$year/"
|
||||
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
header_content=${header_content//\{\{page_title\}\}/"$year_page_title"}
|
||||
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\}\}/"$year_archive_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\}\}/}
|
||||
# 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"}
|
||||
|
||||
# Generate footer
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
|
||||
footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"}
|
||||
|
||||
# Create the year index page content
|
||||
{
|
||||
echo "$header_content"
|
||||
echo "<h1>$year_page_title</h1>"
|
||||
echo "<ul class=\"month-list\">"
|
||||
|
||||
# Get unique months for this year, sorted descending by month number
|
||||
local months_in_year=""
|
||||
if [ -f "$archive_index_file" ] && [ -s "$archive_index_file" ]; then
|
||||
months_in_year=$(grep "^$year|" "$archive_index_file" 2>/dev/null | cut -d'|' -f2,3 | sort -t'|' -k1,1nr | uniq)
|
||||
fi
|
||||
|
||||
# Add month links
|
||||
echo "$months_in_year" | while IFS= read -r month_line; do
|
||||
local month month_name
|
||||
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
|
||||
month_post_count=$(grep -c "^$year|$month|" "$archive_index_file" 2>/dev/null || echo 0)
|
||||
fi
|
||||
|
||||
local month_idx_formatted=$(printf "%02d" "$((10#$month))")
|
||||
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 "$footer_content"
|
||||
|
||||
} > "$year_index_page"
|
||||
|
||||
echo -e "Generated ${GREEN}$year_index_page${NC}"
|
||||
}
|
||||
|
||||
|
||||
# Generate the index page for a specific month (archives/YYYY/MM/index.html)
|
||||
process_single_month() {
|
||||
local year="$1"
|
||||
local month_num="$2" # Expecting MM format (01-12)
|
||||
local archive_index_file="$CACHE_DIR/archive_index.txt"
|
||||
local month_index_page="$OUTPUT_DIR/archives/$year/$month_num/index.html"
|
||||
|
||||
echo "Processing month archive: $year-$month_num -> $month_index_page" >&2 # Debug
|
||||
|
||||
mkdir -p "$(dirname "$month_index_page")"
|
||||
|
||||
# Get month name (from locale or fallback)
|
||||
local month_name_var="MSG_MONTH_${month_num}"
|
||||
local month_name=${!month_name_var}
|
||||
if [[ -z "$month_name" ]]; then # Fallback using date command
|
||||
local input_date_for_month_name="${year}-${month_num}-01"
|
||||
if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == *bsd* ]]; then
|
||||
month_name=$(date -j -f "%Y-%m-%d" "$input_date_for_month_name" "+%B" 2>/dev/null)
|
||||
else
|
||||
month_name=$(date -d "$input_date_for_month_name" "+%B" 2>/dev/null)
|
||||
fi
|
||||
[[ -z "$month_name" ]] && month_name="Month $month_num" # Final fallback
|
||||
fi
|
||||
|
||||
# Generate header
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
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"}
|
||||
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\}\}/"$month_archive_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\}\}/}
|
||||
# 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"}
|
||||
|
||||
# Generate footer
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
|
||||
footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"}
|
||||
|
||||
# Create the month index page content
|
||||
{
|
||||
echo "$header_content"
|
||||
echo "<h1>$month_page_title</h1>"
|
||||
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
|
||||
# --- Start: Card Generation Logic (copied from generate_tags.sh) ---
|
||||
local post_url post_year post_month post_day url_path
|
||||
|
||||
# Replicate URL generation logic from generate_posts.sh
|
||||
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
|
||||
# Fallback if date parsing fails (should be rare with indexing)
|
||||
post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d)
|
||||
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}"
|
||||
|
||||
# Ensure relative URL starts with / and ends with /
|
||||
post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')"
|
||||
# Prepend SITE_URL for the final href
|
||||
post_url="${SITE_URL}${post_url}"
|
||||
|
||||
# Format date
|
||||
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=$(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>
|
||||
EOF
|
||||
|
||||
if [ -n "$image" ]; then
|
||||
local image_url=$(fix_url "$image")
|
||||
local alt_text="${image_caption:-$title}"
|
||||
local figcaption_content="${image_caption:-$title}"
|
||||
# Use cat heredoc for figure structure
|
||||
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
|
||||
# Use cat heredoc for summary structure
|
||||
cat << EOF
|
||||
<div class="summary">
|
||||
$description
|
||||
</div>
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Use cat heredoc for closing article tag
|
||||
cat << EOF
|
||||
</article>
|
||||
EOF
|
||||
# --- End: Card Generation Logic ---
|
||||
done
|
||||
|
||||
# Close the div instead of ul
|
||||
echo "</div>"
|
||||
echo "$footer_content"
|
||||
|
||||
} > "$month_index_page"
|
||||
|
||||
echo -e "Generated ${GREEN}$month_index_page${NC}"
|
||||
|
||||
}
|
||||
|
||||
# Wrapper function for parallel processing to handle argument parsing
|
||||
_process_single_month_parallel_wrapper() {
|
||||
local line="$1"
|
||||
local year month_num
|
||||
# Parse the input line
|
||||
IFS='|' read -r year month_num <<< "$line"
|
||||
# Call the original function with parsed arguments
|
||||
process_single_month "$year" "$month_num"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 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"
|
||||
|
||||
# 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}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# --- Step 1: Handle Main Archive Index Page ---
|
||||
if _check_archive_index_rebuild_needed; then
|
||||
_generate_main_archive_index
|
||||
else
|
||||
echo -e "${GREEN}Main archive index page is up to date, skipping generation.${NC}"
|
||||
fi
|
||||
|
||||
# --- Step 2: Handle Monthly and Yearly Pages based on Affected Months ---
|
||||
|
||||
# Trim leading/trailing whitespace from AFFECTED_ARCHIVE_MONTHS just in case
|
||||
AFFECTED_ARCHIVE_MONTHS=$(echo "$AFFECTED_ARCHIVE_MONTHS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
if [ -z "$AFFECTED_ARCHIVE_MONTHS" ]; then
|
||||
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
|
||||
|
||||
# --- Prepare for potential parallel processing ---
|
||||
local affected_years=()
|
||||
local affected_months_list=()
|
||||
local unique_years_map # Use associative array to track years needing update
|
||||
|
||||
# Bash 4+ required for associative arrays
|
||||
if (( BASH_VERSINFO[0] < 4 )); then
|
||||
echo -e "${RED}Error: Bash 4+ required for optimized archive generation.${NC}" >&2
|
||||
# Fallback to non-parallel or exit? For now, just error out.
|
||||
return 1
|
||||
fi
|
||||
declare -A unique_years_map # Associative array requires Bash 4+
|
||||
|
||||
for month_pair in $AFFECTED_ARCHIVE_MONTHS; do
|
||||
local year month_num
|
||||
IFS='|' read -r year month_num <<< "$month_pair"
|
||||
|
||||
# Ensure month has leading zero for consistency
|
||||
month_num=$(printf "%02d" "$((10#$month_num))")
|
||||
|
||||
# Add to list for month processing
|
||||
affected_months_list+=("$year|$month_num")
|
||||
|
||||
# Mark year as needing its index page updated
|
||||
unique_years_map["$year"]=1
|
||||
done
|
||||
|
||||
# Extract unique years that need updating
|
||||
affected_years=("${!unique_years_map[@]}")
|
||||
|
||||
if [ ${#affected_months_list[@]} -eq 0 ]; then
|
||||
echo -e "${GREEN}No valid affected months parsed. Nothing to do.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Processing ${#affected_months_list[@]} affected month(s) across ${#affected_years[@]} year(s)." >&2 # Debug
|
||||
|
||||
# --- Generate Year Index Pages (Sequentially or Parallel?) ---
|
||||
# Generating year pages is usually fast. Let's do it sequentially first.
|
||||
echo "Generating/Updating affected year index pages..." >&2 # Debug
|
||||
for year in "${affected_years[@]}"; do
|
||||
_generate_year_index "$year"
|
||||
done
|
||||
echo "Affected year index pages updated." >&2 # Debug
|
||||
|
||||
# --- Generate Monthly Pages (Parallel if available) ---
|
||||
echo "Generating/Updating affected monthly index pages..." >&2 # Debug
|
||||
if [ "$HAS_PARALLEL" = true ]; then
|
||||
# --- Parallel processing ---
|
||||
echo -e "${GREEN}Using GNU parallel to process monthly archive pages${NC}"
|
||||
|
||||
# Determine number of cores/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 detected cores
|
||||
if [ $jobs -gt ${#affected_months_list[@]} ]; then jobs=${#affected_months_list[@]}; fi # Don't use more jobs than items
|
||||
|
||||
# Explicitly re-source utils.sh just in case environment is lost (shouldn't be needed if main.sh exports correctly, but safer)
|
||||
# shellcheck source=utils.sh disable=SC1091
|
||||
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to re-source utils.sh for parallel export"; return 1; }
|
||||
|
||||
# Export necessary variables and the processing function
|
||||
export CACHE_DIR OUTPUT_DIR SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME HEADER_TEMPLATE FOOTER_TEMPLATE MSG_ARCHIVES_FOR MSG_MONTH_01 MSG_MONTH_02 MSG_MONTH_03 MSG_MONTH_04 MSG_MONTH_05 MSG_MONTH_06 MSG_MONTH_07 MSG_MONTH_08 MSG_MONTH_09 MSG_MONTH_10 MSG_MONTH_11 MSG_MONTH_12 URL_SLUG_FORMAT DATE_FORMAT TIMEZONE SHOW_TIMEZONE
|
||||
# Export needed functions (utils.sh sourced above, cache.sh sourced at top of main script)
|
||||
export -f process_single_month format_date fix_url get_file_mtime
|
||||
export -f _process_single_month_parallel_wrapper # Export the wrapper function
|
||||
|
||||
# Call the wrapper function, passing the whole line {}
|
||||
printf "%s\n" "${affected_months_list[@]}" | \
|
||||
parallel --jobs "$jobs" --will-cite --no-notice _process_single_month_parallel_wrapper {} || { echo -e "${RED}Parallel monthly archive processing failed.${NC}"; return 1; }
|
||||
|
||||
# Consider unexporting? Not strictly necessary as script exits soon.
|
||||
else
|
||||
# --- Sequential processing ---
|
||||
echo -e "${YELLOW}Using sequential processing for monthly archive pages${NC}"
|
||||
for month_pair in "${affected_months_list[@]}"; do
|
||||
local year month_num
|
||||
IFS='|' read -r year month_num <<< "$month_pair"
|
||||
process_single_month "$year" "$month_num"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Affected monthly index pages updated." >&2 # Debug
|
||||
echo -e "${GREEN}Archive page processing complete.${NC}"
|
||||
}
|
||||
|
||||
# Make the function available for sourcing
|
||||
export -f generate_archive_pages
|
||||
682
scripts/build/generate_authors.sh
Normal file
682
scripts/build/generate_authors.sh
Normal file
|
|
@ -0,0 +1,682 @@
|
|||
#!/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() {
|
||||
echo -e "${YELLOW}Processing author pages${NC}${ENABLE_AUTHOR_RSS:+" and RSS feeds"}...${NC}"
|
||||
|
||||
local authors_index_data
|
||||
authors_index_data=$(ram_mode_get_dataset "authors_index")
|
||||
local main_authors_index_output="$OUTPUT_DIR/authors/index.html"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR/authors"
|
||||
|
||||
if [ -z "$authors_index_data" ]; then
|
||||
echo -e "${YELLOW}No authors found in RAM index. Skipping author page generation.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
declare -A author_posts_by_slug=()
|
||||
declare -A author_name_by_slug=()
|
||||
declare -A author_email_by_slug=()
|
||||
local line author author_slug author_email
|
||||
while IFS= read -r line; do
|
||||
[ -z "$line" ] && continue
|
||||
IFS='|' read -r author author_slug author_email _ <<< "$line"
|
||||
[ -z "$author" ] && continue
|
||||
[ -z "$author_slug" ] && continue
|
||||
if [[ -z "${author_name_by_slug[$author_slug]+_}" ]]; then
|
||||
author_name_by_slug["$author_slug"]="$author"
|
||||
author_email_by_slug["$author_slug"]="$author_email"
|
||||
fi
|
||||
author_posts_by_slug["$author_slug"]+="$line"$'\n'
|
||||
done <<< "$authors_index_data"
|
||||
|
||||
local author_slug_key
|
||||
for author_slug_key in $(printf '%s\n' "${!author_name_by_slug[@]}" | sort); do
|
||||
author="${author_name_by_slug[$author_slug_key]}"
|
||||
local author_data="${author_posts_by_slug[$author_slug_key]}"
|
||||
local author_page_html_file="$OUTPUT_DIR/authors/$author_slug_key/index.html"
|
||||
local author_rss_file="$OUTPUT_DIR/authors/$author_slug_key/${RSS_FILENAME:-rss.xml}"
|
||||
local author_page_rel_url="/authors/${author_slug_key}/"
|
||||
local author_rss_rel_url="/authors/${author_slug_key}/${RSS_FILENAME:-rss.xml}"
|
||||
local post_count
|
||||
post_count=$(printf '%s\n' "$author_data" | awk 'NF { c++ } END { print c+0 }')
|
||||
|
||||
mkdir -p "$(dirname "$author_page_html_file")"
|
||||
|
||||
local author_page_content=""
|
||||
author_page_content+="<h1>${MSG_POSTS_BY:-Posts by} $author</h1>"$'\n'
|
||||
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then
|
||||
author_page_content+="<p><a href=\"$author_rss_rel_url\">${MSG_RSS_FEED:-RSS Feed}</a></p>"$'\n'
|
||||
fi
|
||||
author_page_content+="<div class=\"posts-list\">"$'\n'
|
||||
|
||||
while IFS='|' read -r author_name_inner author_slug_inner author_email_inner post_title post_date post_lastmod post_filename post_slug post_image post_image_caption post_description; do
|
||||
[ -z "$post_title" ] && continue
|
||||
|
||||
local post_url
|
||||
if [ -n "$post_date" ] && [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
local year month day url_path
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
|
||||
url_path="${url_path//Year/$year}"
|
||||
url_path="${url_path//Month/$month}"
|
||||
url_path="${url_path//Day/$day}"
|
||||
url_path="${url_path//slug/$post_slug}"
|
||||
post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')"
|
||||
else
|
||||
post_url="/$(echo "$post_slug" | sed 's|^/||; s|/*$|/|')"
|
||||
fi
|
||||
post_url="${BASE_URL}${post_url}"
|
||||
local formatted_date
|
||||
formatted_date=$(format_date "$post_date")
|
||||
|
||||
author_page_content+="<article>"$'\n'
|
||||
author_page_content+=" <h2><a href=\"$post_url\">$post_title</a></h2>"$'\n'
|
||||
author_page_content+=" <div class=\"meta\">"$'\n'
|
||||
author_page_content+=" <time datetime=\"$post_date\">$formatted_date</time>"$'\n'
|
||||
author_page_content+=" </div>"$'\n'
|
||||
if [ -n "$post_description" ]; then
|
||||
author_page_content+=" <p class=\"summary\">$post_description</p>"$'\n'
|
||||
fi
|
||||
if [ -n "$post_image" ]; then
|
||||
author_page_content+=" <div class=\"author-image\">"$'\n'
|
||||
author_page_content+=" <img src=\"$post_image\" alt=\"$post_image_caption\" loading=\"lazy\">"$'\n'
|
||||
author_page_content+=" </div>"$'\n'
|
||||
fi
|
||||
author_page_content+="</article>"$'\n'
|
||||
done < <(printf '%s\n' "$author_data" | awk 'NF' | sort -t'|' -k5,5r)
|
||||
|
||||
author_page_content+="</div>"$'\n'
|
||||
|
||||
local page_title="${MSG_POSTS_BY:-Posts by} $author"
|
||||
local page_description="${MSG_POSTS_BY:-Posts by} $author - $post_count ${MSG_POSTS:-posts}"
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
header_content=${header_content//\{\{page_title\}\}/"$page_title"}
|
||||
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{og_description\}\}/"$page_description"}
|
||||
header_content=${header_content//\{\{twitter_description\}\}/"$page_description"}
|
||||
header_content=${header_content//\{\{og_type\}\}/"website"}
|
||||
header_content=${header_content//\{\{page_url\}\}/"$author_page_rel_url"}
|
||||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
header_content=${header_content//\{\{og_image\}\}/}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/}
|
||||
header_content=${header_content//\{\{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
|
||||
done
|
||||
|
||||
local page_title="${MSG_ALL_AUTHORS:-All Authors}"
|
||||
local page_description="${MSG_ALL_AUTHORS:-All Authors} - $SITE_DESCRIPTION"
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
local main_content=""
|
||||
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
header_content=${header_content//\{\{page_title\}\}/"$page_title"}
|
||||
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{og_description\}\}/"$page_description"}
|
||||
header_content=${header_content//\{\{twitter_description\}\}/"$page_description"}
|
||||
header_content=${header_content//\{\{og_type\}\}/"website"}
|
||||
header_content=${header_content//\{\{page_url\}\}/"/authors/"}
|
||||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
header_content=${header_content//\{\{og_image\}\}/}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/}
|
||||
header_content=${header_content//\{\{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}"
|
||||
}
|
||||
921
scripts/build/generate_feeds.sh
Executable file
921
scripts/build/generate_feeds.sh
Executable file
|
|
@ -0,0 +1,921 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Feed Generation
|
||||
# Handles the creation of sitemap.xml and rss.xml.
|
||||
#
|
||||
|
||||
# Source dependencies
|
||||
# shellcheck source=utils.sh disable=SC1091
|
||||
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_feeds.sh"; exit 1; }
|
||||
# shellcheck source=cache.sh disable=SC1091
|
||||
source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_feeds.sh"; exit 1; }
|
||||
# Source content.sh to get convert_markdown_to_html
|
||||
# shellcheck source=content.sh disable=SC1091
|
||||
source "$(dirname "$0")/content.sh" || { echo >&2 "Error: Failed to source content.sh from generate_feeds.sh"; exit 1; }
|
||||
# Note: Needs access to primary_pages and SECONDARY_PAGES which should be exported by templates.sh
|
||||
|
||||
declare -gA BSSG_RAM_RSS_FULL_CONTENT_CACHE=()
|
||||
declare -g BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=false
|
||||
declare -gA BSSG_RAM_RSS_PUBDATE_CACHE=()
|
||||
declare -gA BSSG_RAM_RSS_UPDATED_ISO_CACHE=()
|
||||
declare -gA BSSG_RAM_RSS_URL_CACHE=()
|
||||
declare -gA BSSG_RAM_RSS_ITEM_XML_CACHE=()
|
||||
declare -g BSSG_RAM_RSS_METADATA_CACHE_READY=false
|
||||
|
||||
_rfc822_gmt_date() {
|
||||
local input_dt="$1"
|
||||
if [ -z "$input_dt" ]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
if [ "$input_dt" = "now" ]; then
|
||||
LC_ALL=C date -u "+%a, %d %b %Y %H:%M:%S GMT"
|
||||
return
|
||||
fi
|
||||
local cache_key="gmt|$input_dt"
|
||||
if [[ -n "${BSSG_FORMAT_DATE_CACHE[$cache_key]+_}" ]]; then
|
||||
echo "${BSSG_FORMAT_DATE_CACHE[$cache_key]}"
|
||||
return
|
||||
fi
|
||||
local formatted=""
|
||||
if [[ "$BSSG_KERNEL_NAME" == "Darwin" ]] || [[ "$BSSG_KERNEL_NAME" == *"BSD" ]]; then
|
||||
formatted=$(LC_ALL=C date -ujf "%Y-%m-%d %H:%M:%S" "$input_dt" "+%a, %d %b %Y %H:%M:%S GMT" 2>/dev/null)
|
||||
[ -z "$formatted" ] && formatted=$(LC_ALL=C date -ujf "%Y-%m-%d" "$input_dt" "+%a, %d %b %Y %H:%M:%S GMT" 2>/dev/null)
|
||||
[ -z "$formatted" ] && formatted=$(LC_ALL=C date -ujf "%d %b %Y %H:%M:%S %z" "$input_dt" "+%a, %d %b %Y %H:%M:%S GMT" 2>/dev/null)
|
||||
else
|
||||
formatted=$(LC_ALL=C date -ud "$input_dt" "+%a, %d %b %Y %H:%M:%S GMT" 2>/dev/null)
|
||||
fi
|
||||
if [ -z "$formatted" ]; then
|
||||
formatted=$(format_date "$input_dt" "%a, %d %b %Y %H:%M:%S %z" 2>/dev/null)
|
||||
fi
|
||||
BSSG_FORMAT_DATE_CACHE["$cache_key"]="$formatted"
|
||||
echo "$formatted"
|
||||
}
|
||||
|
||||
_get_mime_type() {
|
||||
local filepath="$1"
|
||||
local ext="${filepath##*.}"
|
||||
ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
|
||||
case "$ext" in
|
||||
jpg|jpeg) echo "image/jpeg" ;;
|
||||
png) echo "image/png" ;;
|
||||
gif) echo "image/gif" ;;
|
||||
webp) echo "image/webp" ;;
|
||||
svg) echo "image/svg+xml" ;;
|
||||
avif) echo "image/avif" ;;
|
||||
*) echo "application/octet-stream" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_normalize_relative_url_path() {
|
||||
local path="$1"
|
||||
while [[ "$path" == */ ]]; do
|
||||
path="${path%/}"
|
||||
done
|
||||
path="${path#/}"
|
||||
if [ -z "$path" ]; then
|
||||
printf '/'
|
||||
else
|
||||
printf '/%s/' "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
_ram_strip_frontmatter_for_rss() {
|
||||
awk '
|
||||
BEGIN { in_fm = 0; found_fm = 0; }
|
||||
/^---$/ {
|
||||
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
|
||||
if (in_fm) { in_fm = 0; next; }
|
||||
}
|
||||
{ if (!in_fm) print; }
|
||||
'
|
||||
}
|
||||
|
||||
_ram_cache_full_content_for_file() {
|
||||
local file="$1"
|
||||
local resolved="$file"
|
||||
|
||||
if declare -F ram_mode_resolve_key > /dev/null; then
|
||||
resolved=$(ram_mode_resolve_key "$file")
|
||||
fi
|
||||
|
||||
if [[ -z "$resolved" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "${BSSG_RAM_RSS_FULL_CONTENT_CACHE[$resolved]+_}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! declare -F ram_mode_has_file > /dev/null || ! ram_mode_has_file "$resolved"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local raw_content
|
||||
raw_content=$(ram_mode_get_content "$resolved")
|
||||
|
||||
local stripped_content
|
||||
stripped_content=$(printf '%s\n' "$raw_content" | _ram_strip_frontmatter_for_rss)
|
||||
|
||||
local converted_html
|
||||
converted_html=$(convert_markdown_to_html "$stripped_content" "$resolved")
|
||||
local convert_status=$?
|
||||
if [ $convert_status -ne 0 ] || [ -z "$converted_html" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
BSSG_RAM_RSS_FULL_CONTENT_CACHE["$resolved"]="$converted_html"
|
||||
return 0
|
||||
}
|
||||
|
||||
prepare_ram_rss_full_content_cache() {
|
||||
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RSS_INCLUDE_FULL_CONTENT:-false}" != true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY" = true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
if [ -z "$file_index_data" ]; then
|
||||
BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
|
||||
[ -z "$file" ] && continue
|
||||
_ram_cache_full_content_for_file "$file" > /dev/null || true
|
||||
done <<< "$file_index_data"
|
||||
|
||||
BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=true
|
||||
}
|
||||
|
||||
_ram_prime_rss_metadata_entry() {
|
||||
local date="$1"
|
||||
local lastmod="$2"
|
||||
local slug="$3"
|
||||
local rss_date_fmt="$4"
|
||||
local build_timestamp_iso="$5"
|
||||
local source_file="$6"
|
||||
|
||||
if [ -n "$date" ] && [[ -z "${BSSG_RAM_RSS_PUBDATE_CACHE[$date]+_}" ]]; then
|
||||
BSSG_RAM_RSS_PUBDATE_CACHE["$date"]=$(_rfc822_gmt_date "$date")
|
||||
fi
|
||||
|
||||
if [ -n "$lastmod" ] && [[ -z "${BSSG_RAM_RSS_UPDATED_ISO_CACHE[$lastmod]+_}" ]]; then
|
||||
local updated_date_iso
|
||||
updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z")
|
||||
if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}"
|
||||
fi
|
||||
[ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso"
|
||||
BSSG_RAM_RSS_UPDATED_ISO_CACHE["$lastmod"]="$updated_date_iso"
|
||||
fi
|
||||
|
||||
if [ -n "$date" ] && [ -n "$slug" ]; then
|
||||
local url_key="${date}|${slug}"
|
||||
if [[ -z "${BSSG_RAM_RSS_URL_CACHE[$url_key]+_}" ]]; then
|
||||
local year month day formatted_path item_url
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
echo "Warning: Invalid date format '$date' for file $source_file, cannot precompute RSS URL." >&2
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
formatted_path="${URL_SLUG_FORMAT//Year/$year}"
|
||||
formatted_path="${formatted_path//Month/$month}"
|
||||
formatted_path="${formatted_path//Day/$day}"
|
||||
formatted_path="${formatted_path//slug/$slug}"
|
||||
item_url=$(_normalize_relative_url_path "$formatted_path")
|
||||
BSSG_RAM_RSS_URL_CACHE["$url_key"]=$(fix_url "$item_url")
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
prepare_ram_rss_metadata_cache() {
|
||||
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$BSSG_RAM_RSS_METADATA_CACHE_READY" = true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
if [ -z "$file_index_data" ]; then
|
||||
BSSG_RAM_RSS_METADATA_CACHE_READY=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z"
|
||||
local build_timestamp_iso
|
||||
build_timestamp_iso=$(format_date "now" "%Y-%m-%dT%H:%M:%S%z")
|
||||
if [[ "$build_timestamp_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
build_timestamp_iso="${build_timestamp_iso::${#build_timestamp_iso}-2}:${BASH_REMATCH[2]}"
|
||||
fi
|
||||
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
|
||||
[ -z "$file" ] && continue
|
||||
_ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" >/dev/null || true
|
||||
done <<< "$file_index_data"
|
||||
|
||||
BSSG_RAM_RSS_METADATA_CACHE_READY=true
|
||||
}
|
||||
|
||||
# Function to get the latest lastmod date from a file index, optionally filtered
|
||||
# Usage: get_latest_mod_date <index_file> [field_index] [filter_pattern] [date_format]
|
||||
# Example: get_latest_mod_date "$file_index" 5 "" "%Y-%m-%d" # Latest overall post
|
||||
# Example: get_latest_mod_date "$tags_index" 5 "^tag-slug|" "%Y-%m-%d" # Latest for a tag
|
||||
get_latest_mod_date() {
|
||||
local index_file="$1"
|
||||
local date_field_index="${2:-5}" # Default to 5 for lastmod in file_index/tags_index
|
||||
local filter_pattern="$3" # Optional grep pattern
|
||||
local date_format="${4:-%Y-%m-%d}" # Default sitemap format
|
||||
|
||||
if [ ! -f "$index_file" ]; then
|
||||
echo "$(format_date "now" "$date_format")" # Fallback to now if index missing
|
||||
return
|
||||
fi
|
||||
|
||||
local latest_date_str
|
||||
if [ -n "$filter_pattern" ]; then
|
||||
# Filter, extract date, sort numerically (YYYY-MM-DD is sortable), get latest
|
||||
latest_date_str=$(grep -E "$filter_pattern" "$index_file" | cut -d'|' -f"$date_field_index" | sort -r | head -n 1)
|
||||
else
|
||||
# Extract date, sort numerically, get latest
|
||||
latest_date_str=$(cut -d'|' -f"$date_field_index" "$index_file" | sort -r | head -n 1)
|
||||
fi
|
||||
|
||||
if [ -n "$latest_date_str" ]; then
|
||||
# Attempt to format the found date string
|
||||
local formatted_date=$(format_date "$latest_date_str" "$date_format")
|
||||
if [ -n "$formatted_date" ]; then
|
||||
echo "$formatted_date"
|
||||
else
|
||||
# Fallback if format_date fails (e.g., invalid date string)
|
||||
echo "$(format_date "now" "$date_format")"
|
||||
fi
|
||||
else
|
||||
# Fallback if no matching entries or dates found
|
||||
echo "$(format_date "now" "$date_format")"
|
||||
fi
|
||||
}
|
||||
|
||||
# Fast path for RAM datasets: pick max YYYY-MM-DD from a given field without external sort/head.
|
||||
_ram_latest_date_from_dataset() {
|
||||
local dataset="$1"
|
||||
local field_index="$2"
|
||||
local date_format="${3:-%Y-%m-%d}"
|
||||
|
||||
local latest_date_str
|
||||
latest_date_str=$(printf '%s\n' "$dataset" | awk -F'|' -v field_index="$field_index" '
|
||||
NF {
|
||||
value = substr($field_index, 1, 10)
|
||||
if (value != "" && value > max_date) {
|
||||
max_date = value
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (max_date != "") {
|
||||
print max_date
|
||||
}
|
||||
}
|
||||
')
|
||||
|
||||
if [ -n "$latest_date_str" ]; then
|
||||
printf '%s\n' "$latest_date_str"
|
||||
else
|
||||
format_date "now" "$date_format"
|
||||
fi
|
||||
}
|
||||
|
||||
_generate_sitemap_with_awk_inputs() {
|
||||
local sitemap="$1"
|
||||
local file_index_input="$2"
|
||||
local primary_pages_input="$3"
|
||||
local secondary_pages_input="$4"
|
||||
local tags_index_input="$5"
|
||||
local authors_index_input="$6"
|
||||
local latest_post_mod_date="$7"
|
||||
local latest_tag_page_mod_date="$8"
|
||||
local latest_author_page_mod_date="$9"
|
||||
local sitemap_date_fmt="${10:-%Y-%m-%d}"
|
||||
|
||||
# Determine the best awk command locally to avoid potential scoping issues with AWK_CMD.
|
||||
local effective_awk_cmd="awk"
|
||||
if command -v gawk > /dev/null 2>&1; then
|
||||
effective_awk_cmd="gawk"
|
||||
fi
|
||||
|
||||
"$effective_awk_cmd" -v site_url="$SITE_URL" \
|
||||
-v url_slug_format="$URL_SLUG_FORMAT" \
|
||||
-v latest_post_mod_date="$latest_post_mod_date" \
|
||||
-v latest_tag_page_mod_date="$latest_tag_page_mod_date" \
|
||||
-v latest_author_page_mod_date="$latest_author_page_mod_date" \
|
||||
-v enable_author_pages="${ENABLE_AUTHOR_PAGES:-true}" \
|
||||
-v sitemap_date_fmt="$sitemap_date_fmt" \
|
||||
-F'|' \
|
||||
-f - \
|
||||
"$file_index_input" "$primary_pages_input" "$secondary_pages_input" "$tags_index_input" "$authors_index_input" <<'AWK_EOF' > "$sitemap"
|
||||
# AWK script for sitemap generation.
|
||||
BEGIN {
|
||||
OFS = ""
|
||||
print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
||||
print "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"
|
||||
|
||||
# Homepage
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk("/", site_url) "</loc>"
|
||||
print " <lastmod>" latest_post_mod_date "</lastmod>"
|
||||
print " <changefreq>daily</changefreq>"
|
||||
print " <priority>1.0</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
|
||||
function fix_url_awk(path, base_url) {
|
||||
if (substr(path, 1, 1) == "/") {
|
||||
sub(/\/$/, "", base_url)
|
||||
sub(/^\/+/, "/", path)
|
||||
sub(/\/index\.html$/, "/", path)
|
||||
if (substr(path, length(path), 1) != "/") {
|
||||
path = path "/"
|
||||
}
|
||||
if (base_url == "" || base_url ~ /^http:\/\/localhost(:[0-9]+)?$/) {
|
||||
return path
|
||||
} else {
|
||||
return base_url path
|
||||
}
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
# Process file_index (posts).
|
||||
FILENAME == ARGV[1] {
|
||||
file = $1
|
||||
date = $4
|
||||
lastmod = $5
|
||||
slug = $7
|
||||
if (length(file) == 0 || length(date) == 0 || length(lastmod) == 0 || length(slug) == 0) next
|
||||
|
||||
year = substr(date, 1, 4)
|
||||
month = substr(date, 6, 2)
|
||||
day = substr(date, 9, 2)
|
||||
if (year ~ /^[0-9]{4}$/ && month ~ /^[0-9]{2}$/ && day ~ /^[0-9]{2}$/) {
|
||||
formatted_path = url_slug_format
|
||||
gsub(/Year/, year, formatted_path)
|
||||
gsub(/Month/, month, formatted_path)
|
||||
gsub(/Day/, day, formatted_path)
|
||||
gsub(/slug/, slug, formatted_path)
|
||||
item_url = "/" formatted_path
|
||||
sub(/\/+$/, "/", item_url)
|
||||
|
||||
mod_time = substr(lastmod, 1, 10)
|
||||
if (mod_time == "") next
|
||||
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(item_url, site_url) "</loc>"
|
||||
print " <lastmod>" mod_time "</lastmod>"
|
||||
print " <changefreq>weekly</changefreq>"
|
||||
print " <priority>0.8</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
}
|
||||
|
||||
# Process primary pages.
|
||||
FILENAME == ARGV[2] {
|
||||
url = $2
|
||||
date = $3
|
||||
if (length(url) == 0 || length(date) == 0) next
|
||||
sitemap_url = url
|
||||
sub(/index\.html$/, "", sitemap_url)
|
||||
sub(/\/+$/, "/", sitemap_url)
|
||||
mod_time = substr(date, 1, 10)
|
||||
if (mod_time == "") next
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(sitemap_url, site_url) "</loc>"
|
||||
print " <lastmod>" mod_time "</lastmod>"
|
||||
print " <changefreq>monthly</changefreq>"
|
||||
print " <priority>0.7</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
|
||||
# Process secondary pages.
|
||||
FILENAME == ARGV[3] {
|
||||
url = $2
|
||||
date = $3
|
||||
if (length(url) == 0 || length(date) == 0) next
|
||||
sitemap_url = url
|
||||
sub(/index\.html$/, "", sitemap_url)
|
||||
sub(/\/+$/, "/", sitemap_url)
|
||||
mod_time = substr(date, 1, 10)
|
||||
if (mod_time == "") next
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(sitemap_url, site_url) "</loc>"
|
||||
print " <lastmod>" mod_time "</lastmod>"
|
||||
print " <changefreq>monthly</changefreq>"
|
||||
print " <priority>0.6</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
|
||||
# Process tags index.
|
||||
FILENAME == ARGV[4] {
|
||||
tag_slug = $2
|
||||
if (length(tag_slug) == 0) next
|
||||
if (!(tag_slug in processed_tags)) {
|
||||
processed_tags[tag_slug] = 1
|
||||
item_url = "/tags/" tag_slug "/"
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(item_url, site_url) "</loc>"
|
||||
print " <lastmod>" latest_tag_page_mod_date "</lastmod>"
|
||||
print " <changefreq>weekly</changefreq>"
|
||||
print " <priority>0.5</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
}
|
||||
|
||||
# Process authors index.
|
||||
FILENAME == ARGV[5] && enable_author_pages == "true" {
|
||||
author_slug = $2
|
||||
if (length(author_slug) == 0) next
|
||||
if (!(author_slug in processed_authors)) {
|
||||
processed_authors[author_slug] = 1
|
||||
|
||||
if (!authors_index_added) {
|
||||
authors_index_added = 1
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk("/authors/", site_url) "</loc>"
|
||||
print " <lastmod>" latest_author_page_mod_date "</lastmod>"
|
||||
print " <changefreq>weekly</changefreq>"
|
||||
print " <priority>0.6</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
|
||||
item_url = "/authors/" author_slug "/"
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(item_url, site_url) "</loc>"
|
||||
print " <lastmod>" latest_author_page_mod_date "</lastmod>"
|
||||
print " <changefreq>weekly</changefreq>"
|
||||
print " <priority>0.5</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
print "</urlset>"
|
||||
}
|
||||
AWK_EOF
|
||||
}
|
||||
|
||||
# Core RSS generation function
|
||||
# Usage: _generate_rss_feed <output_file> <feed_title> <feed_description> <feed_link_rel> <feed_atom_link_rel> <post_data_input>
|
||||
# <post_data_input> should be a string containing the filtered, sorted, and limited post data,
|
||||
# with each line formatted as: file|filename|title|date|lastmod|tags|slug|image|image_caption|description
|
||||
# Example Call:
|
||||
# sorted_posts=$(sort -t'|' -k4,4r "$file_index" | head -n "$rss_item_limit")
|
||||
# _generate_rss_feed "$rss" "$feed_title" "$feed_desc" "/" "/rss.xml" "$sorted_posts"
|
||||
_generate_rss_feed() {
|
||||
local output_file="$1"
|
||||
local feed_title="$2"
|
||||
local feed_description="$3"
|
||||
local feed_link_rel="$4" # Relative link for the channel (e.g., "/" or "/tags/tag-slug/")
|
||||
local feed_atom_link_rel="$5" # Relative link for the atom:link (e.g., "/rss.xml" or "/tags/tag-slug/rss.xml")
|
||||
local post_data_input="$6" # String containing post data lines
|
||||
|
||||
# Get build timestamp in ISO 8601 for atom:updated fallback
|
||||
local build_timestamp_iso=$(format_date "now" "%Y-%m-%dT%H:%M:%S%z")
|
||||
# Convert RFC-2822 timezone (+0000) to ISO 8601 (+00:00) if needed
|
||||
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
|
||||
|
||||
# Ensure output directory exists
|
||||
mkdir -p "$(dirname "$output_file")"
|
||||
|
||||
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
|
||||
|
||||
# Process the provided post data
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
|
||||
# Ignore blank trailing lines from callers.
|
||||
if [ -z "$file" ] && [ -z "$filename" ] && [ -z "$title" ] && [ -z "$date" ] && [ -z "$lastmod" ] && [ -z "$tags" ] && [ -z "$slug" ] && [ -z "$image" ] && [ -z "$image_caption" ] && [ -z "$description" ] && [ -z "$author_name" ] && [ -z "$author_email" ]; then
|
||||
continue
|
||||
fi
|
||||
# Skip if essential fields are missing (robustness)
|
||||
if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ] || [ -z "$lastmod" ] || [ -z "$slug" ]; then
|
||||
echo "Warning: Skipping RSS item due to missing fields in input line: file=$file, title=$title, date=$date, lastmod=$lastmod, slug=$slug" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
local rss_item_cache_key=""
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
rss_item_cache_key="${RSS_INCLUDE_FULL_CONTENT:-false}|${file}|${date}|${lastmod}|${slug}|${title}"
|
||||
if [[ -n "${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]+_}" ]]; then
|
||||
printf '%s' "${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]}" >&4
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Format dates and URL (RAM mode caches repeated values across many tag feeds).
|
||||
local pub_date updated_date_iso full_url
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
_ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" || {
|
||||
echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2
|
||||
continue
|
||||
}
|
||||
pub_date="${BSSG_RAM_RSS_PUBDATE_CACHE[$date]}"
|
||||
updated_date_iso="${BSSG_RAM_RSS_UPDATED_ISO_CACHE[$lastmod]}"
|
||||
full_url="${BSSG_RAM_RSS_URL_CACHE[${date}|${slug}]}"
|
||||
else
|
||||
pub_date=$(_rfc822_gmt_date "$date")
|
||||
updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z")
|
||||
if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}"
|
||||
fi
|
||||
[ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso"
|
||||
|
||||
local year month day formatted_path item_url
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2
|
||||
continue
|
||||
fi
|
||||
formatted_path="${URL_SLUG_FORMAT//Year/$year}"
|
||||
formatted_path="${formatted_path//Month/$month}"
|
||||
formatted_path="${formatted_path//Day/$day}"
|
||||
formatted_path="${formatted_path//slug/$slug}"
|
||||
item_url=$(_normalize_relative_url_path "$formatted_path")
|
||||
full_url=$(fix_url "$item_url")
|
||||
fi
|
||||
|
||||
# --- RSS Item Description Enhancement ---
|
||||
local item_description_content=""
|
||||
local figure_part=""
|
||||
local caption_part=""
|
||||
local content_part=""
|
||||
local escaped_title
|
||||
escaped_title=$(html_escape "$title")
|
||||
|
||||
# Build figure part
|
||||
if [ -n "$image" ]; then
|
||||
local img_src
|
||||
[[ "$image" =~ ^https?:// ]] && img_src="$image" || img_src=$(fix_url "$image")
|
||||
# Escape alt/title attributes safely using html_escape from utils.sh
|
||||
local img_alt="$escaped_title"
|
||||
local img_title=$(html_escape "$image_caption")
|
||||
[ -z "$img_title" ] && img_title="$img_alt" # Use alt if title is empty
|
||||
|
||||
figure_part="<figure><img src=\"${img_src}\" alt=\"${img_alt}\" title=\"${img_title}\">" # Open tags
|
||||
|
||||
if [ -n "$image_caption" ]; then
|
||||
local escaped_caption=$(html_escape "$image_caption")
|
||||
caption_part="<figcaption>${escaped_caption}</figcaption>" # Caption
|
||||
fi
|
||||
figure_part="${figure_part}${caption_part}</figure>" # Close figure tag (with caption inside if it exists)
|
||||
fi
|
||||
|
||||
# Build content part (excerpt or full)
|
||||
if [ "${RSS_INCLUDE_FULL_CONTENT:-false}" = true ]; then
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
local resolved_file="$file"
|
||||
if declare -F ram_mode_resolve_key > /dev/null; then
|
||||
resolved_file=$(ram_mode_resolve_key "$file")
|
||||
fi
|
||||
|
||||
if _ram_cache_full_content_for_file "$resolved_file"; then
|
||||
content_part="${BSSG_RAM_RSS_FULL_CONTENT_CACHE[$resolved_file]}"
|
||||
else
|
||||
# RAM mode is memory-only: never fall back to disk cache reads.
|
||||
if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
echo "Warning: RAM content not available for RSS item ($file). Falling back to excerpt." >&2
|
||||
fi
|
||||
content_part="$description"
|
||||
fi
|
||||
else
|
||||
local raw_content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")"
|
||||
if [ -f "$raw_content_cache_file" ]; then
|
||||
local raw_content=$(cat "$raw_content_cache_file")
|
||||
local converted_html=$(convert_markdown_to_html "$raw_content" "$file")
|
||||
local convert_status=$?
|
||||
if [ $convert_status -eq 0 ] && [ -n "$converted_html" ]; then
|
||||
content_part="$converted_html"
|
||||
else
|
||||
echo "Warning: Failed to convert markdown to HTML for RSS item ($file, status: $convert_status). Falling back to excerpt." >&2
|
||||
content_part="$description"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Cached raw markdown content file '$raw_content_cache_file' not found for RSS item ($file). Falling back to excerpt." >&2
|
||||
content_part="$description"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
content_part="$description"
|
||||
fi
|
||||
|
||||
# Combine parts safely
|
||||
item_description_content="${figure_part}${content_part}"
|
||||
|
||||
# Wrap final description in CDATA
|
||||
local final_description="<![CDATA[$item_description_content]]>"
|
||||
|
||||
# Determine author for RSS item (with fallback)
|
||||
local rss_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}"
|
||||
local rss_author_email="${author_email}"
|
||||
|
||||
# Build author element if we have author info
|
||||
local author_element=""
|
||||
if [ -n "$rss_author_name" ]; then
|
||||
if [ -n "$rss_author_email" ]; then
|
||||
author_element=" <dc:creator>$(html_escape "$rss_author_name") ($(html_escape "$rss_author_email"))</dc:creator>"
|
||||
else
|
||||
author_element=" <dc:creator>$(html_escape "$rss_author_name")</dc:creator>"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build enclosure element if image exists
|
||||
local enclosure_element=""
|
||||
if [ -n "$image" ]; then
|
||||
local enclosure_img_path=""
|
||||
if [[ "$image" == /* ]]; then
|
||||
enclosure_img_path="${OUTPUT_DIR}${image}"
|
||||
elif [[ "$image" != http://* && "$image" != https://* ]]; then
|
||||
enclosure_img_path="${OUTPUT_DIR}/${image}"
|
||||
fi
|
||||
if [ -n "$enclosure_img_path" ] && [ -f "$enclosure_img_path" ]; then
|
||||
local file_size file_mime
|
||||
if [[ "$BSSG_KERNEL_NAME" == "Darwin" ]] || [[ "$BSSG_KERNEL_NAME" == *"BSD" ]]; then
|
||||
file_size=$(stat -f "%z" "$enclosure_img_path" 2>/dev/null || echo 0)
|
||||
else
|
||||
file_size=$(stat -c "%s" "$enclosure_img_path" 2>/dev/null || echo 0)
|
||||
fi
|
||||
if [ "$file_size" -gt 0 ]; then
|
||||
file_mime=$(_get_mime_type "$image")
|
||||
enclosure_element=" <enclosure url=\"${img_src}\" length=\"${file_size}\" type=\"${file_mime}\" />"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
local rss_item_xml
|
||||
rss_item_xml=" <item>
|
||||
<title>${escaped_title}</title>
|
||||
<link>${full_url}</link>
|
||||
<guid isPermaLink=\"true\">${full_url}</guid>
|
||||
<pubDate>${pub_date}</pubDate>
|
||||
<atom:updated>${updated_date_iso}</atom:updated>
|
||||
<description>${final_description}</description>
|
||||
${enclosure_element}
|
||||
"
|
||||
if [ -n "$author_element" ]; then
|
||||
rss_item_xml+="${author_element}"$'\n'
|
||||
fi
|
||||
rss_item_xml+=" </item>
|
||||
"
|
||||
|
||||
printf '%s' "$rss_item_xml" >&4
|
||||
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
BSSG_RAM_RSS_ITEM_XML_CACHE["$rss_item_cache_key"]="$rss_item_xml"
|
||||
fi
|
||||
done <<< "$post_data_input"
|
||||
|
||||
# Close the RSS feed
|
||||
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
|
||||
}
|
||||
export -f _generate_rss_feed # Export for potential parallel use or sourcing
|
||||
|
||||
# Generate RSS feed (Main site feed)
|
||||
generate_rss() {
|
||||
echo -e "${YELLOW}Generating main RSS feed...${NC}"
|
||||
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
if [ -z "$file_index_data" ]; then
|
||||
echo -e "${YELLOW}No file index data in RAM. Skipping RSS generation.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
prepare_ram_rss_metadata_cache >/dev/null || true
|
||||
|
||||
local rss="$OUTPUT_DIR/${RSS_FILENAME:-rss.xml}"
|
||||
local feed_title="${MSG_RSS_FEED_TITLE:-${SITE_TITLE} - RSS Feed}"
|
||||
local feed_desc="${MSG_RSS_FEED_DESCRIPTION:-${SITE_DESCRIPTION}}"
|
||||
local feed_link_rel="/"
|
||||
local feed_atom_link_rel="/${RSS_FILENAME:-rss.xml}"
|
||||
local rss_item_limit=${RSS_ITEM_LIMIT:-15}
|
||||
local sorted_posts
|
||||
sorted_posts=$(printf '%s\n' "$file_index_data" | awk 'NF' | sort -t'|' -k4,4r -k5,5r | head -n "$rss_item_limit")
|
||||
_generate_rss_feed "$rss" "$feed_title" "$feed_desc" "$feed_link_rel" "$feed_atom_link_rel" "$sorted_posts"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Ensure needed functions/vars are available
|
||||
if ! command -v convert_markdown_to_html &> /dev/null; then
|
||||
echo -e "${RED}Error: convert_markdown_to_html function not found.${NC}" >&2; return 1; fi
|
||||
if [ -z "${MD5_CMD:-}" ]; then
|
||||
echo -e "${RED}Error: MD5_CMD is not set.${NC}" >&2; return 1; fi
|
||||
if [ -z "${CACHE_DIR:-}" ]; then
|
||||
echo -e "${RED}Error: CACHE_DIR is not set.${NC}" >&2; return 1; fi
|
||||
|
||||
local rss="$OUTPUT_DIR/${RSS_FILENAME:-rss.xml}"
|
||||
local file_index="$CACHE_DIR/file_index.txt"
|
||||
local config_hash_file="$CONFIG_HASH_FILE"
|
||||
local script_path="$BSSG_SCRIPT_DIR/build/generate_feeds.sh"
|
||||
|
||||
# Determine active locale file
|
||||
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
|
||||
|
||||
# Check if RSS feed needs to be rebuilt (Simplified check)
|
||||
local rebuild_needed=false
|
||||
if [ "${FORCE_REBUILD:-false}" = true ]; then
|
||||
rebuild_needed=true
|
||||
elif [ ! -f "$rss" ]; then
|
||||
rebuild_needed=true # Rebuild if RSS file doesn't exist
|
||||
else
|
||||
local rss_mtime=$(get_file_mtime "$rss")
|
||||
# Check file index mtime AND config hash mtime
|
||||
if { [ -f "$file_index" ] && [ "$(get_file_mtime "$file_index")" -gt "$rss_mtime" ]; } || \
|
||||
{ [ -f "$config_hash_file" ] && [ "$(get_file_mtime "$config_hash_file")" -gt "$rss_mtime" ]; }; then \
|
||||
rebuild_needed=true
|
||||
fi
|
||||
# Removed checks for script, locale mtime for simplicity, kept config hash check
|
||||
fi
|
||||
|
||||
# If no rebuild needed, skip
|
||||
if [ "$rebuild_needed" = false ]; then
|
||||
echo -e "${GREEN}Main RSS feed is up to date (based on file index), skipping...${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$file_index" ]; then
|
||||
echo -e "${RED}Error: File index '$file_index' not found. Cannot generate RSS feed.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Prepare data for the reusable function
|
||||
local feed_title="${MSG_RSS_FEED_TITLE:-${SITE_TITLE} - RSS Feed}"
|
||||
local feed_desc="${MSG_RSS_FEED_DESCRIPTION:-${SITE_DESCRIPTION}}"
|
||||
local feed_link_rel="/"
|
||||
local feed_atom_link_rel="/${RSS_FILENAME:-rss.xml}" # Use the config variable
|
||||
local rss_item_limit=${RSS_ITEM_LIMIT:-15}
|
||||
|
||||
# Read file_index.txt, sort by original date (field 4), take top N
|
||||
# Use lastmod (field 5) as secondary sort key if dates are identical (optional, but good practice)
|
||||
local sorted_posts
|
||||
sorted_posts=$(sort -t'|' -k4,4r -k5,5r "$file_index" | head -n "$rss_item_limit")
|
||||
|
||||
# Call the reusable function
|
||||
# echo "DEBUG: In generate_rss, RSS_FILENAME='${RSS_FILENAME:-rss.xml}', output_file='${rss}'" >&2 # DEBUG
|
||||
_generate_rss_feed "$rss" "$feed_title" "$feed_desc" "$feed_link_rel" "$feed_atom_link_rel" "$sorted_posts"
|
||||
|
||||
# The reusable function already prints the success message
|
||||
# echo -e "${GREEN}RSS feed generated!${NC}" # Redundant now
|
||||
}
|
||||
|
||||
# Export public functions
|
||||
export -f generate_rss
|
||||
|
||||
# Generate sitemap.xml
|
||||
generate_sitemap() {
|
||||
echo -e "${YELLOW}Generating sitemap.xml...${NC}"
|
||||
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
local sitemap="$OUTPUT_DIR/sitemap.xml"
|
||||
local file_index_data tags_index_data authors_index_data primary_pages_data secondary_pages_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
tags_index_data=$(ram_mode_get_dataset "tags_index")
|
||||
authors_index_data=$(ram_mode_get_dataset "authors_index")
|
||||
primary_pages_data=$(ram_mode_get_dataset "primary_pages")
|
||||
secondary_pages_data=$(ram_mode_get_dataset "secondary_pages")
|
||||
|
||||
local latest_post_mod_date latest_tag_page_mod_date latest_author_page_mod_date
|
||||
latest_post_mod_date=$(_ram_latest_date_from_dataset "$file_index_data" 5 "%Y-%m-%d")
|
||||
latest_tag_page_mod_date=$(_ram_latest_date_from_dataset "$tags_index_data" 5 "%Y-%m-%d")
|
||||
latest_author_page_mod_date=$(_ram_latest_date_from_dataset "$authors_index_data" 6 "%Y-%m-%d")
|
||||
|
||||
[ -z "$latest_tag_page_mod_date" ] && latest_tag_page_mod_date="$latest_post_mod_date"
|
||||
[ -z "$latest_author_page_mod_date" ] && latest_author_page_mod_date="$latest_post_mod_date"
|
||||
|
||||
_generate_sitemap_with_awk_inputs \
|
||||
"$sitemap" \
|
||||
<(printf '%s\n' "$file_index_data") \
|
||||
<(printf '%s\n' "$primary_pages_data") \
|
||||
<(printf '%s\n' "$secondary_pages_data") \
|
||||
<(printf '%s\n' "$tags_index_data") \
|
||||
<(printf '%s\n' "$authors_index_data") \
|
||||
"$latest_post_mod_date" \
|
||||
"$latest_tag_page_mod_date" \
|
||||
"$latest_author_page_mod_date" \
|
||||
"%Y-%m-%d"
|
||||
|
||||
echo -e "${GREEN}Sitemap generated!${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local sitemap="$OUTPUT_DIR/sitemap.xml"
|
||||
local file_index="$CACHE_DIR/file_index.txt"
|
||||
local tags_index="$CACHE_DIR/tags_index.txt"
|
||||
local authors_index="$CACHE_DIR/authors_index.txt"
|
||||
local primary_pages_cache="$CACHE_DIR/primary_pages.tmp"
|
||||
local secondary_pages_cache="$CACHE_DIR/secondary_pages.tmp"
|
||||
local config_hash_file="$CONFIG_HASH_FILE" # Use the global var
|
||||
local script_path="$BSSG_SCRIPT_DIR/build/generate_feeds.sh" # Path to this script
|
||||
local sitemap_date_fmt="%Y-%m-%d"
|
||||
|
||||
# Determine active locale file
|
||||
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 # Fallback to en
|
||||
active_locale_file="${LOCALE_DIR:-locales}/en.sh"
|
||||
fi
|
||||
|
||||
# Check if sitemap needs rebuild (Simplified check)
|
||||
local rebuild_needed=false
|
||||
if [ "${FORCE_REBUILD:-false}" = true ]; then
|
||||
rebuild_needed=true
|
||||
elif [ ! -f "$sitemap" ]; then
|
||||
rebuild_needed=true # Rebuild if sitemap doesn't exist
|
||||
else
|
||||
local sitemap_mtime=$(get_file_mtime "$sitemap")
|
||||
# Check main content index files
|
||||
if [ -f "$file_index" ] && [ "$(get_file_mtime "$file_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
if ! $rebuild_needed && [ -f "$tags_index" ] && [ "$(get_file_mtime "$tags_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
if ! $rebuild_needed && [ -f "$authors_index" ] && [ "$(get_file_mtime "$authors_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
if ! $rebuild_needed && [ -f "$primary_pages_cache" ] && [ "$(get_file_mtime "$primary_pages_cache")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
if ! $rebuild_needed && [ -f "$secondary_pages_cache" ] && [ "$(get_file_mtime "$secondary_pages_cache")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
# Removed checks for script, config, locale mtime for simplicity to avoid sourcing errors
|
||||
fi
|
||||
|
||||
# If no rebuild needed based on simple checks, skip
|
||||
if [ "$rebuild_needed" = false ]; then
|
||||
echo -e "${GREEN}Sitemap is up to date (based on content indexes), skipping...${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# --- Pre-calculate latest dates (Still needed for Homepage/Tags/Authors) ---
|
||||
local latest_post_mod_date=$(get_latest_mod_date "$file_index" 5 "" "$sitemap_date_fmt")
|
||||
local latest_tag_page_mod_date=$(get_latest_mod_date "$tags_index" 5 "" "$sitemap_date_fmt") # Assumes lastmod is relevant field in tags_index
|
||||
local latest_author_page_mod_date=$(get_latest_mod_date "$authors_index" 6 "" "$sitemap_date_fmt") # Field 6 is lastmod in authors_index
|
||||
|
||||
echo "Generating sitemap content using awk..."
|
||||
_generate_sitemap_with_awk_inputs \
|
||||
"$sitemap" \
|
||||
"$file_index" \
|
||||
"$primary_pages_cache" \
|
||||
"$secondary_pages_cache" \
|
||||
"$tags_index" \
|
||||
"$authors_index" \
|
||||
"$latest_post_mod_date" \
|
||||
"$latest_tag_page_mod_date" \
|
||||
"$latest_author_page_mod_date" \
|
||||
"$sitemap_date_fmt"
|
||||
|
||||
echo -e "${GREEN}Sitemap generated!${NC}"
|
||||
}
|
||||
|
||||
# Export public functions
|
||||
export -f _normalize_relative_url_path
|
||||
export -f _ram_strip_frontmatter_for_rss _ram_cache_full_content_for_file prepare_ram_rss_full_content_cache
|
||||
export -f _rfc822_gmt_date _get_mime_type
|
||||
export -f generate_sitemap generate_rss
|
||||
705
scripts/build/generate_index.sh
Executable file
705
scripts/build/generate_index.sh
Executable file
|
|
@ -0,0 +1,705 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Index/Pagination Generation
|
||||
# Handles the creation of the main index.html and paginated index pages.
|
||||
#
|
||||
|
||||
# Source dependencies
|
||||
# shellcheck source=utils.sh disable=SC1091
|
||||
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_index.sh"; exit 1; }
|
||||
# shellcheck source=cache.sh disable=SC1091
|
||||
source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_index.sh"; exit 1; }
|
||||
|
||||
_generate_index_ram() {
|
||||
echo -e "${YELLOW}Generating index pages...${NC}"
|
||||
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
if [ -z "$file_index_data" ]; then
|
||||
echo -e "${YELLOW}No posts found in RAM file index. Skipping index generation.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local total_posts_orig
|
||||
total_posts_orig=$(printf '%s\n' "$file_index_data" | awk 'NF { c++ } END { print c+0 }')
|
||||
local total_pages=$(( (total_posts_orig + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE ))
|
||||
[ "$total_pages" -eq 0 ] && total_pages=1
|
||||
|
||||
mapfile -t file_index_lines < <(printf '%s\n' "$file_index_data" | awk 'NF')
|
||||
echo -e "Generating ${GREEN}$total_pages${NC} index pages for ${GREEN}$total_posts_orig${NC} posts"
|
||||
|
||||
local current_page
|
||||
for (( current_page = 1; current_page <= total_pages; current_page++ )); do
|
||||
local output_file
|
||||
if [ "$current_page" -eq 1 ]; then
|
||||
output_file="$OUTPUT_DIR/index.html"
|
||||
else
|
||||
output_file="$OUTPUT_DIR/page/$current_page/index.html"
|
||||
mkdir -p "$(dirname "$output_file")"
|
||||
fi
|
||||
|
||||
local page_header="$HEADER_TEMPLATE"
|
||||
if [ "$current_page" -eq 1 ]; then
|
||||
page_header=${page_header//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
page_header=${page_header//\{\{page_title\}\}/"${MSG_HOME:-"Home"}"}
|
||||
page_header=${page_header//\{\{og_type\}\}/"website"}
|
||||
page_header=${page_header//\{\{page_url\}\}/"/"}
|
||||
page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"}
|
||||
local home_schema
|
||||
home_schema=$(cat <<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
|
||||
continue
|
||||
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">« ${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} »</a>
|
||||
PAG_EOF
|
||||
fi
|
||||
cat >> "$output_file" <<EOF
|
||||
</div>
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
cat >> "$output_file" <<EOF
|
||||
$page_footer
|
||||
EOF
|
||||
done
|
||||
|
||||
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)
|
||||
if ! indexes_need_rebuild; then
|
||||
echo -e "${GREEN}Index pages are up to date, skipping...${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Define the index page paths
|
||||
local file_index="$CACHE_DIR/file_index.txt"
|
||||
|
||||
# Check if file index exists
|
||||
if [ ! -f "$file_index" ]; then
|
||||
echo -e "${RED}Error: File index $file_index not found. Cannot generate index pages.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Count total posts
|
||||
local total_posts_orig=$(wc -l < "$file_index")
|
||||
local total_posts=$total_posts_orig
|
||||
local total_pages=$(( (total_posts + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE ))
|
||||
|
||||
# Ensure total_pages is at least 1 even if total_posts is 0
|
||||
if [ $total_pages -eq 0 ]; then
|
||||
total_pages=1
|
||||
fi
|
||||
|
||||
echo -e "Generating ${GREEN}$total_pages${NC} index pages for ${GREEN}$total_posts${NC} posts"
|
||||
|
||||
# Prepare templates (already exported, but good to have locally)
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
|
||||
# Define function to process a single index page
|
||||
process_index_page() {
|
||||
# Ensure current_page is treated as an integer
|
||||
local -i current_page="$1"
|
||||
local -i total_pages="$2"
|
||||
local file_index="$3"
|
||||
local -i total_posts_orig="$4"
|
||||
# Template content is accessed via exported global variables
|
||||
|
||||
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
|
||||
|
||||
# Skip if index page file is up to date relative to file index
|
||||
if ! file_needs_rebuild "$file_index" "$output_file"; then
|
||||
echo -e "Skipping unchanged index page $current_page"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Replace placeholders in the header
|
||||
local page_header="$HEADER_TEMPLATE"
|
||||
page_header=${page_header//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
if [ $current_page -eq 1 ]; then
|
||||
# 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//\{\{site_url\}\}/"$SITE_URL"}
|
||||
|
||||
# Create WebSite schema for homepage
|
||||
local home_url="${SITE_URL}/"
|
||||
local schema_json_ld=""
|
||||
local tmp_schema=$(mktemp)
|
||||
cat > "$tmp_schema" << EOF
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "$SITE_TITLE",
|
||||
"description": "$SITE_DESCRIPTION",
|
||||
"url": "$home_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
|
||||
schema_json_ld=$(cat "$tmp_schema")
|
||||
rm "$tmp_schema"
|
||||
page_header=${page_header//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
else
|
||||
# For pagination pages
|
||||
local pag_title=$(printf "${MSG_PAGINATION_TITLE:-"%s - Page %d"}" "$SITE_TITLE" "$current_page")
|
||||
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"}
|
||||
|
||||
# Create CollectionPage schema for paginated pages
|
||||
local schema_json_ld=""
|
||||
local tmp_schema=$(mktemp)
|
||||
cat > "$tmp_schema" << 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
|
||||
schema_json_ld=$(cat "$tmp_schema")
|
||||
rm "$tmp_schema"
|
||||
page_header=${page_header//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
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\}\}/}
|
||||
|
||||
# Replace placeholders in the footer
|
||||
local page_footer="$FOOTER_TEMPLATE"
|
||||
page_footer=${page_footer//\{\{current_year\}\}/$(date +%Y)}
|
||||
page_footer=${page_footer//\{\{author_name\}\}/"$AUTHOR_NAME"}
|
||||
|
||||
# Create the index page
|
||||
cat > "$output_file" << EOF
|
||||
$page_header
|
||||
EOF
|
||||
|
||||
# If there is an index.md, use that
|
||||
if [ -f "${PAGES_DIR}/index.md" ]; then
|
||||
local input_file="${PAGES_DIR}/index.md"
|
||||
local title=$(parse_metadata "$input_file" "title")
|
||||
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)
|
||||
|
||||
# Extract content after the second --- line
|
||||
content=$(tail -n +$((end_line + 1)) $input_file)
|
||||
|
||||
html_content=$(convert_markdown_to_html "$content")
|
||||
echo "$html_content" >> $output_file
|
||||
|
||||
echo -e "${GREEN}Used custom index.md with title '$title' as the sole homepage content.${NC}"
|
||||
|
||||
# Append footer and finish for the homepage
|
||||
cat >> "$output_file" << EOF
|
||||
$page_footer
|
||||
EOF
|
||||
# echo -e "Generated custom index page ${GREEN}$current_page${NC}" # Optional: Specific message
|
||||
return 0 # Successfully generated custom index page, skip post listing
|
||||
else
|
||||
# No index.md found, proceed with standard "Latest Posts" logic
|
||||
|
||||
# Only add "Latest Posts" section if there are actually posts
|
||||
if [ "$total_posts_orig" -gt 0 ]; then
|
||||
cat >> "$output_file" << EOF
|
||||
<h1>${MSG_LATEST_POSTS:-"Latest Posts"}</h1>
|
||||
<div class="posts-list">
|
||||
EOF
|
||||
|
||||
# Calculate start and end indices
|
||||
local start_index=$(( (current_page - 1) * POSTS_PER_PAGE + 1 ))
|
||||
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
|
||||
# ... (rest of the post item generation logic remains the same) ...
|
||||
if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ]; then
|
||||
continue
|
||||
fi
|
||||
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=$(awk -v m="$((10#${BASH_REMATCH[2]}))" 'BEGIN { printf "%02d", m }')
|
||||
post_day=$(awk -v d="$((10#${BASH_REMATCH[3]}))" 'BEGIN { printf "%02d", d }')
|
||||
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 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=$(format_date "$date" "$display_date_format")
|
||||
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>
|
||||
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}" />
|
||||
</a>
|
||||
</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)
|
||||
cat >> "$output_file" << EOF
|
||||
<div class="summary">
|
||||
$description
|
||||
</div>
|
||||
EOF
|
||||
fi
|
||||
cat >> "$output_file" << EOF
|
||||
|
||||
</article>
|
||||
EOF
|
||||
done # End of while loop reading posts
|
||||
|
||||
# Close the posts list div
|
||||
cat >> "$output_file" << EOF
|
||||
</div> <!-- .posts-list -->
|
||||
EOF
|
||||
|
||||
# Pagination logic (Only needed if there were posts)
|
||||
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">« ${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} »</a>
|
||||
PAG_EOF
|
||||
fi
|
||||
cat >> "$output_file" << EOF
|
||||
</div>
|
||||
EOF
|
||||
fi # End pagination check
|
||||
else
|
||||
# No index.md and no posts - display a message or leave blank?
|
||||
# Currently implies a blank content area between header/footer.
|
||||
echo "No posts found and no custom index.md; homepage will be mostly empty."
|
||||
fi # End of if total_posts_orig > 0
|
||||
|
||||
# Add footer (always needed in the 'else' case)
|
||||
cat >> "$output_file" << EOF
|
||||
$page_footer
|
||||
EOF
|
||||
fi # End of if [ -f "${PAGES_DIR}/index.md" ] ... else ...
|
||||
|
||||
# This message will now only be reached if index.md was NOT used.
|
||||
echo -e "Generated index page ${GREEN}$current_page${NC} of ${GREEN}$total_pages${NC}"
|
||||
}
|
||||
|
||||
# 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)
|
||||
|
||||
# Use all detected cores
|
||||
local jobs=$cores
|
||||
|
||||
# Export required functions and variables
|
||||
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_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
|
||||
|
||||
# Ensure templates are exported
|
||||
if [ -z "$HEADER_TEMPLATE" ] || [ -z "$FOOTER_TEMPLATE" ]; then
|
||||
echo -e "${RED}Error: Header or Footer template not loaded/exported correctly.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Process pages in parallel, passing total_posts_orig as the 4th argument
|
||||
seq 1 $total_pages | parallel --jobs $jobs --will-cite process_index_page {} $total_pages "$file_index" $total_posts_orig || { echo -e "${RED}Parallel index page generation failed.${NC}"; exit 1; }
|
||||
else
|
||||
# Sequential implementation
|
||||
echo -e "${YELLOW}Using sequential processing${NC}"
|
||||
local current_page=1
|
||||
while [ "$current_page" -le "$total_pages" ]; do
|
||||
process_index_page $current_page $total_pages "$file_index" $total_posts_orig
|
||||
current_page=$((current_page + 1))
|
||||
done
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Index pages processed!${NC}"
|
||||
}
|
||||
|
||||
# Make the function available for sourcing
|
||||
export -f generate_index
|
||||
276
scripts/build/generate_pages.sh
Executable file
276
scripts/build/generate_pages.sh
Executable file
|
|
@ -0,0 +1,276 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Static Page Generation
|
||||
# Functions for converting markdown/HTML pages.
|
||||
#
|
||||
|
||||
# Source dependencies
|
||||
# shellcheck source=utils.sh disable=SC1091
|
||||
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_pages.sh"; exit 1; }
|
||||
# shellcheck source=content.sh disable=SC1091
|
||||
source "$(dirname "$0")/content.sh" || { echo >&2 "Error: Failed to source content.sh from generate_pages.sh"; exit 1; }
|
||||
# shellcheck source=cache.sh disable=SC1091
|
||||
source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_pages.sh"; exit 1; } # For file_needs_rebuild checks etc.
|
||||
|
||||
# --- Moved Function Definitions --- START ---
|
||||
|
||||
# Convert a page (Markdown or HTML) to final HTML output
|
||||
convert_page() {
|
||||
local input_file="$1"
|
||||
local output_base_path="$2"
|
||||
local title="$3"
|
||||
local date="$4"
|
||||
local slug="$5"
|
||||
|
||||
# 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
|
||||
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 ! 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}"
|
||||
|
||||
local content="" # Content for reading time calculation (if markdown)
|
||||
local html_content="" # Final body HTML content
|
||||
|
||||
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')
|
||||
# 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")
|
||||
else
|
||||
source_stream=$(cat "$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
|
||||
html_content=$(convert_markdown_to_html "$content")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Markdown conversion failed for page '$input_file', skipping html generation.${NC}" >&2
|
||||
return 1 # Propagate the error
|
||||
fi
|
||||
# --- MODIFIED PART --- END ---
|
||||
fi
|
||||
|
||||
# Calculate reading time (best effort for HTML input)
|
||||
local reading_time
|
||||
reading_time=$(calculate_reading_time "$content")
|
||||
|
||||
# Use pre-loaded templates
|
||||
# IMPORTANT: Assumes HEADER_TEMPLATE, FOOTER_TEMPLATE are exported/available
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
|
||||
# Verify templates are not empty
|
||||
if [ -z "$header_content" ] || [ -z "$footer_content" ]; then
|
||||
echo -e "${RED}Error: Templates are empty in convert_page. Was templates.sh sourced correctly?${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Replace placeholders in the header
|
||||
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
header_content=${header_content//\{\{page_title\}\}/"$title"}
|
||||
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"} # Use site description for pages
|
||||
header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{og_type\}\}/"website"} # Pages are usually 'website' type
|
||||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
# Construct page URL based on format, ensuring trailing slash
|
||||
local formatted_page_path="${PAGE_URL_FORMAT//slug/$slug}"
|
||||
local page_rel_url="/$(echo "$formatted_page_path" | sed 's|^/||; s|/*$|/|')"
|
||||
header_content=${header_content//\{\{page_url\}\}/"$page_rel_url"}
|
||||
|
||||
# Remove schema if it exists, or set default schema for WebPage
|
||||
local page_full_url="${SITE_URL}${page_rel_url}" # Construct full URL
|
||||
local schema_json_ld=$(printf '<script type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "WebPage",\n "name": "%s",\n "url": "%s",\n "isPartOf": {\n "@type": "WebSite",\n "name": "%s",\n "url": "%s"\n }\n}\n</script>' \
|
||||
"$(echo "$title" | sed 's/"/\\"/g')" \
|
||||
"$page_full_url" \
|
||||
"$SITE_TITLE" \
|
||||
"$SITE_URL")
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
|
||||
# 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}"
|
||||
|
||||
# Add page title and content (no post-meta for pages usually)
|
||||
final_html+=$(printf '<article class="page">\n <h1>%s</h1>\n <div class="page-content">\n%s\n </div>\n</article>\n' "$title" "$html_content")
|
||||
|
||||
# Replace placeholders in footer content before appending
|
||||
local current_year=$(date +'%Y')
|
||||
footer_content=${footer_content//\{\{current_year\}\}/$current_year}
|
||||
footer_content=${footer_content//\{\{author_name\}\}/${AUTHOR_NAME:-Anonymous}}
|
||||
|
||||
# Append footer
|
||||
final_html+="${footer_content}"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p "$output_base_path"
|
||||
|
||||
# Write the final HTML to the output file
|
||||
printf '%s' "$final_html" > "$output_html_file"
|
||||
}
|
||||
|
||||
# Define a function for processing a single page file
|
||||
process_single_page_file() {
|
||||
local file="$1"
|
||||
|
||||
# Extract metadata (title, date, slug)
|
||||
# IMPORTANT: Assumes parse_metadata is available (content.sh)
|
||||
local title slug date
|
||||
if [[ "$file" == *.html ]]; then
|
||||
title=$(grep -m 1 '<title>' "$file" 2>/dev/null | sed 's/<[^>]*>//g')
|
||||
slug=$(grep -m 1 'meta name="slug"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/')
|
||||
date=$(grep -m 1 'meta name="date"' "$file" 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/')
|
||||
else
|
||||
title=$(parse_metadata "$file" "title")
|
||||
slug=$(parse_metadata "$file" "slug")
|
||||
date=$(parse_metadata "$file" "date") # Date might be optional for pages
|
||||
fi
|
||||
|
||||
# Fallback/Defaults
|
||||
if [ -z "$title" ]; then title=$(basename "$file" | sed 's/\.[^.]*$//'); fi
|
||||
if [ -z "$slug" ]; then slug=$(generate_slug "$title"); fi # Use generate_slug (utils.sh)
|
||||
|
||||
# Create output path based on PAGE_URL_FORMAT
|
||||
# IMPORTANT: Assumes PAGE_URL_FORMAT, OUTPUT_DIR are exported/available
|
||||
local formatted_path="${PAGE_URL_FORMAT//slug/$slug}"
|
||||
# Ensure the path represents the directory for index.html
|
||||
local output_path="${OUTPUT_DIR:-output}/$(echo "$formatted_path" | sed 's|^/||; s|/*$||')"
|
||||
|
||||
# Call the modified convert_page function (defined above in this script)
|
||||
convert_page "$file" "$output_path" "$title" "$date" "$slug"
|
||||
}
|
||||
|
||||
# --- Moved Function Definitions --- END ---
|
||||
|
||||
# --- Page Generation Functions --- START ---
|
||||
|
||||
# Process all pages found in the PAGES_DIR
|
||||
process_all_pages() {
|
||||
echo -e "${YELLOW}Processing static pages...${NC}"
|
||||
|
||||
# IMPORTANT: Assumes PAGES_DIR is exported/available
|
||||
if [ ! -d "${PAGES_DIR:-pages}" ]; then
|
||||
echo -e "${YELLOW}Pages directory ('${PAGES_DIR:-pages}') not found, skipping page processing.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 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
|
||||
|
||||
local num_pages=${#page_files[@]}
|
||||
if [ "$num_pages" -eq 0 ]; then
|
||||
echo -e "${YELLOW}No pages found in '${PAGES_DIR:-pages}'. Skipping page generation.${NC}"
|
||||
return 0
|
||||
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
|
||||
echo -e "${GREEN}Using GNU parallel to generate pages${NC}"
|
||||
# Determine number of cores
|
||||
local cores
|
||||
cores=$(get_parallel_jobs)
|
||||
|
||||
# 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 # 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
|
||||
|
||||
# Process page files in parallel using newline separation
|
||||
printf '%s\n' "${page_files[@]}" | parallel --jobs "$cores" --will-cite process_single_page_file {}
|
||||
else
|
||||
# Fallback to sequential processing
|
||||
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
|
||||
|
||||
echo -e "${GREEN}Static page processing complete!${NC}"
|
||||
}
|
||||
|
||||
# --- Page Generation Functions --- END ---
|
||||
863
scripts/build/generate_posts.sh
Executable file
863
scripts/build/generate_posts.sh
Executable file
|
|
@ -0,0 +1,863 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Post Generation
|
||||
# Functions for converting markdown posts to HTML.
|
||||
#
|
||||
|
||||
# Source dependencies
|
||||
# shellcheck source=utils.sh disable=SC1091
|
||||
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_posts.sh"; exit 1; }
|
||||
# shellcheck source=content.sh disable=SC1091
|
||||
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"
|
||||
local output_base_path="$2"
|
||||
local title="$3"
|
||||
local date="$4"
|
||||
local lastmod="$5"
|
||||
local tags="$6"
|
||||
local slug="$7"
|
||||
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
|
||||
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
|
||||
fi
|
||||
|
||||
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
echo -e "Processing post: ${GREEN}$(basename "$input_file")${NC}"
|
||||
fi
|
||||
|
||||
# Extract body content (without frontmatter) in one awk pass.
|
||||
# This is materially faster than line-by-line bash parsing on large markdown files.
|
||||
local content=""
|
||||
local source_stream=""
|
||||
local fediverse_creator_override=""
|
||||
if $ram_mode_active; then
|
||||
source_stream=$(ram_mode_get_content "$input_file")
|
||||
else
|
||||
source_stream=$(cat "$input_file")
|
||||
fi
|
||||
if [[ "$input_file" == *.html ]]; then
|
||||
fediverse_creator_override=$(printf '%s\n' "$source_stream" | grep -m 1 -o 'name="fediverse_creator" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/')
|
||||
if [ -z "$fediverse_creator_override" ]; then
|
||||
fediverse_creator_override=$(printf '%s\n' "$source_stream" | grep -m 1 -o 'name="fediverse:creator" content="[^"]*"' 2>/dev/null | sed 's/.*content="\([^"]*\)".*/\1/')
|
||||
fi
|
||||
else
|
||||
fediverse_creator_override=$(parse_metadata "$input_file" "fediverse_creator")
|
||||
fi
|
||||
content=$(printf '%s' "$source_stream" | awk '
|
||||
NR == 1 {
|
||||
if ($0 == "---") {
|
||||
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
|
||||
# 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
|
||||
|
||||
# Calculate reading time
|
||||
local reading_time=0
|
||||
if [ "${SHOW_READING_TIME:-true}" = "true" ]; then
|
||||
reading_time=$(calculate_reading_time "$content")
|
||||
fi
|
||||
|
||||
# Convert markdown content to HTML (No HTML caching here anymore)
|
||||
local html_content
|
||||
if [[ "$input_file" == *.html ]]; then
|
||||
# For HTML files, extract content between <body> tags (simple approach)
|
||||
# Assumes content is already HTML
|
||||
html_content=$(sed -n '/<body.*>/,/<\/body>/p' "$input_file" | sed '1d;$d')
|
||||
# echo -e "Extracted body content from HTML file: ${GREEN}$(basename "$input_file")${NC}" # Can be verbose
|
||||
elif [[ "$input_file" == *.md ]]; then
|
||||
# Original Markdown conversion using the raw content we extracted/cached
|
||||
# This now uses the content *without* frontmatter
|
||||
html_content=$(convert_markdown_to_html "$content")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Markdown conversion failed for '$input_file', skipping html generation.${NC}" >&2
|
||||
# Optionally delete the output file if it exists from a previous run?
|
||||
# rm -f "$output_html_file"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Error: Unknown input file type '$input_file' for content conversion.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create HTML tags for tags
|
||||
local tags_html=""
|
||||
if [ -n "$tags" ]; then
|
||||
tags_html="<div class=\"tags\">"
|
||||
IFS=',' read -ra TAG_ARRAY <<< "$tags"
|
||||
for tag in "${TAG_ARRAY[@]}"; do
|
||||
tag=$(echo "$tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[[ -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>"
|
||||
fi
|
||||
done
|
||||
tags_html+="</div>"
|
||||
fi
|
||||
|
||||
# Use pre-loaded templates
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
|
||||
# Verify templates are not empty
|
||||
if [ -z "$header_content" ] || [ -z "$footer_content" ]; then
|
||||
echo -e "${RED}Error: Header or Footer template is empty. Was templates.sh sourced correctly?${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Replace placeholders in the header
|
||||
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
header_content=${header_content//\{\{page_title\}\}/"$title"}
|
||||
header_content=${header_content//\{\{og_type\}\}/"article"}
|
||||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
|
||||
# Construct page URL based on format
|
||||
local page_url=""
|
||||
if [ -n "$date" ]; then
|
||||
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) # 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/$slug}"
|
||||
# Ensure relative page_url starts with / and ends with /
|
||||
page_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')"
|
||||
else
|
||||
# Ensure relative page_url starts with / and ends with / for slug-only urls
|
||||
page_url="/$(echo "$slug" | sed 's|^/||; s|/*$|/|')"
|
||||
fi
|
||||
header_content=${header_content//\{\{page_url\}\}/"$page_url"}
|
||||
|
||||
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
|
||||
# Trim whitespace from post description
|
||||
local meta_desc
|
||||
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")
|
||||
# Use date as fallback for lastmod, then format
|
||||
iso_lastmod_date=$(format_iso8601_post_date "${lastmod:-$date}")
|
||||
# If lastmod still empty, use iso_date as fallback
|
||||
[ -z "$iso_lastmod_date" ] && iso_lastmod_date="$iso_date"
|
||||
|
||||
# Fallback to build time if both are empty (should be rare)
|
||||
if [ -z "$iso_date" ]; then
|
||||
local now_iso
|
||||
now_iso=$(format_iso8601_post_date "now")
|
||||
iso_date="$now_iso"
|
||||
iso_lastmod_date="$now_iso"
|
||||
fi
|
||||
|
||||
local image_url=""
|
||||
if [ -n "$image" ]; then
|
||||
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>' \
|
||||
"$(echo "$title" | sed 's/"/\"/g')" \
|
||||
"$iso_date" \
|
||||
"$iso_lastmod_date" \
|
||||
"$author_json" \
|
||||
"$SITE_TITLE" \
|
||||
"$SITE_URL" \
|
||||
"$(echo "$meta_desc" | sed 's/"/\"/g')" \
|
||||
"$SITE_URL" "$page_url" \
|
||||
"${image_url:+,
|
||||
\"image\": {
|
||||
\"@type\": \"ImageObject\",
|
||||
\"url\": \"$image_url\"
|
||||
}}")
|
||||
fi
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
|
||||
# Handle image placeholders
|
||||
if [ -n "$image_url" ]; then
|
||||
local og_image_tag="<meta property=\"og:image\" content=\"$image_url\">"
|
||||
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)
|
||||
# Determine the date format based on SHOW_TIMEZONE
|
||||
local display_date_format="$DATE_FORMAT"
|
||||
if [ "${SHOW_TIMEZONE:-false}" = false ]; then
|
||||
# Remove timezone format specifiers (%z or %Z) if they exist
|
||||
display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//')
|
||||
fi
|
||||
|
||||
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> • $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>"
|
||||
fi
|
||||
post_meta+="</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
|
||||
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'
|
||||
|
||||
# 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}
|
||||
|
||||
final_html+="${footer_content}"
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$output_base_path"
|
||||
|
||||
# Write the final HTML
|
||||
printf '%s' "$final_html" > "$output_html_file"
|
||||
local write_status=$?
|
||||
if [ $write_status -ne 0 ]; then
|
||||
echo "${RED}ERROR:${NC} Failed to write HTML file '$output_html_file' (Status: $write_status)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Process all markdown files listed in the file index
|
||||
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" # Define path for modified tags
|
||||
local modified_authors_list="${CACHE_DIR:-.bssg_cache}/modified_authors.list" # Define path for modified authors
|
||||
local file_index_prev="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" # Path to previous index
|
||||
local ram_mode_active=false
|
||||
local file_index_data=""
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
ram_mode_active=true
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
fi
|
||||
|
||||
if ! $ram_mode_active && [ ! -f "$file_index" ]; then
|
||||
echo -e "${RED}Error: File index not found at '$file_index'. Run indexing first.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local total_file_count=0
|
||||
if $ram_mode_active; then
|
||||
total_file_count=$(printf '%s\n' "$file_index_data" | awk 'NF { c++ } END { print c+0 }')
|
||||
else
|
||||
total_file_count=$(wc -l < "$file_index")
|
||||
fi
|
||||
if [ "$total_file_count" -eq 0 ]; then
|
||||
echo -e "${YELLOW}No posts found in file index. Skipping post generation.${NC}"
|
||||
return 0
|
||||
fi
|
||||
echo -e "Checking ${GREEN}$total_file_count${NC} potential posts listed in index."
|
||||
|
||||
# --- OPTIMIZATION: Quick check if any posts need rebuilding ---
|
||||
local needs_pass1=false
|
||||
local posts_needing_rebuild=0
|
||||
|
||||
# Only do expensive Pass 1 if related posts are enabled AND posts might need rebuilding
|
||||
if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && ! $ram_mode_active; then
|
||||
echo -e "${BLUE}DEBUG: Related posts enabled, starting quick scan...${NC}"
|
||||
# Quick scan to see if ANY posts need rebuilding before doing expensive Pass 1
|
||||
echo -e "${YELLOW}Quick scan: Checking if any posts need rebuilding...${NC}"
|
||||
|
||||
while IFS= read -r line; do
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line"
|
||||
|
||||
# Basic check if it looks like a post
|
||||
if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Calculate expected output path
|
||||
local year month day
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
year=$(date +%Y); month=$(date +%m); day=$(date +%d)
|
||||
fi
|
||||
local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
|
||||
url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}";
|
||||
url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}"
|
||||
local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html"
|
||||
|
||||
# Quick rebuild check
|
||||
common_rebuild_check "$output_html_file"
|
||||
local common_result=$?
|
||||
local needs_rebuild=false
|
||||
|
||||
if [ $common_result -eq 0 ]; then
|
||||
needs_rebuild=true
|
||||
else
|
||||
local input_time=$(get_file_mtime "$file")
|
||||
local output_time=$(get_file_mtime "$output_html_file")
|
||||
if (( input_time > output_time )); then
|
||||
needs_rebuild=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if $needs_rebuild; then
|
||||
posts_needing_rebuild=$((posts_needing_rebuild + 1))
|
||||
needs_pass1=true
|
||||
# Early exit optimization: if we find posts needing rebuild, we need Pass 1
|
||||
break
|
||||
fi
|
||||
done < <(
|
||||
if $ram_mode_active; then
|
||||
printf '%s\n' "$file_index_data" | awk 'NF'
|
||||
else
|
||||
cat "$file_index"
|
||||
fi
|
||||
)
|
||||
|
||||
echo -e "Quick scan result: ${GREEN}$posts_needing_rebuild${NC} posts need rebuilding"
|
||||
fi
|
||||
|
||||
# --- PASS 1: Only run if needed (posts need rebuilding AND related posts enabled) ---
|
||||
if [ "$needs_pass1" = true ] && [ "${ENABLE_RELATED_POSTS:-true}" = true ] && ! $ram_mode_active; then
|
||||
echo -e "${BLUE}DEBUG: Both needs_pass1=true and ENABLE_RELATED_POSTS=true, running Pass 1...${NC}"
|
||||
echo -e "${YELLOW}Pass 1: Identifying modified tags for related posts cache invalidation...${NC}"
|
||||
|
||||
# Clear previous modified tags lists
|
||||
rm -f "$modified_tags_list"
|
||||
rm -f "$modified_authors_list"
|
||||
touch "$modified_tags_list" # Ensure file exists even if empty
|
||||
touch "$modified_authors_list" # Ensure file exists even if empty
|
||||
|
||||
while IFS= read -r line; do
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line"
|
||||
|
||||
# Basic check if it looks like a post
|
||||
if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Calculate expected output path
|
||||
local year month day
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
year=$(date +%Y); month=$(date +%m); day=$(date +%d)
|
||||
fi
|
||||
local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
|
||||
url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}";
|
||||
url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}"
|
||||
local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html"
|
||||
|
||||
# Perform the rebuild check here
|
||||
common_rebuild_check "$output_html_file"
|
||||
local common_result=$?
|
||||
local needs_rebuild=false
|
||||
|
||||
if [ $common_result -eq 0 ]; then
|
||||
needs_rebuild=true # Common checks failed (config changed, template newer, output missing)
|
||||
else # common_result is 2 (output exists and newer than templates/locale)
|
||||
local input_time=$(get_file_mtime "$file")
|
||||
local output_time=$(get_file_mtime "$output_html_file")
|
||||
if (( input_time > output_time )); then
|
||||
needs_rebuild=true # Input file is newer
|
||||
fi
|
||||
fi
|
||||
|
||||
# If post needs rebuilding, add its tags to the modified list
|
||||
if $needs_rebuild; then
|
||||
local new_tags="$tags"
|
||||
local old_tags=""
|
||||
# Try to get old tags from the previous index snapshot
|
||||
if [ -f "$file_index_prev" ]; then
|
||||
old_tags=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f6)
|
||||
fi
|
||||
|
||||
# Combine old and new tags
|
||||
local combined_tags="${old_tags},${new_tags}"
|
||||
|
||||
if [ -n "$combined_tags" ]; then
|
||||
# Split by comma, trim, filter empty, sort unique, and add each tag on a new line
|
||||
echo "$combined_tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . | sort -u >> "$modified_tags_list"
|
||||
fi
|
||||
|
||||
# Track modified authors (similar logic to tags)
|
||||
local new_author="$author_name"
|
||||
local old_author=""
|
||||
if [ -f "$file_index_prev" ]; then
|
||||
old_author=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f11)
|
||||
fi
|
||||
|
||||
# Add both old and new authors to the modified list (if they exist)
|
||||
if [ -n "$old_author" ] && [ "$old_author" != "" ]; then
|
||||
echo "$old_author" >> "$modified_authors_list"
|
||||
fi
|
||||
if [ -n "$new_author" ] && [ "$new_author" != "" ]; then
|
||||
echo "$new_author" >> "$modified_authors_list"
|
||||
fi
|
||||
fi
|
||||
done < "$file_index"
|
||||
|
||||
# Unique sort the modified tags and authors lists
|
||||
if [ -f "$modified_tags_list" ]; then
|
||||
local temp_tags_list=$(mktemp)
|
||||
sort -u "$modified_tags_list" > "$temp_tags_list"
|
||||
mv "$temp_tags_list" "$modified_tags_list"
|
||||
fi
|
||||
|
||||
if [ -f "$modified_authors_list" ]; then
|
||||
local temp_authors_list=$(mktemp)
|
||||
sort -u "$modified_authors_list" > "$temp_authors_list"
|
||||
mv "$temp_authors_list" "$modified_authors_list"
|
||||
fi
|
||||
|
||||
# Invalidate related posts cache if there are modified tags
|
||||
if [ -f "$modified_tags_list" ] && [ -s "$modified_tags_list" ]; then
|
||||
# Source related posts functions if not already loaded
|
||||
if ! command -v invalidate_related_posts_cache_for_tags > /dev/null 2>&1; then
|
||||
# shellcheck source=related_posts.sh disable=SC1091
|
||||
source "$(dirname "$0")/related_posts.sh" || { echo -e "${RED}Error: Failed to source related_posts.sh${NC}"; exit 1; }
|
||||
fi
|
||||
|
||||
# Create a temporary file to capture the list of invalidated posts
|
||||
RELATED_POSTS_INVALIDATED_LIST="${CACHE_DIR:-.bssg_cache}/related_posts_invalidated.list"
|
||||
> "$RELATED_POSTS_INVALIDATED_LIST" # Create empty file
|
||||
|
||||
# Call the invalidation function with the output file
|
||||
invalidate_related_posts_cache_for_tags "$modified_tags_list" "$RELATED_POSTS_INVALIDATED_LIST"
|
||||
|
||||
# Export the list for use in pass 2
|
||||
export RELATED_POSTS_INVALIDATED_LIST
|
||||
fi
|
||||
elif $ram_mode_active; then
|
||||
echo -e "${BLUE}DEBUG: RAM mode active, skipping Pass 1 related-posts invalidation (in-memory computation).${NC}"
|
||||
else
|
||||
echo -e "${BLUE}DEBUG: Pass 1 skipped - needs_pass1=$needs_pass1, ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}${NC}"
|
||||
fi
|
||||
|
||||
# --- PASS 2: Process posts with proper rebuild flags ---
|
||||
echo -e "${YELLOW}Pass 2: Processing posts...${NC}"
|
||||
|
||||
# Pre-filter files that need rebuilding
|
||||
local files_to_process_list=()
|
||||
local files_to_process_count=0
|
||||
local skipped_count=0
|
||||
|
||||
if $ram_mode_active && [ "${FORCE_REBUILD:-false}" = true ]; then
|
||||
echo -e "RAM mode force rebuild: skipping per-post rebuild checks."
|
||||
while IFS= read -r line; do
|
||||
local file filename title date
|
||||
IFS='|' read -r file filename _ date _ <<< "$line"
|
||||
if [ -n "$date" ] && [[ "$file" == "$SRC_DIR"* ]]; then
|
||||
files_to_process_list+=("$line")
|
||||
files_to_process_count=$((files_to_process_count + 1))
|
||||
fi
|
||||
done < <(printf '%s\n' "$file_index_data" | awk 'NF')
|
||||
else
|
||||
while IFS= read -r line; do
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line"
|
||||
|
||||
# Basic check if it looks like a post
|
||||
if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then
|
||||
# echo -e "Skipping non-post file listed in index (pre-check): ${YELLOW}$file${NC}" >&2 # Too verbose
|
||||
continue
|
||||
fi
|
||||
|
||||
# Calculate expected output path (logic copied from process_single_file)
|
||||
local output_path
|
||||
local year month day
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
year=$(date +%Y); month=$(date +%m); day=$(date +%d)
|
||||
fi
|
||||
local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
|
||||
url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}";
|
||||
url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}"
|
||||
local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html"
|
||||
|
||||
# Perform the rebuild check here
|
||||
common_rebuild_check "$output_html_file"
|
||||
local common_result=$?
|
||||
local needs_rebuild=false
|
||||
|
||||
if [ $common_result -eq 0 ]; then
|
||||
needs_rebuild=true # Common checks failed (config changed, template newer, output missing)
|
||||
else # common_result is 2 (output exists and newer than templates/locale)
|
||||
local input_time=$(get_file_mtime "$file")
|
||||
local output_time=$(get_file_mtime "$output_html_file")
|
||||
if (( input_time > output_time )); then
|
||||
needs_rebuild=true # Input file is newer
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if this post needs rebuilding due to related posts cache invalidation
|
||||
if ! $ram_mode_active && [ "$needs_rebuild" = false ] && [ -n "${RELATED_POSTS_INVALIDATED_LIST:-}" ] && [ -f "$RELATED_POSTS_INVALIDATED_LIST" ]; then
|
||||
if grep -Fxq "$slug" "$RELATED_POSTS_INVALIDATED_LIST" 2>/dev/null; then
|
||||
needs_rebuild=true # Related posts cache was invalidated
|
||||
echo -e "Rebuilding ${GREEN}$(basename "$file")${NC} due to related posts cache invalidation"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $needs_rebuild; then
|
||||
files_to_process_list+=("$line")
|
||||
files_to_process_count=$((files_to_process_count + 1))
|
||||
else
|
||||
# Only print skip message if not rebuilding
|
||||
echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}"
|
||||
skipped_count=$((skipped_count + 1))
|
||||
fi
|
||||
done < <(
|
||||
if $ram_mode_active; then
|
||||
printf '%s\n' "$file_index_data" | awk 'NF'
|
||||
else
|
||||
cat "$file_index"
|
||||
fi
|
||||
)
|
||||
fi
|
||||
|
||||
# Check if any files need processing
|
||||
if [ $files_to_process_count -eq 0 ]; then
|
||||
echo -e "${GREEN}All $total_file_count posts are up to date.${NC}"
|
||||