Compare commits
47 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
100 changed files with 23120 additions and 4085 deletions
676
README.md
676
README.md
|
|
@ -15,9 +15,11 @@
|
|||
- [Themes](#themes)
|
||||
- [Theme Previews](#theme-previews)
|
||||
- [Admin Interface](#admin-interface)
|
||||
- [BSSG Post Editor](#bssg-post-editor)
|
||||
- [Performance Features](#performance-features)
|
||||
- [Site Configuration](#site-configuration)
|
||||
- [Future Plans](#future-plans)
|
||||
- [Local Development Server](#local-development-server)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Author and License](#author-and-license)
|
||||
- [Documentation](#documentation)
|
||||
|
|
@ -26,15 +28,21 @@
|
|||
|
||||
- Generates HTML from Markdown using pandoc, commonmark, or markdown.pl (configurable)
|
||||
- Supports post metadata (title, date, tags)
|
||||
- Supports `lastmod` timestamp in frontmatter for tracking content updates (used in sitemap, RSS feed, and optionally displayed on posts).
|
||||
- Supports `lastmod` timestamp in frontmatter for tracking content updates (used in sitemap, RSS feed, and optionally displayed on posts)
|
||||
- Full date and time support with timezone awareness
|
||||
- Post descriptions/summaries for previews, OpenGraph, and RSS
|
||||
- Admin interface for managing posts and scheduling publications
|
||||
- Creates tag index pages
|
||||
- Admin interface for managing posts and scheduling publications (planned for future release)
|
||||
- Standalone post editor with modern Ghost-like interface for visual content creation
|
||||
- Creates tag index pages with optional tag RSS feeds
|
||||
- Related Posts: automatically suggests related posts based on shared tags at the end of each post
|
||||
- Author index pages with conditional navigation menu and optional author RSS feeds
|
||||
- Archives by year and month for chronological browsing
|
||||
- Dynamic menu generation based on available pages
|
||||
- Support for primary and secondary pages with automatic menu organization
|
||||
- Generates sitemap.xml and RSS feed with timezone support
|
||||
- Generates `sitemap.xml` and RSS feeds with timezone support
|
||||
- Two build modes: `normal` (incremental, cache-backed) and `ram` (memory-first)
|
||||
- RAM mode stage timing summary printed at the end of each RAM build
|
||||
- Asset pre-compression with incremental and parallel gzip processing (`.html`, `.css`, `.xml`, `.js`)
|
||||
- Clean design
|
||||
- No JavaScript required (except for admin interface)
|
||||
- Works well without images
|
||||
|
|
@ -46,17 +54,16 @@
|
|||
- Supports static files (images, CSS, JS, etc.)
|
||||
- Configurable clean output directory option
|
||||
- Draft posts support
|
||||
- Post scheduling system
|
||||
- Backup and restore functionality
|
||||
- Incremental builds with file caching for improved performance
|
||||
- Smart metadata caching system
|
||||
- Parallel processing support using GNU parallel (if available)
|
||||
- Incremental builds with file and metadata caching for improved performance
|
||||
- Parallel processing with GNU parallel (if available) plus shell-worker fallbacks
|
||||
- File locking for safe concurrent operations
|
||||
- Automatic handling of different operating systems (Linux/macOS/BSDs)
|
||||
- Custom URL slugs with SEO-friendly permalinks
|
||||
- Featured images in posts are displayed in index, tag, and archive pages
|
||||
- Support for static pages with custom URLs
|
||||
- Support for custom homepage - useful if you want to build a website, not a blog
|
||||
- Built-in local development server for easy previewing
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
@ -78,12 +85,17 @@
|
|||
*(This command now invokes the modular build process located in `scripts/build/`)*
|
||||
|
||||
4. View your site in the `output` directory or serve it locally:
|
||||
```bash
|
||||
./bssg.sh server
|
||||
```
|
||||
This will build your site and start a local web server. By default, you can access your site at `http://localhost:8000`.
|
||||
Alternatively, to manually serve the `output` directory (e.g., if you want to use a different server):
|
||||
```bash
|
||||
cd output
|
||||
python3 -m http.server 8000
|
||||
python3 -m http.server 8000 # Or any other simple HTTP server
|
||||
```
|
||||
|
||||
5. Open your browser and navigate to http://localhost:8000
|
||||
5. Open your browser and navigate to the URL provided by the server (e.g., http://localhost:8000).
|
||||
|
||||
## Recommended Setup: Separating Content from Core
|
||||
|
||||
|
|
@ -127,7 +139,7 @@
|
|||
|
||||
BSSG requires the following tools:
|
||||
|
||||
- Bash
|
||||
- Bash (Note: On macOS, the default bash is too old and not compatible. You need to install a newer version using Homebrew: `brew install bash`)
|
||||
- pandoc, commonmark, or markdown.pl (configurable in config.sh.local)
|
||||
- Standard Unix utilities (awk, sed, grep, find, date)
|
||||
|
||||
|
|
@ -136,27 +148,27 @@ BSSG requires the following tools:
|
|||
#### On Debian/Ubuntu:
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install cmark
|
||||
sudo apt-get install cmark socat
|
||||
```
|
||||
|
||||
#### On macOS (using Homebrew):
|
||||
```bash
|
||||
brew install bash cmark
|
||||
brew install bash cmark socat
|
||||
```
|
||||
|
||||
#### On FreeBSD:
|
||||
```bash
|
||||
pkg install bash cmark
|
||||
pkg install bash cmark socat
|
||||
```
|
||||
|
||||
#### On OpenBSD:
|
||||
```bash
|
||||
pkg_add bash cmark
|
||||
pkg_add bash cmark socat
|
||||
```
|
||||
|
||||
#### On NetBSD:
|
||||
```bash
|
||||
pkgin in bash cmark
|
||||
pkgin in bash cmark socat
|
||||
```
|
||||
|
||||
### Using markdown.pl instead of commonmark
|
||||
|
|
@ -190,24 +202,30 @@ Commonmark provides a stricter and more standardized Markdown implementation and
|
|||
```
|
||||
BSSG/
|
||||
├── bssg.sh # Main command interface script
|
||||
├── bssg-editor.html # Standalone post editor (Ghost-like interface)
|
||||
├── generate_theme_previews.sh # Script to generate previews of all themes
|
||||
├── scripts/ # Supporting scripts
|
||||
│ ├── build/ # Modular build scripts
|
||||
│ │ ├── main.sh # Main build orchestrator
|
||||
│ │ ├── utils.sh # Utility functions (colors, formatting, etc.)
|
||||
│ │ ├── cli.sh # Command-line argument parsing
|
||||
│ │ ├── config_loader.sh # Loads default and user configuration
|
||||
│ │ ├── deps.sh # Dependency checking
|
||||
│ │ ├── cache.sh # Cache management functions
|
||||
│ │ ├── content_discovery.sh # Finds posts, pages, drafts
|
||||
│ │ ├── markdown_processor.sh # Markdown conversion logic
|
||||
│ │ ├── process_posts.sh # Processes individual posts
|
||||
│ │ ├── process_pages.sh # Processes individual pages
|
||||
│ │ ├── generate_indexes.sh # Creates index, tag, and archive pages
|
||||
│ │ ├── generate_feeds.sh # Creates RSS feed and sitemap
|
||||
│ │ ├── config_loader.sh # Loads defaults and local overrides
|
||||
│ │ ├── deps.sh # Dependency checks
|
||||
│ │ ├── cache.sh # Cache/config hash helpers
|
||||
│ │ ├── content.sh # Metadata/excerpt/markdown helpers
|
||||
│ │ ├── indexing.sh # File/tags/authors/archive index builders
|
||||
│ │ ├── templates.sh # Template preload/menu generation
|
||||
│ │ ├── generate_posts.sh # Post rendering
|
||||
│ │ ├── generate_pages.sh # Static page rendering
|
||||
│ │ ├── generate_index.sh # Homepage/pagination generation
|
||||
│ │ ├── generate_tags.sh # Tag pages (+ optional tag RSS)
|
||||
│ │ ├── generate_authors.sh # Author pages (+ optional author RSS)
|
||||
│ │ ├── generate_archives.sh # Archive pages (year/month)
|
||||
│ │ ├── generate_feeds.sh # Main RSS + sitemap
|
||||
│ │ ├── generate_secondary_pages.sh # Creates pages.html index
|
||||
│ │ ├── copy_static.sh # Copies static files and theme assets
|
||||
│ │ └── theme_utils.sh # Theme-related utilities
|
||||
│ │ ├── related_posts.sh # Related-post indexing/render helpers
|
||||
│ │ ├── post_process.sh # URL rewrite + permissions fixes
|
||||
│ │ ├── assets.sh # Static copy + CSS/theme handling
|
||||
│ │ ├── ram_mode.sh # RAM-mode preload/in-memory datasets
|
||||
│ │ └── utils.sh # Shared helpers (time, URLs, parallel)
|
||||
│ ├── post.sh # Handles post creation
|
||||
│ ├── page.sh # Handles page creation
|
||||
│ ├── edit.sh # Handles post/page editing (updates lastmod)
|
||||
|
|
@ -215,6 +233,8 @@ BSSG/
|
|||
│ ├── list.sh # Lists posts, pages, drafts, tags
|
||||
│ ├── backup.sh # Backup functionality
|
||||
│ ├── restore.sh # Restore functionality
|
||||
│ ├── benchmark.sh # Build benchmarking helper
|
||||
│ ├── server.sh # Local development server implementation
|
||||
│ ├── theme.sh # Theme management and processing (legacy helper)
|
||||
│ ├── template.sh # Template processing utilities (legacy helper)
|
||||
│ └── css.sh # CSS generation utilities (legacy helper)
|
||||
|
|
@ -248,58 +268,33 @@ BSSG/
|
|||
|
||||
```bash
|
||||
cd BSSG
|
||||
./bssg.sh [command] [options]
|
||||
./bssg.sh [--config <path>] [command] [options]
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
```
|
||||
Usage: ./bssg.sh command [options]
|
||||
Usage: ./bssg.sh [--config <path>] command [options]
|
||||
|
||||
Commands:
|
||||
post [-html] [draft_file] # Interactive: Create/edit post/draft, prompt for title, open editor.
|
||||
# Rebuilds site afterwards if REBUILD_AFTER_POST=true in config.
|
||||
# Use -html for HTML format.
|
||||
post [-html] [draft_file]
|
||||
Interactive: create/edit post or continue a draft.
|
||||
post -t <title> [-T <tags>] [-s <slug>] [--html] [-d] {-c <content> | -f <file> | --stdin} [--build]
|
||||
# Command-line: Create post non-interactively.
|
||||
# -t: Title (required)
|
||||
# -T: Tags (comma-sep)
|
||||
# -s: Slug (optional)
|
||||
# --html: HTML format (default: MD)
|
||||
# -d: Save as draft
|
||||
# -c: Content string
|
||||
# -f: Content file
|
||||
# --stdin: Content from stdin
|
||||
# --build: Force rebuild (overrides REBUILD_AFTER_POST=false)
|
||||
page [-html] [-s] [draft_file] Create a new page (in $PAGES_DIR or $DRAFTS_DIR/pages)
|
||||
or continue editing a draft (in $DRAFTS_DIR/pages)
|
||||
Use -html to edit in HTML instead of Markdown
|
||||
Use -s to mark page as secondary (for menu)
|
||||
edit [-n] <file> Edit an existing post/page/draft (updates lastmod)
|
||||
File path should point to $SRC_DIR, $PAGES_DIR, $DRAFTS_DIR etc.
|
||||
Use -n to rename based on title (posts/drafts only currently)
|
||||
delete [-f] <file> Delete a post/page/draft
|
||||
File path should point to $SRC_DIR, $PAGES_DIR, $DRAFTS_DIR etc.
|
||||
Use -f to skip confirmation
|
||||
list {posts|pages|drafts|tags [-n]}
|
||||
List posts ($SRC_DIR), pages ($PAGES_DIR),
|
||||
drafts ($DRAFTS_DIR and $DRAFTS_DIR/pages), or tags.
|
||||
For tags, use -n to sort by count.
|
||||
backup Create a backup of all posts, pages, drafts, and config
|
||||
restore [backup_file|ID] Restore from a backup (all content by default)
|
||||
Options: --no-content, --no-config
|
||||
backups List all available backups
|
||||
build [opts] Build the site using the modular build system in scripts/build/
|
||||
Options: -c|--clean-output, -f|--force-rebuild,
|
||||
--config FILE, --theme NAME,
|
||||
--site-url URL, --output DIR
|
||||
init <target_directory> Initialize a new, empty site structure in the specified directory.
|
||||
This is useful for separating your site content from the BSSG core scripts.
|
||||
The script will preserve the path format you provide (relative, absolute, or tilde-prefixed)
|
||||
in the generated site 'config.sh.local' for portability.
|
||||
Note: If using '~' for your home directory, quote the path (e.g., '~/mysite' or "~/mysite")
|
||||
to ensure the tilde is preserved in the generated config.
|
||||
help Show this help message
|
||||
Command-line: create post non-interactively.
|
||||
page [-html] [-s] [draft_file]
|
||||
Create a page or continue a page draft.
|
||||
edit [-n] <file> Edit an existing post/page/draft (updates lastmod).
|
||||
delete [-f] <file> Delete a post/page/draft.
|
||||
list List all posts.
|
||||
tags [-n] List all tags. Use -n to sort by post count.
|
||||
drafts List all draft posts.
|
||||
backup Create a backup of posts, pages, drafts, and config.
|
||||
restore [backup_file|ID] Restore from a backup (options: --no-content, --no-config).
|
||||
backups List all available backups.
|
||||
build [options] Build the site (run './bssg.sh build --help' for full options).
|
||||
server [options] Build and run local server (run './bssg.sh server --help').
|
||||
init <target_directory> Initialize a new site in the specified directory.
|
||||
help Show help.
|
||||
```
|
||||
|
||||
### Creating Posts and Pages
|
||||
|
|
@ -456,23 +451,39 @@ You can use these options with restore to selectively restore content:
|
|||
Usage: ./bssg.sh build [options]
|
||||
|
||||
Options:
|
||||
-c, --clean-output Empty the output directory before building
|
||||
--src DIR Override source directory (from config: SRC_DIR)
|
||||
--pages DIR Override pages directory (from config: PAGES_DIR)
|
||||
--drafts DIR Override drafts directory (from config: DRAFTS_DIR)
|
||||
--output DIR Override output directory (from config: OUTPUT_DIR)
|
||||
--templates DIR Override templates directory (from config: TEMPLATES_DIR)
|
||||
--themes-dir DIR Override themes directory (from config: THEMES_DIR)
|
||||
--theme NAME Override theme for this build
|
||||
--static DIR Override static directory (from config: STATIC_DIR)
|
||||
--clean-output [bool] Clean output directory before build (default from config)
|
||||
-f, --force-rebuild Ignore cache and rebuild all files
|
||||
--config FILE Use a specific configuration file (e.g., my_config.sh)
|
||||
instead of the default config.sh
|
||||
--src DIR Override the SRC_DIR specified in the config file
|
||||
--pages DIR Override the PAGES_DIR specified in the config file
|
||||
--drafts DIR Override the DRAFTS_DIR specified in the config file
|
||||
--output DIR Build the site to a specific output directory
|
||||
--templates DIR Override the TEMPLATES_DIR specified in the config file
|
||||
--themes-dir DIR Override the THEMES_DIR specified in the config file
|
||||
--theme NAME Override the theme specified in the config file for this build
|
||||
--static DIR Override the STATIC_DIR specified in the config file
|
||||
--site-url URL Override the SITE_URL specified in the config file for this build
|
||||
--build-mode MODE Build mode: normal or ram
|
||||
--site-title TITLE Override site title
|
||||
--site-url URL Override site URL
|
||||
--site-description DESC Override site description
|
||||
--author-name NAME Override author name
|
||||
--author-email EMAIL Override author email
|
||||
--posts-per-page NUM Override pagination size
|
||||
--deploy Force deployment after successful build (overrides config)
|
||||
--no-deploy Prevent deployment after build (overrides config)
|
||||
--no-deploy Skip deployment after build (overrides config)
|
||||
--help Show build help
|
||||
```
|
||||
|
||||
`--config <path>` is a global option and can be passed with any command (including `build`) to load a specific configuration file.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
./bssg.sh --config /path/to/site/config.sh.local build --build-mode ram
|
||||
./bssg.sh build --output ./public --clean-output true
|
||||
```
|
||||
|
||||
The option list above reflects the current `build --help` output.
|
||||
|
||||
### Internationalization (i18n)
|
||||
|
||||
BSSG supports generating the site in different languages.
|
||||
|
|
@ -528,6 +539,9 @@ slug: custom-slug
|
|||
image: /path/to/image.jpg
|
||||
image_caption: Optional caption for the image
|
||||
description: A brief summary of your post that will appear in listings, social media shares, and RSS feeds.
|
||||
author_name: John Doe # Optional: Override default site author
|
||||
author_email: john@example.com # Optional: Override default site author email
|
||||
fediverse_creator: @john@example.social # Optional: Override the fediverse:creator meta tag for this post
|
||||
---
|
||||
|
||||
Content goes here...
|
||||
|
|
@ -563,6 +577,163 @@ When you specify an image, it will appear:
|
|||
- In the RSS feed
|
||||
- In OpenGraph and Twitter metadata for better social media sharing
|
||||
|
||||
### Multi-Author Support
|
||||
|
||||
BSSG supports multiple authors through optional frontmatter fields that can override the default site author configuration on a per-post basis.
|
||||
|
||||
#### Author Fields
|
||||
|
||||
- `author_name`: The name of the post author (optional)
|
||||
- `author_email`: The email address of the post author (optional)
|
||||
- `fediverse_creator`: Explicit override for the post's `<meta name="fediverse:creator">` tag (optional)
|
||||
|
||||
#### Fallback Behavior
|
||||
|
||||
BSSG uses intelligent fallback logic for author information:
|
||||
|
||||
1. **Custom Author**: If both `author_name` and `author_email` are specified, they will be used for that post
|
||||
2. **Name Only**: If only `author_name` is specified, the name will be used but no email will be included in metadata
|
||||
3. **Default Fallback**: If author fields are empty or missing, the default `AUTHOR_NAME` and `AUTHOR_EMAIL` from your site configuration will be used
|
||||
|
||||
#### Author Index Pages
|
||||
|
||||
When multiple authors are detected in your posts, BSSG automatically generates:
|
||||
|
||||
- **Main Authors Index**: A page at `/authors/` listing all authors with their post counts
|
||||
- **Individual Author Pages**: Pages at `/authors/author-slug/` showing all posts by a specific author
|
||||
- **Conditional Navigation**: An "Authors" menu item that only appears when you have multiple authors (configurable threshold)
|
||||
|
||||
The author pages reuse the same styling as tag pages for visual consistency and include:
|
||||
- Post listings sorted by date (newest first)
|
||||
- Post counts and metadata
|
||||
- Schema.org structured data for SEO
|
||||
- Responsive design that works on all devices
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
You can control author page behavior in your `config.sh.local`:
|
||||
|
||||
```bash
|
||||
# Enable/disable author pages (default: false)
|
||||
ENABLE_AUTHOR_PAGES=false
|
||||
|
||||
# Minimum number of authors to show the Authors menu (default: 2)
|
||||
SHOW_AUTHORS_MENU_THRESHOLD=2
|
||||
|
||||
# Enable author-specific RSS feeds (default: false)
|
||||
ENABLE_AUTHOR_RSS=false
|
||||
```
|
||||
|
||||
#### Where Author Information Appears
|
||||
|
||||
Author information is displayed and used in:
|
||||
|
||||
- **Post Pages**: Copyright notices in the footer
|
||||
- **Index Pages**: "by Author Name" in post listings
|
||||
- **Author Pages**: Dedicated pages listing posts by each author
|
||||
- **Navigation Menu**: "Authors" link (when multiple authors exist)
|
||||
- **RSS Feeds**: Dublin Core `dc:creator` elements with proper author attribution
|
||||
- **Schema.org Metadata**: JSON-LD structured data for search engines
|
||||
- **Archive Pages**: Author information in post listings
|
||||
|
||||
### Fediverse Creator Tag
|
||||
|
||||
BSSG can emit Mastodon's `fediverse:creator` metadata across generated pages so link previews can show and follow the author more easily.
|
||||
|
||||
#### Fallback Order
|
||||
|
||||
BSSG resolves the creator tag in this order:
|
||||
|
||||
1. `fediverse_creator` in the post frontmatter
|
||||
2. `AUTHOR_FEDIVERSE_CREATORS["Author Name"]` from config, matched against `author_name`
|
||||
3. `FEDIVERSE_CREATOR` from config
|
||||
|
||||
If none of those are set, no `fediverse:creator` meta tag is emitted.
|
||||
|
||||
For non-post pages such as the homepage, tags, archives, authors, and static pages, BSSG uses the resolved site-level/default creator. Individual posts can still override that value with `fediverse_creator` in frontmatter.
|
||||
|
||||
#### Configuration
|
||||
|
||||
Add a site-wide default in `config.sh.local`:
|
||||
|
||||
```bash
|
||||
FEDIVERSE_CREATOR="@you@example.social"
|
||||
```
|
||||
|
||||
For multi-author sites, you can optionally add exact-match per-author overrides:
|
||||
|
||||
```bash
|
||||
declare -A AUTHOR_FEDIVERSE_CREATORS=(
|
||||
["Jane Smith"]="@jane@example.social"
|
||||
["John Doe"]="@john@example.com"
|
||||
)
|
||||
```
|
||||
|
||||
If you customize `templates/header.html`, keep `{{fediverse_creator_meta}}` inside `<head>`. The bundled template already includes it, and BSSG also falls back to injecting the tag before `</head>` for older custom headers.
|
||||
|
||||
#### Examples
|
||||
|
||||
**Post with custom author:**
|
||||
```markdown
|
||||
---
|
||||
title: Guest Post Example
|
||||
author_name: Jane Smith
|
||||
author_email: jane@example.com
|
||||
---
|
||||
```
|
||||
|
||||
**Post with name only (no email):**
|
||||
```markdown
|
||||
---
|
||||
title: Anonymous Contributor Post
|
||||
author_name: Anonymous Contributor
|
||||
author_email: # Leave empty - no email will be included
|
||||
---
|
||||
```
|
||||
|
||||
**Post using default site author:**
|
||||
```markdown
|
||||
---
|
||||
title: Regular Post
|
||||
# No author fields - will use AUTHOR_NAME and AUTHOR_EMAIL from config
|
||||
---
|
||||
```
|
||||
|
||||
This feature is particularly useful for:
|
||||
- Guest posts from different authors
|
||||
- Multi-author blogs or publications
|
||||
- Posts where you want to credit a specific contributor
|
||||
- Maintaining author attribution when migrating content from other platforms
|
||||
- Creating author-focused content organization alongside tags and archives
|
||||
|
||||
### Fediverse Profile Verification
|
||||
|
||||
BSSG can also emit one or more site-wide `<link rel="me">` tags in the document `<head>`, which is useful for Mastodon and compatible fediverse profile verification.
|
||||
|
||||
Add this to `config.sh.local` for a single profile:
|
||||
|
||||
```bash
|
||||
REL_ME_URL="https://mastodon.example.com/@john"
|
||||
```
|
||||
|
||||
Or use multiple links:
|
||||
|
||||
```bash
|
||||
REL_ME_URLS=(
|
||||
"https://mastodon.example.com/@john"
|
||||
"https://another-fedi.example/@john"
|
||||
)
|
||||
```
|
||||
|
||||
The default header.html now includes a `{{rel_me_link}}` placeholder, which expands to one or more tags such as:
|
||||
|
||||
```html
|
||||
<link rel="me" href="https://mastodon.example.com/@john">
|
||||
<link rel="me" href="https://another-fedi.example/@john">
|
||||
```
|
||||
|
||||
If both `REL_ME_URL` and `REL_ME_URLS` are set, BSSG emits all unique URLs from both. If neither is set, BSSG omits the tags.
|
||||
|
||||
## Customization
|
||||
|
||||
To customize the appearance of your site, you can edit:
|
||||
|
|
@ -584,40 +755,99 @@ The `config.sh` file contains the default configuration settings for the site ge
|
|||
|
||||
```bash
|
||||
# Directory configuration
|
||||
SRC_DIR="src" # Source directory for posts
|
||||
PAGES_DIR="pages" # Source directory for pages
|
||||
DRAFTS_DIR="drafts" # Source directory for drafts (posts and pages)
|
||||
OUTPUT_DIR="output" # Where the generated site is placed
|
||||
SRC_DIR="src"
|
||||
PAGES_DIR="pages" # Directory for static pages
|
||||
OUTPUT_DIR="output"
|
||||
TEMPLATES_DIR="templates"
|
||||
THEMES_DIR="themes"
|
||||
STATIC_DIR="static"
|
||||
DRAFTS_DIR="drafts" # Directory for drafts
|
||||
THEME="default"
|
||||
CACHE_DIR=".bssg_cache" # Default cache directory location (relative to BSSG root)
|
||||
|
||||
# Build configuration
|
||||
CLEAN_OUTPUT=false
|
||||
CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild
|
||||
REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh)
|
||||
REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh)
|
||||
PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build.
|
||||
BUILD_MODE="normal" # Options: "normal", "ram". RAM mode preloads inputs and keeps build indexes/data in memory.
|
||||
|
||||
# Optional performance tunables (not required):
|
||||
# RAM_MODE_MAX_JOBS=6 # Cap parallel workers in RAM mode (defaults to 6)
|
||||
# RAM_MODE_VERBOSE=false # Extra RAM-mode debug/timing logs
|
||||
# PRECOMPRESS_GZIP_LEVEL=9 # gzip level for precompression (1-9)
|
||||
# PRECOMPRESS_MAX_JOBS=0 # 0=auto based on CPU/RAM mode cap
|
||||
# PRECOMPRESS_VERBOSE=false # Verbose logs for precompression
|
||||
# RAM_RSS_PREFILL_MIN_HITS=2 # RAM tag-RSS cache prefill threshold
|
||||
# RAM_RSS_PREFILL_MAX_POSTS=24 # RAM tag-RSS prefill upper bound
|
||||
|
||||
# Customization
|
||||
CUSTOM_CSS="" # Optional: Path to custom CSS file relative to output root (e.g., "/css/custom.css"). File should be placed in STATIC_DIR.
|
||||
|
||||
# Site information
|
||||
SITE_TITLE="My Journal"
|
||||
SITE_DESCRIPTION="A personal journal and introspective newspaper"
|
||||
SITE_URL="http://localhost"
|
||||
SITE_TITLE="My new BSSG site"
|
||||
SITE_DESCRIPTION="A complete SSG - written in bash"
|
||||
SITE_URL="http://localhost:8000"
|
||||
AUTHOR_NAME="Anonymous"
|
||||
AUTHOR_EMAIL="anonymous@example.com"
|
||||
REL_ME_URL="" # Optional fediverse profile URL for <link rel="me"> verification
|
||||
# REL_ME_URLS=(
|
||||
# "https://mastodon.example.com/@john"
|
||||
# "https://another-fedi.example/@john"
|
||||
# )
|
||||
FEDIVERSE_CREATOR="" # Optional default fediverse:creator value for posts
|
||||
|
||||
# Content configuration
|
||||
DATE_FORMAT="%Y-%m-%d %H:%M:%S %z"
|
||||
TIMEZONE="local" # Options: "local", "GMT", or a specific timezone
|
||||
# Affects how dates are displayed in the generated site based on system interpretation.
|
||||
SHOW_TIMEZONE="false" # Options: "true", "false". Determines if the timezone offset (e.g., +0200) is shown in displayed dates.
|
||||
TIMEZONE="local" # Options: "local", "GMT", or a specific timezone like "America/New_York"
|
||||
SHOW_TIMEZONE="false" # Options: "true", "false". Whether to display the timezone in rendered dates.
|
||||
POSTS_PER_PAGE=10
|
||||
ENABLE_ARCHIVES=true # Enable or disable archives by year/month
|
||||
URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs
|
||||
RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed.
|
||||
RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be included in the RSS feed description instead of the excerpt. Useful for readers that consume entire posts via RSS.
|
||||
ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags/<tag-slug>/rss.xml`.
|
||||
RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". Include full post content in RSS feed.
|
||||
RSS_FILENAME="rss.xml" # The filename for the main RSS feed (e.g., feed.xml, rss.xml)
|
||||
INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". Show full post content on homepage instead of just description/excerpt.
|
||||
ENABLE_ARCHIVES=true # Enable or disable archive pages
|
||||
ENABLE_AUTHOR_PAGES=false # Enable or disable author pages (default: false)
|
||||
ENABLE_AUTHOR_RSS=false # Enable or disable author-specific RSS feeds (default: false)
|
||||
SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2)
|
||||
URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs. Available: Year, Month, Day, slug
|
||||
ENABLE_TAG_RSS=true # Enable or disable tag-specific RSS feed generation (default: true)
|
||||
|
||||
# Optional exact-match per-author fediverse overrides
|
||||
# declare -A AUTHOR_FEDIVERSE_CREATORS=(
|
||||
# ["Jane Smith"]="@jane@example.social"
|
||||
# )
|
||||
|
||||
# Archive Page Configuration
|
||||
ARCHIVES_LIST_ALL_POSTS="false" # Options: "true", "false". If true, list all posts on the main archive page.
|
||||
|
||||
# Page configuration
|
||||
PAGE_URL_FORMAT="slug" # Format for page URLs. Available: slug, filename (without ext)
|
||||
|
||||
# Markdown processing configuration
|
||||
MARKDOWN_PROCESSOR="commonmark" # Options: "pandoc", "commonmark", or "markdown.pl"
|
||||
|
||||
# Language Configuration
|
||||
SITE_LANG="en" # Default language code (e.g., en, es, fr). See locales/ directory.
|
||||
|
||||
# Related Posts Configuration
|
||||
ENABLE_RELATED_POSTS=true # Enable or disable related posts feature
|
||||
RELATED_POSTS_COUNT=3 # Number of related posts to show (default: 3)
|
||||
|
||||
# Server Configuration (for 'bssg.sh server' command)
|
||||
# These are the defaults used by 'bssg.sh server' if not overridden by command-line options.
|
||||
BSSG_SERVER_PORT_DEFAULT="8000" # Default port for the local development server
|
||||
BSSG_SERVER_HOST_DEFAULT="localhost" # Default host for the local development server
|
||||
|
||||
# Deployment configuration
|
||||
DEPLOY_AFTER_BUILD="false" # Options: "true", "false". Automatically deploy after a successful build.
|
||||
DEPLOY_SCRIPT="" # Path to the deployment script to execute if DEPLOY_AFTER_BUILD is true.
|
||||
|
||||
# Terminal colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
```
|
||||
|
||||
#### Date Format Examples
|
||||
|
|
@ -740,6 +970,7 @@ BSSG includes a variety of themes to customize the look of your site. Themes are
|
|||
- `dark` - Dark mode theme
|
||||
- `flat` - Microsoft Metro/Modern UI inspired flat design
|
||||
- `glassmorphism` - Modern frosted glass effect with blue/teal gradient
|
||||
- `liquid-glass` - Fluid translucent surfaces with refractive highlights and layered depth
|
||||
- `material` - Material Design inspired theme
|
||||
- `art-deco` - Inspired by 1920s-30s Art Deco style with geometric patterns, elegant fonts, and gold/black/silver/jewel color palettes
|
||||
- `bauhaus` - Inspired by the Bauhaus school, focusing on functionality, primary geometric shapes, primary colors plus black and white, and clean sans-serif typography
|
||||
|
|
@ -764,9 +995,12 @@ BSSG includes a variety of themes to customize the look of your site. Themes are
|
|||
|
||||
#### Operating System Themes
|
||||
- `beos` - BeOS inspired theme
|
||||
- `freebsd` - FreeBSD-inspired theme with the iconic red/black visual language and orb motif
|
||||
- `macclassic` - Classic Mac OS inspired theme
|
||||
- `macos9` - Mac OS 9 inspired theme
|
||||
- `netbsd` - NetBSD-inspired theme with a navy/orange flag aesthetic and engineering-focused layout
|
||||
- `nextstep` - NeXTSTEP inspired theme
|
||||
- `openbsd` - OpenBSD-inspired yellow/black Puffy-style theme with bold security-flavored styling
|
||||
- `osx` - macOS inspired theme
|
||||
- `win311` - Windows 3.11 inspired theme
|
||||
- `win95` - Windows 95 inspired theme
|
||||
|
|
@ -784,12 +1018,17 @@ BSSG includes a variety of themes to customize the look of your site. Themes are
|
|||
- `docs` - A clean, structured theme ideal for technical documentation with excellent code formatting and clear navigation
|
||||
- `longform` - Optimized for reading long articles with highly readable typography, contained text width, and minimal distractions
|
||||
- `reader-mode` - Simulates browser reader mode with almost total emphasis on text, sepia background, very readable serif font, and minimal graphic elements
|
||||
- `mynotes` - A warm, intimate, text-first journal theme designed for meditative long-form reading
|
||||
- `museum-label` - Museum catalog style with refined serif typography, restrained metadata, and clean archival cards
|
||||
- `field-journal` - Warm paper-inspired writing theme with natural tones and notebook-style presentation
|
||||
- `thoughtful` - A warm, accessible, and performant theme for personal reflection blogs and thoughtful writing
|
||||
- `text-only` - A step beyond minimalism using browser defaults with clean base typography for readability and lightning-fast loading
|
||||
|
||||
#### Special Themes
|
||||
- `brutalist` - Raw, minimalist concrete-inspired design
|
||||
- `newspaper` - Classic newspaper layout
|
||||
- `diary` - Personal diary/journal style
|
||||
- `microfiche` - Monochrome archival projection aesthetic with scanline and microfilm-inspired styling
|
||||
- `random` - Selects a random theme (from the available themes) for each build
|
||||
|
||||
To use a theme, specify it in your config file:
|
||||
|
|
@ -822,47 +1061,157 @@ You can also specify a custom SITE_URL for the previews:
|
|||
./generate_theme_previews.sh --site-url "https://example.com/blog"
|
||||
```
|
||||
|
||||
The script will use the SITE_URL from the following sources in order of precedence:
|
||||
1. Command line argument (--site-url)
|
||||
2. Local config file (config.sh.local)
|
||||
3. Main config file (config.sh)
|
||||
4. Default value (http://localhost)
|
||||
You can also point the preview generator at a site-specific config file, just like `bssg.sh build`:
|
||||
|
||||
```bash
|
||||
./generate_theme_previews.sh --config /path/to/site/config.sh.local
|
||||
```
|
||||
|
||||
BSSG configuration is resolved in this order:
|
||||
1. Command line argument (`--config`)
|
||||
2. `BSSG_LCONF` environment variable
|
||||
3. Local config file (`config.sh.local`)
|
||||
4. Main config file (`config.sh`)
|
||||
|
||||
The preview `SITE_URL` is then chosen from:
|
||||
1. Command line argument (`--site-url`)
|
||||
2. The selected BSSG configuration
|
||||
3. Default value (`http://localhost`)
|
||||
|
||||
Each theme preview will be accessible at `SITE_URL/theme` (e.g., `https://example.com/blog/dark`).
|
||||
|
||||
## Admin Interface
|
||||
|
||||
BSSG includes an admin interface for managing your blog. To use the admin interface:
|
||||
**Note: The admin interface is currently in development and has not been released yet. This section describes planned features for a future release.**
|
||||
|
||||
1. Make sure you have Node.js installed
|
||||
2. Navigate to the `admin` directory
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
4. Start the admin server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
The admin interface provides a user-friendly way to:
|
||||
BSSG will include an admin interface for managing your blog. When released, the admin interface will provide a user-friendly way to:
|
||||
- Create and edit posts with a WYSIWYG Markdown editor
|
||||
- Create and manage drafts
|
||||
- Schedule posts for future publication
|
||||
- Organize posts with tags
|
||||
- View statistics about your blog
|
||||
|
||||
For more detailed information about the admin interface, see the [admin/README.md](admin/README.md) file.
|
||||
The admin interface will feature:
|
||||
1. Node.js-based server
|
||||
2. Modern web interface
|
||||
3. Post scheduling capabilities
|
||||
4. Draft management
|
||||
5. Blog statistics and analytics
|
||||
|
||||
### Post Scheduling
|
||||
### Post Scheduling (Planned Feature)
|
||||
|
||||
The admin interface allows you to schedule posts for future publication. When you create or edit a post, you can:
|
||||
The planned admin interface will allow you to schedule posts for future publication. When available, you will be able to:
|
||||
|
||||
1. Choose "Schedule for later" option
|
||||
2. Select the date and time for publication
|
||||
3. The post will be stored as a draft until the scheduled time
|
||||
4. At the scheduled time, the post will be automatically published
|
||||
|
||||
## BSSG Post Editor
|
||||
|
||||
BSSG includes a standalone post editor (`bssg-editor.html`) that provides a modern, Ghost-like writing experience entirely in your browser. This editor is perfect for users who prefer a visual interface over command-line tools.
|
||||
|
||||
### Features
|
||||
|
||||
- **Modern Interface**: Clean, distraction-free design inspired by Ghost CMS
|
||||
- **Split-Pane Editor**: Side-by-side markdown editor and live preview (toggleable)
|
||||
- **Complete BSSG Integration**: Full support for all BSSG frontmatter fields
|
||||
- **Smart Auto-Save**: Automatically saves your work every 10 words or after 5 seconds of inactivity
|
||||
- **Article Management**: Save, load, search, and organize multiple articles locally
|
||||
- **Unsplash Integration**: Built-in image browser with search and automatic attribution
|
||||
- **Rich Toolbar**: Quick formatting buttons for headers, lists, links, images, code, and more
|
||||
- **Keyboard Shortcuts**: Full keyboard support (Ctrl+B for bold, Ctrl+I for italic, Ctrl+S to save, etc.)
|
||||
- **Theme Support**: Dark/light mode toggle
|
||||
- **Focus Mode**: Distraction-free writing environment
|
||||
- **Export Options**: Export to .md files, copy to clipboard, or import existing files
|
||||
- **Responsive Design**: Works on desktop, tablet, and mobile devices
|
||||
- **Offline Capable**: No server required - runs entirely in your browser
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. **Open the Editor**: Simply open `bssg-editor.html` in your web browser
|
||||
2. **Configure Settings** (Optional): Add your Unsplash API key in the settings panel for real image search
|
||||
3. **Start Writing**: Fill in the post metadata in the sidebar and start writing in the editor
|
||||
4. **Save Your Work**: Use Ctrl+S to save articles locally, or use the auto-save feature
|
||||
5. **Export**: When ready, export your post as a .md file with proper BSSG formatting
|
||||
|
||||
### Usage Tips
|
||||
|
||||
- **Frontmatter**: All BSSG frontmatter fields are supported - title, date, tags, slug, description, image, etc.
|
||||
- **File Naming**: Exported files follow BSSG naming convention: `YYYY-MM-DD-slug.md`
|
||||
- **Image Integration**: Use the Unsplash button (🖼️) to search and insert images with proper attribution
|
||||
- **Article Management**: Save multiple articles locally and switch between them using the Load button
|
||||
- **Keyboard Shortcuts**:
|
||||
- `Ctrl+N`: New article
|
||||
- `Ctrl+S`: Save article
|
||||
- `Ctrl+O`: Load article
|
||||
- `Ctrl+P`: Toggle preview
|
||||
- `Ctrl+B`: Bold text
|
||||
- `Ctrl+I`: Italic text
|
||||
- `Ctrl+K`: Insert link
|
||||
- `Esc`: Exit focus mode
|
||||
|
||||
### Unsplash Integration
|
||||
|
||||
To use real Unsplash images instead of demo placeholders:
|
||||
|
||||
1. Get a free API key from [Unsplash Developers](https://unsplash.com/developers)
|
||||
2. Enter your API key in the Settings section of the editor
|
||||
3. Use the image button (🖼️) to search and select professional photos
|
||||
4. Images are automatically attributed according to Unsplash guidelines
|
||||
|
||||
The editor works without an API key using demo images, but real Unsplash integration provides access to millions of high-quality photos.
|
||||
|
||||
### Integration with BSSG Workflow
|
||||
|
||||
The BSSG Post Editor generates markdown files that are fully compatible with your BSSG workflow:
|
||||
|
||||
1. **Write** your post in the editor
|
||||
2. **Export** the .md file to your BSSG `src/` directory
|
||||
3. **Build** your site with `./bssg.sh build`
|
||||
4. **Publish** as usual
|
||||
|
||||
The editor can also import existing BSSG posts for editing, making it easy to update content with a visual interface.
|
||||
|
||||
### Embedding the Editor in Your Website
|
||||
|
||||
Since the BSSG Post Editor runs entirely in the browser with no server dependencies, you can safely embed it directly in your published website. This allows you to access the editor from anywhere and provides a convenient way to create content on-the-go.
|
||||
|
||||
**To embed the editor:**
|
||||
|
||||
1. **Copy the editor file** to your static directory:
|
||||
```bash
|
||||
cp bssg-editor.html static/editor.html
|
||||
```
|
||||
|
||||
2. **Build your site** as usual:
|
||||
```bash
|
||||
./bssg.sh build
|
||||
```
|
||||
|
||||
3. **Access the editor** through your website:
|
||||
```
|
||||
https://yoursite.com/editor.html
|
||||
```
|
||||
|
||||
**Benefits of embedding:**
|
||||
|
||||
- **Remote Access**: Write posts from any device with internet access
|
||||
- **No Installation**: No need to have BSSG installed locally to create content
|
||||
- **Secure**: Since it's client-side only, there are no security implications
|
||||
- **Convenient**: Always available alongside your published content
|
||||
- **Mobile Friendly**: The responsive design works well on tablets and phones
|
||||
|
||||
**Workflow with embedded editor:**
|
||||
|
||||
1. **Access** the editor at `yoursite.com/editor.html`
|
||||
2. **Write** your post using the visual interface
|
||||
3. **Export** the markdown file when finished
|
||||
4. **Upload** the file to your `src/` directory (via FTP, Git, or your preferred method)
|
||||
5. **Rebuild** your site to publish the new content
|
||||
|
||||
**Security Note**: The editor stores data only in your browser's local storage and never transmits content to external servers (except for optional Unsplash image search). All article management and auto-save functionality works entirely offline, making it safe to embed in public websites.
|
||||
|
||||
## Performance Features
|
||||
|
||||
BSSG is designed to be efficient even with large sites, using several performance-enhancing techniques:
|
||||
|
|
@ -884,13 +1233,25 @@ The system maintains a cache of extracted metadata from markdown files to reduce
|
|||
- File index information is stored in `.bssg_cache/file_index.txt`
|
||||
- Tags index information is stored in `.bssg_cache/tags_index.txt`
|
||||
|
||||
### RAM Build Mode
|
||||
|
||||
BSSG supports a RAM-first build mode for faster full rebuilds and lower disk churn:
|
||||
|
||||
- Set `BUILD_MODE="ram"` in `config.sh.local`, or run `./bssg.sh build --build-mode ram`
|
||||
- Source/posts/pages/templates/locales are preloaded in memory
|
||||
- Build indexes (file/tags/authors/archive, plus page lists) are kept in memory
|
||||
- RAM mode intentionally skips cache persistence and always behaves like an in-memory full rebuild
|
||||
- A stage timing summary is printed at the end of RAM-mode builds
|
||||
- On low-end disk-bound hosts, RAM mode can significantly reduce build time by avoiding repeated disk reads
|
||||
|
||||
### Parallel Processing
|
||||
|
||||
If GNU parallel is installed on your system, BSSG can process multiple files simultaneously:
|
||||
BSSG uses multiple execution strategies to process files in parallel:
|
||||
|
||||
- Automatically detects GNU parallel and enables it for builds with many files
|
||||
- Uses 80% of available CPU cores for optimal performance
|
||||
- Falls back to sequential processing if parallel is not available
|
||||
- Falls back to internal shell workers when GNU parallel is unavailable or unsuitable for a stage
|
||||
- Auto-detects CPU core count for worker sizing
|
||||
- In RAM mode, worker count is capped by `RAM_MODE_MAX_JOBS` (default: `6`) to reduce memory pressure
|
||||
|
||||
To take advantage of parallel processing, install GNU parallel:
|
||||
|
||||
|
|
@ -905,6 +1266,10 @@ brew install parallel
|
|||
pkg install parallel
|
||||
```
|
||||
|
||||
### Real-World Result
|
||||
|
||||
On a single-core OpenBSD server with spinning disks, the maintainer observed build time dropping to about one third of the previous release when building with `BUILD_MODE="ram"`.
|
||||
|
||||
## Site Configuration
|
||||
|
||||
Key configuration options:
|
||||
|
|
@ -922,11 +1287,34 @@ DATE_FORMAT="%Y-%m-%d %H:%M:%S %z"
|
|||
TIMEZONE="local" # Options: "local", "GMT", or a specific timezone
|
||||
SHOW_TIMEZONE="false" # Options: "true", "false". Determines if the timezone offset (e.g., +0200) is shown in displayed dates.
|
||||
POSTS_PER_PAGE=10
|
||||
BUILD_MODE="normal" # "normal" (incremental cache-backed) or "ram" (memory-first)
|
||||
ENABLE_ARCHIVES=true # Enable or disable archives by year/month
|
||||
URL_SLUG_FORMAT="Year/Month/Day/slug" # Format for post URLs
|
||||
RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed.
|
||||
RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be included in the RSS feed description instead of the excerpt. Useful for readers that consume entire posts via RSS.
|
||||
INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". If set to "true", the full post content will be displayed on the homepage and paginated index pages instead of just the description/excerpt.
|
||||
ENABLE_TAG_RSS=true # Options: "true", "false". If set to "true" (default), an additional RSS feed will be generated for each tag at `output/tags/<tag-slug>/rss.xml`.
|
||||
|
||||
# Precompression options
|
||||
PRECOMPRESS_ASSETS="false" # Generate .gz siblings for changed text assets
|
||||
# PRECOMPRESS_GZIP_LEVEL=9
|
||||
# PRECOMPRESS_MAX_JOBS=0
|
||||
# PRECOMPRESS_VERBOSE=false
|
||||
|
||||
# RAM-mode tuning (optional)
|
||||
# RAM_MODE_MAX_JOBS=6
|
||||
# RAM_MODE_VERBOSE=false
|
||||
# RAM_RSS_PREFILL_MIN_HITS=2
|
||||
# RAM_RSS_PREFILL_MAX_POSTS=24
|
||||
|
||||
# Related Posts configuration
|
||||
ENABLE_RELATED_POSTS=true # Options: "true", "false". If set to "true" (default), related posts based on shared tags will be shown at the end of each post.
|
||||
RELATED_POSTS_COUNT=3 # Number of related posts to display (default: 3, recommended maximum: 5).
|
||||
|
||||
# Multi-author configuration
|
||||
ENABLE_AUTHOR_PAGES=false # Options: "true", "false". If set to "true", author index pages will be generated.
|
||||
ENABLE_AUTHOR_RSS=false # Options: "true", "false". If set to "true", RSS feeds will be generated for each author.
|
||||
SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum number of authors required to show the "Authors" menu item.
|
||||
```
|
||||
|
||||
The `URL_SLUG_FORMAT` setting determines how your post URLs are structured. By default, it uses `Year/Month/Day/slug` which creates URLs like `http://yoursite.com/2023/01/15/my-post-title/`.
|
||||
|
|
@ -936,12 +1324,48 @@ Other possible formats include:
|
|||
- `Year/slug` - For `/2023/post-title/` URLs
|
||||
- `Year/Month/slug` - For `/2023/01/post-title/` URLs
|
||||
|
||||
## Local Development Server
|
||||
|
||||
BSSG includes a simple built-in web server to help you preview your site locally.
|
||||
|
||||
```bash
|
||||
./bssg.sh server [options]
|
||||
```
|
||||
|
||||
This command will:
|
||||
1. **Build your site**: It automatically runs the build process.
|
||||
2. **Adjust `SITE_URL`**: For the duration of this build, it temporarily sets `SITE_URL` to match the local server's address (e.g., `http://localhost:8000` or `http://<your-host>:<your-port>`). This ensures that all generated links and asset paths work correctly during local preview. The original `SITE_URL` in your configuration files remains unchanged for regular builds.
|
||||
3. **Start the server**: It serves files from your configured `OUTPUT_DIR`.
|
||||
|
||||
**Server Options:**
|
||||
|
||||
- `--port <PORT>`: Specifies the port for the server to listen on.
|
||||
- Default: Value of `BSSG_SERVER_PORT_DEFAULT` from your configuration (typically `8000`).
|
||||
- `--host <HOST>`: Specifies the host/IP address for the server.
|
||||
- Default: Value of `BSSG_SERVER_HOST_DEFAULT` from your configuration (typically `localhost`).
|
||||
- `--no-build`: Skips the build step and immediately starts the server with the existing content in the `OUTPUT_DIR`. Useful if you have just built the site and want to quickly restart the server.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
# Build and serve on http://localhost:8080
|
||||
./bssg.sh server --port 8080
|
||||
|
||||
# Serve on a specific host, accessible on your local network (if firewall allows)
|
||||
./bssg.sh server --host 192.168.0.2 --port 8000
|
||||
|
||||
# Serve existing build without rebuilding
|
||||
./bssg.sh server --no-build
|
||||
```
|
||||
|
||||
Press `Ctrl+C` to stop the server.
|
||||
|
||||
|
||||
## Future Plans
|
||||
|
||||
While BSSG is designed to be simple, there are a few enhancements planned for the future:
|
||||
|
||||
- **Stale Content Banner:** Add an option to display a banner on posts that haven't been updated in a configurable amount of time (e.g., more than X days/months).
|
||||
- **Performance Refactor:** Address identified performance bottlenecks and improve the overall efficiency of the build process.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
2925
bssg-editor.html
Normal file
2925
bssg-editor.html
Normal file
File diff suppressed because it is too large
Load diff
39
config.sh
39
config.sh
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# BSSG - Configuration File
|
||||
# Version 0.15
|
||||
# Version 0.32
|
||||
# Contains all configurable parameters for the static site generator
|
||||
# Developed by Stefano Marinelli (stefano@dragas.it)
|
||||
#
|
||||
|
|
@ -21,11 +21,14 @@ THEMES_DIR="themes"
|
|||
STATIC_DIR="static"
|
||||
DRAFTS_DIR="drafts" # Directory for drafts
|
||||
THEME="default"
|
||||
CACHE_DIR=".bssg_cache" # Default cache directory location (relative to BSSG root)
|
||||
|
||||
# Build configuration
|
||||
CLEAN_OUTPUT=false # If true, BSSG will always perform a full rebuild
|
||||
REBUILD_AFTER_POST=true # Build site automatically after creating a new post (scripts/post.sh)
|
||||
REBUILD_AFTER_EDIT=true # Build site automatically after editing a post (scripts/edit.sh)
|
||||
PRECOMPRESS_ASSETS="false" # Options: "true", "false". If true, compress text assets (HTML, CSS, XML, JS) with gzip during build.
|
||||
BUILD_MODE="ram" # Options: "normal", "ram". "ram" preloads inputs and keeps build state in memory (writes only output artifacts).
|
||||
|
||||
# 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.
|
||||
|
|
@ -36,6 +39,19 @@ SITE_DESCRIPTION="A complete SSG - written in bash"
|
|||
SITE_URL="http://localhost:8000"
|
||||
AUTHOR_NAME="Anonymous"
|
||||
AUTHOR_EMAIL="anonymous@example.com"
|
||||
REL_ME_URL="" # Optional fediverse profile URL for <link rel="me"> verification, e.g. "https://mastodon.example/@john"
|
||||
# Optional additional rel="me" verification links. BSSG emits all unique values from
|
||||
# REL_ME_URL and REL_ME_URLS.
|
||||
# REL_ME_URLS=(
|
||||
# "https://mastodon.example/@john"
|
||||
# "https://another-fedi.example/@john"
|
||||
# )
|
||||
FEDIVERSE_CREATOR="" # Optional default fediverse:creator value for posts, e.g. "@you@example.social"
|
||||
# Optional per-author overrides matched against author_name exactly.
|
||||
# declare -A AUTHOR_FEDIVERSE_CREATORS=(
|
||||
# ["Jane Smith"]="@jane@example.social"
|
||||
# ["John Doe"]="@john@example.com"
|
||||
# )
|
||||
|
||||
# Content configuration
|
||||
DATE_FORMAT="%Y-%m-%d %H:%M:%S %z"
|
||||
|
|
@ -44,12 +60,20 @@ SHOW_TIMEZONE="false" # Options: "true", "false". Whether to display the timezon
|
|||
POSTS_PER_PAGE=10
|
||||
RSS_ITEM_LIMIT=15 # Number of items to include in the RSS feed.
|
||||
RSS_INCLUDE_FULL_CONTENT="false" # Options: "true", "false". Include full post content in RSS feed.
|
||||
RSS_FILENAME="rss.xml" # The filename for the main RSS feed (e.g., feed.xml, rss.xml)
|
||||
INDEX_SHOW_FULL_CONTENT="false" # Options: "true", "false". Show full post content on homepage instead of just description/excerpt.
|
||||
ENABLE_ARCHIVES=true # Enable or disable archive pages
|
||||
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)
|
||||
|
||||
# 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"
|
||||
|
|
@ -57,6 +81,15 @@ 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.
|
||||
|
|
|
|||
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"
|
||||
|
|
@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d Min. Lesezeit"
|
|||
export MSG_MINUTE="Minute"
|
||||
export MSG_MINUTES="Minuten"
|
||||
export MSG_UPDATED_ON="Aktualisiert am"
|
||||
export MSG_BACK_TO_TOP="Nach oben"
|
||||
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"
|
||||
|
|
@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min read"
|
|||
export MSG_MINUTE="minute"
|
||||
export MSG_MINUTES="minutes"
|
||||
export MSG_UPDATED_ON="Updated on"
|
||||
export MSG_BACK_TO_TOP="Back to Top"
|
||||
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"
|
||||
|
|
@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de lectura"
|
|||
export MSG_MINUTE="minuto"
|
||||
export MSG_MINUTES="minutos"
|
||||
export MSG_UPDATED_ON="Actualizado el"
|
||||
export MSG_BACK_TO_TOP="Volver arriba"
|
||||
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 à"
|
||||
|
|
@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de lecture"
|
|||
export MSG_MINUTE="minute"
|
||||
export MSG_MINUTES="minutes"
|
||||
export MSG_UPDATED_ON="Mis à jour le"
|
||||
export MSG_BACK_TO_TOP="Retour en haut"
|
||||
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"
|
||||
|
|
@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min di lettura"
|
|||
export MSG_MINUTE="minuto"
|
||||
export MSG_MINUTES="minuti"
|
||||
export MSG_UPDATED_ON="Aggiornato il"
|
||||
export MSG_BACK_TO_TOP="Torna su"
|
||||
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="に戻る"
|
||||
|
|
@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="読了時間 %d分"
|
|||
export MSG_MINUTE="分"
|
||||
export MSG_MINUTES="分"
|
||||
export MSG_UPDATED_ON="更新日"
|
||||
export MSG_BACK_TO_TOP="トップに戻る"
|
||||
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"
|
||||
|
|
@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="%d min de leitura"
|
|||
export MSG_MINUTE="minuto"
|
||||
export MSG_MINUTES="minutos"
|
||||
export MSG_UPDATED_ON="Atualizado em"
|
||||
export MSG_BACK_TO_TOP="Voltar ao topo"
|
||||
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="返回"
|
||||
|
|
@ -44,4 +47,5 @@ export MSG_READING_TIME_TEMPLATE="阅读时间 %d 分钟"
|
|||
export MSG_MINUTE="分钟"
|
||||
export MSG_MINUTES="分钟"
|
||||
export MSG_UPDATED_ON="更新于"
|
||||
export MSG_BACK_TO_TOP="返回顶部"
|
||||
export MSG_BACK_TO_TOP="返回顶部"
|
||||
export MSG_RELATED_POSTS="相关文章"
|
||||
359
scripts/bssg.sh
359
scripts/bssg.sh
|
|
@ -9,6 +9,53 @@
|
|||
|
||||
set -e
|
||||
|
||||
# --- Argument Parsing for --config --- START ---
|
||||
# We need to parse arguments early to catch --config before loading the config.
|
||||
# This allows the specified config file to override defaults.
|
||||
CMD_LINE_CONFIG_FILE=""
|
||||
declare -a OTHER_ARGS # Array to hold arguments not related to --config
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
--config)
|
||||
if [[ -z "$2" || "$2" == -* ]]; then # Check if value is missing or looks like another flag
|
||||
echo -e "${RED}Error: --config option requires a path argument.${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
CMD_LINE_CONFIG_FILE="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
*) # unknown option or command
|
||||
OTHER_ARGS+=("$1") # save it in an array for later
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Restore positional parameters from the filtered arguments
|
||||
set -- "${OTHER_ARGS[@]}"
|
||||
# --- Argument Parsing for --config --- END ---
|
||||
|
||||
# --- Configuration Override Logic --- START ---
|
||||
# Priority: --config > BSSG_LCONF > Default (config.sh.local)
|
||||
FINAL_CONFIG_OVERRIDE=""
|
||||
|
||||
if [ -n "$CMD_LINE_CONFIG_FILE" ]; then
|
||||
# --config flag was used, prioritize it
|
||||
FINAL_CONFIG_OVERRIDE="$CMD_LINE_CONFIG_FILE"
|
||||
echo "Info: Using configuration file specified via --config: $FINAL_CONFIG_OVERRIDE"
|
||||
elif [ -v BSSG_LCONF ] && [ -n "${BSSG_LCONF}" ]; then
|
||||
# --config not used, check BSSG_LCONF environment variable
|
||||
FINAL_CONFIG_OVERRIDE="${BSSG_LCONF}"
|
||||
echo "Info: Using configuration file specified via BSSG_LCONF environment variable: $FINAL_CONFIG_OVERRIDE"
|
||||
# else
|
||||
# Neither --config nor BSSG_LCONF is set, config_loader.sh will check for default config.sh.local
|
||||
# No message needed here, config_loader will print messages.
|
||||
fi
|
||||
# --- Configuration Override Logic --- END ---
|
||||
|
||||
# Load configuration (DEPRECATED - Moved to config_loader.sh)
|
||||
# CONFIG_FILE="config.sh"
|
||||
# if [ -f "$CONFIG_FILE" ]; then
|
||||
|
|
@ -29,6 +76,7 @@ set -e
|
|||
# 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 )"
|
||||
|
|
@ -36,9 +84,12 @@ 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"
|
||||
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 -e "${RED}Error: Config loader script not found at '$CONFIG_LOADER_SCRIPT'${NC}" >&2
|
||||
exit 1
|
||||
|
|
@ -46,20 +97,36 @@ fi
|
|||
# --- Centralized Configuration Loading --- END ---
|
||||
|
||||
# Terminal colors (still needed here if config_loader doesn't export them, though it should)
|
||||
RED='${RED:-\\033[0;31m}' # Default if not exported
|
||||
GREEN='${GREEN:-\\033[0;32m}'
|
||||
YELLOW='${YELLOW:-\\033[0;33m}'
|
||||
NC='${NC:-\\033[0m}'
|
||||
# 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
|
||||
|
||||
# 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.15)"
|
||||
echo "BSSG - Bash Static Site Generator (v0.33)"
|
||||
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"
|
||||
|
|
@ -78,26 +145,82 @@ show_help() {
|
|||
echo " restore [backup_file|ID] Restore from a backup (all content by default)"
|
||||
echo " Options: --no-content, --no-config"
|
||||
echo " backups List all available backups"
|
||||
echo " build 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 "$@"
|
||||
|
|
@ -131,9 +254,212 @@ main() {
|
|||
;;
|
||||
build)
|
||||
# Call the new build orchestrator script in the build/ directory
|
||||
# Pass along any additional arguments (e.g., --force-rebuild)
|
||||
echo "Invoking new build process..."
|
||||
scripts/build/main.sh "$@"
|
||||
# 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
|
||||
|
|
@ -156,4 +482,5 @@ main() {
|
|||
}
|
||||
|
||||
# Run the main function
|
||||
# Pass the filtered arguments (command and its options) to main
|
||||
main "$@"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
#
|
||||
|
||||
# Define cache paths (should match exported config, but useful here too)
|
||||
CACHE_DIR=".bssg_cache"
|
||||
CONFIG_HASH_FILE="$CACHE_DIR/config_hash.md5"
|
||||
# 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)
|
||||
|
||||
|
|
@ -20,7 +20,9 @@ create_config_hash() {
|
|||
# IMPORTANT: Requires BSSG_CONFIG_VARS to be exported from config_loader.sh
|
||||
local config_string=""
|
||||
local var_name
|
||||
local config_vars_array=($BSSG_CONFIG_VARS)
|
||||
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}"
|
||||
|
|
@ -57,7 +59,9 @@ config_has_changed() {
|
|||
# IMPORTANT: Requires BSSG_CONFIG_VARS to be exported from config_loader.sh
|
||||
local config_string=""
|
||||
local var_name
|
||||
local config_vars_array=($BSSG_CONFIG_VARS)
|
||||
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}"
|
||||
|
|
@ -87,20 +91,22 @@ config_has_changed() {
|
|||
# other implicit theme-related changes that weren't previously tracked.
|
||||
# For now, it assumes `config_has_changed` correctly reflects all non-theme changes.
|
||||
only_theme_changed() {
|
||||
local theme_cache_file="${CACHE_DIR}/theme.txt"
|
||||
# If no hash file exists, more than just theme has changed
|
||||
if [ ! -f "$CONFIG_HASH_FILE" ] || [ ! -f "$CACHE_DIR/theme.txt" ]; then
|
||||
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=$(cat "$CACHE_DIR/theme.txt")
|
||||
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" > "$CACHE_DIR/theme.txt"
|
||||
echo "$THEME" > "$theme_cache_file"
|
||||
|
||||
# Check if any other config has changed
|
||||
if ! config_has_changed; then
|
||||
|
|
@ -118,7 +124,6 @@ clean_stale_cache() {
|
|||
if [ "${FORCE_REBUILD:-false}" = true ]; then # Check exported FORCE_REBUILD
|
||||
echo -e "${YELLOW}Force rebuild enabled, deleting entire cache...${NC}"
|
||||
rm -rf "$CACHE_DIR"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
mkdir -p "$CACHE_DIR/meta"
|
||||
mkdir -p "$CACHE_DIR/content"
|
||||
echo -e "${GREEN}Cache deleted!${NC}"
|
||||
|
|
@ -166,19 +171,25 @@ clean_stale_cache() {
|
|||
if [ "$posts_removed" = true ]; then
|
||||
echo -e "${YELLOW}Posts were removed, forcing regeneration of index, tags, archives, sitemap, and RSS feed${NC}"
|
||||
# Remove marker files to force regeneration
|
||||
rm -f "$CACHE_DIR/tags_index.txt"
|
||||
rm -f "$CACHE_DIR/archive_index.txt"
|
||||
rm -f "$CACHE_DIR/index_marker"
|
||||
rm -f "${CACHE_DIR}/tags_index.txt"
|
||||
rm -f "${CACHE_DIR}/archive_index.txt"
|
||||
rm -f "${CACHE_DIR}/index_marker"
|
||||
# Remove the tags flag file as well
|
||||
rm -f "${CACHE_DIR:-.bssg_cache}/has_tags.flag"
|
||||
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.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}"
|
||||
|
|
@ -318,7 +329,7 @@ indexes_need_rebuild() {
|
|||
newest_meta_time=$(find "$meta_cache_dir" -type f -printf '%T@\n' 2>/dev/null | sort -nr | head -n 1)
|
||||
newest_meta_time=${newest_meta_time:-0} # Handle empty dir
|
||||
# Convert float timestamp to integer
|
||||
newest_meta_time=$(printf "%.0f" "$newest_meta_time")
|
||||
newest_meta_time=${newest_meta_time%.*} # Truncate to integer
|
||||
else
|
||||
# Fallback for non-GNU find (less efficient)
|
||||
local meta_files
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
# 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.
|
||||
|
|
@ -13,20 +17,29 @@ 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}"
|
||||
|
|
@ -36,19 +49,38 @@ 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
|
||||
|
||||
# --- 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
|
||||
RED='${RED:-\\033[0;31m}'
|
||||
GREEN='${GREEN:-\\033[0;32m}'
|
||||
YELLOW='${YELLOW:-\\033[0;33m}'
|
||||
BLUE='${BLUE:-\\033[0;34m}' # Added Blue for print_info
|
||||
NC='${NC:-\\033[0m}' # No Color
|
||||
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 ---
|
||||
|
||||
|
||||
|
|
@ -66,8 +98,25 @@ if [ -f "$UTILS_SCRIPT" ]; then
|
|||
exit 1
|
||||
fi
|
||||
else
|
||||
# Fallback to standard error echo if utils not found, but this is critical
|
||||
echo "Error: Utilities script not found at '$UTILS_SCRIPT'. Required by config_loader.sh." >&2
|
||||
# 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 ---
|
||||
|
|
@ -79,17 +128,32 @@ fi
|
|||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# shellcheck source=/dev/null disable=SC1090,SC1091
|
||||
source "$CONFIG_FILE"
|
||||
echo -e "${GREEN}Configuration loaded from $CONFIG_FILE${NC}"
|
||||
print_success "Default configuration loaded from $CONFIG_FILE"
|
||||
else
|
||||
print_warning "Configuration file '$CONFIG_FILE' not found, using defaults."
|
||||
print_warning "Default configuration file '$CONFIG_FILE' not found, using defaults."
|
||||
fi
|
||||
|
||||
# Check for local override config file (relative to the main config file)
|
||||
LOCAL_CONFIG_OVERRIDE="${CONFIG_FILE}.local"
|
||||
if [ -f "$LOCAL_CONFIG_OVERRIDE" ]; then
|
||||
# shellcheck source=/dev/null disable=SC1090,SC1091
|
||||
source "$LOCAL_CONFIG_OVERRIDE"
|
||||
print_success "Local configuration loaded from ${LOCAL_CONFIG_OVERRIDE}"
|
||||
# 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
|
||||
|
||||
|
||||
|
|
@ -136,7 +200,7 @@ export LOCAL_CONFIG_FILE # Export it for other scripts
|
|||
# After all configs are sourced, expand ~ in relevant paths before exporting.
|
||||
# This ensures scripts use the resolved paths, even if config stores portable '~'.
|
||||
print_info "Expanding tilde (~) in configuration paths..."
|
||||
PATHS_TO_EXPAND=("SRC_DIR" "PAGES_DIR" "DRAFTS_DIR" "OUTPUT_DIR" "TEMPLATES_DIR" "THEMES_DIR" "STATIC_DIR" "BACKUP_DIR") # Added BACKUP_DIR
|
||||
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}"
|
||||
|
|
@ -159,6 +223,68 @@ done
|
|||
# --- Expand Tilde in Path Variables --- END ---
|
||||
|
||||
|
||||
# --- Derived Configuration --- START ---
|
||||
serialize_author_fediverse_creators() {
|
||||
local serialized=""
|
||||
local author_name
|
||||
|
||||
if ! declare -p AUTHOR_FEDIVERSE_CREATORS >/dev/null 2>&1; then
|
||||
printf '%s' "$serialized"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$(declare -p AUTHOR_FEDIVERSE_CREATORS 2>/dev/null)" != "declare -A"* ]]; then
|
||||
print_warning "AUTHOR_FEDIVERSE_CREATORS is set but is not an associative array. Ignoring it."
|
||||
printf '%s' "$serialized"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r author_name; do
|
||||
serialized+="${author_name}"$'\t'"${AUTHOR_FEDIVERSE_CREATORS[$author_name]}"$'\n'
|
||||
done < <(printf '%s\n' "${!AUTHOR_FEDIVERSE_CREATORS[@]}" | LC_ALL=C sort)
|
||||
|
||||
printf '%s' "$serialized"
|
||||
}
|
||||
|
||||
serialize_rel_me_urls() {
|
||||
local serialized=""
|
||||
local rel_me_url=""
|
||||
|
||||
rel_me_url=$(printf '%s' "$REL_ME_URL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
if [ -n "$rel_me_url" ]; then
|
||||
serialized+="${rel_me_url}"$'\n'
|
||||
fi
|
||||
|
||||
if declare -p REL_ME_URLS >/dev/null 2>&1; then
|
||||
local rel_me_decl
|
||||
rel_me_decl="$(declare -p REL_ME_URLS 2>/dev/null)"
|
||||
if [[ "$rel_me_decl" == "declare -a"* ]]; then
|
||||
local rel_me_entry
|
||||
for rel_me_entry in "${REL_ME_URLS[@]}"; do
|
||||
rel_me_entry=$(printf '%s' "$rel_me_entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
if [ -n "$rel_me_entry" ]; then
|
||||
serialized+="${rel_me_entry}"$'\n'
|
||||
fi
|
||||
done
|
||||
else
|
||||
print_warning "REL_ME_URLS is set but is not a standard array. Ignoring it."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$serialized" ]; then
|
||||
printf '%s' ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s' "$serialized" | awk 'NF && !seen[$0]++'
|
||||
}
|
||||
|
||||
REL_ME_URLS_SERIALIZED="$(serialize_rel_me_urls)"
|
||||
AUTHOR_FEDIVERSE_CREATORS_SERIALIZED="$(serialize_author_fediverse_creators)"
|
||||
SITE_FEDIVERSE_CREATOR_META_TAG="$(build_fediverse_creator_meta_tag "${AUTHOR_NAME:-Anonymous}" "")"
|
||||
# --- Derived Configuration --- END ---
|
||||
|
||||
|
||||
# --- Export All Variables --- START ---
|
||||
|
||||
# Define the list of configuration variables relevant for hashing/exporting
|
||||
|
|
@ -166,15 +292,22 @@ done
|
|||
# and that should trigger a cache rebuild if changed.
|
||||
BSSG_CONFIG_VARS_ARRAY=(
|
||||
CONFIG_FILE SRC_DIR OUTPUT_DIR TEMPLATES_DIR THEMES_DIR STATIC_DIR THEME
|
||||
SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL
|
||||
DATE_FORMAT TIMEZONE SHOW_TIMEZONE POSTS_PER_PAGE RSS_ITEM_LIMIT RSS_INCLUDE_FULL_CONTENT CLEAN_OUTPUT
|
||||
FORCE_REBUILD SITE_LANG LOCALE_DIR PAGES_DIR MARKDOWN_PROCESSOR
|
||||
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
|
||||
BACKUP_DIR
|
||||
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
|
||||
# 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
|
||||
|
|
@ -195,14 +328,22 @@ 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
|
||||
|
|
@ -216,7 +357,24 @@ 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
|
||||
|
||||
# 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,
|
||||
|
|
@ -236,4 +394,9 @@ 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 ---
|
||||
# --- 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 ---
|
||||
|
|
|
|||
|
|
@ -14,10 +14,35 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s
|
|||
parse_metadata() {
|
||||
local file="$1"
|
||||
local field="$2"
|
||||
local value=""
|
||||
|
||||
# Ignore empty or directory inputs so callers can safely scan optional lists.
|
||||
if [[ -z "$file" || -d "$file" ]]; then
|
||||
echo "$value"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# RAM mode: parse directly from preloaded content to avoid disk/cache I/O.
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then
|
||||
local file_content frontmatter
|
||||
file_content=$(ram_mode_get_content "$file")
|
||||
frontmatter=$(printf '%s\n' "$file_content" | awk '
|
||||
BEGIN { in_fm = 0; found_fm = 0; }
|
||||
/^---$/ {
|
||||
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
|
||||
if (in_fm) { exit; }
|
||||
}
|
||||
in_fm { print; }
|
||||
')
|
||||
if [ -n "$frontmatter" ]; then
|
||||
value=$(printf '%s\n' "$frontmatter" | grep -m 1 "^$field:[[:space:]]*" | cut -d ':' -f 2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
fi
|
||||
echo "$value"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# IMPORTANT: Assumes CACHE_DIR is exported/available
|
||||
local cache_file="${CACHE_DIR:-.bssg_cache}/meta/$(basename "$file")"
|
||||
local value=""
|
||||
|
||||
# Get locks for cache access
|
||||
# IMPORTANT: Assumes lock_file/unlock_file are sourced/available
|
||||
|
|
@ -68,11 +93,20 @@ parse_metadata() {
|
|||
# Extract metadata from markdown file (builds cache)
|
||||
extract_metadata() {
|
||||
local file="$1"
|
||||
if [[ -z "$file" || -d "$file" ]]; then
|
||||
echo "ERROR_FILE_NOT_FOUND"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local metadata_cache_file="${CACHE_DIR:-.bssg_cache}/meta/$(basename "$file")"
|
||||
local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker"
|
||||
local ram_mode_active=false
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then
|
||||
ram_mode_active=true
|
||||
fi
|
||||
|
||||
# Check if file exists
|
||||
if [ ! -f "$file" ]; then
|
||||
if ! $ram_mode_active && [ ! -f "$file" ]; then
|
||||
echo "ERROR_FILE_NOT_FOUND"
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -81,7 +115,7 @@ extract_metadata() {
|
|||
local frontmatter_changed=false
|
||||
|
||||
# Check if cache exists and is newer than the source file
|
||||
if [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$metadata_cache_file" ] && [ "$metadata_cache_file" -nt "$file" ]; then
|
||||
if ! $ram_mode_active && [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$metadata_cache_file" ] && [ "$metadata_cache_file" -nt "$file" ]; then
|
||||
# Read from cache file (optimized - read once)
|
||||
echo "$(cat "$metadata_cache_file")"
|
||||
return 0
|
||||
|
|
@ -91,30 +125,46 @@ extract_metadata() {
|
|||
fi
|
||||
|
||||
# If we're here, we need to parse the file
|
||||
local title="" date="" lastmod="" tags="" slug="" image="" image_caption="" description=""
|
||||
local title="" date="" lastmod="" tags="" slug="" image="" image_caption="" description="" author_name="" author_email=""
|
||||
|
||||
# Check file type and parse accordingly
|
||||
if [[ "$file" == *.html ]]; then
|
||||
# Parse <meta> tags for HTML files
|
||||
# Use grep -m 1 for efficiency, handle missing tags gracefully
|
||||
# Note: This is basic parsing, assumes simple meta tag structure.
|
||||
title=$(grep -m 1 -o '<title>[^<]*</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/')
|
||||
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 awk with a here document for reliable script passing
|
||||
|
||||
# Run awk and read results
|
||||
# Use a shared awk parser for both disk and RAM paths.
|
||||
local parsed_data
|
||||
parsed_data=$(awk -f - "$file" <<'EOF'
|
||||
local awk_frontmatter_parser
|
||||
awk_frontmatter_parser=$(cat <<'EOF'
|
||||
BEGIN {
|
||||
in_fm = 0;
|
||||
found_fm = 0;
|
||||
|
|
@ -122,6 +172,7 @@ extract_metadata() {
|
|||
vars["title"] = ""; vars["date"] = ""; vars["lastmod"] = "";
|
||||
vars["tags"] = ""; vars["slug"] = ""; vars["image"] = "";
|
||||
vars["image_caption"] = ""; vars["description"] = "";
|
||||
vars["author_name"] = ""; vars["author_email"] = "";
|
||||
}
|
||||
/^---$/ {
|
||||
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
|
||||
|
|
@ -154,12 +205,19 @@ extract_metadata() {
|
|||
# Print values in specific order
|
||||
print vars["title"] "|" vars["date"] "|" vars["lastmod"] "|" \
|
||||
vars["tags"] "|" vars["slug"] "|" vars["image"] "|" \
|
||||
vars["image_caption"] "|" vars["description"];
|
||||
vars["image_caption"] "|" vars["description"] "|" \
|
||||
vars["author_name"] "|" vars["author_email"];
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
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 <<< "$parsed_data"
|
||||
IFS='|' read -r title date lastmod tags slug image image_caption description author_name author_email <<< "$parsed_data"
|
||||
|
||||
else
|
||||
echo "Warning: Unknown file type '$file' for metadata extraction." >&2
|
||||
|
|
@ -177,8 +235,10 @@ EOF
|
|||
if [ -z "$lastmod" ]; then
|
||||
lastmod="$date"
|
||||
fi
|
||||
if [ -z "$slug" ]; then
|
||||
if [ -z "$slug" ]; then
|
||||
slug=$(generate_slug "$title")
|
||||
else
|
||||
slug=$(generate_slug "$slug")
|
||||
fi
|
||||
if [ -z "$description" ]; then
|
||||
# Generate excerpt only if description is missing
|
||||
|
|
@ -186,12 +246,22 @@ EOF
|
|||
echo "[DEBUG] Generating excerpt for $file" >&2
|
||||
description=$(generate_excerpt "$file")
|
||||
fi
|
||||
|
||||
# Apply fallback logic for author fields
|
||||
if [ -z "$author_name" ]; then
|
||||
author_name="${AUTHOR_NAME:-Anonymous}"
|
||||
fi
|
||||
if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then
|
||||
# Only use default email if using default name
|
||||
author_email="${AUTHOR_EMAIL:-anonymous@example.com}"
|
||||
fi
|
||||
# If author_name is specified but author_email is empty, leave email empty
|
||||
|
||||
# Construct the metadata string for comparison and caching
|
||||
local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description"
|
||||
local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description|$author_name|$author_email"
|
||||
|
||||
# Check if there was a previous metadata file and compare
|
||||
if [ -f "$metadata_cache_file" ]; then
|
||||
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
|
||||
|
|
@ -199,13 +269,15 @@ EOF
|
|||
fi
|
||||
|
||||
# Store all metadata in one write operation
|
||||
lock_file "$metadata_cache_file"
|
||||
mkdir -p "$(dirname "$metadata_cache_file")"
|
||||
echo "$new_metadata" > "$metadata_cache_file"
|
||||
unlock_file "$metadata_cache_file"
|
||||
if ! $ram_mode_active; then
|
||||
lock_file "$metadata_cache_file"
|
||||
mkdir -p "$(dirname "$metadata_cache_file")"
|
||||
echo "$new_metadata" > "$metadata_cache_file"
|
||||
unlock_file "$metadata_cache_file"
|
||||
fi
|
||||
|
||||
# If frontmatter has changed, update the marker file's timestamp
|
||||
if $frontmatter_changed; then
|
||||
if ! $ram_mode_active && $frontmatter_changed; then
|
||||
touch "$frontmatter_changes_marker"
|
||||
fi
|
||||
|
||||
|
|
@ -218,17 +290,30 @@ generate_excerpt() {
|
|||
local file="$1"
|
||||
local max_length="${2:-160}" # Default to 160 characters
|
||||
|
||||
# Extract content after frontmatter
|
||||
local start_line=$(grep -n "^---$" "$file" | head -1 | cut -d: -f1)
|
||||
local end_line=$(grep -n "^---$" "$file" | head -n 2 | tail -1 | cut -d: -f1)
|
||||
|
||||
local raw_content_stream
|
||||
if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then
|
||||
# Stream content after frontmatter
|
||||
raw_content_stream=$(tail -n +$((end_line + 1)) "$file")
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then
|
||||
# Remove frontmatter directly from preloaded content
|
||||
raw_content_stream=$(printf '%s\n' "$(ram_mode_get_content "$file")" | awk '
|
||||
BEGIN { in_fm = 0; found_fm = 0; }
|
||||
/^---$/ {
|
||||
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
|
||||
if (in_fm) { in_fm = 0; next; }
|
||||
}
|
||||
{ if (!in_fm) print; }
|
||||
')
|
||||
else
|
||||
# No valid frontmatter, stream the whole file
|
||||
raw_content_stream=$(cat "$file")
|
||||
# Extract content after frontmatter
|
||||
local start_line end_line
|
||||
start_line=$(grep -n "^---$" "$file" | head -1 | cut -d: -f1)
|
||||
end_line=$(grep -n "^---$" "$file" | head -n 2 | tail -1 | cut -d: -f1)
|
||||
|
||||
if [[ -n "$start_line" && -n "$end_line" && $start_line -lt $end_line ]]; then
|
||||
# Stream content after frontmatter
|
||||
raw_content_stream=$(tail -n +$((end_line + 1)) "$file")
|
||||
else
|
||||
# No valid frontmatter, stream the whole file
|
||||
raw_content_stream=$(cat "$file")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sanitize and extract the first non-empty paragraph/line
|
||||
|
|
@ -308,26 +393,19 @@ convert_markdown_to_html() {
|
|||
elif [ "$MARKDOWN_PROCESSOR" = "markdown.pl" ]; then
|
||||
# Preprocess content to handle fenced code blocks for markdown.pl
|
||||
local preprocessed_content="$content"
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
# Use printf to avoid issues with content starting with -
|
||||
printf '%s' "$preprocessed_content" > "$temp_file"
|
||||
|
||||
# Handle fenced code blocks (``` and ~~~) -> indented
|
||||
# Requires awk
|
||||
if command -v awk &> /dev/null; then
|
||||
preprocessed_content=$(awk '
|
||||
preprocessed_content=$(printf '%s' "$preprocessed_content" | awk '
|
||||
BEGIN { in_code = 0; }
|
||||
/^```[a-zA-Z0-9]*$/ || /^~~~[a-zA-Z0-9]*$/ { if (!in_code) { in_code = 1; print ""; next; } }
|
||||
/^```$/ || /^~~~$/ { if (in_code) { in_code = 0; print ""; next; } }
|
||||
{ if (in_code) { print " " $0; } else { print $0; } }
|
||||
' "$temp_file")
|
||||
rm "$temp_file"
|
||||
')
|
||||
else
|
||||
echo -e "${YELLOW}Warning: awk not found, markdown.pl fenced code block conversion skipped.${NC}" >&2
|
||||
# Content remains as original if awk fails
|
||||
preprocessed_content=$(cat "$temp_file")
|
||||
rm "$temp_file"
|
||||
preprocessed_content="$content"
|
||||
fi
|
||||
|
||||
# Ensure MARKDOWN_PL_PATH is set and executable
|
||||
|
|
@ -350,4 +428,4 @@ convert_markdown_to_html() {
|
|||
return 0
|
||||
}
|
||||
|
||||
# --- Content Functions --- END ---
|
||||
# --- Content Functions --- END ---
|
||||
|
|
|
|||
2
scripts/build/deps.sh
Executable file → Normal file
2
scripts/build/deps.sh
Executable file → Normal file
|
|
@ -96,7 +96,7 @@ check_dependencies() {
|
|||
if [[ "$(uname)" == "NetBSD" ]]; then
|
||||
echo -e "${YELLOW}Parallel processing is unreliable on NetBSD. Using sequential processing.${NC}"
|
||||
export HAS_PARALLEL=false
|
||||
elif command -v parallel > /dev/null 2>&1; then
|
||||
elif command -v parallel > /dev/null 2>&1 && { read -r _version < <(parallel -V 2>/dev/null ) && [[ "${_version:0:3}" = "GNU" ]]; }; then
|
||||
echo -e "${GREEN}GNU parallel found! Using parallel processing.${NC}"
|
||||
export HAS_PARALLEL=true
|
||||
else
|
||||
|
|
|
|||
|
|
@ -14,12 +14,322 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s
|
|||
# Helper Functions for Archive Generation
|
||||
# ==============================================================================
|
||||
|
||||
_generate_ram_year_archive_page() {
|
||||
local year="$1"
|
||||
[ -z "$year" ] && return 0
|
||||
|
||||
local year_index_page="$OUTPUT_DIR/archives/$year/index.html"
|
||||
mkdir -p "$(dirname "$year_index_page")"
|
||||
|
||||
local year_header="$HEADER_TEMPLATE"
|
||||
local year_footer="$FOOTER_TEMPLATE"
|
||||
local year_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $year"
|
||||
local year_archive_rel_url="/archives/$year/"
|
||||
year_header=${year_header//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
year_header=${year_header//\{\{page_title\}\}/"$year_page_title"}
|
||||
year_header=${year_header//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
|
||||
year_header=${year_header//\{\{og_description\}\}/"$SITE_DESCRIPTION"}
|
||||
year_header=${year_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"}
|
||||
year_header=${year_header//\{\{og_type\}\}/"website"}
|
||||
year_header=${year_header//\{\{page_url\}\}/"$year_archive_rel_url"}
|
||||
year_header=${year_header//\{\{site_url\}\}/"$SITE_URL"}
|
||||
year_header=${year_header//\{\{og_image\}\}/""}
|
||||
year_header=${year_header//\{\{twitter_image\}\}/""}
|
||||
year_header=${year_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
local year_schema_json
|
||||
year_schema_json='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$year_page_title"'","description": "Archive of posts from '"$year"'","url": "'"$SITE_URL$year_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>'
|
||||
year_header=${year_header//\{\{schema_json_ld\}\}/"$year_schema_json"}
|
||||
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//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
local month_schema_json
|
||||
month_schema_json='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$month_page_title"'","description": "Archive of posts from '"$month_name $year"'","url": "'"$SITE_URL$month_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>'
|
||||
month_header=${month_header//\{\{schema_json_ld\}\}/"$month_schema_json"}
|
||||
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" />
|
||||
</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//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
local schema_json_ld
|
||||
schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "Archives","description": "'"$SITE_DESCRIPTION"'","url": "'"$SITE_URL"'/archives/","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>'
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
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."
|
||||
|
|
@ -41,15 +351,28 @@ _check_archive_index_rebuild_needed() {
|
|||
if (( header_time > output_time )) || (( footer_time > output_time )); then
|
||||
rebuild_reason="Header or footer template changed."
|
||||
fi
|
||||
# 5. Explicit rebuild needed based on month count comparison
|
||||
elif [ "${ARCHIVE_INDEX_NEEDS_REBUILD:-false}" = true ]; then
|
||||
rebuild_reason="Archive month counts changed (detected by identify_affected_archive_months)."
|
||||
fi
|
||||
|
||||
# --- Content-based Rebuild Reasons ---
|
||||
# If no core reason found yet, check if content changed and if it affects the main index page.
|
||||
if [ -z "$rebuild_reason" ]; then
|
||||
if [ -n "$AFFECTED_ARCHIVE_MONTHS" ]; then # Check if any month had post changes
|
||||
if [ "${ARCHIVES_LIST_ALL_POSTS:-false}" = true ]; then
|
||||
# If listing all posts, ANY change in affected months requires main index rebuild
|
||||
rebuild_reason="List all posts enabled and archive content changed."
|
||||
elif [ "${ARCHIVE_INDEX_NEEDS_REBUILD:-false}" = true ]; then
|
||||
# If *not* listing all posts, only rebuild main index if month *counts* changed
|
||||
rebuild_reason="Archive month counts changed."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# --- End Content-based Check ---
|
||||
|
||||
if [ -n "$rebuild_reason" ]; then
|
||||
echo -e "${YELLOW}Main archive index rebuild needed: $rebuild_reason${NC}" >&2 # Debug
|
||||
return 0 # Needs rebuild
|
||||
else
|
||||
# No message here - generate_archive_pages will print the skipping message if needed.
|
||||
return 1 # No rebuild needed
|
||||
fi
|
||||
}
|
||||
|
|
@ -79,10 +402,11 @@ _generate_main_archive_index() {
|
|||
header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{og_type\}\}/"website"}
|
||||
header_content=${header_content//\{\{page_url\}\}/"archives/"} # Relative URL for header placeholder
|
||||
header_content=${header_content//\{\{page_url\}\}/"/archives/"} # Relative URL for header placeholder
|
||||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
header_content=${header_content//\{\{og_image\}\}/""}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/""}
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
# Add schema
|
||||
local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "Archives","description": "'"$SITE_DESCRIPTION"'","url": "'"$SITE_URL"'/archives/","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>'
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
|
|
@ -99,7 +423,7 @@ _generate_main_archive_index() {
|
|||
echo "<h1>${MSG_ARCHIVES:-"Archives"}</h1>"
|
||||
echo "<div class=\"archives-list year-list\">"
|
||||
|
||||
# Loop through years
|
||||
# Loop through years (Existing logic for Year/Month links)
|
||||
echo "$unique_years" | while IFS= read -r year; do
|
||||
[ -z "$year" ] && continue
|
||||
|
||||
|
|
@ -107,7 +431,8 @@ _generate_main_archive_index() {
|
|||
year_url=$(fix_url "/archives/$year/")
|
||||
|
||||
echo " <h2><a href=\"$year_url\">$year</a></h2>"
|
||||
echo " <ul class=\"month-list-inline\">"
|
||||
# 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=""
|
||||
|
|
@ -115,14 +440,16 @@ _generate_main_archive_index() {
|
|||
months_in_year=$(grep "^$year|" "$archive_index_file" 2>/dev/null | cut -d'|' -f2,3 | sort -t'|' -k1,1nr | uniq)
|
||||
fi
|
||||
|
||||
# Add month links
|
||||
# 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
|
||||
|
||||
|
|
@ -132,13 +459,56 @@ _generate_main_archive_index() {
|
|||
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>"
|
||||
# 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>"
|
||||
echo " </ul>" # End of month-list-detailed
|
||||
done
|
||||
|
||||
echo "</div>"
|
||||
echo "</div>" # End of year-list div
|
||||
|
||||
echo "$footer_content"
|
||||
|
||||
} > "$archives_index_page"
|
||||
|
|
@ -171,6 +541,7 @@ _generate_year_index() {
|
|||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
header_content=${header_content//\{\{og_image\}\}/""}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/""}
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
# Add schema
|
||||
local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$year_page_title"'","description": "Archive of posts from '"$year"'","url": "'"$SITE_URL$year_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>'
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
|
|
@ -247,7 +618,7 @@ process_single_month() {
|
|||
|
||||
# Generate header
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
local month_page_title="${MSG_ARCHIVES_FOR:-\"Archives for\"} $month_name $year"
|
||||
local month_page_title="${MSG_ARCHIVES_FOR:-"Archives for"} $month_name $year"
|
||||
local month_archive_rel_url="/archives/$year/$month_num/"
|
||||
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
header_content=${header_content//\{\{page_title\}\}/"$month_page_title"}
|
||||
|
|
@ -259,6 +630,7 @@ process_single_month() {
|
|||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
header_content=${header_content//\{\{og_image\}\}/""}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/""}
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
# Add schema
|
||||
local schema_json_ld='<script type="application/ld+json">{"@context": "https://schema.org","@type": "CollectionPage","name": "'"$month_page_title"'","description": "Archive of posts from '"$month_name $year"'","url": "'"$SITE_URL$month_archive_rel_url"'","isPartOf": {"@type": "WebSite","name": "'"$SITE_TITLE"'","url": "'"$SITE_URL"'"}}</script>'
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
|
|
@ -275,7 +647,7 @@ process_single_month() {
|
|||
echo "<div class=\"posts-list\">"
|
||||
|
||||
# Grep for posts from this specific month and year
|
||||
grep "^$year|$month_num|" "$archive_index_file" 2>/dev/null | while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description; do
|
||||
grep "^$year|$month_num|" "$archive_index_file" 2>/dev/null | while IFS='|' read -r _ _ _ title date lastmod filename slug image image_caption description author_name author_email; do
|
||||
# --- Start: Card Generation Logic (copied from generate_tags.sh) ---
|
||||
local post_url post_year post_month post_day url_path
|
||||
|
||||
|
|
@ -307,11 +679,14 @@ process_single_month() {
|
|||
fi
|
||||
local formatted_date=$(format_date "$date" "$display_date_format")
|
||||
|
||||
# Determine author for display (with fallback)
|
||||
local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}"
|
||||
|
||||
# Use cat heredoc for multi-line article structure
|
||||
cat << EOF
|
||||
<article>
|
||||
<h3><a href="${post_url}">$title</a></h3>
|
||||
<div class="meta">${MSG_PUBLISHED_ON:-\"Published on\"} $formatted_date</div>
|
||||
<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
|
||||
|
|
@ -369,6 +744,11 @@ _process_single_month_parallel_wrapper() {
|
|||
# Main Archive Generation Orchestrator
|
||||
# ==============================================================================
|
||||
generate_archive_pages() {
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
_generate_archive_pages_ram
|
||||
return $?
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Processing archive pages...${NC}"
|
||||
|
||||
local archive_index_file="$CACHE_DIR/archive_index.txt"
|
||||
|
|
@ -486,4 +866,4 @@ generate_archive_pages() {
|
|||
}
|
||||
|
||||
# Make the function available for sourcing
|
||||
export -f generate_archive_pages
|
||||
export -f generate_archive_pages
|
||||
|
|
|
|||
676
scripts/build/generate_authors.sh
Normal file
676
scripts/build/generate_authors.sh
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
#!/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//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
|
||||
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then
|
||||
local author_rss_link="<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$author RSS Feed\" href=\"$SITE_URL$author_rss_rel_url\">"
|
||||
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//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
|
||||
# Remove the placeholder for the tag-specific RSS feed link in the main authors index
|
||||
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
|
||||
|
||||
# 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}"
|
||||
}
|
||||
|
|
@ -14,6 +14,180 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s
|
|||
source "$(dirname "$0")/content.sh" || { echo >&2 "Error: Failed to source content.sh from generate_feeds.sh"; exit 1; }
|
||||
# Note: Needs access to primary_pages and SECONDARY_PAGES which should be exported by templates.sh
|
||||
|
||||
declare -gA BSSG_RAM_RSS_FULL_CONTENT_CACHE=()
|
||||
declare -g BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=false
|
||||
declare -gA BSSG_RAM_RSS_PUBDATE_CACHE=()
|
||||
declare -gA BSSG_RAM_RSS_UPDATED_ISO_CACHE=()
|
||||
declare -gA BSSG_RAM_RSS_URL_CACHE=()
|
||||
declare -gA BSSG_RAM_RSS_ITEM_XML_CACHE=()
|
||||
declare -g BSSG_RAM_RSS_METADATA_CACHE_READY=false
|
||||
|
||||
_normalize_relative_url_path() {
|
||||
local path="$1"
|
||||
while [[ "$path" == */ ]]; do
|
||||
path="${path%/}"
|
||||
done
|
||||
path="${path#/}"
|
||||
if [ -z "$path" ]; then
|
||||
printf '/'
|
||||
else
|
||||
printf '/%s/' "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
_ram_strip_frontmatter_for_rss() {
|
||||
awk '
|
||||
BEGIN { in_fm = 0; found_fm = 0; }
|
||||
/^---$/ {
|
||||
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
|
||||
if (in_fm) { in_fm = 0; next; }
|
||||
}
|
||||
{ if (!in_fm) print; }
|
||||
'
|
||||
}
|
||||
|
||||
_ram_cache_full_content_for_file() {
|
||||
local file="$1"
|
||||
local resolved="$file"
|
||||
|
||||
if declare -F ram_mode_resolve_key > /dev/null; then
|
||||
resolved=$(ram_mode_resolve_key "$file")
|
||||
fi
|
||||
|
||||
if [[ -z "$resolved" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "${BSSG_RAM_RSS_FULL_CONTENT_CACHE[$resolved]+_}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! declare -F ram_mode_has_file > /dev/null || ! ram_mode_has_file "$resolved"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local raw_content
|
||||
raw_content=$(ram_mode_get_content "$resolved")
|
||||
|
||||
local stripped_content
|
||||
stripped_content=$(printf '%s\n' "$raw_content" | _ram_strip_frontmatter_for_rss)
|
||||
|
||||
local converted_html
|
||||
converted_html=$(convert_markdown_to_html "$stripped_content" "$resolved")
|
||||
local convert_status=$?
|
||||
if [ $convert_status -ne 0 ] || [ -z "$converted_html" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
BSSG_RAM_RSS_FULL_CONTENT_CACHE["$resolved"]="$converted_html"
|
||||
return 0
|
||||
}
|
||||
|
||||
prepare_ram_rss_full_content_cache() {
|
||||
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RSS_INCLUDE_FULL_CONTENT:-false}" != true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY" = true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
if [ -z "$file_index_data" ]; then
|
||||
BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
|
||||
[ -z "$file" ] && continue
|
||||
_ram_cache_full_content_for_file "$file" > /dev/null || true
|
||||
done <<< "$file_index_data"
|
||||
|
||||
BSSG_RAM_RSS_FULL_CONTENT_CACHE_READY=true
|
||||
}
|
||||
|
||||
_ram_prime_rss_metadata_entry() {
|
||||
local date="$1"
|
||||
local lastmod="$2"
|
||||
local slug="$3"
|
||||
local rss_date_fmt="$4"
|
||||
local build_timestamp_iso="$5"
|
||||
local source_file="$6"
|
||||
|
||||
if [ -n "$date" ] && [[ -z "${BSSG_RAM_RSS_PUBDATE_CACHE[$date]+_}" ]]; then
|
||||
BSSG_RAM_RSS_PUBDATE_CACHE["$date"]=$(format_date "$date" "$rss_date_fmt")
|
||||
fi
|
||||
|
||||
if [ -n "$lastmod" ] && [[ -z "${BSSG_RAM_RSS_UPDATED_ISO_CACHE[$lastmod]+_}" ]]; then
|
||||
local updated_date_iso
|
||||
updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z")
|
||||
if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}"
|
||||
fi
|
||||
[ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso"
|
||||
BSSG_RAM_RSS_UPDATED_ISO_CACHE["$lastmod"]="$updated_date_iso"
|
||||
fi
|
||||
|
||||
if [ -n "$date" ] && [ -n "$slug" ]; then
|
||||
local url_key="${date}|${slug}"
|
||||
if [[ -z "${BSSG_RAM_RSS_URL_CACHE[$url_key]+_}" ]]; then
|
||||
local year month day formatted_path item_url
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
echo "Warning: Invalid date format '$date' for file $source_file, cannot precompute RSS URL." >&2
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
formatted_path="${URL_SLUG_FORMAT//Year/$year}"
|
||||
formatted_path="${formatted_path//Month/$month}"
|
||||
formatted_path="${formatted_path//Day/$day}"
|
||||
formatted_path="${formatted_path//slug/$slug}"
|
||||
item_url=$(_normalize_relative_url_path "$formatted_path")
|
||||
BSSG_RAM_RSS_URL_CACHE["$url_key"]=$(fix_url "$item_url")
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
prepare_ram_rss_metadata_cache() {
|
||||
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$BSSG_RAM_RSS_METADATA_CACHE_READY" = true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
if [ -z "$file_index_data" ]; then
|
||||
BSSG_RAM_RSS_METADATA_CACHE_READY=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z"
|
||||
local build_timestamp_iso
|
||||
build_timestamp_iso=$(format_date "now" "%Y-%m-%dT%H:%M:%S%z")
|
||||
if [[ "$build_timestamp_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
build_timestamp_iso="${build_timestamp_iso::${#build_timestamp_iso}-2}:${BASH_REMATCH[2]}"
|
||||
fi
|
||||
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
|
||||
[ -z "$file" ] && continue
|
||||
_ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" >/dev/null || true
|
||||
done <<< "$file_index_data"
|
||||
|
||||
BSSG_RAM_RSS_METADATA_CACHE_READY=true
|
||||
}
|
||||
|
||||
# Function to get the latest lastmod date from a file index, optionally filtered
|
||||
# Usage: get_latest_mod_date <index_file> [field_index] [filter_pattern] [date_format]
|
||||
# Example: get_latest_mod_date "$file_index" 5 "" "%Y-%m-%d" # Latest overall post
|
||||
|
|
@ -53,6 +227,212 @@ get_latest_mod_date() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Fast path for RAM datasets: pick max YYYY-MM-DD from a given field without external sort/head.
|
||||
_ram_latest_date_from_dataset() {
|
||||
local dataset="$1"
|
||||
local field_index="$2"
|
||||
local date_format="${3:-%Y-%m-%d}"
|
||||
|
||||
local latest_date_str
|
||||
latest_date_str=$(printf '%s\n' "$dataset" | awk -F'|' -v field_index="$field_index" '
|
||||
NF {
|
||||
value = substr($field_index, 1, 10)
|
||||
if (value != "" && value > max_date) {
|
||||
max_date = value
|
||||
}
|
||||
}
|
||||
END {
|
||||
if (max_date != "") {
|
||||
print max_date
|
||||
}
|
||||
}
|
||||
')
|
||||
|
||||
if [ -n "$latest_date_str" ]; then
|
||||
printf '%s\n' "$latest_date_str"
|
||||
else
|
||||
format_date "now" "$date_format"
|
||||
fi
|
||||
}
|
||||
|
||||
_generate_sitemap_with_awk_inputs() {
|
||||
local sitemap="$1"
|
||||
local file_index_input="$2"
|
||||
local primary_pages_input="$3"
|
||||
local secondary_pages_input="$4"
|
||||
local tags_index_input="$5"
|
||||
local authors_index_input="$6"
|
||||
local latest_post_mod_date="$7"
|
||||
local latest_tag_page_mod_date="$8"
|
||||
local latest_author_page_mod_date="$9"
|
||||
local sitemap_date_fmt="${10:-%Y-%m-%d}"
|
||||
|
||||
# Determine the best awk command locally to avoid potential scoping issues with AWK_CMD.
|
||||
local effective_awk_cmd="awk"
|
||||
if command -v gawk > /dev/null 2>&1; then
|
||||
effective_awk_cmd="gawk"
|
||||
fi
|
||||
|
||||
"$effective_awk_cmd" -v site_url="$SITE_URL" \
|
||||
-v url_slug_format="$URL_SLUG_FORMAT" \
|
||||
-v latest_post_mod_date="$latest_post_mod_date" \
|
||||
-v latest_tag_page_mod_date="$latest_tag_page_mod_date" \
|
||||
-v latest_author_page_mod_date="$latest_author_page_mod_date" \
|
||||
-v enable_author_pages="${ENABLE_AUTHOR_PAGES:-true}" \
|
||||
-v sitemap_date_fmt="$sitemap_date_fmt" \
|
||||
-F'|' \
|
||||
-f - \
|
||||
"$file_index_input" "$primary_pages_input" "$secondary_pages_input" "$tags_index_input" "$authors_index_input" <<'AWK_EOF' > "$sitemap"
|
||||
# AWK script for sitemap generation.
|
||||
BEGIN {
|
||||
OFS = ""
|
||||
print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
||||
print "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"
|
||||
|
||||
# Homepage
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk("/", site_url) "</loc>"
|
||||
print " <lastmod>" latest_post_mod_date "</lastmod>"
|
||||
print " <changefreq>daily</changefreq>"
|
||||
print " <priority>1.0</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
|
||||
function fix_url_awk(path, base_url) {
|
||||
if (substr(path, 1, 1) == "/") {
|
||||
sub(/\/$/, "", base_url)
|
||||
sub(/^\/+/, "/", path)
|
||||
sub(/\/index\.html$/, "/", path)
|
||||
if (substr(path, length(path), 1) != "/") {
|
||||
path = path "/"
|
||||
}
|
||||
if (base_url == "" || base_url ~ /^http:\/\/localhost(:[0-9]+)?$/) {
|
||||
return path
|
||||
} else {
|
||||
return base_url path
|
||||
}
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
# Process file_index (posts).
|
||||
FILENAME == ARGV[1] {
|
||||
file = $1
|
||||
date = $4
|
||||
lastmod = $5
|
||||
slug = $7
|
||||
if (length(file) == 0 || length(date) == 0 || length(lastmod) == 0 || length(slug) == 0) next
|
||||
|
||||
year = substr(date, 1, 4)
|
||||
month = substr(date, 6, 2)
|
||||
day = substr(date, 9, 2)
|
||||
if (year ~ /^[0-9]{4}$/ && month ~ /^[0-9]{2}$/ && day ~ /^[0-9]{2}$/) {
|
||||
formatted_path = url_slug_format
|
||||
gsub(/Year/, year, formatted_path)
|
||||
gsub(/Month/, month, formatted_path)
|
||||
gsub(/Day/, day, formatted_path)
|
||||
gsub(/slug/, slug, formatted_path)
|
||||
item_url = "/" formatted_path
|
||||
sub(/\/+$/, "/", item_url)
|
||||
|
||||
mod_time = substr(lastmod, 1, 10)
|
||||
if (mod_time == "") next
|
||||
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(item_url, site_url) "</loc>"
|
||||
print " <lastmod>" mod_time "</lastmod>"
|
||||
print " <changefreq>weekly</changefreq>"
|
||||
print " <priority>0.8</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
}
|
||||
|
||||
# Process primary pages.
|
||||
FILENAME == ARGV[2] {
|
||||
url = $2
|
||||
date = $3
|
||||
if (length(url) == 0 || length(date) == 0) next
|
||||
sitemap_url = url
|
||||
sub(/index\.html$/, "", sitemap_url)
|
||||
sub(/\/+$/, "/", sitemap_url)
|
||||
mod_time = substr(date, 1, 10)
|
||||
if (mod_time == "") next
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(sitemap_url, site_url) "</loc>"
|
||||
print " <lastmod>" mod_time "</lastmod>"
|
||||
print " <changefreq>monthly</changefreq>"
|
||||
print " <priority>0.7</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
|
||||
# Process secondary pages.
|
||||
FILENAME == ARGV[3] {
|
||||
url = $2
|
||||
date = $3
|
||||
if (length(url) == 0 || length(date) == 0) next
|
||||
sitemap_url = url
|
||||
sub(/index\.html$/, "", sitemap_url)
|
||||
sub(/\/+$/, "/", sitemap_url)
|
||||
mod_time = substr(date, 1, 10)
|
||||
if (mod_time == "") next
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(sitemap_url, site_url) "</loc>"
|
||||
print " <lastmod>" mod_time "</lastmod>"
|
||||
print " <changefreq>monthly</changefreq>"
|
||||
print " <priority>0.6</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
|
||||
# Process tags index.
|
||||
FILENAME == ARGV[4] {
|
||||
tag_slug = $2
|
||||
if (length(tag_slug) == 0) next
|
||||
if (!(tag_slug in processed_tags)) {
|
||||
processed_tags[tag_slug] = 1
|
||||
item_url = "/tags/" tag_slug "/"
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(item_url, site_url) "</loc>"
|
||||
print " <lastmod>" latest_tag_page_mod_date "</lastmod>"
|
||||
print " <changefreq>weekly</changefreq>"
|
||||
print " <priority>0.5</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
}
|
||||
|
||||
# Process authors index.
|
||||
FILENAME == ARGV[5] && enable_author_pages == "true" {
|
||||
author_slug = $2
|
||||
if (length(author_slug) == 0) next
|
||||
if (!(author_slug in processed_authors)) {
|
||||
processed_authors[author_slug] = 1
|
||||
|
||||
if (!authors_index_added) {
|
||||
authors_index_added = 1
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk("/authors/", site_url) "</loc>"
|
||||
print " <lastmod>" latest_author_page_mod_date "</lastmod>"
|
||||
print " <changefreq>weekly</changefreq>"
|
||||
print " <priority>0.6</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
|
||||
item_url = "/authors/" author_slug "/"
|
||||
print " <url>"
|
||||
print " <loc>" fix_url_awk(item_url, site_url) "</loc>"
|
||||
print " <lastmod>" latest_author_page_mod_date "</lastmod>"
|
||||
print " <changefreq>weekly</changefreq>"
|
||||
print " <priority>0.5</priority>"
|
||||
print " </url>"
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
print "</urlset>"
|
||||
}
|
||||
AWK_EOF
|
||||
}
|
||||
|
||||
# Core RSS generation function
|
||||
# Usage: _generate_rss_feed <output_file> <feed_title> <feed_description> <feed_link_rel> <feed_atom_link_rel> <post_data_input>
|
||||
# <post_data_input> should be a string containing the filtered, sorted, and limited post data,
|
||||
|
|
@ -80,67 +460,95 @@ _generate_rss_feed() {
|
|||
# Ensure output directory exists
|
||||
mkdir -p "$(dirname "$output_file")"
|
||||
|
||||
# Create the RSS feed header
|
||||
cat > "$output_file" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>${feed_title}</title>
|
||||
<link>$(fix_url "$feed_link_rel")</link>
|
||||
<description>${feed_description}</description>
|
||||
<language>${SITE_LANG:-en}</language>
|
||||
<lastBuildDate>$(format_date "now" "$rss_date_fmt")</lastBuildDate>
|
||||
<atom:link href="$(fix_url "$feed_atom_link_rel")" rel="self" type="application/rss+xml" />
|
||||
EOF
|
||||
local escaped_feed_title escaped_feed_description feed_link feed_atom_link channel_last_build_date
|
||||
escaped_feed_title=$(html_escape "$feed_title")
|
||||
escaped_feed_description=$(html_escape "$feed_description")
|
||||
feed_link=$(fix_url "$feed_link_rel")
|
||||
feed_atom_link=$(fix_url "$feed_atom_link_rel")
|
||||
channel_last_build_date=$(format_date "now" "$rss_date_fmt")
|
||||
|
||||
exec 4> "$output_file" || return 1
|
||||
printf '%s\n' \
|
||||
'<?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
|
||||
echo "$post_data_input" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description; do
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
|
||||
# Ignore blank trailing lines from callers.
|
||||
if [ -z "$file" ] && [ -z "$filename" ] && [ -z "$title" ] && [ -z "$date" ] && [ -z "$lastmod" ] && [ -z "$tags" ] && [ -z "$slug" ] && [ -z "$image" ] && [ -z "$image_caption" ] && [ -z "$description" ] && [ -z "$author_name" ] && [ -z "$author_email" ]; then
|
||||
continue
|
||||
fi
|
||||
# Skip if essential fields are missing (robustness)
|
||||
if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ] || [ -z "$lastmod" ] || [ -z "$slug" ]; then
|
||||
echo "Warning: Skipping RSS item due to missing fields in input line: file=$file, title=$title, date=$date, lastmod=$lastmod, slug=$slug" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Format dates for RSS
|
||||
local pub_date=$(format_date "$date" "$rss_date_fmt")
|
||||
local updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z")
|
||||
# Convert timezone format again if needed
|
||||
if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}"
|
||||
local rss_item_cache_key=""
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
rss_item_cache_key="${RSS_INCLUDE_FULL_CONTENT:-false}|${file}|${date}|${lastmod}|${slug}|${title}"
|
||||
if [[ -n "${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]+_}" ]]; then
|
||||
printf '%s' "${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]}" >&4
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
# Fallback for updated_date_iso
|
||||
[ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso"
|
||||
|
||||
# Construct post URL based on URL_SLUG_FORMAT
|
||||
local year month day formatted_path item_url
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
# Format dates and URL (RAM mode caches repeated values across many tag feeds).
|
||||
local pub_date updated_date_iso full_url
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
_ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" || {
|
||||
echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2
|
||||
continue
|
||||
}
|
||||
pub_date="${BSSG_RAM_RSS_PUBDATE_CACHE[$date]}"
|
||||
updated_date_iso="${BSSG_RAM_RSS_UPDATED_ISO_CACHE[$lastmod]}"
|
||||
full_url="${BSSG_RAM_RSS_URL_CACHE[${date}|${slug}]}"
|
||||
else
|
||||
echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2
|
||||
continue # Skip item if URL cannot be generated
|
||||
fi
|
||||
formatted_path="${URL_SLUG_FORMAT//Year/$year}"
|
||||
formatted_path="${formatted_path//Month/$month}"
|
||||
formatted_path="${formatted_path//Day/$day}"
|
||||
formatted_path="${formatted_path//slug/$slug}"
|
||||
item_url="/$(echo "$formatted_path" | sed 's|/*$|/|')" # Ensure trailing slash
|
||||
pub_date=$(format_date "$date" "$rss_date_fmt")
|
||||
updated_date_iso=$(format_date "$lastmod" "%Y-%m-%dT%H:%M:%S%z")
|
||||
if [[ "$updated_date_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
updated_date_iso="${updated_date_iso::${#updated_date_iso}-2}:${BASH_REMATCH[2]}"
|
||||
fi
|
||||
[ -z "$updated_date_iso" ] && updated_date_iso="$build_timestamp_iso"
|
||||
|
||||
local full_url=$(fix_url "$item_url") # Use fix_url to prepend SITE_URL
|
||||
local year month day formatted_path item_url
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
echo "Warning: Invalid date format '$date' for file $file, cannot generate URL." >&2
|
||||
continue
|
||||
fi
|
||||
formatted_path="${URL_SLUG_FORMAT//Year/$year}"
|
||||
formatted_path="${formatted_path//Month/$month}"
|
||||
formatted_path="${formatted_path//Day/$day}"
|
||||
formatted_path="${formatted_path//slug/$slug}"
|
||||
item_url=$(_normalize_relative_url_path "$formatted_path")
|
||||
full_url=$(fix_url "$item_url")
|
||||
fi
|
||||
|
||||
# --- RSS Item Description Enhancement ---
|
||||
local item_description_content=""
|
||||
local figure_part=""
|
||||
local caption_part=""
|
||||
local content_part=""
|
||||
local escaped_title
|
||||
escaped_title=$(html_escape "$title")
|
||||
|
||||
# Build figure part
|
||||
if [ -n "$image" ]; then
|
||||
local img_src
|
||||
[[ "$image" =~ ^https?:// ]] && img_src="$image" || img_src=$(fix_url "$image")
|
||||
# Escape alt/title attributes safely using html_escape from utils.sh
|
||||
local img_alt=$(html_escape "$title")
|
||||
local img_alt="$escaped_title"
|
||||
local img_title=$(html_escape "$image_caption")
|
||||
[ -z "$img_title" ] && img_title="$img_alt" # Use alt if title is empty
|
||||
|
||||
|
|
@ -155,8 +563,24 @@ EOF
|
|||
|
||||
# Build content part (excerpt or full)
|
||||
if [ "${RSS_INCLUDE_FULL_CONTENT:-false}" = true ]; then
|
||||
local raw_content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")"
|
||||
if [ -f "$raw_content_cache_file" ]; then
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
local resolved_file="$file"
|
||||
if declare -F ram_mode_resolve_key > /dev/null; then
|
||||
resolved_file=$(ram_mode_resolve_key "$file")
|
||||
fi
|
||||
|
||||
if _ram_cache_full_content_for_file "$resolved_file"; then
|
||||
content_part="${BSSG_RAM_RSS_FULL_CONTENT_CACHE[$resolved_file]}"
|
||||
else
|
||||
# RAM mode is memory-only: never fall back to disk cache reads.
|
||||
if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
echo "Warning: RAM content not available for RSS item ($file). Falling back to excerpt." >&2
|
||||
fi
|
||||
content_part="$description"
|
||||
fi
|
||||
else
|
||||
local raw_content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")"
|
||||
if [ -f "$raw_content_cache_file" ]; then
|
||||
local raw_content=$(cat "$raw_content_cache_file")
|
||||
local converted_html=$(convert_markdown_to_html "$raw_content" "$file")
|
||||
local convert_status=$?
|
||||
|
|
@ -166,39 +590,64 @@ EOF
|
|||
echo "Warning: Failed to convert markdown to HTML for RSS item ($file, status: $convert_status). Falling back to excerpt." >&2
|
||||
content_part="$description"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Cached raw markdown content file '$raw_content_cache_file' not found for RSS item ($file). Falling back to excerpt." >&2
|
||||
content_part="$description"
|
||||
else
|
||||
echo "Warning: Cached raw markdown content file '$raw_content_cache_file' not found for RSS item ($file). Falling back to excerpt." >&2
|
||||
content_part="$description"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
content_part="$description"
|
||||
fi
|
||||
|
||||
# Combine parts safely
|
||||
item_description_content="${figure_part}${caption_part}${content_part}"
|
||||
item_description_content="${figure_part}${content_part}"
|
||||
|
||||
# Wrap final description in CDATA
|
||||
local final_description="<![CDATA[$item_description_content]]>"
|
||||
|
||||
cat >> "$output_file" << EOF
|
||||
<item>
|
||||
<title>${title}</title>
|
||||
# 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
|
||||
|
||||
local rss_item_xml
|
||||
rss_item_xml=" <item>
|
||||
<title>${escaped_title}</title>
|
||||
<link>${full_url}</link>
|
||||
<guid isPermaLink="true">${full_url}</guid>
|
||||
<guid isPermaLink=\"true\">${full_url}</guid>
|
||||
<pubDate>${pub_date}</pubDate>
|
||||
<atom:updated>${updated_date_iso}</atom:updated>
|
||||
<description>${final_description}</description>
|
||||
</item>
|
||||
EOF
|
||||
done
|
||||
"
|
||||
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
|
||||
cat >> "$output_file" << EOF
|
||||
</channel>
|
||||
</rss>
|
||||
EOF
|
||||
printf '%s\n' '</channel>' '</rss>' >&4
|
||||
exec 4>&-
|
||||
|
||||
echo -e "${GREEN}RSS feed generated at $output_file${NC}"
|
||||
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
echo -e "${GREEN}RSS feed generated at $output_file${NC}"
|
||||
fi
|
||||
}
|
||||
export -f _generate_rss_feed # Export for potential parallel use or sourcing
|
||||
|
||||
|
|
@ -206,6 +655,28 @@ export -f _generate_rss_feed # Export for potential parallel use or sourcing
|
|||
generate_rss() {
|
||||
echo -e "${YELLOW}Generating main RSS feed...${NC}"
|
||||
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
if [ -z "$file_index_data" ]; then
|
||||
echo -e "${YELLOW}No file index data in RAM. Skipping RSS generation.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
prepare_ram_rss_metadata_cache >/dev/null || true
|
||||
|
||||
local rss="$OUTPUT_DIR/${RSS_FILENAME:-rss.xml}"
|
||||
local feed_title="${MSG_RSS_FEED_TITLE:-${SITE_TITLE} - RSS Feed}"
|
||||
local feed_desc="${MSG_RSS_FEED_DESCRIPTION:-${SITE_DESCRIPTION}}"
|
||||
local feed_link_rel="/"
|
||||
local feed_atom_link_rel="/${RSS_FILENAME:-rss.xml}"
|
||||
local rss_item_limit=${RSS_ITEM_LIMIT:-15}
|
||||
local sorted_posts
|
||||
sorted_posts=$(printf '%s\n' "$file_index_data" | awk 'NF' | sort -t'|' -k4,4r -k5,5r | head -n "$rss_item_limit")
|
||||
_generate_rss_feed "$rss" "$feed_title" "$feed_desc" "$feed_link_rel" "$feed_atom_link_rel" "$sorted_posts"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Ensure needed functions/vars are available
|
||||
if ! command -v convert_markdown_to_html &> /dev/null; then
|
||||
echo -e "${RED}Error: convert_markdown_to_html function not found.${NC}" >&2; return 1; fi
|
||||
|
|
@ -214,7 +685,7 @@ generate_rss() {
|
|||
if [ -z "${CACHE_DIR:-}" ]; then
|
||||
echo -e "${RED}Error: CACHE_DIR is not set.${NC}" >&2; return 1; fi
|
||||
|
||||
local rss="$OUTPUT_DIR/rss.xml"
|
||||
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"
|
||||
|
|
@ -258,7 +729,7 @@ generate_rss() {
|
|||
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.xml"
|
||||
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
|
||||
|
|
@ -267,6 +738,7 @@ generate_rss() {
|
|||
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
|
||||
|
|
@ -280,9 +752,43 @@ export -f generate_rss
|
|||
generate_sitemap() {
|
||||
echo -e "${YELLOW}Generating sitemap.xml...${NC}"
|
||||
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
local sitemap="$OUTPUT_DIR/sitemap.xml"
|
||||
local file_index_data tags_index_data authors_index_data primary_pages_data secondary_pages_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
tags_index_data=$(ram_mode_get_dataset "tags_index")
|
||||
authors_index_data=$(ram_mode_get_dataset "authors_index")
|
||||
primary_pages_data=$(ram_mode_get_dataset "primary_pages")
|
||||
secondary_pages_data=$(ram_mode_get_dataset "secondary_pages")
|
||||
|
||||
local latest_post_mod_date latest_tag_page_mod_date latest_author_page_mod_date
|
||||
latest_post_mod_date=$(_ram_latest_date_from_dataset "$file_index_data" 5 "%Y-%m-%d")
|
||||
latest_tag_page_mod_date=$(_ram_latest_date_from_dataset "$tags_index_data" 5 "%Y-%m-%d")
|
||||
latest_author_page_mod_date=$(_ram_latest_date_from_dataset "$authors_index_data" 6 "%Y-%m-%d")
|
||||
|
||||
[ -z "$latest_tag_page_mod_date" ] && latest_tag_page_mod_date="$latest_post_mod_date"
|
||||
[ -z "$latest_author_page_mod_date" ] && latest_author_page_mod_date="$latest_post_mod_date"
|
||||
|
||||
_generate_sitemap_with_awk_inputs \
|
||||
"$sitemap" \
|
||||
<(printf '%s\n' "$file_index_data") \
|
||||
<(printf '%s\n' "$primary_pages_data") \
|
||||
<(printf '%s\n' "$secondary_pages_data") \
|
||||
<(printf '%s\n' "$tags_index_data") \
|
||||
<(printf '%s\n' "$authors_index_data") \
|
||||
"$latest_post_mod_date" \
|
||||
"$latest_tag_page_mod_date" \
|
||||
"$latest_author_page_mod_date" \
|
||||
"%Y-%m-%d"
|
||||
|
||||
echo -e "${GREEN}Sitemap generated!${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local sitemap="$OUTPUT_DIR/sitemap.xml"
|
||||
local file_index="$CACHE_DIR/file_index.txt"
|
||||
local tags_index="$CACHE_DIR/tags_index.txt"
|
||||
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
|
||||
|
|
@ -308,6 +814,7 @@ generate_sitemap() {
|
|||
# Check main content index files
|
||||
if [ -f "$file_index" ] && [ "$(get_file_mtime "$file_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
if ! $rebuild_needed && [ -f "$tags_index" ] && [ "$(get_file_mtime "$tags_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
if ! $rebuild_needed && [ -f "$authors_index" ] && [ "$(get_file_mtime "$authors_index")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
if ! $rebuild_needed && [ -f "$primary_pages_cache" ] && [ "$(get_file_mtime "$primary_pages_cache")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
if ! $rebuild_needed && [ -f "$secondary_pages_cache" ] && [ "$(get_file_mtime "$secondary_pages_cache")" -gt "$sitemap_mtime" ]; then rebuild_needed=true; fi
|
||||
# Removed checks for script, config, locale mtime for simplicity to avoid sourcing errors
|
||||
|
|
@ -319,167 +826,28 @@ generate_sitemap() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
# --- Pre-calculate latest dates (Still needed for Homepage/Tags) ---
|
||||
# --- Pre-calculate latest dates (Still needed for Homepage/Tags/Authors) ---
|
||||
local latest_post_mod_date=$(get_latest_mod_date "$file_index" 5 "" "$sitemap_date_fmt")
|
||||
local latest_tag_page_mod_date=$(get_latest_mod_date "$tags_index" 5 "" "$sitemap_date_fmt") # Assumes lastmod is relevant field in tags_index
|
||||
local latest_author_page_mod_date=$(get_latest_mod_date "$authors_index" 6 "" "$sitemap_date_fmt") # Field 6 is lastmod in authors_index
|
||||
|
||||
# --- Generate Sitemap using AWK --- START ---
|
||||
echo "Generating sitemap content using awk..."
|
||||
|
||||
# Determine the best awk command locally to avoid potential scoping issues with AWK_CMD
|
||||
local effective_awk_cmd="awk" # Default to standard awk
|
||||
if command -v gawk > /dev/null 2>&1; then
|
||||
effective_awk_cmd="gawk" # Prefer gawk if available
|
||||
fi
|
||||
|
||||
# Use awk with a here-doc for the script for cleaner quoting
|
||||
# Use the locally determined effective_awk_cmd
|
||||
"$effective_awk_cmd" -v site_url="$SITE_URL" \
|
||||
-v url_slug_format="$URL_SLUG_FORMAT" \
|
||||
-v latest_post_mod_date="$latest_post_mod_date" \
|
||||
-v latest_tag_page_mod_date="$latest_tag_page_mod_date" \
|
||||
-v sitemap_date_fmt="$sitemap_date_fmt" \
|
||||
-F'|' \
|
||||
-f - \
|
||||
"$file_index" "$primary_pages_cache" "$secondary_pages_cache" "$tags_index" <<'AWK_EOF' > "$sitemap"
|
||||
# AWK script for sitemap generation (fed via here-doc)
|
||||
BEGIN {
|
||||
OFS=""; # No output field separator needed for XML
|
||||
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>";
|
||||
}
|
||||
|
||||
# Custom function to replicate fix_url shell function logic
|
||||
function fix_url_awk(path, base_url) {
|
||||
if (substr(path, 1, 1) == "/") {
|
||||
# Remove trailing slash from base_url if present
|
||||
sub(/\/$/, "", base_url);
|
||||
# Ensure path doesnt start with //
|
||||
sub(/^\/+/, "/", path);
|
||||
# Remove index.html if present
|
||||
sub(/\/index\.html$/, "/", path);
|
||||
# Ensure trailing slash
|
||||
if (substr(path, length(path), 1) != "/") {
|
||||
path = path "/";
|
||||
}
|
||||
# Handle case where base_url is empty or just http://localhost* - skip prepending
|
||||
if (base_url == "" || base_url ~ /^http:\/\/localhost(:[0-9]+)?$/) {
|
||||
return path
|
||||
} else {
|
||||
return base_url path;
|
||||
}
|
||||
} else {
|
||||
return path; # Should not happen for sitemap paths?
|
||||
}
|
||||
}
|
||||
|
||||
# Process file_index.txt (Posts)
|
||||
FILENAME == ARGV[1] {
|
||||
file=$1; filename=$2; title=$3; date=$4; lastmod=$5; tags=$6; slug=$7;
|
||||
if (length(file) == 0 || length(date) == 0 || length(lastmod) == 0 || length(slug) == 0) next;
|
||||
|
||||
year=substr(date, 1, 4);
|
||||
month=substr(date, 6, 2);
|
||||
day=substr(date, 9, 2);
|
||||
# Ensure valid numbers? Basic check:
|
||||
if (year ~ /^[0-9]{4}$/ && month ~ /^[0-9]{2}$/ && day ~ /^[0-9]{2}$/) {
|
||||
formatted_path = url_slug_format;
|
||||
gsub(/Year/, year, formatted_path);
|
||||
gsub(/Month/, month, formatted_path);
|
||||
gsub(/Day/, day, formatted_path);
|
||||
gsub(/slug/, slug, formatted_path);
|
||||
item_url = "/" formatted_path;
|
||||
# Clean URL logic from shell script
|
||||
sub(/\/+$/, "/", item_url);
|
||||
|
||||
mod_time = substr(lastmod, 1, 10); # Extract YYYY-MM-DD from lastmod ($5)
|
||||
if (mod_time == "") next; # Skip if date is invalid/empty
|
||||
|
||||
print " <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.tmp
|
||||
FILENAME == ARGV[2] {
|
||||
url=$2; date=$3; # $1=_, $4=source_file
|
||||
if (length(url) == 0 || length(date) == 0) next;
|
||||
sitemap_url = url;
|
||||
sub(/index\.html$/, "", sitemap_url); # Remove index.html
|
||||
sub(/\/+$/, "/", sitemap_url); # Ensure trailing slash
|
||||
mod_time = substr(date, 1, 10); # Extract YYYY-MM-DD from date ($3)
|
||||
if (mod_time == "") next; # Skip if date is invalid/empty
|
||||
print " <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.tmp
|
||||
FILENAME == ARGV[3] {
|
||||
url=$2; date=$3; # $1=_, $4=source_file
|
||||
if (length(url) == 0 || length(date) == 0) next;
|
||||
sitemap_url = url;
|
||||
sub(/index\.html$/, "", sitemap_url);
|
||||
sub(/\/+$/, "/", sitemap_url);
|
||||
mod_time = substr(date, 1, 10); # Extract YYYY-MM-DD from date ($3)
|
||||
if (mod_time == "") next; # Skip if date is invalid/empty
|
||||
print " <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>"; # Lower priority for secondary?
|
||||
print " </url>";
|
||||
}
|
||||
|
||||
# Process tags_index.txt (Tag Pages)
|
||||
FILENAME == ARGV[4] {
|
||||
tag=$1; tag_slug=$2; # $5 = lastmod for posts with this tag
|
||||
if (length(tag_slug) == 0) next;
|
||||
# Check if tag slug already processed
|
||||
if ( !(tag_slug in processed_tags) ) {
|
||||
processed_tags[tag_slug] = 1; # Mark as processed
|
||||
item_url = "/tags/" tag_slug "/";
|
||||
# Use the overall latest tag mod date for all tag pages?
|
||||
mod_time = latest_tag_page_mod_date;
|
||||
print " <url>";
|
||||
print " <loc>" fix_url_awk(item_url, site_url) "</loc>";
|
||||
print " <lastmod>" mod_time "</lastmod>";
|
||||
print " <changefreq>weekly</changefreq>";
|
||||
print " <priority>0.5</priority>";
|
||||
print " </url>";
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
print "</urlset>";
|
||||
}
|
||||
AWK_EOF
|
||||
# awk exit status check - optional
|
||||
# local awk_status=$?
|
||||
# if [ $awk_status -ne 0 ]; then
|
||||
# echo -e "${RED}Error: awk script for sitemap generation failed with status $awk_status${NC}" >&2
|
||||
# # Decide whether to return 1 or continue
|
||||
# fi
|
||||
|
||||
# --- Generate Sitemap using AWK --- END ---
|
||||
_generate_sitemap_with_awk_inputs \
|
||||
"$sitemap" \
|
||||
"$file_index" \
|
||||
"$primary_pages_cache" \
|
||||
"$secondary_pages_cache" \
|
||||
"$tags_index" \
|
||||
"$authors_index" \
|
||||
"$latest_post_mod_date" \
|
||||
"$latest_tag_page_mod_date" \
|
||||
"$latest_author_page_mod_date" \
|
||||
"$sitemap_date_fmt"
|
||||
|
||||
echo -e "${GREEN}Sitemap generated!${NC}"
|
||||
}
|
||||
|
||||
# Export public functions
|
||||
export -f generate_sitemap generate_rss
|
||||
export -f _normalize_relative_url_path
|
||||
export -f _ram_strip_frontmatter_for_rss _ram_cache_full_content_for_file prepare_ram_rss_full_content_cache
|
||||
export -f generate_sitemap generate_rss
|
||||
|
|
|
|||
|
|
@ -10,8 +10,298 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s
|
|||
# shellcheck source=cache.sh disable=SC1091
|
||||
source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_index.sh"; exit 1; }
|
||||
|
||||
_generate_index_ram() {
|
||||
echo -e "${YELLOW}Generating index pages...${NC}"
|
||||
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
if [ -z "$file_index_data" ]; then
|
||||
echo -e "${YELLOW}No posts found in RAM file index. Skipping index generation.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local total_posts_orig
|
||||
total_posts_orig=$(printf '%s\n' "$file_index_data" | awk 'NF { c++ } END { print c+0 }')
|
||||
local total_pages=$(( (total_posts_orig + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE ))
|
||||
[ "$total_pages" -eq 0 ] && total_pages=1
|
||||
|
||||
mapfile -t file_index_lines < <(printf '%s\n' "$file_index_data" | awk 'NF')
|
||||
echo -e "Generating ${GREEN}$total_pages${NC} index pages for ${GREEN}$total_posts_orig${NC} posts"
|
||||
|
||||
local current_page
|
||||
for (( current_page = 1; current_page <= total_pages; current_page++ )); do
|
||||
local output_file
|
||||
if [ "$current_page" -eq 1 ]; then
|
||||
output_file="$OUTPUT_DIR/index.html"
|
||||
else
|
||||
output_file="$OUTPUT_DIR/page/$current_page/index.html"
|
||||
mkdir -p "$(dirname "$output_file")"
|
||||
fi
|
||||
|
||||
local page_header="$HEADER_TEMPLATE"
|
||||
if [ "$current_page" -eq 1 ]; then
|
||||
page_header=${page_header//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
page_header=${page_header//\{\{page_title\}\}/"${MSG_HOME:-"Home"}"}
|
||||
page_header=${page_header//\{\{og_type\}\}/"website"}
|
||||
page_header=${page_header//\{\{page_url\}\}/"/"}
|
||||
page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"}
|
||||
local home_schema
|
||||
home_schema=$(cat <<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//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
|
||||
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}" />
|
||||
</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 [ -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)
|
||||
|
|
@ -75,7 +365,7 @@ generate_index() {
|
|||
# For the homepage
|
||||
page_header=${page_header//\{\{page_title\}\}/"${MSG_HOME:-"Home"}"}
|
||||
page_header=${page_header//\{\{og_type\}\}/"website"}
|
||||
page_header=${page_header//\{\{page_url\}\}/""}
|
||||
page_header=${page_header//\{\{page_url\}\}/"/"}
|
||||
page_header=${page_header//\{\{site_url\}\}/"$SITE_URL"}
|
||||
|
||||
# Create WebSite schema for homepage
|
||||
|
|
@ -143,6 +433,7 @@ EOF
|
|||
page_header=${page_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"}
|
||||
page_header=${page_header//\{\{og_image\}\}/""}
|
||||
page_header=${page_header//\{\{twitter_image\}\}/""}
|
||||
page_header=${page_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
|
||||
# Replace placeholders in the footer
|
||||
local page_footer="$FOOTER_TEMPLATE"
|
||||
|
|
@ -190,7 +481,7 @@ EOF
|
|||
local end_index=$(( current_page * POSTS_PER_PAGE ))
|
||||
|
||||
# Add posts to the index page
|
||||
awk -v start="$start_index" -v end="$end_index" 'NR >= start && NR <= end { print }' "$file_index" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description; do
|
||||
awk -v start="$start_index" -v end="$end_index" 'NR >= start && NR <= end { print }' "$file_index" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
|
||||
# ... (rest of the post item generation logic remains the same) ...
|
||||
if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ]; then
|
||||
continue
|
||||
|
|
@ -215,8 +506,8 @@ EOF
|
|||
local post_link="/$formatted_path/"
|
||||
cat >> "$output_file" << EOF
|
||||
<article>
|
||||
<h3><a href="$(fix_url "$post_link")">$title</a></h3>
|
||||
<div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${AUTHOR_NAME:+" ${MSG_BY:-"by"} $AUTHOR_NAME"}</div>
|
||||
<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"
|
||||
|
|
@ -229,7 +520,79 @@ EOF
|
|||
</div>
|
||||
EOF
|
||||
fi
|
||||
if [ -n "$description" ]; then
|
||||
# Show either full content or just description based on config
|
||||
if [ "${INDEX_SHOW_FULL_CONTENT:-false}" = "true" ]; then
|
||||
# Show full post content
|
||||
local post_content=""
|
||||
local content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")"
|
||||
|
||||
# Try 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 [ -n "$description" ]; then
|
||||
# Show just the description/excerpt (default behavior)
|
||||
cat >> "$output_file" << EOF
|
||||
<div class="summary">
|
||||
$description
|
||||
|
|
@ -294,9 +657,8 @@ EOF
|
|||
# Use GNU parallel if available and beneficial
|
||||
if [ "${HAS_PARALLEL:-false}" = true ] && [ "$total_pages" -gt 2 ] ; then
|
||||
echo -e "${GREEN}Using GNU parallel to process index pages${NC}"
|
||||
local cores=1
|
||||
if command -v nproc > /dev/null 2>&1; then cores=$(nproc);
|
||||
elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi
|
||||
local cores
|
||||
cores=$(get_parallel_jobs)
|
||||
|
||||
# Use all detected cores
|
||||
local jobs=$cores
|
||||
|
|
@ -305,10 +667,11 @@ EOF
|
|||
export OUTPUT_DIR URL_SLUG_FORMAT POSTS_PER_PAGE CACHE_DIR
|
||||
export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME DATE_FORMAT SITE_URL
|
||||
export FORCE_REBUILD HEADER_TEMPLATE FOOTER_TEMPLATE SHOW_TIMEZONE
|
||||
export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY
|
||||
export INDEX_SHOW_FULL_CONTENT
|
||||
export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY
|
||||
export MSG_NEWER_POSTS MSG_OLDER_POSTS MSG_PAGE_INFO_TEMPLATE
|
||||
# Note: total_posts_orig is NOT exported, passed as argument now
|
||||
export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url
|
||||
export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url convert_markdown_to_html
|
||||
|
||||
# Ensure templates are exported
|
||||
if [ -z "$HEADER_TEMPLATE" ] || [ -z "$FOOTER_TEMPLATE" ]; then
|
||||
|
|
@ -332,4 +695,4 @@ EOF
|
|||
}
|
||||
|
||||
# Make the function available for sourcing
|
||||
export -f generate_index
|
||||
export -f generate_index
|
||||
|
|
|
|||
|
|
@ -24,9 +24,13 @@ convert_page() {
|
|||
|
||||
# IMPORTANT: Assumes CACHE_DIR, FORCE_REBUILD, PAGES_DIR, SITE_TITLE, SITE_DESCRIPTION, SITE_URL, AUTHOR_NAME are exported/available
|
||||
local output_html_file="$output_base_path/index.html"
|
||||
local ram_mode_active=false
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$input_file"; then
|
||||
ram_mode_active=true
|
||||
fi
|
||||
|
||||
# Check if the source file exists
|
||||
if [ ! -f "$input_file" ]; then
|
||||
if ! $ram_mode_active && [ ! -f "$input_file" ]; then
|
||||
echo -e "${RED}Error: Source page '$input_file' not found${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -45,21 +49,31 @@ convert_page() {
|
|||
|
||||
if [[ "$input_file" == *.html ]]; then
|
||||
# For HTML files, extract content between <body> tags (simple approach)
|
||||
html_content=$(sed -n '/<body>/,/<\/body>/p' "$input_file" | sed '1d;$d')
|
||||
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 start_line=$(grep -n "^---$" "$input_file" | head -1 | cut -d: -f1)
|
||||
local end_line=$(grep -n "^---$" "$input_file" | head -2 | tail -1 | cut -d: -f1)
|
||||
|
||||
if [[ -z "$start_line" || -z "$end_line" || ! $start_line -lt $end_line ]]; then
|
||||
# No valid frontmatter found, use the whole file
|
||||
content=$(cat "$input_file")
|
||||
local source_stream=""
|
||||
if $ram_mode_active; then
|
||||
source_stream=$(ram_mode_get_content "$input_file")
|
||||
else
|
||||
# Extract content after the second --- line
|
||||
content=$(tail -n +$((end_line + 1)) "$input_file")
|
||||
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
|
||||
|
|
@ -111,6 +125,7 @@ convert_page() {
|
|||
# Handle image placeholders (remove for pages as they don't have featured images)
|
||||
header_content=${header_content//\{\{og_image\}\}/}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/}
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
|
||||
# Assemble the final HTML
|
||||
local final_html="${header_content}"
|
||||
|
|
@ -178,10 +193,13 @@ process_all_pages() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
echo -e "Checking ${GREEN}${#page_files[@]}${NC} pages for changes"
|
||||
|
||||
# Use mapfile -t to read sorted files into array (newline-separated, trailing newline stripped)
|
||||
mapfile -t page_files < <(find "${PAGES_DIR:-pages}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort)
|
||||
local page_files=()
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_list_page_files > /dev/null; then
|
||||
mapfile -t page_files < <(ram_mode_list_page_files)
|
||||
else
|
||||
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
|
||||
|
|
@ -190,14 +208,40 @@ process_all_pages() {
|
|||
fi
|
||||
echo -e "Found ${GREEN}$num_pages${NC} potential pages."
|
||||
|
||||
local ram_mode_active=false
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
ram_mode_active=true
|
||||
fi
|
||||
|
||||
# RAM mode keeps source content only in-process (bash arrays).
|
||||
# GNU parallel spawns fresh shells that cannot access those arrays.
|
||||
if $ram_mode_active; then
|
||||
if [ "$num_pages" -gt 1 ]; then
|
||||
echo -e "${YELLOW}Using shell parallel workers for $num_pages RAM-mode pages${NC}"
|
||||
local cores
|
||||
cores=$(get_parallel_jobs)
|
||||
|
||||
{
|
||||
local file quoted_file
|
||||
for file in "${page_files[@]}"; do
|
||||
[[ -z "$file" ]] && continue
|
||||
printf -v quoted_file '%q' "$file"
|
||||
echo "process_single_page_file $quoted_file"
|
||||
done
|
||||
} | run_parallel "$cores"
|
||||
else
|
||||
echo -e "${YELLOW}Using sequential processing for RAM-mode pages${NC}"
|
||||
if [[ -n "${page_files[0]}" ]]; then
|
||||
process_single_page_file "${page_files[0]}"
|
||||
fi
|
||||
fi
|
||||
# Use GNU parallel if available, otherwise fallback
|
||||
# IMPORTANT: Assumes HAS_PARALLEL is exported/available
|
||||
if [ "${HAS_PARALLEL:-false}" = true ]; then
|
||||
elif [ "${HAS_PARALLEL:-false}" = true ]; then
|
||||
echo -e "${GREEN}Using GNU parallel to generate pages${NC}"
|
||||
# Determine number of cores
|
||||
local cores=1
|
||||
if command -v nproc > /dev/null 2>&1; then cores=$(nproc);
|
||||
elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi
|
||||
local cores
|
||||
cores=$(get_parallel_jobs)
|
||||
|
||||
# Export functions needed by the parallel process and its children
|
||||
export -f convert_page process_single_page_file
|
||||
|
|
@ -216,6 +260,7 @@ process_all_pages() {
|
|||
echo -e "${YELLOW}Using sequential processing for pages${NC}"
|
||||
local file
|
||||
for file in "${page_files[@]}"; do
|
||||
[[ -z "$file" ]] && continue
|
||||
process_single_page_file "$file"
|
||||
done
|
||||
fi
|
||||
|
|
@ -223,4 +268,4 @@ process_all_pages() {
|
|||
echo -e "${GREEN}Static page processing complete!${NC}"
|
||||
}
|
||||
|
||||
# --- Page Generation Functions --- END ---
|
||||
# --- Page Generation Functions --- END ---
|
||||
|
|
|
|||
|
|
@ -11,9 +11,68 @@ source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.s
|
|||
source "$(dirname "$0")/content.sh" || { echo >&2 "Error: Failed to source content.sh from generate_posts.sh"; exit 1; }
|
||||
# shellcheck source=cache.sh disable=SC1091
|
||||
source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_posts.sh"; exit 1; } # For file_needs_rebuild checks etc.
|
||||
# shellcheck source=related_posts.sh disable=SC1091
|
||||
source "$(dirname "$0")/related_posts.sh" || { echo >&2 "Error: Failed to source related_posts.sh from generate_posts.sh"; exit 1; } # For related posts functionality
|
||||
|
||||
# --- Post Generation Functions --- START ---
|
||||
|
||||
declare -gA BSSG_POST_ISO8601_CACHE=()
|
||||
|
||||
format_iso8601_post_date() {
|
||||
local input_dt="$1"
|
||||
local iso_dt=""
|
||||
|
||||
if [ -z "$input_dt" ]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
|
||||
local cache_key="${TIMEZONE:-local}|${input_dt}"
|
||||
if [[ "$(declare -p BSSG_POST_ISO8601_CACHE 2>/dev/null || true)" != "declare -A"* ]]; then
|
||||
unset BSSG_POST_ISO8601_CACHE 2>/dev/null || true
|
||||
declare -gA BSSG_POST_ISO8601_CACHE=()
|
||||
fi
|
||||
if [[ -n "${BSSG_POST_ISO8601_CACHE[$cache_key]+_}" ]]; then
|
||||
echo "${BSSG_POST_ISO8601_CACHE[$cache_key]}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Handle "now" separately
|
||||
if [ "$input_dt" = "now" ]; then
|
||||
iso_dt=$(LC_ALL=C date +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
else
|
||||
# Try parsing different formats based on OS
|
||||
if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == *"bsd"* ]]; then
|
||||
# Format 1: YYYY-MM-DD HH:MM:SS ZZZZ (e.g., +0200)
|
||||
iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
# Format 2: YYYY-MM-DD HH:MM:SS
|
||||
[ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
# Format 3: YYYY-MM-DD (assume T00:00:00)
|
||||
[ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d" "$input_dt" +"%Y-%m-%dT00:00:00%z" 2>/dev/null)
|
||||
# Format 4: RFC 2822 subset (e.g., 07 Sep 2023 08:10:00 +0200)
|
||||
[ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%d %b %Y %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
else
|
||||
# GNU date -d handles many formats.
|
||||
iso_dt=$(LC_ALL=C date -d "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normalize timezone from +0000 to Z and +hhmm to +hh:mm.
|
||||
if [ -n "$iso_dt" ] && [[ "$iso_dt" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
local tz_offset="${BASH_REMATCH[0]}"
|
||||
local tz_hh="${BASH_REMATCH[1]}"
|
||||
local tz_mm="${BASH_REMATCH[2]}"
|
||||
if [ "$tz_hh" = "+00" ] && [ "$tz_mm" = "00" ]; then
|
||||
iso_dt="${iso_dt%$tz_offset}Z"
|
||||
else
|
||||
iso_dt="${iso_dt%$tz_offset}${tz_hh}:${tz_mm}"
|
||||
fi
|
||||
fi
|
||||
|
||||
BSSG_POST_ISO8601_CACHE["$cache_key"]="$iso_dt"
|
||||
echo "$iso_dt"
|
||||
}
|
||||
|
||||
# Convert markdown to HTML
|
||||
convert_markdown() {
|
||||
local input_file="$1"
|
||||
|
|
@ -26,61 +85,85 @@ convert_markdown() {
|
|||
local image="$8"
|
||||
local image_caption="$9"
|
||||
local description="${10}"
|
||||
local author_name="${11}"
|
||||
local author_email="${12}"
|
||||
local skip_rebuild_check="${13:-false}"
|
||||
|
||||
local content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$input_file")"
|
||||
local output_html_file="$output_base_path/index.html"
|
||||
local ram_mode_active=false
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$input_file"; then
|
||||
ram_mode_active=true
|
||||
fi
|
||||
|
||||
# Check if the source file exists
|
||||
if [ ! -f "$input_file" ]; then
|
||||
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
|
||||
if ! file_needs_rebuild "$input_file" "$output_html_file"; then
|
||||
echo -e "Skipping unchanged file: ${YELLOW}$(basename "$input_file")${NC}"
|
||||
return 0
|
||||
# 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
|
||||
|
||||
echo -e "Processing post: ${GREEN}$(basename "$input_file")${NC}"
|
||||
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
echo -e "Processing post: ${GREEN}$(basename "$input_file")${NC}"
|
||||
fi
|
||||
|
||||
# IMPORTANT: Assumes lock_file/unlock_file are sourced/available
|
||||
lock_file "$content_cache_file"
|
||||
|
||||
# Try to get content from cache or file
|
||||
# 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 in_frontmatter=false
|
||||
local found_frontmatter=false
|
||||
{
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" == "---" ]]; then
|
||||
if ! $in_frontmatter && ! $found_frontmatter; then
|
||||
in_frontmatter=true
|
||||
found_frontmatter=true
|
||||
continue
|
||||
elif $in_frontmatter; then
|
||||
in_frontmatter=false
|
||||
continue # Skip the closing --- line itself
|
||||
fi
|
||||
fi
|
||||
if ! $in_frontmatter && $found_frontmatter; then
|
||||
content+="$line"$'\n'
|
||||
fi
|
||||
done
|
||||
} < "$input_file"
|
||||
|
||||
# If no frontmatter was found, use the whole file as content
|
||||
if ! $found_frontmatter; then
|
||||
content=$(cat "$input_file")
|
||||
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 [ -n "$CACHE_DIR" ] && [ -d "${CACHE_DIR}/content" ]; then
|
||||
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
|
||||
|
||||
unlock_file "$content_cache_file"
|
||||
|
||||
# Calculate reading time
|
||||
local reading_time
|
||||
|
|
@ -118,7 +201,7 @@ convert_markdown() {
|
|||
[[ -z "$tag" ]] && continue
|
||||
local tag_slug=$(echo "$tag" | tr '[:upper:]' '[:lower:]' | sed -e 's/ /-/g' -e 's/[^a-z0-9-]//g')
|
||||
if [[ -n "$tag_slug" ]]; then # Ensure tag slug is not empty
|
||||
tags_html+=$(printf ' <a href="%s/tags/%s/" class="tag">%s</a>' "${SITE_URL:-}" "$tag_slug" "$tag")
|
||||
tags_html+=" <a href=\"${SITE_URL:-}/tags/${tag_slug}/\" class=\"tag\">${tag}</a>"
|
||||
fi
|
||||
done
|
||||
tags_html+="</div>"
|
||||
|
|
@ -168,68 +251,34 @@ convert_markdown() {
|
|||
meta_desc=$(echo "${description:-$SITE_DESCRIPTION}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
header_content=${header_content//\{\{og_description\}\}/"$meta_desc"}
|
||||
header_content=${header_content//\{\{twitter_description\}\}/"$meta_desc"}
|
||||
local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}"
|
||||
local fediverse_creator_meta_tag=""
|
||||
fediverse_creator_meta_tag=$(build_fediverse_creator_meta_tag "$display_author_name" "$fediverse_creator_override")
|
||||
if [[ "$header_content" == *"{{fediverse_creator_meta}}"* ]]; then
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"$fediverse_creator_meta_tag"}
|
||||
elif [ -n "$fediverse_creator_meta_tag" ]; then
|
||||
if [[ "$header_content" == *"</head>"* ]]; then
|
||||
header_content=${header_content/<\/head>/$'\n'"$fediverse_creator_meta_tag"$'\n''</head>'}
|
||||
else
|
||||
header_content+=$'\n'"$fediverse_creator_meta_tag"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate Schema.org JSON-LD for articles
|
||||
local schema_json_ld=""
|
||||
if [ -n "$date" ]; then
|
||||
local iso_date iso_lastmod_date
|
||||
|
||||
# Function to format date to ISO 8601 with corrected timezone
|
||||
format_iso8601() {
|
||||
local input_dt="$1"
|
||||
local iso_dt=""
|
||||
if [ -z "$input_dt" ]; then echo ""; return; fi
|
||||
|
||||
# Handle "now" separately
|
||||
if [ "$input_dt" = "now" ]; then
|
||||
iso_dt=$(LC_ALL=C date +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
else
|
||||
# Try parsing different formats based on OS
|
||||
# Add LC_ALL=C for consistent parsing
|
||||
if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == *"bsd"* ]]; then
|
||||
# macOS/BSD: Try formats one by one with date -j -f
|
||||
# Format 1: YYYY-MM-DD HH:MM:SS ZZZZ (e.g., +0200)
|
||||
iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
# Format 2: YYYY-MM-DD HH:MM:SS
|
||||
[ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d %H:%M:%S" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
# Format 3: YYYY-MM-DD (assume T00:00:00)
|
||||
[ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%Y-%m-%d" "$input_dt" +"%Y-%m-%dT00:00:00%z" 2>/dev/null)
|
||||
# Format 4: RFC 2822 subset (e.g., 07 Sep 2023 08:10:00 +0200)
|
||||
[ -z "$iso_dt" ] && iso_dt=$(LC_ALL=C date -j -f "%d %b %Y %H:%M:%S %z" "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
else # Linux
|
||||
# GNU date -d is more flexible and handles many formats automatically
|
||||
iso_dt=$(LC_ALL=C date -d "$input_dt" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
# If parsing succeeded, fix timezone format
|
||||
if [ -n "$iso_dt" ]; then
|
||||
# Fix timezone format from +0000 to +00:00 or Z
|
||||
if [[ "$iso_dt" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
local tz_offset="${BASH_REMATCH[0]}"
|
||||
local tz_hh="${BASH_REMATCH[1]}"
|
||||
local tz_mm="${BASH_REMATCH[2]}"
|
||||
if [ "$tz_hh" == "+00" ] && [ "$tz_mm" == "00" ]; then
|
||||
iso_dt="${iso_dt%$tz_offset}Z"
|
||||
else
|
||||
iso_dt="${iso_dt%$tz_offset}${tz_hh}:${tz_mm}"
|
||||
fi
|
||||
fi
|
||||
echo "$iso_dt"
|
||||
else
|
||||
echo "" # Return empty if formatting failed
|
||||
fi
|
||||
}
|
||||
|
||||
iso_date=$(format_iso8601 "$date")
|
||||
iso_date=$(format_iso8601_post_date "$date")
|
||||
# Use date as fallback for lastmod, then format
|
||||
iso_lastmod_date=$(format_iso8601 "${lastmod:-$date}")
|
||||
iso_lastmod_date=$(format_iso8601_post_date "${lastmod:-$date}")
|
||||
# If lastmod still empty, use iso_date as fallback
|
||||
[ -z "$iso_lastmod_date" ] && iso_lastmod_date="$iso_date"
|
||||
|
||||
# Fallback to build time if both are empty (should be rare)
|
||||
if [ -z "$iso_date" ]; then
|
||||
local now_iso=$(format_iso8601 "now")
|
||||
local now_iso
|
||||
now_iso=$(format_iso8601_post_date "now")
|
||||
iso_date="$now_iso"
|
||||
iso_lastmod_date="$now_iso"
|
||||
fi
|
||||
|
|
@ -239,13 +288,23 @@ convert_markdown() {
|
|||
image_url=$(fix_url "$image")
|
||||
fi
|
||||
|
||||
# Create JSON-LD
|
||||
schema_json_ld=$(printf '<script type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "Article",\n "headline": "%s",\n "datePublished": "%s",\n "dateModified": "%s",\n "author": {\n "@type": "Person",\n "name": "%s",\n "email": "%s"\n },\n "publisher": {\n "@type": "Organization",\n "name": "%s",\n "logo": {\n "@type": "ImageObject",\n "url": "%s/logo.png"\n }\n },\n "description": "%s",\n "mainEntityOfPage": {\n "@type": "WebPage",\n "@id": "%s%s"\n }%s\n}\n</script>' \
|
||||
# Create JSON-LD using post-specific author info
|
||||
local post_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}"
|
||||
local post_author_email="${author_email:-${AUTHOR_EMAIL:-anonymous@example.com}}"
|
||||
|
||||
# Build author JSON - only include email if it's provided
|
||||
local author_json
|
||||
if [ -n "$author_email" ]; then
|
||||
author_json=$(printf '{\n "@type": "Person",\n "name": "%s",\n "email": "%s"\n }' "$post_author_name" "$post_author_email")
|
||||
else
|
||||
author_json=$(printf '{\n "@type": "Person",\n "name": "%s"\n }' "$post_author_name")
|
||||
fi
|
||||
|
||||
schema_json_ld=$(printf '<script type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "Article",\n "headline": "%s",\n "datePublished": "%s",\n "dateModified": "%s",\n "author": %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_NAME:-Anonymous}" \
|
||||
"${AUTHOR_EMAIL:-anonymous@example.com}" \
|
||||
"$author_json" \
|
||||
"$SITE_TITLE" \
|
||||
"$SITE_URL" \
|
||||
"$(echo "$meta_desc" | sed 's/"/\"/g')" \
|
||||
|
|
@ -281,11 +340,18 @@ convert_markdown() {
|
|||
local formatted_lastmod=$(format_date "$lastmod" "$display_date_format")
|
||||
local post_meta_reading_time
|
||||
post_meta_reading_time=$(printf "${MSG_READING_TIME_TEMPLATE:-%d min read}" "$reading_time")
|
||||
local post_meta="<div class=\"page-meta\">${MSG_PUBLISHED_ON:-Published on}: $formatted_date"
|
||||
local post_meta="<div class=\"page-meta\">"
|
||||
post_meta+="<p class=\"meta\">"
|
||||
post_meta+="${MSG_PUBLISHED_ON:-Published on}: <time datetime=\"$date\">$formatted_date</time> ${MSG_BY:-by} <strong>$display_author_name</strong>"
|
||||
post_meta+="</p>"
|
||||
if [ "$formatted_date" != "$formatted_lastmod" ]; then
|
||||
post_meta+=" • ${MSG_UPDATED_ON:-Updated on}: $formatted_lastmod"
|
||||
post_meta+="<p class=\"meta reading-time\">"
|
||||
post_meta+="${MSG_UPDATED_ON:-Updated on}: <time datetime=\"$lastmod\">$formatted_lastmod</time> • $post_meta_reading_time"
|
||||
post_meta+="</p>"
|
||||
else
|
||||
post_meta+="<p class=\"meta reading-time\">$post_meta_reading_time</p>"
|
||||
fi
|
||||
post_meta+=" • $post_meta_reading_time</div>"
|
||||
post_meta+="</div>"
|
||||
|
||||
# Construct featured image HTML
|
||||
local image_html=""
|
||||
|
|
@ -294,14 +360,47 @@ convert_markdown() {
|
|||
image_html="<div class=\"featured-image\"><img src=\"$(fix_url "$image")\" alt=\"$alt_text\"><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+=$(printf '<article class="post">\n <h1>%s</h1>\n%s\n%s\n%s\n%s\n</article>\n' "$title" "$post_meta" "$image_html" "$html_content" "$tags_html")
|
||||
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\}\}/${AUTHOR_NAME:-Anonymous}}
|
||||
footer_content=${footer_content//\{\{author_name\}\}/$post_author_name}
|
||||
|
||||
final_html+="${footer_content}"
|
||||
|
||||
|
|
@ -325,131 +424,300 @@ process_all_markdown_files() {
|
|||
|
||||
local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt"
|
||||
local modified_tags_list="${CACHE_DIR:-.bssg_cache}/modified_tags.list" # Define path for modified tags
|
||||
local modified_authors_list="${CACHE_DIR:-.bssg_cache}/modified_authors.list" # Define path for modified authors
|
||||
local file_index_prev="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" # Path to previous index
|
||||
local ram_mode_active=false
|
||||
local file_index_data=""
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
ram_mode_active=true
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
fi
|
||||
|
||||
if [ ! -f "$file_index" ]; then
|
||||
if ! $ram_mode_active && [ ! -f "$file_index" ]; then
|
||||
echo -e "${RED}Error: File index not found at '$file_index'. Run indexing first.${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local total_file_count=$(wc -l < "$file_index")
|
||||
local total_file_count=0
|
||||
if $ram_mode_active; then
|
||||
total_file_count=$(printf '%s\n' "$file_index_data" | awk 'NF { c++ } END { print c+0 }')
|
||||
else
|
||||
total_file_count=$(wc -l < "$file_index")
|
||||
fi
|
||||
if [ "$total_file_count" -eq 0 ]; then
|
||||
echo -e "${YELLOW}No posts found in file index. Skipping post generation.${NC}"
|
||||
return 0
|
||||
fi
|
||||
echo -e "Checking ${GREEN}$total_file_count${NC} potential posts listed in index."
|
||||
|
||||
# --- Start Change: Clear previous modified tags list ---
|
||||
echo "Clearing previous modified tags list: $modified_tags_list" >&2 # Debug message
|
||||
rm -f "$modified_tags_list"
|
||||
touch "$modified_tags_list" # Ensure file exists even if empty
|
||||
# --- End Change ---
|
||||
# --- 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
|
||||
|
||||
# Get template/locale mtimes once (requires utils.sh and cache.sh to be sourced)
|
||||
# IMPORTANT: Assumes get_file_mtime, TEMPLATES_DIR, THEME, LOCALE_DIR, SITE_LANG are available
|
||||
local template_dir="${TEMPLATES_DIR:-templates}"
|
||||
if [ -d "$template_dir/${THEME:-default}" ]; then
|
||||
template_dir="$template_dir/${THEME:-default}"
|
||||
fi
|
||||
local header_template="$template_dir/header.html"
|
||||
local footer_template="$template_dir/footer.html"
|
||||
local active_locale_file=""
|
||||
if [ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]; then
|
||||
active_locale_file="${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh"
|
||||
elif [ -f "${LOCALE_DIR:-locales}/en.sh" ]; then
|
||||
active_locale_file="${LOCALE_DIR:-locales}/en.sh"
|
||||
fi
|
||||
local header_time=$(get_file_mtime "$header_template")
|
||||
local footer_time=$(get_file_mtime "$footer_template")
|
||||
local locale_time=$(get_file_mtime "$active_locale_file")
|
||||
|
||||
while IFS= read -r line; do
|
||||
local file filename title date lastmod tags slug image image_caption description
|
||||
IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line"
|
||||
|
||||
# Basic check if it looks like a post
|
||||
if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then
|
||||
# echo -e "Skipping non-post file listed in index (pre-check): ${YELLOW}$file${NC}" >&2 # Too verbose
|
||||
continue
|
||||
fi
|
||||
|
||||
# Calculate expected output path (logic copied from process_single_file)
|
||||
local output_path
|
||||
local year month day
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
year="${BASH_REMATCH[1]}"
|
||||
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
year=$(date +%Y); month=$(date +%m); day=$(date +%d)
|
||||
fi
|
||||
local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
|
||||
url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}";
|
||||
url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}"
|
||||
local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html"
|
||||
|
||||
# Perform the rebuild check here
|
||||
# IMPORTANT: Requires common_rebuild_check, get_file_mtime to be available
|
||||
# Requires BSSG_CONFIG_CHANGED_STATUS to be exported by main.sh
|
||||
common_rebuild_check "$output_html_file"
|
||||
local common_result=$?
|
||||
local needs_rebuild=false
|
||||
|
||||
if [ $common_result -eq 0 ]; then
|
||||
needs_rebuild=true # Common checks failed (config changed, template newer, output missing)
|
||||
else # common_result is 2 (output exists and newer than templates/locale)
|
||||
local input_time=$(get_file_mtime "$file")
|
||||
local output_time=$(get_file_mtime "$output_html_file")
|
||||
if (( input_time > output_time )); then
|
||||
needs_rebuild=true # Input file is newer
|
||||
if $ram_mode_active && [ "${FORCE_REBUILD:-false}" = true ]; then
|
||||
echo -e "RAM mode force rebuild: skipping per-post rebuild checks."
|
||||
while IFS= read -r line; do
|
||||
local file filename title date
|
||||
IFS='|' read -r file filename _ date _ <<< "$line"
|
||||
if [ -n "$date" ] && [[ "$file" == "$SRC_DIR"* ]]; then
|
||||
files_to_process_list+=("$line")
|
||||
files_to_process_count=$((files_to_process_count + 1))
|
||||
fi
|
||||
fi
|
||||
done < <(printf '%s\n' "$file_index_data" | awk 'NF')
|
||||
else
|
||||
while IFS= read -r line; do
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line"
|
||||
|
||||
if $needs_rebuild; then
|
||||
files_to_process_list+=("$line")
|
||||
files_to_process_count=$((files_to_process_count + 1))
|
||||
# --- Start Change: Track ALL modified tags (old and new) ---
|
||||
# 'tags' variable holds the NEW tags from the current file_index line
|
||||
local new_tags="$tags"
|
||||
local old_tags=""
|
||||
# Try to get old tags from the previous index snapshot
|
||||
if [ -f "$file_index_prev" ]; then
|
||||
# Grep for the exact file path ($file), assuming it's the first field
|
||||
# Extract the 6th field (tags)
|
||||
old_tags=$(grep "^${file}|" "$file_index_prev" | cut -d'|' -f6)
|
||||
# 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
|
||||
|
||||
# Combine old and new tags
|
||||
local combined_tags="${old_tags},${new_tags}"
|
||||
|
||||
#echo "Tracking combined tags for modified file: $file -> Old: '$old_tags' New: '$new_tags' Combined: '$combined_tags'" >&2 # Debug message
|
||||
|
||||
if [ -n "$combined_tags" ]; then
|
||||
# Split by comma, trim, filter empty, sort unique, and add each tag on a new line
|
||||
echo "$combined_tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep . | sort -u >> "$modified_tags_list"
|
||||
# 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
|
||||
# --- End Change ---
|
||||
else
|
||||
# Only print skip message if not rebuilding
|
||||
echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}"
|
||||
skipped_count=$((skipped_count + 1))
|
||||
fi
|
||||
done < "$file_index"
|
||||
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"
|
||||
|
||||
# --- Start Change: Unique sort the modified tags list (redundant now but safe) ---
|
||||
if [ -f "$modified_tags_list" ]; then
|
||||
echo "Sorting and making modified tags list unique: $modified_tags_list" >&2 # Debug message
|
||||
local temp_tags_list=$(mktemp)
|
||||
# Sort unique again just in case duplicates were added somehow
|
||||
sort -u "$modified_tags_list" > "$temp_tags_list"
|
||||
mv "$temp_tags_list" "$modified_tags_list"
|
||||
# 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
|
||||
# --- End Change ---
|
||||
|
||||
# Check if any files need processing
|
||||
if [ $files_to_process_count -eq 0 ]; then
|
||||
|
|
@ -460,14 +728,17 @@ process_all_markdown_files() {
|
|||
|
||||
echo -e "Found ${GREEN}$files_to_process_count${NC} posts needing processing out of $total_file_count (Skipped: $skipped_count)."
|
||||
|
||||
if $ram_mode_active && [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then
|
||||
prepare_related_posts_ram_cache "${RELATED_POSTS_COUNT:-3}"
|
||||
fi
|
||||
|
||||
# Define a function for processing a single file line from the *filtered* list
|
||||
# Note: This function now assumes the file *needs* processing.
|
||||
process_single_file_for_rebuild() {
|
||||
local line="$1"
|
||||
|
||||
# Read the line from the argument variable
|
||||
local file filename title date lastmod tags slug image image_caption description
|
||||
IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line"
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line"
|
||||
|
||||
# No need for the basic check here, already done in pre-filter
|
||||
|
||||
|
|
@ -486,21 +757,59 @@ process_all_markdown_files() {
|
|||
url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}"
|
||||
output_path="${OUTPUT_DIR:-output}/$url_path"
|
||||
|
||||
# Call the main conversion function
|
||||
# We no longer rely on its internal file_needs_rebuild check
|
||||
# TODO: Consider modifying convert_markdown to accept a force flag or skip its check
|
||||
if ! convert_markdown "$file" "$output_path" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description"; then
|
||||
# Call the conversion function, skipping internal rebuild checks because this
|
||||
# function only receives files pre-selected for rebuild.
|
||||
if ! convert_markdown "$file" "$output_path" "$title" "$date" "$lastmod" "$tags" "$slug" "$image" "$image_caption" "$description" "$author_name" "$author_email" true; then
|
||||
local exit_code=$?
|
||||
echo -e "${RED}ERROR:${NC} convert_markdown failed for '$file' with exit code $exit_code. Output HTML may be missing or incomplete." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Use GNU parallel if available
|
||||
if [ "${HAS_PARALLEL:-false}" = true ]; then
|
||||
if $ram_mode_active; then
|
||||
local cores
|
||||
cores=$(get_parallel_jobs)
|
||||
if [ "$cores" -gt "$files_to_process_count" ]; then
|
||||
cores="$files_to_process_count"
|
||||
fi
|
||||
|
||||
if [ "$files_to_process_count" -gt 1 ] && [ "$cores" -gt 1 ]; then
|
||||
echo -e "${YELLOW}Using shell parallel workers for $files_to_process_count RAM-mode posts${NC}"
|
||||
|
||||
local worker_pids=()
|
||||
local worker_idx
|
||||
for ((worker_idx = 0; worker_idx < cores; worker_idx++)); do
|
||||
(
|
||||
local idx
|
||||
for ((idx = worker_idx; idx < files_to_process_count; idx += cores)); do
|
||||
process_single_file_for_rebuild "${files_to_process_list[$idx]}"
|
||||
done
|
||||
) &
|
||||
worker_pids+=("$!")
|
||||
done
|
||||
|
||||
local pid
|
||||
local worker_failed=false
|
||||
for pid in "${worker_pids[@]}"; do
|
||||
if ! wait "$pid"; then
|
||||
worker_failed=true
|
||||
fi
|
||||
done
|
||||
if $worker_failed; then
|
||||
echo -e "${RED}Parallel RAM-mode post processing failed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}Using sequential processing for $files_to_process_count RAM-mode posts${NC}"
|
||||
local line
|
||||
for line in "${files_to_process_list[@]}"; do
|
||||
process_single_file_for_rebuild "$line"
|
||||
done
|
||||
fi
|
||||
elif [ "${HAS_PARALLEL:-false}" = true ]; then
|
||||
echo -e "${GREEN}Using GNU parallel to process $files_to_process_count posts${NC}"
|
||||
local cores=1
|
||||
if command -v nproc > /dev/null 2>&1; then cores=$(nproc);
|
||||
elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi
|
||||
local cores
|
||||
cores=$(get_parallel_jobs)
|
||||
|
||||
# Export functions and variables needed by parallel tasks
|
||||
# Note: We export the new process function
|
||||
|
|
@ -508,11 +817,15 @@ process_all_markdown_files() {
|
|||
# Export dependencies of convert_markdown and its helpers
|
||||
export -f file_needs_rebuild get_file_mtime common_rebuild_check config_has_changed # Still needed by convert_markdown *internally* for now
|
||||
export -f calculate_reading_time generate_slug format_date fix_url parse_metadata extract_metadata convert_markdown_to_html
|
||||
export -f trim_whitespace resolve_fediverse_creator build_fediverse_creator_meta_tag
|
||||
export -f format_iso8601_post_date
|
||||
export -f portable_md5sum # Used by cache funcs
|
||||
export CACHE_DIR FORCE_REBUILD OUTPUT_DIR SITE_URL URL_SLUG_FORMAT HEADER_TEMPLATE FOOTER_TEMPLATE
|
||||
export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME MARKDOWN_PROCESSOR MARKDOWN_PL_PATH DATE_FORMAT TIMEZONE SHOW_TIMEZONE
|
||||
export FEDIVERSE_CREATOR AUTHOR_FEDIVERSE_CREATORS_SERIALIZED
|
||||
export MSG_PUBLISHED_ON MSG_UPDATED_ON MSG_READING_TIME_TEMPLATE # Export needed locale messages
|
||||
export CONFIG_HASH_FILE BSSG_CONFIG_CHANGED_STATUS # Export status for common_rebuild_check
|
||||
export ENABLE_RELATED_POSTS RELATED_POSTS_COUNT # Export related posts configuration
|
||||
|
||||
# Process filtered lines in parallel
|
||||
printf "%s\n" "${files_to_process_list[@]}" | parallel --jobs "$cores" --will-cite process_single_file_for_rebuild {} || { echo -e "${RED}Parallel post processing failed.${NC}"; exit 1; }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ generate_pages_index() {
|
|||
# --- Define Target File ---
|
||||
local pages_index="$OUTPUT_DIR/pages.html"
|
||||
local secondary_pages_list_file="${CACHE_DIR:-.bssg_cache}/secondary_pages.list"
|
||||
local ram_mode_active=false
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
ram_mode_active=true
|
||||
fi
|
||||
|
||||
# --- Cache Check --- START ---
|
||||
# Rebuild if force flag is set OR if list file exists and output is older than list file
|
||||
|
|
@ -22,13 +26,13 @@ generate_pages_index() {
|
|||
if [[ "${FORCE_REBUILD:-false}" == true ]]; then
|
||||
should_rebuild=true
|
||||
echo -e "${YELLOW}Forcing pages index rebuild (--force-rebuild).${NC}"
|
||||
elif [ ! -f "$secondary_pages_list_file" ]; then
|
||||
elif ! $ram_mode_active && [ ! -f "$secondary_pages_list_file" ]; then
|
||||
# If list file doesn't exist, we need to generate pages.html (or handle absence)
|
||||
# This case might mean 0 secondary pages after a clean build.
|
||||
# Let the existing logic handle the case of 0 pages later.
|
||||
should_rebuild=true
|
||||
echo -e "${YELLOW}Secondary pages list file not found, rebuilding pages index.${NC}"
|
||||
elif [ ! -f "$pages_index" ] || [ "$pages_index" -ot "$secondary_pages_list_file" ]; then
|
||||
elif ! $ram_mode_active && { [ ! -f "$pages_index" ] || [ "$pages_index" -ot "$secondary_pages_list_file" ]; }; then
|
||||
should_rebuild=true
|
||||
echo -e "${YELLOW}Pages index is older than secondary pages list, rebuilding.${NC}"
|
||||
# Add checks for template file changes? More complex, rely on overall rebuild for now.
|
||||
|
|
@ -47,7 +51,9 @@ generate_pages_index() {
|
|||
# --- Read secondary pages from cache file --- START ---
|
||||
local temp_secondary_pages=()
|
||||
|
||||
if [ -f "$secondary_pages_list_file" ]; then
|
||||
if $ram_mode_active; then
|
||||
mapfile -t temp_secondary_pages < <(printf '%s\n' "$(ram_mode_get_dataset "secondary_pages")" | awk 'NF')
|
||||
elif [ -f "$secondary_pages_list_file" ]; then
|
||||
# Use mapfile (readarray) to read lines into the array
|
||||
mapfile -t temp_secondary_pages < "$secondary_pages_list_file"
|
||||
# Optional: Trim whitespace from each element if necessary (mapfile usually handles newlines)
|
||||
|
|
@ -81,15 +87,13 @@ generate_pages_index() {
|
|||
header_content=${header_content//\{\{og_type\}\}/"website"}
|
||||
|
||||
# Set proper URL in og:url
|
||||
header_content=${header_content//\{\{page_url\}\}/"pages.html"}
|
||||
header_content=${header_content//\{\{page_url\}\}/"/pages.html"}
|
||||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
|
||||
# Generate CollectionPage schema
|
||||
local schema_json_ld=""
|
||||
local tmp_schema=$(mktemp)
|
||||
|
||||
# Create CollectionPage schema
|
||||
cat > "$tmp_schema" << EOF
|
||||
schema_json_ld=$(cat << EOF
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
|
|
@ -105,12 +109,7 @@ generate_pages_index() {
|
|||
}
|
||||
</script>
|
||||
EOF
|
||||
|
||||
# Read the schema from the temporary file
|
||||
schema_json_ld=$(cat "$tmp_schema")
|
||||
|
||||
# Remove the temporary file
|
||||
rm "$tmp_schema"
|
||||
)
|
||||
|
||||
# Add schema markup to header
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
|
|
@ -118,6 +117,7 @@ EOF
|
|||
# Remove image placeholders
|
||||
header_content=${header_content//\{\{og_image\}\}/""}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/""}
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
|
||||
# Replace placeholders in the footer
|
||||
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
|
||||
|
|
@ -135,7 +135,7 @@ EOF
|
|||
IFS='|' read -r title url _ <<< "$page" # Ignore date for menu
|
||||
cat >> "$pages_index" << EOF
|
||||
<article>
|
||||
<h3><a href="$url">$title</a></h3>
|
||||
<h2><a href="$url">$title</a></h2>
|
||||
</article>
|
||||
EOF
|
||||
done
|
||||
|
|
@ -150,4 +150,4 @@ EOF
|
|||
}
|
||||
|
||||
# Make function available for sourcing
|
||||
export -f generate_pages_index
|
||||
export -f generate_pages_index
|
||||
|
|
|
|||
|
|
@ -13,8 +13,604 @@ source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.s
|
|||
# shellcheck source=generate_feeds.sh disable=SC1091
|
||||
source "$(dirname "$0")/generate_feeds.sh" || { echo >&2 "Error: Failed to source generate_feeds.sh from generate_tags.sh"; exit 1; }
|
||||
|
||||
declare -gA BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=()
|
||||
declare -gA BSSG_RAM_TAG_POST_COUNT_BY_SLUG=()
|
||||
declare -gA BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=()
|
||||
declare -gA BSSG_RAM_RSS_TEMPLATE_BY_SLUG=()
|
||||
declare -g BSSG_RAM_TAG_DISPLAY_DATE_FORMAT=""
|
||||
declare -g BSSG_RAM_TAG_HEADER_BASE=""
|
||||
declare -g BSSG_RAM_TAG_FOOTER_CONTENT=""
|
||||
|
||||
_bssg_tags_now_ms() {
|
||||
if declare -F _bssg_ram_timing_now_ms > /dev/null; then
|
||||
_bssg_ram_timing_now_ms
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -n "${EPOCHREALTIME:-}" ]; then
|
||||
local epoch_norm sec frac ms_part
|
||||
# Some locales expose EPOCHREALTIME with ',' instead of '.' as decimal separator.
|
||||
epoch_norm="${EPOCHREALTIME/,/.}"
|
||||
if [[ "$epoch_norm" =~ ^([0-9]+)([.][0-9]+)?$ ]]; then
|
||||
sec="${BASH_REMATCH[1]}"
|
||||
frac="${BASH_REMATCH[2]#.}"
|
||||
frac="${frac}000"
|
||||
ms_part="${frac:0:3}"
|
||||
printf '%s\n' $(( 10#$sec * 1000 + 10#$ms_part ))
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v perl >/dev/null 2>&1; then
|
||||
perl -MTime::HiRes=time -e 'printf("%.0f\n", time()*1000)'
|
||||
else
|
||||
printf '%s\n' $(( $(date +%s) * 1000 ))
|
||||
fi
|
||||
}
|
||||
|
||||
_bssg_tags_format_ms() {
|
||||
local ms="${1:-0}"
|
||||
printf '%d.%03ds' $((ms / 1000)) $((ms % 1000))
|
||||
}
|
||||
|
||||
_write_tag_rss_from_cached_items_ram() {
|
||||
local output_file="$1"
|
||||
local feed_link_rel="$2"
|
||||
local feed_atom_link_rel="$3"
|
||||
local tag="$4"
|
||||
local rss_items_xml="$5"
|
||||
|
||||
local feed_title="${SITE_TITLE} - ${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag"
|
||||
local feed_description="${MSG_POSTS_TAGGED_WITH:-"Posts tagged with"}: $tag"
|
||||
local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z"
|
||||
|
||||
local escaped_feed_title escaped_feed_description feed_link feed_atom_link channel_last_build_date
|
||||
escaped_feed_title=$(html_escape "$feed_title")
|
||||
escaped_feed_description=$(html_escape "$feed_description")
|
||||
feed_link=$(fix_url "$feed_link_rel")
|
||||
feed_atom_link=$(fix_url "$feed_atom_link_rel")
|
||||
channel_last_build_date=$(format_date "now" "$rss_date_fmt")
|
||||
|
||||
exec 4> "$output_file" || return 1
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8" ?>' \
|
||||
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">' \
|
||||
'<channel>' \
|
||||
" <title>${escaped_feed_title}</title>" \
|
||||
" <link>${feed_link}</link>" \
|
||||
" <description>${escaped_feed_description}</description>" \
|
||||
" <language>${SITE_LANG:-en}</language>" \
|
||||
" <lastBuildDate>${channel_last_build_date}</lastBuildDate>" \
|
||||
" <atom:link href=\"${feed_atom_link}\" rel=\"self\" type=\"application/rss+xml\" />" >&4
|
||||
|
||||
if [ -n "$rss_items_xml" ]; then
|
||||
printf '%s' "$rss_items_xml" >&4
|
||||
fi
|
||||
|
||||
printf '%s\n' '</channel>' '</rss>' >&4
|
||||
exec 4>&-
|
||||
|
||||
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
echo -e "${GREEN}RSS feed generated at $output_file${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
_process_single_tag_page_ram() {
|
||||
local tag_url="$1"
|
||||
local tag="$2"
|
||||
local tag_page_html_file="$OUTPUT_DIR/tags/$tag_url/index.html"
|
||||
local tag_rss_file="$OUTPUT_DIR/tags/$tag_url/${RSS_FILENAME:-rss.xml}"
|
||||
local tag_page_rel_url="/tags/${tag_url}/"
|
||||
local tag_rss_rel_url="/tags/${tag_url}/${RSS_FILENAME:-rss.xml}"
|
||||
mkdir -p "$(dirname "$tag_page_html_file")"
|
||||
|
||||
local header_content="$BSSG_RAM_TAG_HEADER_BASE"
|
||||
header_content=${header_content//\{\{page_title\}\}/"${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag"}
|
||||
header_content=${header_content//\{\{page_url\}\}/"$tag_page_rel_url"}
|
||||
if [ "${ENABLE_TAG_RSS:-false}" = true ]; then
|
||||
header_content=${header_content//<!-- bssg:tag_rss_link -->/<link rel="alternate" type="application/rss+xml" title="${SITE_TITLE} - Posts tagged with ${tag}" href="${SITE_URL}${tag_rss_rel_url}">}
|
||||
else
|
||||
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
|
||||
fi
|
||||
local schema_json_ld
|
||||
schema_json_ld=$(cat <<EOF
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "CollectionPage",
|
||||
"name": "Posts tagged with: $tag",
|
||||
"description": "Posts with tag: $tag",
|
||||
"url": "$SITE_URL${tag_page_rel_url}",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "$SITE_TITLE",
|
||||
"url": "$SITE_URL"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
EOF
|
||||
)
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
local footer_content="$BSSG_RAM_TAG_FOOTER_CONTENT"
|
||||
|
||||
exec 3> "$tag_page_html_file"
|
||||
printf '%s\n' "$header_content" >&3
|
||||
printf '<h1>%s: %s</h1>\n' "${MSG_TAG_PAGE_TITLE:-Posts tagged with}" "$tag" >&3
|
||||
printf '<div class="posts-list">\n' >&3
|
||||
|
||||
local rss_item_limit=${RSS_ITEM_LIMIT:-15}
|
||||
local rss_count=0
|
||||
local cached_rss_items=""
|
||||
local rss_all_items_cached=true
|
||||
local -a selected_rss_templates=()
|
||||
local tag_post_slugs=""
|
||||
if [[ -n "${BSSG_RAM_TAG_POST_SLUGS_BY_SLUG[$tag_url]+_}" ]]; then
|
||||
tag_post_slugs="${BSSG_RAM_TAG_POST_SLUGS_BY_SLUG[$tag_url]}"
|
||||
fi
|
||||
|
||||
local slug cached_article_html rss_template
|
||||
while IFS= read -r slug; do
|
||||
[ -z "$slug" ] && continue
|
||||
cached_article_html="${BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG[$slug]}"
|
||||
if [ -n "$cached_article_html" ]; then
|
||||
printf '%s' "$cached_article_html" >&3
|
||||
fi
|
||||
if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_count" -lt "$rss_item_limit" ]; then
|
||||
rss_template="${BSSG_RAM_RSS_TEMPLATE_BY_SLUG[$slug]}"
|
||||
if [ -n "$rss_template" ]; then
|
||||
selected_rss_templates+=("$rss_template")
|
||||
if $rss_all_items_cached; then
|
||||
local rss_file rss_filename rss_title rss_date rss_lastmod rss_tags rss_slug rss_image rss_image_caption rss_description rss_author_name rss_author_email
|
||||
IFS='|' read -r rss_file rss_filename rss_title rss_date rss_lastmod rss_tags rss_slug rss_image rss_image_caption rss_description rss_author_name rss_author_email <<< "$rss_template"
|
||||
local rss_item_cache_key="${RSS_INCLUDE_FULL_CONTENT:-false}|${rss_file}|${rss_date}|${rss_lastmod}|${rss_slug}|${rss_title}"
|
||||
local rss_item_xml="${BSSG_RAM_RSS_ITEM_XML_CACHE[$rss_item_cache_key]-}"
|
||||
if [ -n "$rss_item_xml" ]; then
|
||||
cached_rss_items+="$rss_item_xml"
|
||||
else
|
||||
rss_all_items_cached=false
|
||||
fi
|
||||
fi
|
||||
rss_count=$((rss_count + 1))
|
||||
fi
|
||||
fi
|
||||
done <<< "$tag_post_slugs"
|
||||
|
||||
printf '</div>\n' >&3
|
||||
printf '<p><a href="%s/tags/">%s</a></p>\n' "$SITE_URL" "${MSG_ALL_TAGS:-All Tags}" >&3
|
||||
printf '%s\n' "$footer_content" >&3
|
||||
exec 3>&-
|
||||
|
||||
if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "${#selected_rss_templates[@]}" -gt 0 ]; then
|
||||
if $rss_all_items_cached; then
|
||||
_write_tag_rss_from_cached_items_ram "$tag_rss_file" "$tag_page_rel_url" "$tag_rss_rel_url" "$tag" "$cached_rss_items"
|
||||
else
|
||||
local tag_post_data=""
|
||||
local rss_template_entry
|
||||
for rss_template_entry in "${selected_rss_templates[@]}"; do
|
||||
tag_post_data+="${rss_template_entry//%TAG%/$tag}"$'\n'
|
||||
done
|
||||
_generate_rss_feed "$tag_rss_file" "${SITE_TITLE} - ${MSG_TAG_PAGE_TITLE:-"Posts tagged with"}: $tag" "${MSG_POSTS_TAGGED_WITH:-"Posts tagged with"}: $tag" "$tag_page_rel_url" "$tag_rss_rel_url" "$tag_post_data"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_generate_tag_pages_ram() {
|
||||
echo -e "${YELLOW}Processing tag pages${NC}${ENABLE_TAG_RSS:+" and RSS feeds"}...${NC}"
|
||||
local ram_tags_timing_enabled=false
|
||||
if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
ram_tags_timing_enabled=true
|
||||
fi
|
||||
local tags_total_start_ms=0
|
||||
local tags_phase_start_ms=0
|
||||
local tags_prep_ms=0
|
||||
local tags_render_ms=0
|
||||
local tags_index_ms=0
|
||||
local tags_total_ms=0
|
||||
if [ "$ram_tags_timing_enabled" = true ]; then
|
||||
tags_total_start_ms="$(_bssg_tags_now_ms)"
|
||||
tags_phase_start_ms="$tags_total_start_ms"
|
||||
fi
|
||||
|
||||
local tags_index_data
|
||||
tags_index_data=$(ram_mode_get_dataset "tags_index")
|
||||
local main_tags_index_output="$OUTPUT_DIR/tags/index.html"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR/tags"
|
||||
|
||||
if [ -z "$tags_index_data" ]; then
|
||||
echo -e "${YELLOW}No tags found in RAM index. Skipping tag page generation.${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=()
|
||||
BSSG_RAM_TAG_POST_COUNT_BY_SLUG=()
|
||||
BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=()
|
||||
BSSG_RAM_RSS_TEMPLATE_BY_SLUG=()
|
||||
declare -A tag_name_by_slug=()
|
||||
local sorted_tag_urls=()
|
||||
declare -A rss_prefill_slug_set=()
|
||||
declare -A rss_prefill_slug_hits=()
|
||||
local rss_prefill_slugs=()
|
||||
local rss_prefill_occurrences=0
|
||||
local rss_item_limit="${RSS_ITEM_LIMIT:-15}"
|
||||
local rss_prefill_min_hits="${RAM_RSS_PREFILL_MIN_HITS:-2}"
|
||||
local rss_prefill_max_posts="${RAM_RSS_PREFILL_MAX_POSTS:-24}"
|
||||
if ! [[ "$rss_prefill_min_hits" =~ ^[0-9]+$ ]] || [ "$rss_prefill_min_hits" -lt 1 ]; then
|
||||
rss_prefill_min_hits=1
|
||||
fi
|
||||
if ! [[ "$rss_prefill_max_posts" =~ ^[0-9]+$ ]]; then
|
||||
rss_prefill_max_posts=24
|
||||
fi
|
||||
declare -A seen_post_slugs=()
|
||||
local display_date_format="$DATE_FORMAT"
|
||||
if [ "${SHOW_TIMEZONE:-false}" = false ]; then
|
||||
display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//')
|
||||
fi
|
||||
BSSG_RAM_TAG_DISPLAY_DATE_FORMAT="$display_date_format"
|
||||
|
||||
# Prime per-post caches once from file_index (one row per post), then build
|
||||
# lightweight tag->post mappings from tags_index (many rows per post).
|
||||
local file_index_data
|
||||
file_index_data=$(ram_mode_get_dataset "file_index")
|
||||
|
||||
local can_prime_rss_metadata=false
|
||||
local rss_date_fmt="%a, %d %b %Y %H:%M:%S %z"
|
||||
local build_timestamp_iso=""
|
||||
if [ "${ENABLE_TAG_RSS:-false}" = true ] && declare -F _ram_prime_rss_metadata_entry > /dev/null; then
|
||||
can_prime_rss_metadata=true
|
||||
build_timestamp_iso=$(format_date "now" "%Y-%m-%dT%H:%M:%S%z")
|
||||
if [[ "$build_timestamp_iso" =~ ([+-][0-9]{2})([0-9]{2})$ ]]; then
|
||||
build_timestamp_iso="${build_timestamp_iso::${#build_timestamp_iso}-2}:${BASH_REMATCH[2]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
|
||||
[ -z "$file" ] && continue
|
||||
[ -z "$slug" ] && continue
|
||||
[[ -n "${seen_post_slugs[$slug]+_}" ]] && continue
|
||||
seen_post_slugs["$slug"]=1
|
||||
|
||||
local post_year post_month post_day
|
||||
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
|
||||
post_year="${BASH_REMATCH[1]}"
|
||||
post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
||||
post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
||||
else
|
||||
post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d)
|
||||
fi
|
||||
|
||||
local formatted_path="${URL_SLUG_FORMAT//Year/$post_year}"
|
||||
formatted_path="${formatted_path//Month/$post_month}"
|
||||
formatted_path="${formatted_path//Day/$post_day}"
|
||||
formatted_path="${formatted_path//slug/$slug}"
|
||||
local post_link="/${formatted_path}/"
|
||||
local formatted_date
|
||||
formatted_date=$(format_date "$date" "$display_date_format")
|
||||
|
||||
local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}"
|
||||
local article_html=""
|
||||
article_html+=' <article>'$'\n'
|
||||
article_html+=" <h2><a href=\"${SITE_URL}${post_link}\">${title}</a></h2>"$'\n'
|
||||
article_html+=" <div class=\"meta\">${MSG_PUBLISHED_ON:-Published on} ${formatted_date} ${MSG_BY:-by} <strong>${display_author_name}</strong></div>"$'\n'
|
||||
if [ -n "$image" ]; then
|
||||
local image_url alt_text figcaption_content
|
||||
image_url=$(fix_url "$image")
|
||||
alt_text="${image_caption:-$title}"
|
||||
figcaption_content="${image_caption:-$title}"
|
||||
article_html+=' <figure class="featured-image tag-image">'$'\n'
|
||||
article_html+=" <a href=\"${SITE_URL}${post_link}\">"$'\n'
|
||||
article_html+=" <img src=\"${image_url}\" alt=\"${alt_text}\" />"$'\n'
|
||||
article_html+=' </a>'$'\n'
|
||||
article_html+=" <figcaption>${figcaption_content}</figcaption>"$'\n'
|
||||
article_html+=' </figure>'$'\n'
|
||||
fi
|
||||
if [ -n "$description" ]; then
|
||||
article_html+=' <div class="summary">'$'\n'
|
||||
article_html+=" ${description}"$'\n'
|
||||
article_html+=' </div>'$'\n'
|
||||
fi
|
||||
article_html+=' </article>'$'\n'
|
||||
BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG["$slug"]="$article_html"
|
||||
BSSG_RAM_RSS_TEMPLATE_BY_SLUG["$slug"]="${filename}|${filename}|${title}|${date}|${lastmod}|%TAG%|${slug}|${image}|${image_caption}|${description}|${author_name}|${author_email}"
|
||||
|
||||
if $can_prime_rss_metadata; then
|
||||
_ram_prime_rss_metadata_entry "$date" "$lastmod" "$slug" "$rss_date_fmt" "$build_timestamp_iso" "$file" >/dev/null || true
|
||||
fi
|
||||
done <<< "$file_index_data"
|
||||
|
||||
if $can_prime_rss_metadata; then
|
||||
BSSG_RAM_RSS_METADATA_CACHE_READY=true
|
||||
fi
|
||||
|
||||
# Sort once globally by tag slug, then by publish date/lastmod descending.
|
||||
# Aggregate per-tag rows in awk to reduce per-line bash map churn.
|
||||
local aggregated_tags_data
|
||||
aggregated_tags_data=$(printf '%s\n' "$tags_index_data" | awk 'NF' | LC_ALL=C sort -t'|' -k2,2 -k4,4r -k5,5r | awk -F'|' -v OFS='|' '
|
||||
{
|
||||
tag = $1
|
||||
tag_slug = $2
|
||||
post_slug = $7
|
||||
if (tag == "" || tag_slug == "") next
|
||||
|
||||
if (current_tag_slug != "" && tag_slug != current_tag_slug) {
|
||||
print current_tag_slug, current_tag_name, current_count, current_post_slugs
|
||||
current_count = 0
|
||||
current_post_slugs = ""
|
||||
}
|
||||
|
||||
if (tag_slug != current_tag_slug) {
|
||||
current_tag_slug = tag_slug
|
||||
current_tag_name = tag
|
||||
}
|
||||
|
||||
if (post_slug != "") {
|
||||
if (current_post_slugs == "") {
|
||||
current_post_slugs = post_slug
|
||||
} else {
|
||||
current_post_slugs = current_post_slugs "," post_slug
|
||||
}
|
||||
}
|
||||
current_count++
|
||||
}
|
||||
END {
|
||||
if (current_tag_slug != "") {
|
||||
print current_tag_slug, current_tag_name, current_count, current_post_slugs
|
||||
}
|
||||
}')
|
||||
|
||||
local tag_slug tag_name tag_count_value tag_post_slugs_csv
|
||||
while IFS='|' read -r tag_slug tag_name tag_count_value tag_post_slugs_csv; do
|
||||
[ -z "$tag_slug" ] && continue
|
||||
tag_name_by_slug["$tag_slug"]="$tag_name"
|
||||
BSSG_RAM_TAG_POST_COUNT_BY_SLUG["$tag_slug"]="$tag_count_value"
|
||||
local tag_post_slugs_newline=""
|
||||
if [ -n "$tag_post_slugs_csv" ]; then
|
||||
tag_post_slugs_newline="${tag_post_slugs_csv//,/$'\n'}"
|
||||
fi
|
||||
BSSG_RAM_TAG_POST_SLUGS_BY_SLUG["$tag_slug"]="$tag_post_slugs_newline"
|
||||
sorted_tag_urls+=("$tag_slug")
|
||||
|
||||
if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ -n "$tag_post_slugs_newline" ]; then
|
||||
local rss_prefill_count=0
|
||||
local rss_prefill_slug=""
|
||||
while IFS= read -r rss_prefill_slug; do
|
||||
[ -z "$rss_prefill_slug" ] && continue
|
||||
rss_prefill_occurrences=$((rss_prefill_occurrences + 1))
|
||||
rss_prefill_slug_hits["$rss_prefill_slug"]=$(( ${rss_prefill_slug_hits[$rss_prefill_slug]:-0} + 1 ))
|
||||
if [[ -z "${rss_prefill_slug_set[$rss_prefill_slug]+_}" ]]; then
|
||||
rss_prefill_slug_set["$rss_prefill_slug"]=1
|
||||
rss_prefill_slugs+=("$rss_prefill_slug")
|
||||
fi
|
||||
rss_prefill_count=$((rss_prefill_count + 1))
|
||||
if [ "$rss_prefill_count" -ge "$rss_item_limit" ]; then
|
||||
break
|
||||
fi
|
||||
done <<< "$tag_post_slugs_newline"
|
||||
fi
|
||||
done <<< "$aggregated_tags_data"
|
||||
|
||||
if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_prefill_min_hits" -gt 1 ] && [ "${#rss_prefill_slugs[@]}" -gt 0 ]; then
|
||||
local -a rss_prefill_filtered_slugs=()
|
||||
local rss_prefill_slug
|
||||
for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do
|
||||
if [ "${rss_prefill_slug_hits[$rss_prefill_slug]:-0}" -ge "$rss_prefill_min_hits" ]; then
|
||||
rss_prefill_filtered_slugs+=("$rss_prefill_slug")
|
||||
fi
|
||||
done
|
||||
if [ "${#rss_prefill_filtered_slugs[@]}" -gt 0 ]; then
|
||||
rss_prefill_slugs=("${rss_prefill_filtered_slugs[@]}")
|
||||
fi
|
||||
fi
|
||||
|
||||
local rss_prefill_pool_count="${#rss_prefill_slugs[@]}"
|
||||
if [ "${ENABLE_TAG_RSS:-false}" = true ] && [ "$rss_prefill_max_posts" -gt 0 ] && [ "${#rss_prefill_slugs[@]}" -gt "$rss_prefill_max_posts" ]; then
|
||||
local -a rss_prefill_ranked_lines=()
|
||||
local rss_prefill_slug
|
||||
for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do
|
||||
rss_prefill_ranked_lines+=("${rss_prefill_slug_hits[$rss_prefill_slug]:-0}|$rss_prefill_slug")
|
||||
done
|
||||
|
||||
local -a rss_prefill_capped_slugs=()
|
||||
local rss_prefill_rank_line
|
||||
while IFS= read -r rss_prefill_rank_line; do
|
||||
[ -z "$rss_prefill_rank_line" ] && continue
|
||||
rss_prefill_capped_slugs+=("${rss_prefill_rank_line#*|}")
|
||||
done < <(
|
||||
printf '%s\n' "${rss_prefill_ranked_lines[@]}" \
|
||||
| LC_ALL=C sort -t'|' -k1,1nr -k2,2 \
|
||||
| head -n "$rss_prefill_max_posts"
|
||||
)
|
||||
|
||||
if [ "${#rss_prefill_capped_slugs[@]}" -gt 0 ]; then
|
||||
rss_prefill_slugs=("${rss_prefill_capped_slugs[@]}")
|
||||
fi
|
||||
fi
|
||||
|
||||
local footer_base="$FOOTER_TEMPLATE"
|
||||
footer_base=${footer_base//\{\{current_year\}\}/$(date +%Y)}
|
||||
footer_base=${footer_base//\{\{author_name\}\}/"$AUTHOR_NAME"}
|
||||
BSSG_RAM_TAG_FOOTER_CONTENT="$footer_base"
|
||||
|
||||
local header_base="$HEADER_TEMPLATE"
|
||||
header_base=${header_base//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
header_base=${header_base//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_base=${header_base//\{\{og_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_base=${header_base//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_base=${header_base//\{\{og_type\}\}/"website"}
|
||||
header_base=${header_base//\{\{site_url\}\}/"$SITE_URL"}
|
||||
header_base=${header_base//\{\{og_image\}\}/""}
|
||||
header_base=${header_base//\{\{twitter_image\}\}/""}
|
||||
header_base=${header_base//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
BSSG_RAM_TAG_HEADER_BASE="$header_base"
|
||||
|
||||
local tag_count="${#sorted_tag_urls[@]}"
|
||||
echo -e "Generating ${GREEN}$tag_count${NC} tag pages from RAM index."
|
||||
|
||||
if [ "${ENABLE_TAG_RSS:-false}" = true ]; then
|
||||
if declare -F prepare_ram_rss_metadata_cache > /dev/null; then
|
||||
prepare_ram_rss_metadata_cache
|
||||
fi
|
||||
if [ "${RSS_INCLUDE_FULL_CONTENT:-false}" = true ] && declare -F prepare_ram_rss_full_content_cache > /dev/null; then
|
||||
prepare_ram_rss_full_content_cache
|
||||
fi
|
||||
|
||||
# Pre-warm RAM RSS item XML cache once in parent process so worker
|
||||
# subshells inherit it read-only and avoid rebuilding duplicate items.
|
||||
if declare -F _generate_rss_feed > /dev/null; then
|
||||
local rss_prefill_post_data=""
|
||||
local rss_prefill_slug rss_template_entry
|
||||
for rss_prefill_slug in "${rss_prefill_slugs[@]}"; do
|
||||
rss_template_entry="${BSSG_RAM_RSS_TEMPLATE_BY_SLUG[$rss_prefill_slug]}"
|
||||
[ -z "$rss_template_entry" ] && continue
|
||||
rss_prefill_post_data+="${rss_template_entry//%TAG%/__prefill__}"$'\n'
|
||||
done
|
||||
if [ -n "$rss_prefill_post_data" ]; then
|
||||
if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
|
||||
local max_posts_label="unlimited"
|
||||
if [ "$rss_prefill_max_posts" -gt 0 ]; then
|
||||
max_posts_label="$rss_prefill_max_posts"
|
||||
fi
|
||||
echo -e "DEBUG: Pre-warming RAM RSS item cache for ${#rss_prefill_slugs[@]} posts (${rss_prefill_occurrences} tag-RSS slots, min hits: ${rss_prefill_min_hits}, max posts: ${max_posts_label}, pool: ${rss_prefill_pool_count})."
|
||||
fi
|
||||
_generate_rss_feed "/dev/null" "__prefill__" "__prefill__" "/" "/rss.xml" "$rss_prefill_post_data" >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$ram_tags_timing_enabled" = true ]; then
|
||||
local now_ms
|
||||
now_ms="$(_bssg_tags_now_ms)"
|
||||
tags_prep_ms=$((now_ms - tags_phase_start_ms))
|
||||
tags_phase_start_ms="$now_ms"
|
||||
fi
|
||||
|
||||
local tag_url
|
||||
local cores
|
||||
cores=$(get_parallel_jobs)
|
||||
if [ "$cores" -gt "$tag_count" ]; then
|
||||
cores="$tag_count"
|
||||
fi
|
||||
|
||||
if [ "$tag_count" -gt 1 ] && [ "$cores" -gt 1 ]; then
|
||||
local worker_pids=()
|
||||
local worker_idx
|
||||
for ((worker_idx = 0; worker_idx < cores; worker_idx++)); do
|
||||
(
|
||||
local idx local_tag_url local_tag
|
||||
for ((idx = worker_idx; idx < tag_count; idx += cores)); do
|
||||
local_tag_url="${sorted_tag_urls[$idx]}"
|
||||
local_tag="${tag_name_by_slug[$local_tag_url]}"
|
||||
_process_single_tag_page_ram "$local_tag_url" "$local_tag"
|
||||
done
|
||||
) &
|
||||
worker_pids+=("$!")
|
||||
done
|
||||
|
||||
local pid
|
||||
local worker_failed=false
|
||||
for pid in "${worker_pids[@]}"; do
|
||||
if ! wait "$pid"; then
|
||||
worker_failed=true
|
||||
fi
|
||||
done
|
||||
if $worker_failed; then
|
||||
echo -e "${RED}Parallel RAM-mode tag processing failed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
for tag_url in "${sorted_tag_urls[@]}"; do
|
||||
tag="${tag_name_by_slug[$tag_url]}"
|
||||
_process_single_tag_page_ram "$tag_url" "$tag"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$ram_tags_timing_enabled" = true ]; then
|
||||
local now_ms
|
||||
now_ms="$(_bssg_tags_now_ms)"
|
||||
tags_render_ms=$((now_ms - tags_phase_start_ms))
|
||||
tags_phase_start_ms="$now_ms"
|
||||
fi
|
||||
|
||||
local header_content="$HEADER_TEMPLATE"
|
||||
local footer_content="$FOOTER_TEMPLATE"
|
||||
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
|
||||
header_content=${header_content//\{\{page_title\}\}/"${MSG_ALL_TAGS:-"All Tags"}"}
|
||||
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{og_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"}
|
||||
header_content=${header_content//\{\{og_type\}\}/"website"}
|
||||
header_content=${header_content//\{\{page_url\}\}/"/tags/"}
|
||||
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
|
||||
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
|
||||
local tags_schema_json
|
||||
tags_schema_json=$(cat <<EOF
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "CollectionPage",
|
||||
"name": "${MSG_ALL_TAGS:-"All Tags"}",
|
||||
"description": "List of all tags on $SITE_TITLE",
|
||||
"url": "$SITE_URL/tags/",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "$SITE_TITLE",
|
||||
"url": "$SITE_URL"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
EOF
|
||||
)
|
||||
header_content=${header_content//\{\{schema_json_ld\}\}/"$tags_schema_json"}
|
||||
header_content=${header_content//\{\{og_image\}\}/""}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/""}
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
|
||||
footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"}
|
||||
|
||||
exec 5> "$main_tags_index_output"
|
||||
printf '%s\n' "$header_content" >&5
|
||||
printf '<h1>%s</h1>\n' "${MSG_ALL_TAGS:-All Tags}" >&5
|
||||
printf '<div class="tags-list">\n' >&5
|
||||
for tag_url in "${sorted_tag_urls[@]}"; do
|
||||
tag="${tag_name_by_slug[$tag_url]}"
|
||||
local post_count="${BSSG_RAM_TAG_POST_COUNT_BY_SLUG[$tag_url]:-0}"
|
||||
printf ' <a href="%s/tags/%s/">%s <span class="tag-count">(%s)</span></a>\n' "$SITE_URL" "$tag_url" "$tag" "$post_count" >&5
|
||||
done
|
||||
printf '</div>\n' >&5
|
||||
printf '%s\n' "$footer_content" >&5
|
||||
exec 5>&-
|
||||
|
||||
if [ "$ram_tags_timing_enabled" = true ]; then
|
||||
local now_ms
|
||||
now_ms="$(_bssg_tags_now_ms)"
|
||||
tags_index_ms=$((now_ms - tags_phase_start_ms))
|
||||
tags_total_ms=$((now_ms - tags_total_start_ms))
|
||||
echo -e "${BLUE}RAM tags sub-timing:${NC}"
|
||||
echo -e " Prepare maps/cache: $(_bssg_tags_format_ms "$tags_prep_ms")"
|
||||
echo -e " Tag pages+RSS: $(_bssg_tags_format_ms "$tags_render_ms")"
|
||||
echo -e " tags/index.html: $(_bssg_tags_format_ms "$tags_index_ms")"
|
||||
echo -e " Total tags stage: $(_bssg_tags_format_ms "$tags_total_ms")"
|
||||
fi
|
||||
|
||||
BSSG_RAM_TAG_POST_SLUGS_BY_SLUG=()
|
||||
BSSG_RAM_TAG_POST_COUNT_BY_SLUG=()
|
||||
BSSG_RAM_TAG_ARTICLE_HTML_BY_SLUG=()
|
||||
BSSG_RAM_RSS_TEMPLATE_BY_SLUG=()
|
||||
BSSG_RAM_TAG_HEADER_BASE=""
|
||||
BSSG_RAM_TAG_FOOTER_CONTENT=""
|
||||
BSSG_RAM_TAG_DISPLAY_DATE_FORMAT=""
|
||||
|
||||
echo -e "${GREEN}Tag pages processed!${NC}"
|
||||
}
|
||||
|
||||
# Generate tag pages
|
||||
generate_tag_pages() {
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
|
||||
_generate_tag_pages_ram
|
||||
return $?
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Processing tag pages${NC}${ENABLE_TAG_RSS:+" and RSS feeds"}...${NC}"
|
||||
|
||||
local tags_index_file="$CACHE_DIR/tags_index.txt"
|
||||
|
|
@ -184,9 +780,9 @@ generate_tag_pages() {
|
|||
|
||||
if [ -n "$tag" ]; then
|
||||
local tag_page_html_file="$OUTPUT_DIR/tags/$tag_url/index.html"
|
||||
local tag_rss_file="$OUTPUT_DIR/tags/$tag_url/rss.xml"
|
||||
local tag_rss_file="$OUTPUT_DIR/tags/$tag_url/${RSS_FILENAME:-rss.xml}"
|
||||
local tag_page_rel_url="/tags/${tag_url}/"
|
||||
local tag_rss_rel_url="/tags/${tag_url}/rss.xml"
|
||||
local tag_rss_rel_url="/tags/${tag_url}/${RSS_FILENAME:-rss.xml}"
|
||||
local rebuild_html=false
|
||||
local rebuild_rss=false
|
||||
|
||||
|
|
@ -285,6 +881,7 @@ EOF
|
|||
# Remove image placeholders
|
||||
header_content=${header_content//\{\{og_image\}\}/""}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/""}
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
|
||||
# Replace placeholders in the footer
|
||||
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
|
||||
|
|
@ -307,8 +904,8 @@ EOF
|
|||
if [ -z "$post_line" ]; then continue; fi
|
||||
# echo "DEBUG (process_tag for '$tag'): Processing post_line: $post_line" >&2 # Removed
|
||||
|
||||
local _ _ title date lastmod filename slug image image_caption description
|
||||
IFS='|' read -r _ _ title date lastmod filename slug image image_caption description <<< "$post_line"
|
||||
local _ _ title date lastmod filename slug image image_caption description author_name author_email
|
||||
IFS='|' read -r _ _ title date lastmod filename slug image image_caption description author_name author_email <<< "$post_line"
|
||||
|
||||
# Create slug-based URL path
|
||||
local post_year post_month post_day
|
||||
|
|
@ -333,14 +930,17 @@ EOF
|
|||
fi
|
||||
local formatted_date=$(format_date "$date" "$display_date_format")
|
||||
|
||||
# Determine author for display (with fallback)
|
||||
local display_author_name="${author_name:-${AUTHOR_NAME:-Anonymous}}"
|
||||
|
||||
# --- Start Debug: Check variables before appending article ---
|
||||
#echo "DEBUGAPPEND (tag='$tag', title='$title'): Appending article HTML with link='$post_link', date='$formatted_date'" >&2
|
||||
# --- End Debug ---
|
||||
|
||||
cat >> "$tag_page_html_file" << EOF
|
||||
<article>
|
||||
<h3><a href="${SITE_URL}${post_link}">$title</a></h3>
|
||||
<div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date</div>
|
||||
<h2><a href="${SITE_URL}${post_link}">$title</a></h2>
|
||||
<div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date ${MSG_BY:-"by"} <strong>$display_author_name</strong></div>
|
||||
EOF
|
||||
|
||||
if [ -n "$image" ]; then
|
||||
|
|
@ -393,9 +993,9 @@ EOF
|
|||
|
||||
# Get post data for this tag from the tags index
|
||||
# Sort by post date (field 4), then lastmod (field 5) reverse, limit
|
||||
# IMPORTANT: tags_index.txt has format: Tag|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|Image|ImageCaption|PostDescription|OriginalFilePath
|
||||
# IMPORTANT: tags_index.txt has format: Tag|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|Image|ImageCaption|PostDescription|AuthorName|AuthorEmail
|
||||
# We need to map this to the format expected by _generate_rss_feed:
|
||||
# file|filename|title|date|lastmod|tags|slug|image|image_caption|description
|
||||
# file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email
|
||||
# We lack the original 'file' path and 'tags' string here. We can approximate.
|
||||
|
||||
local tag_post_data_tmp=$(mktemp)
|
||||
|
|
@ -404,8 +1004,8 @@ EOF
|
|||
head -n "$rss_item_limit" | \
|
||||
awk -F'|' -v tag_val="$tag" 'BEGIN {OFS="|"} {
|
||||
# Reconstruct needed fields. Use filename ($6) as placeholder for first field.
|
||||
# file (placeholder) | filename | title | date | lastmod | tags | slug | image | image_caption | description
|
||||
print $6 "|" $6 "|" $3 "|" $4 "|" $5 "|" tag_val "|" $7 "|" $8 "|" $9 "|" $10
|
||||
# file (placeholder) | filename | title | date | lastmod | tags | slug | image | image_caption | description | author_name | author_email
|
||||
print $6 "|" $6 "|" $3 "|" $4 "|" $5 "|" tag_val "|" $7 "|" $8 "|" $9 "|" $10 "|" $11 "|" $12
|
||||
}' > "$tag_post_data_tmp"
|
||||
|
||||
local tag_post_data=$(cat "$tag_post_data_tmp")
|
||||
|
|
@ -417,6 +1017,7 @@ EOF
|
|||
else
|
||||
# Call the reusable function from generate_feeds.sh
|
||||
# Ensure necessary vars like SITE_URL, SITE_LANG etc. are exported/available
|
||||
# echo "DEBUG: In process_tag for '$tag', RSS_FILENAME='${RSS_FILENAME:-rss.xml}', tag_rss_file='${tag_rss_file}'" >&2 # DEBUG
|
||||
_generate_rss_feed "$tag_rss_file" "$feed_title" "$feed_desc" "$feed_link_rel" "$feed_atom_link_rel" "$tag_post_data"
|
||||
echo -e " Generated RSS feed for: ${GREEN}$tag${NC}"
|
||||
fi
|
||||
|
|
@ -451,7 +1052,7 @@ EOF
|
|||
local tag tag_url
|
||||
IFS='|' read -r tag tag_url <<< "$tag_line"
|
||||
local tag_page_html_file="$OUTPUT_DIR/tags/$tag_url/index.html"
|
||||
local tag_rss_file="$OUTPUT_DIR/tags/$tag_url/rss.xml"
|
||||
local tag_rss_file="$OUTPUT_DIR/tags/$tag_url/${RSS_FILENAME:-rss.xml}"
|
||||
local process_this_tag=false # Flag to decide if this tag needs processing
|
||||
|
||||
# --- Refined Check: Check if tag needs processing ---
|
||||
|
|
@ -489,9 +1090,8 @@ EOF
|
|||
# Use parallel
|
||||
if [ "${HAS_PARALLEL:-false}" = true ] ; then
|
||||
echo -e "${GREEN}Using GNU parallel to process tag pages${NC}${ENABLE_TAG_RSS:+/feeds}"
|
||||
local cores=1
|
||||
if command -v nproc > /dev/null 2>&1; then cores=$(nproc);
|
||||
elif command -v sysctl > /dev/null 2>&1; then cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1); fi
|
||||
local cores
|
||||
cores=$(get_parallel_jobs)
|
||||
local jobs=$cores # Use all cores for tags by default if parallel
|
||||
|
||||
# Export necessary functions and variables
|
||||
|
|
@ -618,6 +1218,7 @@ EOF
|
|||
header_content=${header_content//\{\{schema_json_ld\}\}/"$schema_json_ld"}
|
||||
header_content=${header_content//\{\{og_image\}\}/""}
|
||||
header_content=${header_content//\{\{twitter_image\}\}/""}
|
||||
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
|
||||
|
||||
# Replace placeholders in the footer
|
||||
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ _build_raw_file_index() {
|
|||
vars["title"] = ""; vars["date"] = ""; vars["lastmod"] = "";
|
||||
vars["tags"] = ""; vars["slug"] = ""; vars["image"] = "";
|
||||
vars["image_caption"] = ""; vars["description"] = "";
|
||||
vars["author_name"] = ""; vars["author_email"] = "";
|
||||
in_fm = 0; found_fm = 0;
|
||||
is_html = (FILENAME ~ /\.html$/);
|
||||
is_md = (FILENAME ~ /\.md$/);
|
||||
|
|
@ -40,7 +41,8 @@ _build_raw_file_index() {
|
|||
if (NR > 1) {
|
||||
# Print previous file raw data
|
||||
print current_filename, current_basename, vars["title"], vars["date"], vars["lastmod"], \
|
||||
vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"];
|
||||
vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"], \
|
||||
vars["author_name"], vars["author_email"];
|
||||
}
|
||||
reset_vars();
|
||||
current_filename = FILENAME;
|
||||
|
|
@ -101,7 +103,8 @@ _build_raw_file_index() {
|
|||
if (NR > 0) {
|
||||
# Print last file raw data
|
||||
print current_filename, current_basename, vars["title"], vars["date"], vars["lastmod"], \
|
||||
vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"];
|
||||
vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"], \
|
||||
vars["author_name"], vars["author_email"];
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
|
@ -119,9 +122,9 @@ _process_raw_file_index() {
|
|||
|
||||
> "$output_processed_index" # Ensure output file is empty
|
||||
|
||||
local file filename title date lastmod tags slug image image_caption description
|
||||
local file filename title date lastmod tags slug image image_caption description author_name author_email
|
||||
local file_mtime
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description || [[ -n "$file" ]]; do
|
||||
while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email || [[ -n "$file" ]]; do
|
||||
# Fallback for Title (use filename without extension)
|
||||
if [ -z "$title" ]; then
|
||||
title="${filename%.*}"
|
||||
|
|
@ -138,10 +141,14 @@ _process_raw_file_index() {
|
|||
lastmod="$date"
|
||||
fi
|
||||
|
||||
if [ -n "$slug" ]; then
|
||||
# Ensure slug is sanitized
|
||||
slug=$(generate_slug "$slug")
|
||||
fi
|
||||
# Fallback for Slug (generate from title)
|
||||
if [ -z "$slug" ]; then
|
||||
# Ensure title is available for slug generation
|
||||
if [ -z "$title" ]; then title="${filename%.*}"; fi
|
||||
if [ -z "$title" ]; then title="${filename%.*}"; fi
|
||||
slug=$(generate_slug "$title")
|
||||
fi
|
||||
|
||||
|
|
@ -151,19 +158,50 @@ _process_raw_file_index() {
|
|||
description=$(generate_excerpt "$file")
|
||||
fi
|
||||
|
||||
# Apply fallback logic for author fields
|
||||
if [ -z "$author_name" ]; then
|
||||
author_name="${AUTHOR_NAME:-Anonymous}"
|
||||
fi
|
||||
if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then
|
||||
# Only use default email if using default name
|
||||
author_email="${AUTHOR_EMAIL:-}"
|
||||
fi
|
||||
# If author_name is specified but author_email is empty, leave email empty
|
||||
|
||||
# Output the fully processed line to the final index file
|
||||
echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description" >> "$output_processed_index"
|
||||
echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description|$author_name|$author_email" >> "$output_processed_index"
|
||||
done < "$input_raw_index"
|
||||
wait # Ensure background processes from potential subshells (like generate_excerpt) finish
|
||||
}
|
||||
|
||||
# Optimized file index building - orchestrates raw build and processing
|
||||
_build_file_index_from_ram() {
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
local metadata
|
||||
metadata=$(extract_metadata "$file") || continue
|
||||
local filename
|
||||
filename=$(basename "$file")
|
||||
echo "$file|$filename|$metadata"
|
||||
done < <(ram_mode_list_src_files) | sort -t '|' -k 4,4r -k 1,1
|
||||
}
|
||||
|
||||
optimized_build_file_index() {
|
||||
echo -e "${YELLOW}Building file index...${NC}"
|
||||
|
||||
local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt"
|
||||
local index_marker="${CACHE_DIR:-.bssg_cache}/index_marker"
|
||||
local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker"
|
||||
|
||||
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_list_src_files > /dev/null; then
|
||||
local file_index_data
|
||||
file_index_data=$(_build_file_index_from_ram)
|
||||
ram_mode_set_dataset "file_index" "$file_index_data"
|
||||
ram_mode_clear_dataset "file_index_prev"
|
||||
ram_mode_set_dataset "frontmatter_changes_marker" "1"
|
||||
echo -e "${GREEN}File index built from RAM preload with $(ram_mode_dataset_line_count "file_index") complete entries!${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if rebuild is needed
|
||||
if [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$file_index" ] && [ -f "$index_marker" ]; then
|
||||
|
|
@ -172,7 +210,7 @@ optimized_build_file_index() {
|
|||
if find --version >/dev/null 2>&1 && grep -q GNU <<< "$(find --version)"; then
|
||||
newest_file_time=$(find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" -printf '%T@\n' 2>/dev/null | sort -nr | head -n 1)
|
||||
newest_file_time=${newest_file_time:-0} # Handle empty dir
|
||||
newest_file_time=$(printf "%.0f" "$newest_file_time")
|
||||
newest_file_time=${newest_file_time%.*} # Truncate to integer
|
||||
else # POSIX/BSD find
|
||||
local src_files
|
||||
# Use -exec stat for better portability than parsing ls
|
||||
|
| ||||