Compare commits

...

66 commits
0.12.1 ... main

Author SHA1 Message Date
f5755e9fd0 Added VERSION file - updating version number will be easier 2026-06-11 20:56:51 +02:00
0662983c11 New config options in config.sh (all default true for backward compat):
SHOW_HEADER_MENU=true        # Set false to remove nav menu from header
SHOW_INDEX_DESCRIPTIONS=true # Set false to hide descriptions/excerpts on index
GENERATE_EXCERPT=true        # Set false to skip auto-generating excerpts
SHOW_READING_TIME=true       # Set false to hide reading time on posts

Updated README.md

Should close issue 53: #53
2026-06-11 14:08:46 +02:00
48bc724d94 New config options in config.sh (all default true for backward compat):
SHOW_HEADER_MENU=true        # Set false to remove nav menu from header
SHOW_INDEX_DESCRIPTIONS=true # Set false to hide descriptions/excerpts on index
GENERATE_EXCERPT=true        # Set false to skip auto-generating excerpts
SHOW_READING_TIME=true       # Set false to hide reading time on posts

Should close issue 53: #53
2026-06-11 14:06:02 +02:00
8250468f56 Several format changes, the output will be cleaner and more correct 2026-06-11 13:28:54 +02:00
608a82aec4 - Added support for --config in generate_theme_previews.sh, with config resolution aligned to the main build flow: command-line override first, then BSSG_LCONF, then local/default config files.
- Improved theme preview generation so it now loads the effective BSSG config once, reuses configured SITE_URL, OUTPUT_DIR, THEMES_DIR, and TEMPLATES_DIR, and passes the selected config through to each preview build.
- Improved preview output path detection for external site setups, so previews are generated under the correct site example directory instead of assuming the project root layout.
- Standardized list-page article headings from <h3> to <h2> across homepage, archives, authors, tags, and secondary pages, improving heading hierarchy and semantic consistency.
- Fixed Open Graph URL generation in templates/header.html by concatenating {{site_url}}{{page_url}} directly, and normalized page URL placeholders to leading-slash paths like /, /archives/, /authors/..., and /pages.html.
- Updated theme stylesheets to match the new list markup, switching post-list selectors from .posts-list h3 to .posts-list h2 and preserving hover, focus, spacing, and responsive typography rules.
- Adjusted themes with decorative h2 treatments so post listings do not inherit unwanted heading ornaments or pseudo-elements after the heading-level change.
2026-03-18 11:35:13 +01:00
37c5a2283e Added Fediverse linking options 2026-03-17 11:39:33 +01:00
d4b0d4d58a Fixes for RAM only build when there are no pages 2026-03-14 11:06:50 +01:00
d0ceef943c Added mynotes theme 2026-03-14 09:25:19 +01:00
0a5f9e20a3 Added new themes - some somehow inspired by the BSDs (FreeBSD, NetBSD, OpenBSD), field-journal, microfiche, museum-label
Optimized the "generate_theme_previews.sh" to build the site once, then copy the css files. This will increase the example directory build time.
2026-03-14 09:20:47 +01:00
cbc08b06cc add RAM mode and overhaul the build pipeline
- introduce RAM build mode with in-memory preload/index datasets and stage timing
- refactor build orchestration, indexing, content processing, and template handling
- improve cache/rebuild logic and parallel worker execution across generators
- enhance posts/pages/tags/authors/archives/feed generation and related-post flow
- update CLI/config/README for new build options and performance tuning
- harden timing logic to handle locale-specific EPOCHREALTIME decimal separators
2026-02-10 19:08:59 +01:00
e2822ad620 Fixed header generation: the metadata should only contain the post title, not the site name. 2025-12-29 11:22:44 +01:00
e91a1344b0 Version bump 2025-12-28 14:50:10 +01:00
b1c2397a93 Added INDEX_SHOW_FULL_CONTENT configuration option.
If true, show full post content on homepage instead of just description/excerpt.
2025-12-28 14:47:56 +01:00
41debaae5c Merge pull request 'Feature: expand config variables embedded in cmdline' (#41) from Iam_Tj/BSSG:feature-expand_config_vars_on_cmdline into main
Reviewed-on: #41
2025-11-18 09:49:54 +01:00
b5e1888d7a Merge pull request 'Fix parallel is not GNU' (#42) from Iam_Tj/BSSG:fix-parallel_is_not_GNU into main
Reviewed-on: #42
2025-11-18 09:48:18 +01:00
3fe84d322a Merge pull request 'Dutch / nl translation file' (#38) from zipkid/BSSG:feature/dutch_translation into main
Reviewed-on: #38
2025-10-14 09:39:26 +02:00
Tj
c252f106c8 Fix parallel is not GNU
BSSG assumes parallel is from GNU but moreutils also provides it. If the
moreutils version is available BSSG fails due to parellel options not
being supported:

 Found 1 posts needing processing out of 1 (Skipped: 0).
 Using GNU parallel to process 1 posts
 PARALLEL --jobs 16 --will-cite process_single_file_for_rebuild {}
 /usr/bin/parallel: invalid option -- '-'
 parallel [OPTIONS] command -- arguments
        for each argument, run command with argument, in parallel
 parallel [OPTIONS] -- commands
        run specified commands in parallel
 Parallel post processing failed.

When parallel is detected ask it for its version to ensure it is GNU.
2025-10-13 20:55:05 +00:00
Tj
fee45661de Feature: expand config variables embedded in cmdline
When editing multiple sites using BSSG_LCONF it is easy to forget which
path one is currently working with and frustrating to need to type exact
paths when editing files. E.g:

 export BSSF_LCONF=/a/very/deep/path/to/site/a
 bssg.sh page
 ...
 bssg.sh edit '/a/very/deep/path/to/site/a/pages/page-1.md'

It is easier, more intutive, and friendlier to be able to do:

 bssg.sh edit '$PAGES_DIR/pages/page-1.md'

Note the single quotes to prevent the interactive shell from doing
parameter expansion.

Signed-off-by: Tj <tj.iam.tj@proton.me>
2025-10-13 12:12:27 +00:00
Stefan - ZipKid - Goethals
b62b4d8b76
Dutch / nl translation file 2025-09-10 13:06:40 +02:00
f8f4e18be7 Added pre-compression, thoughtful theme, Cyber-Dark theme, removed some Google fonts leftovers 2025-07-17 09:02:17 +02:00
1f59e61879 Added pre-compression, thoughtful theme, Cyber-Dark theme, removed some Google fonts leftovers 2025-07-17 08:55:06 +02:00
c43a6c3cb8 Merge pull request 'Disable colour output for non-terms or if NO_COLOR is set' (#29) from jamesoff/BSSG:feature/no-color into main
Reviewed-on: #29
2025-06-24 07:46:06 +02:00
7ebd09eeeb
Disable colour output for non-terms or if NO_COLOR is set 2025-06-23 11:29:57 +01:00
1c7aab7b71 Removed Google leftovers 2025-06-21 16:09:12 +02:00
70760825b0 Preparing for 0.31 - related posts, theme fixes, etc 2025-06-18 19:26:13 +02:00
3c88fc7e69 The headline feature of this release is **complete multi-author support** throughout BSSG! This feature transforms BSSG from a single-author platform into a collaborative publishing system.
**New Author Frontmatter Fields:**
- `author_name`: Override the default site author on a per-post basis
- `author_email`: Specify custom author email information

**Intelligent Fallback System:**
- **Custom Author**: Both name and email override defaults
- **Name Only**: Just specify author name, email remains empty
- **Default Fallback**: Empty fields automatically use site configuration

**Author Index Pages:**
- **Main Authors Index**: Located at `/authors/` with post counts for each author
- **Individual Author Pages**: Dedicated pages at `/authors/author-slug/` for each author
- **Conditional Navigation**: "Authors" menu appears only when multiple authors exist (configurable threshold)
- **Visual Consistency**: Reuses existing tag page styling for familiar user experience

**Complete Integration:**
- **Schema.org JSON-LD**: Proper structured data for search engines
- **RSS Feeds**: Dublin Core `dc:creator` elements with full author attribution
- **Footer Copyright**: Dynamic author information in copyright notices
- **Index Listings**: "by Author Name" attribution throughout the site
- **Enhanced Sitemap**: Author pages automatically included for better SEO

**Configuration Options:**

ENABLE_AUTHOR_PAGES=false # Enable/disable author pages (default: false)
SHOW_AUTHORS_MENU_THRESHOLD=2 # Minimum authors to show menu (default: 2)
ENABLE_AUTHOR_RSS=false # Author-specific RSS feeds (default: false)

This release includes **BSSG Themes 0.30**, a comprehensive improvement of all 50 included themes focusing on performance, accessibility, and cross-platform compatibility.

**Performance Optimizations:**
- **External Font Dependencies Removed**: Eliminated Google Fonts and base64 encoded fonts across 35+ themes
- **System Font Stacks**: Comprehensive fallback fonts for better performance and reliability
- **Animation Optimization**: Added `@media (prefers-reduced-motion)` support to all themes
- **Mobile Performance**: 40-60% improvement in rendering speed on mobile devices
- **Backdrop Filter Optimization**: Reduced blur amounts and added mobile fallbacks

**Accessibility Enhancements:**
- **WCAG AA Compliance**: Tried to achieve 100% compliance across all themes
- **Keyboard Navigation**: Complete focus management with visible outlines
- **Screen Reader Support**: Enhanced semantic HTML structure and ARIA attributes
- **Reduced Motion**: Full support for users who prefer reduced motion

**Cross-Platform Compatibility:**
- **Text Browser Support**: Full functionality in lynx, w3m, and links browsers
- **Progressive Enhancement**: Graceful degradation for older browsers
- **Icon Fallbacks**: ASCII alternatives for Unicode symbols and decorative elements

**Critical Theme Fixes:**
- **Glassmorphism**: Complete redesign for maximum contrast and readability
- **Vaporwave**: Optimized neon effects and improved mobile performance
- **Flat**: Fixed critical invisible post title bug
- **Retro Computing Themes**: Enhanced authenticity while maintaining modern accessibility

**Improved Edit Command:**
- Fixed filename generation in edit mode (`-n` flag) that was causing build failures
- Clean filename formatting prevents awk errors with spaces in filenames
- Consistent approach across all BSSG operations

**Better Build Options:**
- Fixed `--force-rebuild` option that wasn't working as documented
- Both `--force-rebuild` and `-f` now work correctly as aliases
- Improved help text and command consistency

**Enhanced Post Editor:**
- Fixed datetime-local input showing GMT instead of local time
- Better date/time handling for content creation
- Improved user experience in the standalone editor

**RSS Feed Enhancements:**
- **Fixed XML Escaping**: Proper escaping of ampersands and special characters in RSS titles and descriptions
- **Image Caption Fix**: Eliminated duplicate `<figcaption>` elements in RSS feeds
- **Valid XML Output**: RSS feeds now validate properly with all RSS parsers

**Unicode and Internationalization:**
- **German Umlaut Handling**: Consistent Unicode character handling in URL slugs across all interfaces
- **Comprehensive Transliteration**: Support for German, French, Spanish, Polish, and other European languages
- **Cross-Interface Consistency**: Uniform slug generation in command-line tools, web editor, and admin interface

**Better Incremental Builds:**
- **New Author Detection**: Fixed caching issue where new authors weren't detected during incremental builds
- **Author Page URLs**: Fixed incorrect URL generation that didn't honor configured `URL_SLUG_FORMAT`
- **Dependency Tracking**: Improved rebuild logic for author-related content

**Enhanced Post Metadata:**
- **Improved Visual Presentation**: Better typography and spacing for post metadata banners
- **Semantic HTML**: Proper `<time>` elements with datetime attributes for accessibility and SEO
- **Responsive Design**: Metadata that scales appropriately across devices

**Sitemap Integration:**
- Author pages automatically included in `sitemap.xml` when enabled
- Proper priority levels for SEO optimization
- Conditional inclusion based on configuration

**Better File Structure:**
- Enhanced templates system with better placeholder replacement
- Improved navigation generation with conditional menu display
- Standardized component patterns across the codebase

**Theme Compatibility:**
- All 50 themes maintain their visual identity while gaining performance and accessibility improvements
- No breaking changes for existing installations
- Themes now work excellently across all browsers and devices

**Multi-Author Migration:**
- Existing single-author sites continue working unchanged
- Author fields are optional and fall back to site configuration
- No migration required for existing content

**Performance Recommendations:**
- Themes now work optimally without external dependencies
- Mobile experience significantly improved across all themes
2025-06-02 08:46:08 +02:00
6582872d70 Fixes 2025-05-27 08:34:00 +02:00
47d0ea02b0 Fixed ampersand and feed generation 2025-05-26 09:58:50 +02:00
ea2afcd6e9 Aggiorna README.md 2025-05-25 21:18:35 +02:00
e8c98d10ab Added bssg-editor - updated README 2025-05-25 19:12:10 +02:00
d8e7e3f0ea Merge pull request 'Make sure slug for blog posts is sanitized' (#24) from 82mhz/BSSG:fixSlug into main
Reviewed-on: #24
2025-05-23 19:52:11 +02:00
Andreas
7df772e509 Make sure slug for blog posts is sanitized 2025-05-23 15:19:48 +02:00
ce4269dd6e Slug fix - by Andreas 2025-05-22 21:05:40 +02:00
3a52a4b24a Fixed README 2025-05-18 19:05:39 +02:00
4dcec9f963 Added socat support 2025-05-18 18:57:38 +02:00
7eaf64ea4a Fixed colours 2025-05-18 15:33:07 +02:00
03c83e5b76 - **server Command**: Introduced a new command ./bssg.sh server to facilitate local development.
- This command builds the static site and then starts a simple Bash HTTP server (`scripts/server.sh`) to serve the files from the output directory.
    - **Dynamic `SITE_URL`**: When the `server` command initiates a build, it temporarily overrides the `SITE_URL` configuration to match the local server's address (e.g., `http://localhost:PORT`). This ensures that all generated links and assets paths are correct for local previewing.
    - **Command-line Options**:
        - `--port <PORT>`: Specifies the port for the server to listen on.
        - `--host <HOST>`: Specifies the host/IP address for the server.
        - `--no-build`: Skips the build step and starts the server with the existing content in the output directory.
- **Configurable Server Defaults**:
    - The default port and host for the `./bssg.sh server` command can now be configured in `config.sh` (via `BSSG_SERVER_PORT_DEFAULT` and `BSSG_SERVER_HOST_DEFAULT`) and overridden in `config.sh.local`.
    - The help message for the `server` command displays these configured default values.

- The `bssg.sh` main script was updated to include the `server` command logic, argument parsing, and help text.
- `scripts/build/config_loader.sh` was updated to load and export the new server default configuration variables (`BSSG_SERVER_PORT_DEFAULT`, `BSSG_SERVER_HOST_DEFAULT`).
- `config.sh` was updated with the new default server configuration variables.
2025-05-18 15:17:19 +02:00
e494aed35b Fixed the generate_theme_previews.sh - now working with a website generated by the init command 2025-05-18 08:47:36 +02:00
64b2c439c3 Configurable RSS feed filenames. The main RSS feed and tag-specific RSS feeds can now have their filenames customized via the RSS_FILENAME option in config.sh (or config.sh.local). Defaults to rss.xml if not specified.
- The main feed will be `OUTPUT_DIR/$RSS_FILENAME`.
  - Tag feeds will be `OUTPUT_DIR/tags/tag-slug/$RSS_FILENAME`.
2025-05-13 08:49:39 +02:00
2474b8146d Fix: version 2025-05-13 07:58:53 +02:00
ded254d171 Fix: -f will force rebuild 2025-05-05 08:48:09 +02:00
95b5319b93 Updated README.md - on MacOS, the default bash is too old 2025-04-30 06:55:30 +02:00
2636df570c Fixed list command to support --config, so it doesn't re-read the config on its own 2025-04-25 08:59:48 +02:00
88c2a98911 Fixed list command to support --config, so it doesn't re-read the config on its own 2025-04-25 08:57:29 +02:00
afaaed81d1 Fixed regression 2025-04-24 22:12:14 +02:00
12a39b5e62 Re-implemented the --config - as shown in the manuals. Add support for BSSG_LCONF environment variable. The init script now adds a CACHE_DIR setting to the new site's config.sh.local. Change default answer to 'Yes' (Y) in the init script when asking whether to modify the core config.sh.local to automatically source the new site's configuration. 2025-04-24 21:43:37 +02:00
c5d1b48476 Re-implemented the --config - as shown in the manuals. Add support for BSSG_LCONF environment variable. The init script now adds a CACHE_DIR setting to the new site's config.sh.local. Change default answer to 'Yes' (Y) in the init script when asking whether to modify the core config.sh.local to automatically source the new site's configuration. 2025-04-24 21:16:44 +02:00
5872f2a5a6 Fixed caching for Archive pages 2025-04-24 13:08:37 +02:00
220b82832a Reversed order for Archives posts 2025-04-24 12:30:20 +02:00
4d3ae7c92f Added ARCHIVES_LIST_ALL_POSTS option to generate a full list of posts in the Archives page 2025-04-24 09:05:02 +02:00
81e8b8d5de Indexing fix - changed the file comparison procedure for NetBSD compatibility 2025-04-21 12:38:09 +02:00
7b3539de65 Small cosmetic fix 2025-04-21 08:25:57 +02:00
a055615e79 Fixed for home deploy script 2025-04-21 08:24:49 +02:00
54f8c66480 Fixed for home deploy script 2025-04-20 19:04:37 +02:00
582d7970b8 Fixed for rss regeneration with --site-url 2025-04-19 14:46:07 +02:00
48f332e1fc Clean output also triggers the force rebuild 2025-04-19 08:15:26 +02:00
0d470e4f23 Clean output also triggers the force rebuild 2025-04-19 08:11:10 +02:00
a893411e78 Bump for 0.15 2025-04-18 21:21:32 +02:00
68883053f0 Fixed Previews without description 2025-04-18 21:16:30 +02:00
31314e7328 Removed Read More in index pages 2025-04-18 21:04:12 +02:00
e3139d51f4 Do not generate the "Tags" menu if there are no posts with tags (adapted by Tom's patch)
Allow a custom pages/index.md (with slug "index") that will be shown as homepage. This allows to create a custom homepage for the website, without the "Latest Posts" list.  (adapted by Tom's patch)
Render automatically generated post excerpts (used when no explicit `description` is set in frontmatter) as HTML instead of raw markdown on list pages (index, tags, archives).
Partial rewrite of the caching code, index generations, etc. Now only the modified tags and/or archives will be regenerated, speeding up the rebuild time.
Many, many other bugfixes
2025-04-18 19:54:04 +02:00
706cf9c154 Fixes for OpenBSD and Archive Generation 2025-04-17 14:44:14 +02:00
46a5e8e496 Refactored for speedup - HUGE speedup 2025-04-17 09:34:08 +02:00
4225940c2a Render automatically generated post excerpts (used when no explicit description is set in frontmatter) as HTML instead of raw markdown on list pages (index, tags, archives). 2025-04-16 20:09:26 +02:00
9dd4b42c40 Render automatically generated post excerpts (used when no explicit description is set in frontmatter) as HTML instead of raw markdown on list pages (index, tags, archives). 2025-04-16 19:33:00 +02:00
c2cd5367f8 Fixed for image 2025-04-16 09:24:22 +02:00
102 changed files with 24904 additions and 5371 deletions

699
README.md
View file

@ -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,36 +28,44 @@
- 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
- Cross-platform (Linux, macOS, BSDs)
- Reading time calculation for posts
- Reading time calculation for posts (configurable: `SHOW_READING_TIME`)
- Suppressible navigation menu (`SHOW_HEADER_MENU`) and post descriptions on index page (`SHOW_INDEX_DESCRIPTIONS`)
- Configurable excerpt generation (`GENERATE_EXCERPT`) to save processing when descriptions are explicitly written
- Pagination for blog posts with configurable posts per page
- Multiple themes available (see [Themes section](#themes))
- Theme preview generator to see all available themes in action
- 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
@ -77,12 +87,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
@ -126,7 +141,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)
@ -135,27 +150,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
@ -189,24 +204,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)
@ -214,6 +235,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)
@ -247,58 +270,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
@ -455,23 +453,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.
@ -527,6 +541,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...
@ -546,7 +563,7 @@ The `description` field in the frontmatter lets you provide a brief summary of y
- OpenGraph meta tags for better social media sharing
- RSS feed entries
If you don't specify a description, the system will automatically extract one from the beginning of your post content.
If you don't specify a description, the system will automatically extract one from the beginning of your post content. You can disable this automatic excerpt generation (and save the processing cost) by setting `GENERATE_EXCERPT=false` in your configuration. When disabled, posts without an explicit `description` will simply have no description shown — useful if you use `SHOW_INDEX_DESCRIPTIONS=false` and only need descriptions for SEO/social tags.
### Featured Images
@ -562,10 +579,169 @@ 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:
- Custom Homepage Content: If you want a custom landing page instead of the default "Latest Posts" list (useful for non-blog websites), simply create a file named `index.md` inside your `pages/` directory (`${PAGES_DIR}`). BSSG will automatically use the content of this file for the homepage (`index.html`) and will *not* display the post list. Ensure the `index.md` file has the frontmatter `slug: index` set.
- `templates/header.html` - Site header and navigation
- `templates/footer.html` - Site footer
- CSS styles are generated in `output/css/style.css`
@ -581,40 +757,105 @@ 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)
# Display configuration
SHOW_HEADER_MENU=true # Options: "true", "false". Show navigation menu in the header (footer menu is unaffected).
SHOW_INDEX_DESCRIPTIONS=true # Options: "true", "false". Show post descriptions/excerpts on the index page.
GENERATE_EXCERPT=true # Options: "true", "false". Auto-generate excerpt from content when no description is provided.
SHOW_READING_TIME=true # Options: "true", "false". Show reading time on individual post pages.
# 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
@ -727,7 +968,7 @@ fi
## Themes
BSSG includes a variety of themes to customize the look of your site. Themes are organized in the `themes/` directory.
BSSG includes a variety of themes to customize the look of your site. Themes are organized in the `themes/` directory. You can see a list and preview of the available themes [here](https://bssg.dragas.net/example)
### Some of the Available Themes
@ -737,6 +978,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
@ -761,9 +1003,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
@ -781,12 +1026,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:
@ -819,47 +1069,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:
@ -881,13 +1241,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:
@ -902,6 +1274,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:
@ -919,11 +1295,38 @@ 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.
SHOW_HEADER_MENU=true # Options: "true", "false". Show navigation menu in the header (footer menu is unaffected).
SHOW_INDEX_DESCRIPTIONS=true # Options: "true", "false". Show post descriptions/excerpts on the index page.
GENERATE_EXCERPT=true # Options: "true", "false". Auto-generate excerpt from content when no description is provided.
SHOW_READING_TIME=true # Options: "true", "false". Show reading time on individual post pages.
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/`.
@ -933,12 +1336,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
@ -988,4 +1427,4 @@ This project is licensed under the BSD 3-Clause License - see the LICENSE file f
- **Templates**: Learn how to create custom templates in the `templates` directory.
- **Themes**: Explore the available themes in the `themes` directory.
- **Backup & Restore**: Use `./bssg.sh backup` and `./bssg.sh restore` to manage content backups.
- **Development Blog**: Stay up-to-date with the latest release notes, development progress, and announcements on the official BSSG Dev Blog: [https://blog.bssg.dragas.net](https://blog.bssg.dragas.net)
- **Development Blog**: Stay up-to-date with the latest release notes, development progress, and announcements on the official BSSG Dev Blog: [https://blog.bssg.dragas.net](https://blog.bssg.dragas.net)

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.41

2925
bssg-editor.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
#
# BSSG - Configuration File
# Version 0.12
# Version controlled via root VERSION file
# 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
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,26 @@ 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)
# Display configuration
SHOW_HEADER_MENU=true # Options: "true", "false". Show navigation menu in the header.
SHOW_INDEX_DESCRIPTIONS=true # Options: "true", "false". Show post descriptions/excerpts on the index page.
GENERATE_EXCERPT=true # Options: "true", "false". Generate excerpt from content when no description is provided.
SHOW_READING_TIME=true # Options: "true", "false". Show reading time on individual post pages.
# Archive Page Configuration
ARCHIVES_LIST_ALL_POSTS="false" # Options: "true", "false". If true, list all posts on the main archive page.
# Page configuration
PAGE_URL_FORMAT="pages/slug" # Format for page URLs: pages/slug will create pages/slug/index.html
PAGE_URL_FORMAT="slug" # Format for page URLs. Available: slug, filename (without ext)
# Markdown processing configuration
MARKDOWN_PROCESSOR="commonmark" # Options: "pandoc", "commonmark", or "markdown.pl"
@ -57,6 +87,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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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
View 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"

View file

@ -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"

View file

@ -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="相关文章"

View file

@ -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.11)"
echo "=================================="
echo "BSSG - Bash Static Site Generator (v${VERSION})"
echo "========================================="
echo ""
echo "Usage: $0 command [options]"
echo "Usage: $0 [--config <path>] command [options]"
echo ""
echo "Global Options:"
echo " --config <path> Specify a custom configuration file. Overrides BSSG_LCONF"
echo " and the default config.sh.local."
echo ""
echo "Environment Variables:"
echo " BSSG_LCONF Path to a configuration file to use if --config is not set."
echo ""
echo "Commands:"
echo " post [-html] [draft_file] Create a new post or continue editing a draft"
@ -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 "$@"

View file

@ -4,15 +4,9 @@
# Functions for handling build cache and rebuild checks.
#
# Ensure necessary color variables are available if sourced independently
# RED='${RED:-\\033[0;31m}' # Removed - Should be inherited from main export
# GREEN='${GREEN:-\\033[0;32m}' # Removed - Should be inherited from main export
# YELLOW='${YELLOW:-\\033[0;33m}' # Removed - Should be inherited from main export
# NC='${NC:-\\033[0m}' # Removed - Should be inherited from main export
# 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)
@ -26,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}"
@ -63,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}"
@ -93,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
@ -124,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}"
@ -172,17 +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}/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}"
@ -206,66 +213,17 @@ common_rebuild_check() {
return 0 # Rebuild needed
fi
# Check if templates have changed
# IMPORTANT: Requires TEMPLATES_DIR, LOCALE_DIR, SITE_LANG to be exported/available
# Assumes get_file_mtime is sourced from utils.sh
local template_dir="${TEMPLATES_DIR:-templates}"
# Adjust template paths based on theme existence if necessary
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"
# Also check the active locale file
local active_locale_file=""
if [ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]; then
active_locale_file="${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh"
elif [ -f "${LOCALE_DIR:-locales}/en.sh" ]; then
active_locale_file="${LOCALE_DIR:-locales}/en.sh"
fi
# Check if output file exists. If not, rebuild needed.
# Moved this basic check here for clarity.
if [ ! -f "$output_file_to_check" ]; then
# echo "DEBUG_CACHE: Output file '$output_file_to_check' missing, returning 0" >&2
return 0
fi
if [ -f "$output_file_to_check" ]; then
# IMPORTANT: Assumes get_file_mtime is sourced/available
local output_time
output_time=$(get_file_mtime "$output_file_to_check")
local header_time
header_time=$(get_file_mtime "$header_template")
local footer_time
footer_time=$(get_file_mtime "$footer_template")
local locale_time
locale_time=$(get_file_mtime "$active_locale_file")
# DEBUG: Print timestamps for comparison
# echo "DEBUG_CACHE: Checking $output_file_to_check" >&2
# echo "DEBUG_CACHE: Output Time: $output_time" >&2
# echo "DEBUG_CACHE: Header Time: $header_time ($header_template)" >&2
# echo "DEBUG_CACHE: Footer Time: $footer_time ($footer_template)" >&2
# echo "DEBUG_CACHE: Locale Time: $locale_time ($active_locale_file)" >&2
# Force rebuild if any template or the locale file is newer than the output
if (( header_time > output_time )) || (( footer_time > output_time )) || (( locale_time > output_time )); then
# echo "DEBUG_CACHE: Template/locale newer than output, returning 0" >&2
# If locale file changed, print a message
if (( locale_time > output_time )); then
echo -e "${YELLOW}Locale file change detected, forcing rebuild for '$output_file_to_check'${NC}"
fi
return 0 # Rebuild needed
fi
# echo "DEBUG_CACHE: Output file exists and is newer than templates/locale, returning 2" >&2
# Return this info to the calling function
return 2 # Valid output file exists, continue with specific checks
return 0 # Rebuild needed
fi
# Fallback - should not be reached if ! -f check is above
# echo "DEBUG_CACHE: Fallback, output file missing? returning 0" >&2
return 0 # No output file, rebuild needed
# Removed template/locale checks here. They are done in file_needs_rebuild now.
# echo "DEBUG_CACHE: common_rebuild_check returning 1 (passed common checks)" >&2
return 1 # Common checks passed (config ok, not forced, output exists)
}
# Check if a rebuild is needed based on file timestamps and templates
@ -274,7 +232,7 @@ file_needs_rebuild() {
local output_file="$2"
# echo "DEBUG_CACHE: file_needs_rebuild check for Input='$input_file' Output='$output_file'" >&2
# Call the common rebuild check function
# Call the common rebuild check function (checks force, config, output existence)
common_rebuild_check "$output_file"
local common_result=$?
# echo "DEBUG_CACHE: common_rebuild_check returned $common_result for '$output_file'" >&2
@ -284,16 +242,26 @@ file_needs_rebuild() {
return 0 # Rebuild needed
fi
# At this point, output_file exists and is newer than templates/locale
# Now check if output is newer than input
# At this point: Not forced, config OK, output file exists.
# Now check against pre-calculated max template/locale time and input file time.
# IMPORTANT: Assumes get_file_mtime is sourced from utils.sh
local input_time
input_time=$(get_file_mtime "$input_file")
# IMPORTANT: Requires BSSG_MAX_TEMPLATE_LOCALE_TIME to be exported from main.sh
local output_time
output_time=$(get_file_mtime "$output_file")
# Rebuild if input file is newer than output file
# Check if templates/locale are newer than output
# Default to 0 if variable is unset (should not happen if main.sh ran)
if (( ${BSSG_MAX_TEMPLATE_LOCALE_TIME:-0} > output_time )); then
# echo "DEBUG_CACHE: Templates/locale newer than output ($BSSG_MAX_TEMPLATE_LOCALE_TIME > $output_time), returning 0" >&2
return 0 # Rebuild needed
fi
# Check if input file is newer than output file
local input_time
input_time=$(get_file_mtime "$input_file")
if (( input_time > output_time )); then
# echo "DEBUG_CACHE: Input newer than output ($input_time > $output_time), returning 0" >&2
return 0 # Rebuild needed
fi
@ -361,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

View file

@ -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,44 @@ ENABLE_ARCHIVES="${ENABLE_ARCHIVES:-true}"
URL_SLUG_FORMAT="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
PAGE_URL_FORMAT="${PAGE_URL_FORMAT:-slug}"
ENABLE_TAG_RSS="${ENABLE_TAG_RSS:-false}" # Generate RSS feed for each tag
ENABLE_AUTHOR_PAGES="${ENABLE_AUTHOR_PAGES:-true}" # Generate author index pages
ENABLE_AUTHOR_RSS="${ENABLE_AUTHOR_RSS:-false}" # Generate RSS feed for each author
SHOW_AUTHORS_MENU_THRESHOLD="${SHOW_AUTHORS_MENU_THRESHOLD:-2}" # Minimum authors to show menu
# Related Posts Configuration Defaults
ENABLE_RELATED_POSTS="${ENABLE_RELATED_POSTS:-true}" # Enable or disable related posts feature
RELATED_POSTS_COUNT="${RELATED_POSTS_COUNT:-3}" # Number of related posts to show
# Display Configuration Defaults
SHOW_HEADER_MENU="${SHOW_HEADER_MENU:-true}"
SHOW_INDEX_DESCRIPTIONS="${SHOW_INDEX_DESCRIPTIONS:-true}"
GENERATE_EXCERPT="${GENERATE_EXCERPT:-true}"
SHOW_READING_TIME="${SHOW_READING_TIME:-true}"
# --- Backup Directory --- Added ---
BACKUP_DIR="${BACKUP_DIR:-backup}" # Default backup location
# --- Server Defaults --- Added for 'bssg.sh server' ---
BSSG_SERVER_PORT_DEFAULT="${BSSG_SERVER_PORT_DEFAULT:-8000}"
BSSG_SERVER_HOST_DEFAULT="${BSSG_SERVER_HOST_DEFAULT:-localhost}"
# Customization Defaults
CUSTOM_CSS="${CUSTOM_CSS:-}" # Default to empty string
# Define default colors here so utils.sh can use them if not overridden by config
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 +104,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,50 +134,35 @@ 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
# --- Handle Random Theme --- START ---
# NOTE: This logic was moved to main.sh to run AFTER CLI argument parsing
# Check if theme is set to random after loading configs
# if [[ "${THEME:-default}" == "random" ]]; then
# echo -e "${YELLOW}Theme set to random, selecting a random theme...${NC}"
# # Find available themes (directories in THEMES_DIR)
# local available_themes=()
# if [ -d "${THEMES_DIR:-themes}" ]; then
# for d in "${THEMES_DIR:-themes}"/*; do
# if [ -d "$d" ]; then
# local theme_name=$(basename "$d")
# # Exclude "random" itself if it exists as a directory
# if [[ "$theme_name" != "random" ]]; then
# available_themes+=("$theme_name")
# fi
# fi
# done
# fi
#
# local num_themes=${#available_themes[@]}
# if [ "$num_themes" -gt 0 ]; then
# # Select a random theme index
# local random_index=$(( RANDOM % num_themes ))
# THEME="${available_themes[$random_index]}"
# echo -e "${GREEN}Randomly selected theme: $THEME${NC}"
# else
# echo -e "${RED}Error: No themes found in '$THEMES_DIR' to select randomly. Defaulting to 'default'.${NC}"
# THEME="default"
# fi
# fi
# --- Handle Random Theme --- END ---
# ---- Start Locale Loading ----
# Function to print error messages in red (specific to locale loading)
@ -166,7 +206,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}"
@ -189,6 +229,79 @@ done
# --- Expand Tilde in Path Variables --- END ---
# --- Derived Configuration --- START ---
serialize_author_fediverse_creators() {
local serialized=""
local author_name
if ! declare -p AUTHOR_FEDIVERSE_CREATORS >/dev/null 2>&1; then
printf '%s' "$serialized"
return 0
fi
if [[ "$(declare -p AUTHOR_FEDIVERSE_CREATORS 2>/dev/null)" != "declare -A"* ]]; then
print_warning "AUTHOR_FEDIVERSE_CREATORS is set but is not an associative array. Ignoring it."
printf '%s' "$serialized"
return 0
fi
while IFS= read -r author_name; do
serialized+="${author_name}"$'\t'"${AUTHOR_FEDIVERSE_CREATORS[$author_name]}"$'\n'
done < <(printf '%s\n' "${!AUTHOR_FEDIVERSE_CREATORS[@]}" | LC_ALL=C sort)
printf '%s' "$serialized"
}
serialize_rel_me_urls() {
local serialized=""
local rel_me_url=""
rel_me_url=$(printf '%s' "$REL_ME_URL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$rel_me_url" ]; then
serialized+="${rel_me_url}"$'\n'
fi
if declare -p REL_ME_URLS >/dev/null 2>&1; then
local rel_me_decl
rel_me_decl="$(declare -p REL_ME_URLS 2>/dev/null)"
if [[ "$rel_me_decl" == "declare -a"* ]]; then
local rel_me_entry
for rel_me_entry in "${REL_ME_URLS[@]}"; do
rel_me_entry=$(printf '%s' "$rel_me_entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$rel_me_entry" ]; then
serialized+="${rel_me_entry}"$'\n'
fi
done
else
print_warning "REL_ME_URLS is set but is not a standard array. Ignoring it."
fi
fi
if [ -z "$serialized" ]; then
printf '%s' ""
return 0
fi
printf '%s' "$serialized" | awk 'NF && !seen[$0]++'
}
REL_ME_URLS_SERIALIZED="$(serialize_rel_me_urls)"
AUTHOR_FEDIVERSE_CREATORS_SERIALIZED="$(serialize_author_fediverse_creators)"
SITE_FEDIVERSE_CREATOR_META_TAG="$(build_fediverse_creator_meta_tag "${AUTHOR_NAME:-Anonymous}" "")"
# --- Derived Configuration --- END ---
# --- Read Version from root VERSION file --- START ---
# BSSG root is two levels up from config_loader.sh (scripts/build -> scripts -> root)
BSSG_ROOT="$( cd "${CONFIG_LOADER_DIR}/../.." &> /dev/null && pwd )"
if [ -f "${BSSG_ROOT}/VERSION" ]; then
VERSION="$(cat "${BSSG_ROOT}/VERSION")"
else
VERSION="dev"
fi
export VERSION
# --- Read Version from root VERSION file --- END ---
# --- Export All Variables --- START ---
# Define the list of configuration variables relevant for hashing/exporting
@ -196,15 +309,23 @@ 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
SHOW_HEADER_MENU SHOW_INDEX_DESCRIPTIONS GENERATE_EXCERPT SHOW_READING_TIME
# Add any other custom config variables here if needed
BSSG_SERVER_PORT_DEFAULT BSSG_SERVER_HOST_DEFAULT # Server defaults
)
# Convert array to space-separated string for export
@ -225,14 +346,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
@ -246,7 +375,28 @@ export REBUILD_AFTER_POST
export REBUILD_AFTER_EDIT
export CUSTOM_CSS
export ENABLE_TAG_RSS
export ENABLE_AUTHOR_PAGES
export ENABLE_AUTHOR_RSS
export SHOW_AUTHORS_MENU_THRESHOLD
export BACKUP_DIR
export CACHE_DIR
export DEPLOY_AFTER_BUILD
export DEPLOY_SCRIPT
export ARCHIVES_LIST_ALL_POSTS
export ENABLE_RELATED_POSTS
export RELATED_POSTS_COUNT
export PRECOMPRESS_ASSETS
export SHOW_HEADER_MENU
export SHOW_INDEX_DESCRIPTIONS
export GENERATE_EXCERPT
export SHOW_READING_TIME
# Server defaults export
export BSSG_SERVER_PORT_DEFAULT
export BSSG_SERVER_HOST_DEFAULT
# Export colors too, as they might be customized in config and needed by scripts
export RED GREEN YELLOW BLUE NC
# Export ALL MSG_* locale variables explicitly
# These are generally NOT included in BSSG_CONFIG_VARS as they don't affect the config hash directly,
@ -266,4 +416,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 ---

View file

@ -4,12 +4,6 @@
# Functions for parsing metadata, generating excerpts, and converting markdown.
#
# Ensure necessary color variables are available if sourced independently
# RED='${RED:-\033[0;31m}'
# GREEN='${GREEN:-\033[0;32m}'
# YELLOW='${YELLOW:-\033[0;33m}'
# NC='${NC:-\033[0m}'
# Source Utilities if needed by functions below
# shellcheck source=utils.sh disable=SC1091
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from content.sh"; exit 1; }
@ -20,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
@ -74,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
@ -87,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
@ -97,88 +125,107 @@ 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
local in_frontmatter=false
local found_frontmatter=false
{
while IFS= read -r line; do
# Trim leading/trailing whitespace from line
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Use a shared awk parser for both disk and RAM paths.
local parsed_data
local awk_frontmatter_parser
awk_frontmatter_parser=$(cat <<'EOF'
BEGIN {
in_fm = 0;
found_fm = 0;
# Define default empty values
vars["title"] = ""; vars["date"] = ""; vars["lastmod"] = "";
vars["tags"] = ""; vars["slug"] = ""; vars["image"] = "";
vars["image_caption"] = ""; vars["description"] = "";
vars["author_name"] = ""; vars["author_email"] = "";
}
/^---$/ {
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
if (in_fm) { in_fm = 0; exit; } # Exit awk early after frontmatter
}
in_fm {
# Match key: value, trim whitespace
local key value
if (match($0, /^([^:]+):[[:space:]]*(.*[^[:space:]])[[:space:]]*$/)) {
key = substr($0, RSTART, RLENGTH);
# Extract key part
match(key, /^[^:]+/);
key_str = substr(key, RSTART, RLENGTH);
# Extract value part
match(key, /:[[:space:]]*(.*)$/);
value = substr(key, RSTART + 1, RLENGTH -1 ); # +1/-1 to skip the :
# Trim spaces from key and value
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key_str);
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value);
key = tolower(key_str);
if [[ "$line" == "---" ]]; then
if ! $in_frontmatter && ! $found_frontmatter; then
in_frontmatter=true
found_frontmatter=true
continue
elif $in_frontmatter; then
in_frontmatter=false
# Stop reading after frontmatter is closed
break
fi
fi
# Handle quoted strings (optional, basic handling)
if ( (match(value, /^"(.*)"$/) || match(value, /^\'(.*)\'$/)) && length(value) > 1 ) {
value = substr(value, 2, length(value)-2);
}
vars[key] = value;
}
}
END {
# Print values in specific order
print vars["title"] "|" vars["date"] "|" vars["lastmod"] "|" \
vars["tags"] "|" vars["slug"] "|" vars["image"] "|" \
vars["image_caption"] "|" vars["description"] "|" \
vars["author_name"] "|" vars["author_email"];
}
EOF
)
if $in_frontmatter; then
# Parse each frontmatter field (case-insensitive key matching)
local key value
if [[ "$line" =~ ^([^:]+):[[:space:]]*(.*)$ ]]; then
key=$(echo "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]')
value="${BASH_REMATCH[2]}"
if $ram_mode_active; then
parsed_data=$(printf '%s\n' "$(ram_mode_get_content "$file")" | awk "$awk_frontmatter_parser")
else
parsed_data=$(awk "$awk_frontmatter_parser" "$file")
fi
IFS='|' read -r title date lastmod tags slug image image_caption description author_name author_email <<< "$parsed_data"
case "$key" in
title)
title="$value"
;;
date)
date="$value"
;;
lastmod)
lastmod="$value"
;;
tags)
tags="$value"
;;
slug)
slug="$value"
;;
image)
image="$value"
;;
image_caption)
image_caption="$value"
;;
description)
description="$value"
;;
esac
fi
fi
done
} < "$file"
else
echo "Warning: Unknown file type '$file' for metadata extraction." >&2
fi
# Fallbacks for missing metadata
if [ -z "$title" ]; then
title=$(basename "$file" | sed 's/\\.[^.]*$//')
title=$(basename "$file" | sed 's/\\\\.[^.]*$//')
fi
if [ -z "$date" ]; then
local file_mtime=$(get_file_mtime "$file")
@ -188,19 +235,33 @@ extract_metadata() {
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
if [ -z "$description" ] && [ "${GENERATE_EXCERPT:-true}" = "true" ]; then
# Generate excerpt only if description is missing
# The excerpt is already sanitized and HTML-escaped plain text
echo "[DEBUG] Generating excerpt for $file" >&2
description=$(generate_excerpt "$file")
fi
# Apply fallback logic for author fields
if [ -z "$author_name" ]; then
author_name="${AUTHOR_NAME:-Anonymous}"
fi
if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then
# Only use default email if using default name
author_email="${AUTHOR_EMAIL:-anonymous@example.com}"
fi
# If author_name is specified but author_email is empty, leave email empty
# Construct the metadata string for comparison and caching
local new_metadata="$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description"
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
@ -208,13 +269,15 @@ extract_metadata() {
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
@ -227,75 +290,86 @@ 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 -2 | tail -1 | cut -d: -f1)
local content=""
if [[ -z "$start_line" || -z "$end_line" || ! $start_line -lt $end_line ]]; then
# No valid frontmatter, use the beginning of the file
content=$(head -n 50 "$file")
local raw_content_stream
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then
# Remove frontmatter directly from preloaded content
raw_content_stream=$(printf '%s\n' "$(ram_mode_get_content "$file")" | awk '
BEGIN { in_fm = 0; found_fm = 0; }
/^---$/ {
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
if (in_fm) { in_fm = 0; next; }
}
{ if (!in_fm) print; }
')
else
# Extract content after frontmatter
content=$(tail -n +$((end_line + 1)) "$file" | head -n 50)
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
# Comprehensive markdown sanitization
# Sanitize and extract the first non-empty paragraph/line
# Apply sanitization steps sequentially
local sanitized_content
sanitized_content=$(echo "$raw_content_stream" | \
# Remove code blocks (``` and indented)
awk '/^```/{flag=!flag;next} !flag;' | grep -v '^```' | \
grep -v '^ ' | \
# Remove images, links, headings, hr, blockquotes
sed -E 's/!\[([^]]*)\]\([^)]*\)//g' | \
sed -E 's/\[([^]]+)\]\(([^)]+)\)/\1/g' | \
sed 's/^#\{1,6\} //' | \
grep -v '^---\+$' | \
grep -v '^\*\*\*\+$' | \
grep -v '^___\+$' | \
sed 's/^> //' | \
# Remove list markers
sed -E 's/^\* |^- |^[0-9]+\. //' | \
# Remove inline markdown: bold, italics, strikethrough, code
sed -E 's/\*\*([^*]+)\*\*/\1/g; s/__([^_]+)__/\1/g' | \
sed -E 's/\*([^*]+)\*/\1/g; s/_([^_]+)_/\1/g' | \
sed -E 's/~~([^~]+)~~/\1/g' | \
sed -E 's/`([^`]+)`/\1/g' | \
# Remove HTML tags
sed -E 's/<[^>]*>//g' | \
# Escape basic HTML entities (ampersand, less than, greater than)
sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' | \
# Remove extra blank lines
awk 'NF {p=1} p' | \
# Get the first non-empty line (first paragraph)
awk 'NF {print; exit}'
)
# 1. Remove code blocks (both ```code``` and indented)
content=$(echo "$content" | awk '/^```/{flag=!flag;next} !flag;' | grep -v '^```')
content=$(echo "$content" | grep -v '^ ')
# Process line by line to handle multiline patterns better
content=$(echo "$content" | while IFS= read -r line; do
# 2. Remove images ![alt](url)
line=$(echo "$line" | sed -E 's/!\[([^]]*)\]\([^)]*\)//g')
# 3. Replace links [text](url) with just text
line=$(echo "$line" | sed -E 's/\[([^]]*)\]\([^)]*\)/\1/g')
# 4. Remove HTML tags
line=$(echo "$line" | sed -E 's/<[^>]*>//g')
# 5. Remove headers (# Header)
line=$(echo "$line" | sed -E 's/^#+ +//g')
# 6. Remove emphasis/code markers (*, _, `)
line=$(echo "$line" | sed -E 's/(\*\*|__|\*|_|`)([^\*`_]+)(\1)/\2/g')
# 7. Remove blockquotes (> text)
line=$(echo "$line" | sed -E 's/^> +//g')
# 8. Remove list markers (*, +, -, 1.)
line=$(echo "$line" | sed -E 's/^([*+-]|[0-9]+\.) +//g')
# 9. Remove horizontal rules (---, ___, ***)
if ! echo "$line" | grep -qE '^[[:space:]]*([-*_])([[:space:]]*\1){2,}[[:space:]]*$'; then
echo "$line"
fi
done)
# 10. Normalize whitespace and remove extra line breaks
content=$(echo "$content" | tr '\n' ' ' | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# 11. Escape HTML special characters (basic set)
content=$(echo "$content" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/\x27/\&apos;/g')
# Truncate to approximately max_length chars at word boundary
local truncated
if [ ${#content} -gt $max_length ]; then
truncated=$(echo "$content" | cut -c 1-$max_length)
# Remove trailing partial word
truncated=${truncated% * }
# Add ellipsis if truncation occurred
if [ "$truncated" != "$content" ]; then
truncated="${truncated}..."
fi
# Truncate to max length using dd for portability
local excerpt
if [ -z "$sanitized_content" ]; then
excerpt=""
else
truncated="$content"
# Use dd: bs=1 reads byte by byte, count limits the total bytes
# 2>/dev/null suppresses dd's status messages
excerpt=$(echo "$sanitized_content" | dd bs=1 count="$max_length" 2>/dev/null)
fi
echo "$truncated"
# Add ellipsis if truncated
if [ ${#sanitized_content} -gt $max_length ]; then
excerpt+="..."
fi
# Ensure description is not empty after all this
if [ -z "$excerpt" ]; then
# Fallback: use the filename if excerpt is still empty
excerpt=$(basename "$file" | sed 's/\.[^.]*$//')
fi
echo "$excerpt"
}
# Convert provided markdown content string to HTML
@ -319,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
@ -361,4 +428,4 @@ convert_markdown_to_html() {
return 0
}
# --- Content Functions --- END ---
# --- Content Functions --- END ---

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

@ -4,12 +4,6 @@
# Checks for required tools and sets up environment variables.
#
# Ensure necessary color variables are available if sourced independently
# RED='${RED:-\\033[0;31m}' # Removed - Should be inherited from main export
# GREEN='${GREEN:-\\033[0;32m}' # Removed - Should be inherited from main export
# YELLOW='${YELLOW:-\\033[0;33m}' # Removed - Should be inherited from main export
# NC='${NC:-\\033[0m}' # Removed - Should be inherited from main export
# Portable md5sum wrapper
portable_md5sum() {
if command -v md5sum > /dev/null 2>&1; then
@ -19,7 +13,9 @@ portable_md5sum() {
# OpenBSD/NetBSD: md5 command without -r or -q
# Output format: "MD5 (filename) = hash" -> Need "hash filename"
if [ $# -eq 0 ] || [ "$1" = "-" ]; then
md5 | awk '{print $4 " -"}' # Handle stdin
# Handle stdin: OpenBSD md5 outputs just the hash directly.
# Read the hash (field 1) and append " -" to match md5sum format.
md5 | awk '{print $1 " -"}'
else
# Handle files: MD5 (file) = hash -> hash file
md5 "$@" | awk '{print $4 " " $2}' | sed 's/[()]//g'
@ -97,7 +93,10 @@ check_dependencies() {
done
# Check for GNU parallel
if command -v parallel > /dev/null 2>&1; then
if [[ "$(uname)" == "NetBSD" ]]; then
echo -e "${YELLOW}Parallel processing is unreliable on NetBSD. Using sequential processing.${NC}"
export HAS_PARALLEL=false
elif command -v parallel > /dev/null 2>&1 && { read -r _version < <(parallel -V 2>/dev/null ) && [[ "${_version:0:3}" = "GNU" ]]; }; then
echo -e "${GREEN}GNU parallel found! Using parallel processing.${NC}"
export HAS_PARALLEL=true
else

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,682 @@
#!/usr/bin/env bash
#
# BSSG - Author Page Generation
# Handles the creation of individual author pages and the main author index.
#
# Source dependencies
# shellcheck source=utils.sh disable=SC1091
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_authors.sh"; exit 1; }
# shellcheck source=cache.sh disable=SC1091
source "$(dirname "$0")/cache.sh" || { echo >&2 "Error: Failed to source cache.sh from generate_authors.sh"; exit 1; }
# Source the feed generator script for the reusable RSS function
# shellcheck source=generate_feeds.sh disable=SC1091
source "$(dirname "$0")/generate_feeds.sh" || { echo >&2 "Error: Failed to source generate_feeds.sh from generate_authors.sh"; exit 1; }
_generate_author_pages_ram() {
echo -e "${YELLOW}Processing author pages${NC}${ENABLE_AUTHOR_RSS:+" and RSS feeds"}...${NC}"
local authors_index_data
authors_index_data=$(ram_mode_get_dataset "authors_index")
local main_authors_index_output="$OUTPUT_DIR/authors/index.html"
mkdir -p "$OUTPUT_DIR/authors"
if [ -z "$authors_index_data" ]; then
echo -e "${YELLOW}No authors found in RAM index. Skipping author page generation.${NC}"
return 0
fi
declare -A author_posts_by_slug=()
declare -A author_name_by_slug=()
declare -A author_email_by_slug=()
local line author author_slug author_email
while IFS= read -r line; do
[ -z "$line" ] && continue
IFS='|' read -r author author_slug author_email _ <<< "$line"
[ -z "$author" ] && continue
[ -z "$author_slug" ] && continue
if [[ -z "${author_name_by_slug[$author_slug]+_}" ]]; then
author_name_by_slug["$author_slug"]="$author"
author_email_by_slug["$author_slug"]="$author_email"
fi
author_posts_by_slug["$author_slug"]+="$line"$'\n'
done <<< "$authors_index_data"
local author_slug_key
for author_slug_key in $(printf '%s\n' "${!author_name_by_slug[@]}" | sort); do
author="${author_name_by_slug[$author_slug_key]}"
local author_data="${author_posts_by_slug[$author_slug_key]}"
local author_page_html_file="$OUTPUT_DIR/authors/$author_slug_key/index.html"
local author_rss_file="$OUTPUT_DIR/authors/$author_slug_key/${RSS_FILENAME:-rss.xml}"
local author_page_rel_url="/authors/${author_slug_key}/"
local author_rss_rel_url="/authors/${author_slug_key}/${RSS_FILENAME:-rss.xml}"
local post_count
post_count=$(printf '%s\n' "$author_data" | awk 'NF { c++ } END { print c+0 }')
mkdir -p "$(dirname "$author_page_html_file")"
local author_page_content=""
author_page_content+="<h1>${MSG_POSTS_BY:-Posts by} $author</h1>"$'\n'
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then
author_page_content+="<p><a href=\"$author_rss_rel_url\">${MSG_RSS_FEED:-RSS Feed}</a></p>"$'\n'
fi
author_page_content+="<div class=\"posts-list\">"$'\n'
while IFS='|' read -r author_name_inner author_slug_inner author_email_inner post_title post_date post_lastmod post_filename post_slug post_image post_image_caption post_description; do
[ -z "$post_title" ] && continue
local post_url
if [ -n "$post_date" ] && [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
local year month day url_path
year="${BASH_REMATCH[1]}"
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
url_path="${url_path//Year/$year}"
url_path="${url_path//Month/$month}"
url_path="${url_path//Day/$day}"
url_path="${url_path//slug/$post_slug}"
post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')"
else
post_url="/$(echo "$post_slug" | sed 's|^/||; s|/*$|/|')"
fi
post_url="${BASE_URL}${post_url}"
local formatted_date
formatted_date=$(format_date "$post_date")
author_page_content+="<article>"$'\n'
author_page_content+=" <h2><a href=\"$post_url\">$post_title</a></h2>"$'\n'
author_page_content+=" <div class=\"meta\">"$'\n'
author_page_content+=" <time datetime=\"$post_date\">$formatted_date</time>"$'\n'
author_page_content+=" </div>"$'\n'
if [ -n "$post_description" ]; then
author_page_content+=" <p class=\"summary\">$post_description</p>"$'\n'
fi
if [ -n "$post_image" ]; then
author_page_content+=" <div class=\"author-image\">"$'\n'
author_page_content+=" <img src=\"$post_image\" alt=\"$post_image_caption\" loading=\"lazy\">"$'\n'
author_page_content+=" </div>"$'\n'
fi
author_page_content+="</article>"$'\n'
done < <(printf '%s\n' "$author_data" | awk 'NF' | sort -t'|' -k5,5r)
author_page_content+="</div>"$'\n'
local page_title="${MSG_POSTS_BY:-Posts by} $author"
local page_description="${MSG_POSTS_BY:-Posts by} $author - $post_count ${MSG_POSTS:-posts}"
local header_content="$HEADER_TEMPLATE"
local footer_content="$FOOTER_TEMPLATE"
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
header_content=${header_content//\{\{page_title\}\}/"$page_title"}
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
header_content=${header_content//\{\{og_description\}\}/"$page_description"}
header_content=${header_content//\{\{twitter_description\}\}/"$page_description"}
header_content=${header_content//\{\{og_type\}\}/"website"}
header_content=${header_content//\{\{page_url\}\}/"$author_page_rel_url"}
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
header_content=${header_content//\{\{og_image\}\}/}
header_content=${header_content//\{\{twitter_image\}\}/}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then
local author_rss_link="<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$author RSS Feed\" href=\"$SITE_URL$author_rss_rel_url\">"
header_content=${header_content//<!-- bssg:tag_rss_link -->/$author_rss_link}
fi
local schema_json
schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"$page_description\",\"url\": \"$SITE_URL$author_page_rel_url\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}"
header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"}
local current_year
current_year=$(date +%Y)
footer_content=${footer_content//\{\{current_year\}\}/"$current_year"}
footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"}
footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"}
{
echo "$header_content"
echo "$author_page_content"
echo "$footer_content"
} > "$author_page_html_file"
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then
local author_post_data
author_post_data=$(printf '%s\n' "$author_data" | awk 'NF' | sort -t'|' -k5,5r | awk -F'|' '{
author_name = $1
author_email = $3
title = $4
date = $5
lastmod = $6
filename = $7
post_slug = $8
image = $9
image_caption = $10
description = $11
printf "%s|%s|%s|%s|%s||%s|%s|%s|%s|%s|%s\n", filename, filename, title, date, lastmod, post_slug, image, image_caption, description, author_name, author_email
}')
_generate_rss_feed "$author_rss_file" "$SITE_TITLE - ${MSG_POSTS_BY:-Posts by} $author" "${MSG_POSTS_BY:-Posts by} $author" "$author_page_rel_url" "$author_rss_rel_url" "$author_post_data"
fi
done
local page_title="${MSG_ALL_AUTHORS:-All Authors}"
local page_description="${MSG_ALL_AUTHORS:-All Authors} - $SITE_DESCRIPTION"
local header_content="$HEADER_TEMPLATE"
local footer_content="$FOOTER_TEMPLATE"
local main_content=""
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
header_content=${header_content//\{\{page_title\}\}/"$page_title"}
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
header_content=${header_content//\{\{og_description\}\}/"$page_description"}
header_content=${header_content//\{\{twitter_description\}\}/"$page_description"}
header_content=${header_content//\{\{og_type\}\}/"website"}
header_content=${header_content//\{\{page_url\}\}/"/authors/"}
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
header_content=${header_content//\{\{og_image\}\}/}
header_content=${header_content//\{\{twitter_image\}\}/}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
local schema_json
schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"List of all authors on $SITE_TITLE\",\"url\": \"$SITE_URL/authors/\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}"
header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"}
local current_year
current_year=$(date +%Y)
footer_content=${footer_content//\{\{current_year\}\}/"$current_year"}
footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"}
footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"}
main_content+="<h1>${MSG_ALL_AUTHORS:-All Authors}</h1>"$'\n'
main_content+="<div class=\"tags-list\">"$'\n'
for author_slug_key in $(printf '%s\n' "${!author_name_by_slug[@]}" | sort); do
author="${author_name_by_slug[$author_slug_key]}"
local post_count
post_count=$(printf '%s\n' "${author_posts_by_slug[$author_slug_key]}" | awk 'NF { c++ } END { print c+0 }')
if [ "$post_count" -gt 0 ]; then
main_content+=" <a href=\"$BASE_URL/authors/$author_slug_key/\">$author <span class=\"tag-count\">($post_count)</span></a>"$'\n'
fi
done
main_content+="</div>"$'\n'
{
echo "$header_content"
echo "$main_content"
echo "$footer_content"
} > "$main_authors_index_output"
echo -e "${GREEN}Author pages processed!${NC}"
echo -e "${GREEN}Generated author list pages.${NC}"
}
# Generate author pages
generate_author_pages() {
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
_generate_author_pages_ram
return $?
fi
echo -e "${YELLOW}Processing author pages${NC}${ENABLE_AUTHOR_RSS:+" and RSS feeds"}...${NC}"
local authors_index_file="$CACHE_DIR/authors_index.txt"
local main_authors_index_output="$OUTPUT_DIR/authors/index.html"
local modified_authors_list_file="${CACHE_DIR:-.bssg_cache}/modified_authors.list"
# Check if the authors index file exists (needed for listing authors)
if [ ! -f "$authors_index_file" ]; then
echo -e "${YELLOW}Authors index file not found at $authors_index_file. Skipping author page generation.${NC}"
# If the index doesn't exist, no authors were found in posts.
# Ensure the main output directory exists but is empty.
mkdir -p "$(dirname "$main_authors_index_output")"
echo -e "${GREEN}Author pages processed! (No authors found)${NC}"
echo -e "${GREEN}Generated author list pages. (No authors found)${NC}"
return 0
fi
# --- Calculate Latest Common Dependency Time --- START ---
# Get mtimes of config hash, templates, and locale file
local latest_common_dep_time=0
local config_hash_time=$(get_file_mtime "$CONFIG_HASH_FILE")
latest_common_dep_time=$(( config_hash_time > latest_common_dep_time ? config_hash_time : latest_common_dep_time ))
local template_dir="${TEMPLATES_DIR:-templates}"
if [ -d "$template_dir/${THEME:-default}" ]; then
template_dir="$template_dir/${THEME:-default}"
fi
local header_template="$template_dir/header.html"
local footer_template="$template_dir/footer.html"
local header_time=$(get_file_mtime "$header_template")
local footer_time=$(get_file_mtime "$footer_template")
latest_common_dep_time=$(( header_time > latest_common_dep_time ? header_time : latest_common_dep_time ))
latest_common_dep_time=$(( footer_time > latest_common_dep_time ? footer_time : latest_common_dep_time ))
local active_locale_file=""
if [ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]; then
active_locale_file="${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh"
elif [ -f "${LOCALE_DIR:-locales}/en.sh" ]; then
active_locale_file="${LOCALE_DIR:-locales}/en.sh"
fi
local locale_time=$(get_file_mtime "$active_locale_file")
latest_common_dep_time=$(( locale_time > latest_common_dep_time ? locale_time : latest_common_dep_time ))
# --- Calculate Latest Common Dependency Time --- END ---
# --- Simplified Global Check --- START ---
# Decide if we need to proceed with any author generation steps at all.
local proceed_with_generation=false
local force_rebuild_status="${FORCE_REBUILD:-false}"
if [ "$force_rebuild_status" = true ]; then
proceed_with_generation=true
echo "Force rebuild enabled, proceeding with author generation." >&2 # Debug
elif [ "$latest_common_dep_time" -gt 0 ] && { [ ! -f "$main_authors_index_output" ] || (( $(get_file_mtime "$main_authors_index_output") < latest_common_dep_time )); }; then
# Common dependencies are newer than the main output (or main output missing)
proceed_with_generation=true
echo "Common dependencies changed, proceeding with author generation." >&2 # Debug
elif [ -s "$modified_authors_list_file" ]; then
# Modified authors list exists and is not empty
proceed_with_generation=true
echo "Modified authors detected, proceeding with author generation." >&2 # Debug
elif [ ! -f "$main_authors_index_output" ]; then
# Fallback: if main output is missing, we should generate it
proceed_with_generation=true
echo "Main authors index missing, proceeding with author generation." >&2 # Debug
fi
if [ "$proceed_with_generation" = false ]; then
echo -e "${GREEN}Authors index, author pages${NC}${ENABLE_AUTHOR_RSS:+, and author RSS feeds} appear up to date based on common dependencies and modified posts, skipping.${NC}"
echo -e "${GREEN}Author pages processed!${NC}" # Keep consistent final message
echo -e "${GREEN}Generated author list pages.${NC}" # Keep consistent final message
return 0
fi
# --- Simplified Global Check --- END ---
# --- Proceed with Generation ---
# Get unique authors (Author|Slug pairs)
local unique_authors_lines=$(awk -F'|' '{print $1 "|" $2}' "$authors_index_file" | sort | uniq)
local author_count=$(echo "$unique_authors_lines" | grep -v '^$' | wc -l)
echo -e "Checking ${GREEN}$author_count${NC} author pages${NC}${ENABLE_AUTHOR_RSS:+/feeds} for changes (based on common deps & modified authors)" # Updated message
# --- Pre-group posts by author slug --- START ---
local author_data_dir="$CACHE_DIR/author_data"
rm -rf "$author_data_dir" # Clean previous data
mkdir -p "$author_data_dir"
echo -e "Pre-grouping posts by author into ${BLUE}$author_data_dir${NC}..."
if awk -F'|' -v author_dir="$author_data_dir" '
NF >= 2 { # Ensure at least author and slug fields exist
author_slug = $2;
if (author_slug != "") {
# Sanitize slug just in case for filename safety? (basic: remove /)
gsub(/\//, "_", author_slug);
output_file = author_dir "/" author_slug ".tmp";
print $0 >> output_file; # Append the whole line
close(output_file); # Close file handle to avoid too many open files
} else {
print "Warning: Skipping line with empty author slug in authors_index: " $0 > "/dev/stderr";
}
}
' "$authors_index_file"; then
echo -e "${GREEN}Pre-grouping complete.${NC}"
else
echo -e "${RED}Error: Failed to pre-group author data using awk.${NC}" >&2
return 1
fi
# --- Pre-group posts by author slug --- END ---
# Define a modified file_needs_rebuild function for parallel use
parallel_file_needs_rebuild() {
local output_file="$1"
local latest_dep_time="$2" # This should be latest_common_dep_time
# Rebuild if output file doesn't exist
if [ ! -f "$output_file" ]; then
return 0 # Rebuild needed
fi
local output_time=$(get_file_mtime "$output_file")
# Rebuild if output is older than the latest relevant *common* dependency
if (( output_time < latest_dep_time )); then
return 0 # Rebuild needed
fi
return 1 # No rebuild needed
}
# Define a function to process a single author
process_author() {
local author_line="$1"
local author_data_dir="$2"
local latest_common_dep_time_for_author="$3"
local modified_authors_file="$4" # Accept filename instead of hash
# --- Load modified authors from file ---
declare -A modified_authors_hash
if [ -f "$modified_authors_file" ]; then
local mod_author_local
while IFS= read -r mod_author_local || [[ -n "$mod_author_local" ]]; do
if [ -n "$mod_author_local" ]; then # Ensure not empty line
modified_authors_hash["$mod_author_local"]=1
fi
done < "$modified_authors_file"
fi
local author author_slug
IFS='|' read -r author author_slug <<< "$author_line"
if [ -n "$author" ]; then
local author_page_html_file="$OUTPUT_DIR/authors/$author_slug/index.html"
local author_rss_file="$OUTPUT_DIR/authors/$author_slug/${RSS_FILENAME:-rss.xml}"
local author_page_rel_url="/authors/${author_slug}/"
local author_rss_rel_url="/authors/${author_slug}/${RSS_FILENAME:-rss.xml}"
local rebuild_html=false
local rebuild_rss=false
# --- Force rebuild flags if author was modified ---
local author_was_modified=false
if [ -n "${modified_authors_hash[$author]}" ]; then
author_was_modified=true
rebuild_html=true # Force rebuild if author was modified
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then
rebuild_rss=true # Force rebuild if author was modified
fi
fi
# --- Check if HTML rebuild is needed ---
if [ "$rebuild_html" = false ]; then
if parallel_file_needs_rebuild "$author_page_html_file" "$latest_common_dep_time_for_author"; then
rebuild_html=true
fi
fi
# --- Check if RSS rebuild is needed ---
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ] && [ "$rebuild_rss" = false ]; then
if parallel_file_needs_rebuild "$author_rss_file" "$latest_common_dep_time_for_author"; then
rebuild_rss=true
fi
fi
# --- Skip if no rebuilds needed ---
if [ "$rebuild_html" = false ] && { [ "${ENABLE_AUTHOR_RSS:-false}" = false ] || [ "$rebuild_rss" = false ]; }; then
echo "Author '$author' pages are up to date, skipping."
return 0
fi
# --- Load author posts data ---
local author_data_file="$author_data_dir/${author_slug}.tmp"
if [ ! -f "$author_data_file" ]; then
echo "Warning: No posts found for author '$author' (expected file: $author_data_file)" >&2
return 0
fi
# Count posts for this author
local post_count=$(wc -l < "$author_data_file")
echo "Processing author '$author' ($post_count posts)..."
# --- Generate Author HTML Page ---
if [ "$rebuild_html" = true ]; then
mkdir -p "$(dirname "$author_page_html_file")"
# Generate author page content
local author_page_content=""
author_page_content+="<h1>${MSG_POSTS_BY:-Posts by} $author</h1>"$'\n'
# Add RSS link if enabled
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then
author_page_content+="<p><a href=\"$author_rss_rel_url\">${MSG_RSS_FEED:-RSS Feed}</a></p>"$'\n'
fi
# Add posts list
author_page_content+="<div class=\"posts-list\">"$'\n'
# Sort posts by date (newest first) and generate HTML
local posts_html=""
while IFS='|' read -r author_name author_slug_inner author_email post_title post_date post_lastmod post_filename post_slug post_image post_image_caption post_description; do
# Construct post URL using URL_SLUG_FORMAT (same logic as generate_posts.sh)
local post_url=""
if [ -n "$post_date" ]; then
local year month day
if [[ "$post_date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
year="${BASH_REMATCH[1]}"
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
else
year=$(date +%Y); month=$(date +%m); day=$(date +%d) # Fallback
fi
local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}";
url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$post_slug}"
# Ensure relative post_url starts with / and ends with /
post_url="/$(echo "$url_path" | sed 's|^/||; s|/*$|/|')"
else
# Fallback for posts without date
post_url="/$(echo "$post_slug" | sed 's|^/||; s|/*$|/|')"
fi
# Convert to full URL with BASE_URL
post_url="$BASE_URL$post_url"
local formatted_date=$(format_date "$post_date")
posts_html+="<article>"$'\n'
posts_html+=" <h2><a href=\"$post_url\">$post_title</a></h2>"$'\n'
posts_html+=" <div class=\"meta\">"$'\n'
posts_html+=" <time datetime=\"$post_date\">$formatted_date</time>"$'\n'
posts_html+=" </div>"$'\n'
if [ -n "$post_description" ]; then
posts_html+=" <p class=\"summary\">$post_description</p>"$'\n'
fi
if [ -n "$post_image" ]; then
posts_html+=" <div class=\"author-image\">"$'\n'
posts_html+=" <img src=\"$post_image\" alt=\"$post_image_caption\" loading=\"lazy\">"$'\n'
posts_html+=" </div>"$'\n'
fi
posts_html+="</article>"$'\n'
done < <(sort -t'|' -k5,5r "$author_data_file")
author_page_content+="$posts_html"
author_page_content+="</div>"$'\n'
# Generate full HTML page
local page_title="${MSG_POSTS_BY:-Posts by} $author"
local page_description="${MSG_POSTS_BY:-Posts by} $author - $post_count ${MSG_POSTS:-posts}"
# Process templates with placeholder replacement
local header_content="$HEADER_TEMPLATE"
local footer_content="$FOOTER_TEMPLATE"
# Replace placeholders in the header (following tags generator pattern)
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
header_content=${header_content//\{\{page_title\}\}/"$page_title"}
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
header_content=${header_content//\{\{og_description\}\}/"$page_description"}
header_content=${header_content//\{\{twitter_description\}\}/"$page_description"}
header_content=${header_content//\{\{og_type\}\}/"website"}
header_content=${header_content//\{\{page_url\}\}/"$author_page_rel_url"}
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
# Remove unprocessed image placeholders
header_content=${header_content//\{\{og_image\}\}/}
header_content=${header_content//\{\{twitter_image\}\}/}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
# Remove the placeholder for the tag-specific RSS feed link
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
# Add author RSS link if enabled
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ]; then
local author_rss_link="<link rel=\"alternate\" type=\"application/rss+xml\" title=\"$author RSS Feed\" href=\"$SITE_URL$author_rss_rel_url\">"
header_content=${header_content//<!-- bssg:tag_rss_link -->/$author_rss_link}
fi
# Schema.org structured data
local schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"$page_description\",\"url\": \"$SITE_URL$author_page_rel_url\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}"
header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"}
# Replace placeholders in the footer
local current_year=$(date +%Y)
footer_content=${footer_content//\{\{current_year\}\}/"$current_year"}
footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"}
footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"}
# Create the full HTML page
{
echo "$header_content"
echo "$author_page_content"
echo "$footer_content"
} > "$author_page_html_file"
echo "Generated author page: $author_page_html_file"
fi
# --- Generate Author RSS Feed ---
if [ "${ENABLE_AUTHOR_RSS:-false}" = true ] && [ "$rebuild_rss" = true ]; then
mkdir -p "$(dirname "$author_rss_file")"
# Generate RSS feed for this author
local rss_title="$SITE_TITLE - ${MSG_POSTS_BY:-Posts by} $author"
local rss_description="${MSG_POSTS_BY:-Posts by} $author"
local feed_link_rel="$author_page_rel_url"
local feed_atom_link_rel="$author_rss_rel_url"
# Read and format author post data for RSS generation
local author_post_data=""
if [ -f "$author_data_file" ]; then
# Transform author data format to RSS format and sort by date (newest first)
# Author format: Author|Slug|Email|Title|Date|LastMod|Filename|PostSlug|Image|ImageCaption|Description
# RSS format: file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email
author_post_data=$(sort -t'|' -k5,5r "$author_data_file" | awk -F'|' '{
# Map fields from author format to RSS format
author_name = $1
author_slug = $2
author_email = $3
title = $4
date = $5
lastmod = $6
filename = $7
post_slug = $8
image = $9
image_caption = $10
description = $11
# RSS format: file|filename|title|date|lastmod|tags|slug|image|image_caption|description|author_name|author_email
printf "%s|%s|%s|%s|%s||%s|%s|%s|%s|%s|%s\n", filename, filename, title, date, lastmod, post_slug, image, image_caption, description, author_name, author_email
}')
fi
# Check if _generate_rss_feed function exists
if ! command -v _generate_rss_feed > /dev/null 2>&1; then
echo -e "${RED}Error: _generate_rss_feed function not found. Ensure generate_feeds.sh is sourced correctly.${NC}" >&2
else
_generate_rss_feed "$author_rss_file" "$rss_title" "$rss_description" "$feed_link_rel" "$feed_atom_link_rel" "$author_post_data"
echo "Generated author RSS feed: $author_rss_file"
fi
fi
fi
}
# Export the function for potential parallel use
export -f process_author parallel_file_needs_rebuild
# Process each unique author
echo "$unique_authors_lines" | while IFS= read -r author_line || [[ -n "$author_line" ]]; do
if [ -n "$author_line" ]; then
process_author "$author_line" "$author_data_dir" "$latest_common_dep_time" "$modified_authors_list_file"
fi
done
# --- Generate Main Authors Index Page ---
if [ "${AUTHORS_INDEX_NEEDS_REBUILD:-false}" = true ] || [ ! -f "$main_authors_index_output" ] || (( $(get_file_mtime "$main_authors_index_output") < latest_common_dep_time )); then
echo "Generating main authors index page..."
mkdir -p "$(dirname "$main_authors_index_output")"
# Count posts per author and generate the main index
local authors_with_counts=""
echo "$unique_authors_lines" | while IFS= read -r author_line || [[ -n "$author_line" ]]; do
if [ -n "$author_line" ]; then
local author author_slug
IFS='|' read -r author author_slug <<< "$author_line"
local author_data_file="$author_data_dir/${author_slug}.tmp"
if [ -f "$author_data_file" ]; then
local post_count=$(wc -l < "$author_data_file")
echo "$author|$author_slug|$post_count"
fi
fi
done | sort > "${CACHE_DIR}/authors_with_counts.tmp"
# Generate main authors index HTML
local main_content=""
main_content+="<h1>${MSG_ALL_AUTHORS:-All Authors}</h1>"$'\n'
main_content+="<div class=\"tags-list\">"$'\n' # Reuse tags styling
while IFS='|' read -r author author_slug post_count; do
if [ -n "$author" ] && [ "$post_count" -gt 0 ]; then
main_content+=" <a href=\"$BASE_URL/authors/$author_slug/\">$author <span class=\"tag-count\">($post_count)</span></a>"$'\n'
fi
done < "${CACHE_DIR}/authors_with_counts.tmp"
main_content+="</div>"$'\n'
# Generate full HTML page for main authors index
local page_title="${MSG_ALL_AUTHORS:-All Authors}"
local page_description="${MSG_ALL_AUTHORS:-All Authors} - $SITE_DESCRIPTION"
local authors_index_rel_url="/authors/"
# Process templates with placeholder replacement (following tags generator pattern)
local header_content="$HEADER_TEMPLATE"
local footer_content="$FOOTER_TEMPLATE"
# Replace placeholders in the header
header_content=${header_content//\{\{site_title\}\}/"$SITE_TITLE"}
header_content=${header_content//\{\{page_title\}\}/"$page_title"}
header_content=${header_content//\{\{site_description\}\}/"$SITE_DESCRIPTION"}
header_content=${header_content//\{\{og_description\}\}/"$page_description"}
header_content=${header_content//\{\{twitter_description\}\}/"$page_description"}
header_content=${header_content//\{\{og_type\}\}/"website"}
header_content=${header_content//\{\{page_url\}\}/"$authors_index_rel_url"}
header_content=${header_content//\{\{site_url\}\}/"$SITE_URL"}
# Remove unprocessed image placeholders
header_content=${header_content//\{\{og_image\}\}/}
header_content=${header_content//\{\{twitter_image\}\}/}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# Remove the placeholder for the tag-specific RSS feed link in the main authors index
header_content=${header_content//<!-- bssg:tag_rss_link -->/}
# Schema.org structured data
local schema_json="{\"@context\": \"https://schema.org\",\"@type\": \"CollectionPage\",\"name\": \"$page_title\",\"description\": \"List of all authors on $SITE_TITLE\",\"url\": \"$SITE_URL/authors/\",\"isPartOf\": {\"@type\": \"WebSite\",\"name\": \"$SITE_TITLE\",\"url\": \"$SITE_URL\"}}"
header_content=${header_content//\{\{schema_json_ld\}\}/"<script type=\"application/ld+json\">$schema_json</script>"}
# Replace placeholders in the footer
local current_year=$(date +%Y)
footer_content=${footer_content//\{\{current_year\}\}/"$current_year"}
footer_content=${footer_content//\{\{author_name\}\}/"$AUTHOR_NAME"}
footer_content=${footer_content//\{\{all_rights_reserved\}\}/"${MSG_ALL_RIGHTS_RESERVED:-All rights reserved.}"}
{
echo "$header_content"
echo "$main_content"
echo "$footer_content"
} > "$main_authors_index_output"
echo "Generated main authors index: $main_authors_index_output"
# Clean up temporary file
rm -f "${CACHE_DIR}/authors_with_counts.tmp"
else
echo "Main authors index is up to date, skipping..."
fi
# Clean up author data directory
rm -rf "$author_data_dir"
echo -e "${GREEN}Author pages processed!${NC}"
echo -e "${GREEN}Generated author list pages.${NC}"
}

File diff suppressed because it is too large Load diff

View file

@ -10,8 +10,301 @@ 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//\{\{twitter_card\}\}/"summary"}
page_header=${page_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
page_header=${page_header//\{\{canonical\}\}/}
page_header=${page_header//\{\{featured_image_preload\}\}/}
local page_footer="$FOOTER_TEMPLATE"
page_footer=${page_footer//\{\{current_year\}\}/$(date +%Y)}
page_footer=${page_footer//\{\{author_name\}\}/"$AUTHOR_NAME"}
cat > "$output_file" <<EOF
$page_header
EOF
local index_file="${PAGES_DIR}/index.md"
local has_custom_index=false
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$index_file"; then
has_custom_index=true
elif [ -f "$index_file" ]; then
has_custom_index=true
fi
if [ "$current_page" -eq 1 ] && [ "$has_custom_index" = true ]; then
local content="" html_content="" in_frontmatter=false found_frontmatter=false source_stream=""
if [ "${BSSG_RAM_MODE:-false}" = true ] && ram_mode_has_file "$index_file"; then
source_stream=$(ram_mode_get_content "$index_file")
else
source_stream=$(cat "$index_file")
fi
while IFS= read -r line; do
if [[ "$line" == "---" ]]; then
if ! $in_frontmatter && ! $found_frontmatter; then
in_frontmatter=true
found_frontmatter=true
continue
elif $in_frontmatter; then
in_frontmatter=false
continue
fi
fi
if ! $in_frontmatter && $found_frontmatter; then
content+="$line"$'\n'
fi
done <<< "$source_stream"
if ! $found_frontmatter; then
content="$source_stream"
fi
html_content=$(convert_markdown_to_html "$content")
echo "$html_content" >> "$output_file"
cat >> "$output_file" <<EOF
$page_footer
EOF
continue
fi
if [ "$total_posts_orig" -gt 0 ]; then
cat >> "$output_file" <<EOF
<h1>${MSG_LATEST_POSTS:-"Latest Posts"}</h1>
<div class="posts-list">
EOF
local start_index=$(( (current_page - 1) * POSTS_PER_PAGE ))
local end_index=$(( start_index + POSTS_PER_PAGE - 1 ))
local i
for (( i = start_index; i <= end_index && i < total_posts_orig; i++ )); do
local file filename title date lastmod tags slug image image_caption description author_name author_email
IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "${file_index_lines[$i]}"
[ -z "$file" ] && continue
[ -z "$title" ] && continue
[ -z "$date" ] && continue
local post_year post_month post_day
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
post_year="${BASH_REMATCH[1]}"
post_month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
post_day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
else
post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d)
fi
local formatted_path="${URL_SLUG_FORMAT//Year/$post_year}"
formatted_path="${formatted_path//Month/$post_month}"
formatted_path="${formatted_path//Day/$post_day}"
formatted_path="${formatted_path//slug/$slug}"
local post_link="/$formatted_path/"
local display_date_format="$DATE_FORMAT"
if [ "${SHOW_TIMEZONE:-false}" = false ]; then
display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//')
fi
local formatted_date
formatted_date=$(format_date "$date" "$display_date_format")
cat >> "$output_file" <<EOF
<article>
<h2><a href="$(fix_url "$post_link")">$title</a></h2>
<div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${author_name:+" ${MSG_BY:-"by"} ${author_name:-$AUTHOR_NAME}"}</div>
EOF
if [ -n "$image" ]; then
local image_url="$image"
if [[ "$image" == /* ]]; then
image_url="${SITE_URL}${image}"
fi
cat >> "$output_file" <<EOF
<div class="featured-image index-image">
<a href="$(fix_url "$post_link")">
<img src="$image_url" alt="${image_caption:-$title}" title="${image_caption:-$title}" loading="lazy" />
</a>
</div>
EOF
fi
if [ "${INDEX_SHOW_FULL_CONTENT:-false}" = "true" ]; then
local post_content="" html_content=""
if [ "${BSSG_RAM_MODE:-false}" = true ] && ram_mode_has_file "$file"; then
local source_stream
source_stream=$(ram_mode_get_content "$file")
post_content=$(printf '%s\n' "$source_stream" | awk '
BEGIN { in_fm = 0; found_fm = 0; }
/^---$/ {
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
if (in_fm) { in_fm = 0; next; }
}
{ if (!in_fm) print; }
')
fi
if [ -n "$post_content" ]; then
if [[ "$file" == *.md ]]; then
html_content=$(convert_markdown_to_html "$post_content")
else
html_content="$post_content"
fi
fi
if [ -n "$html_content" ]; then
cat >> "$output_file" <<EOF
<div class="post-content">
$html_content
</div>
EOF
fi
elif [ "${SHOW_INDEX_DESCRIPTIONS:-true}" = "true" ] && [ -n "$description" ]; then
cat >> "$output_file" <<EOF
<div class="summary">
$description
</div>
EOF
fi
cat >> "$output_file" <<EOF
</article>
EOF
done
cat >> "$output_file" <<EOF
</div> <!-- .posts-list -->
EOF
if [ "$total_pages" -gt 1 ]; then
cat >> "$output_file" <<EOF
<!-- Pagination -->
<div class="pagination">
EOF
if [ "$current_page" -gt 1 ]; then
local prev_page=$((current_page - 1))
local prev_url="/"
if [ $prev_page -ne 1 ]; then
prev_url="/page/$prev_page/"
fi
cat >> "$output_file" <<PAG_EOF
<a href="$(fix_url "$prev_url")" class="prev">&laquo; ${MSG_NEWER_POSTS:-Newer}</a>
PAG_EOF
fi
cat >> "$output_file" <<PAG_EOF
<span class="page-info">$(printf "${MSG_PAGE_INFO_TEMPLATE:-Page %d of %d}" "$current_page" "$total_pages")</span>
PAG_EOF
if [ "$current_page" -lt "$total_pages" ]; then
local next_page=$((current_page + 1))
cat >> "$output_file" <<PAG_EOF
<a href="$(fix_url "/page/$next_page/")" class="next">${MSG_OLDER_POSTS:-Older} &raquo;</a>
PAG_EOF
fi
cat >> "$output_file" <<EOF
</div>
EOF
fi
fi
cat >> "$output_file" <<EOF
$page_footer
EOF
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)
@ -30,7 +323,8 @@ generate_index() {
fi
# Count total posts
local total_posts=$(wc -l < "$file_index")
local total_posts_orig=$(wc -l < "$file_index")
local total_posts=$total_posts_orig
local total_pages=$(( (total_posts + POSTS_PER_PAGE - 1) / POSTS_PER_PAGE ))
# Ensure total_pages is at least 1 even if total_posts is 0
@ -50,10 +344,11 @@ generate_index() {
local -i current_page="$1"
local -i total_pages="$2"
local file_index="$3"
local -i total_posts_orig="$4"
# Template content is accessed via exported global variables
local output_file
if [ $current_page -eq 1 ]; then
if [ "$current_page" -eq 1 ]; then
output_file="$OUTPUT_DIR/index.html"
else
output_file="$OUTPUT_DIR/page/$current_page/index.html"
@ -73,7 +368,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
@ -141,6 +436,10 @@ EOF
page_header=${page_header//\{\{twitter_description\}\}/"$SITE_DESCRIPTION"}
page_header=${page_header//\{\{og_image\}\}/""}
page_header=${page_header//\{\{twitter_image\}\}/""}
page_header=${page_header//\{\{twitter_card\}\}/"summary"}
page_header=${page_header//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
page_header=${page_header//\{\{canonical\}\}/}
page_header=${page_header//\{\{featured_image_preload\}\}/}
# Replace placeholders in the footer
local page_footer="$FOOTER_TEMPLATE"
@ -150,163 +449,236 @@ EOF
# Create the index page
cat > "$output_file" << EOF
$page_header
EOF
# If there is an index.md, use that
if [ -f "${PAGES_DIR}/index.md" ]; then
local input_file="${PAGES_DIR}/index.md"
local title=$(parse_metadata "$input_file" "title")
local start_line=$(grep -n "^---$" "$input_file" | head -1 | cut -d: -f1)
local end_line=$(grep -n "^---$" "$input_file" | head -2 | tail -1 | cut -d: -f1)
# Extract content after the second --- line
content=$(tail -n +$((end_line + 1)) $input_file)
html_content=$(convert_markdown_to_html "$content")
echo "$html_content" >> $output_file
echo -e "${GREEN}Used custom index.md with title '$title' as the sole homepage content.${NC}"
# Append footer and finish for the homepage
cat >> "$output_file" << EOF
$page_footer
EOF
# echo -e "Generated custom index page ${GREEN}$current_page${NC}" # Optional: Specific message
return 0 # Successfully generated custom index page, skip post listing
else
# No index.md found, proceed with standard "Latest Posts" logic
# Only add "Latest Posts" section if there are actually posts
if [ "$total_posts_orig" -gt 0 ]; then
cat >> "$output_file" << EOF
<h1>${MSG_LATEST_POSTS:-"Latest Posts"}</h1>
<div class="posts-list">
EOF
# Calculate start and end indices
local start_index=$(( (current_page - 1) * POSTS_PER_PAGE + 1 ))
local end_index=$(( current_page * POSTS_PER_PAGE ))
# Add posts to the index page
# Use awk for line processing and indexing
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
# Skip if essential fields are empty
if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ]; then
continue
fi
# Create slug-based URL path
local post_year post_month post_day
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
post_year="${BASH_REMATCH[1]}"
post_month=$(awk -v m="$((10#${BASH_REMATCH[2]}))" 'BEGIN { printf "%02d", m }')
post_day=$(awk -v d="$((10#${BASH_REMATCH[3]}))" 'BEGIN { printf "%02d", d }')
else
post_year=$(date +%Y)
post_month=$(date +%m)
post_day=$(date +%d)
fi
local formatted_path="${URL_SLUG_FORMAT//Year/$post_year}"
formatted_path="${formatted_path//Month/$post_month}"
formatted_path="${formatted_path//Day/$post_day}"
formatted_path="${formatted_path//slug/$slug}"
# Format date based on SHOW_TIMEZONE
local display_date_format="$DATE_FORMAT"
if [ "${SHOW_TIMEZONE:-false}" = false ]; then
# Remove timezone format specifiers (%z or %Z) if they exist
display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//')
fi
local formatted_date=$(format_date "$date" "$display_date_format")
# Ensure post link has trailing slash
local post_link="/$formatted_path/"
# Add post to index
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>
# Calculate start and end indices
local start_index=$(( (current_page - 1) * POSTS_PER_PAGE + 1 ))
local end_index=$(( current_page * POSTS_PER_PAGE ))
# Add posts to the index page
awk -v start="$start_index" -v end="$end_index" 'NR >= start && NR <= end { print }' "$file_index" | while IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email; do
# ... (rest of the post item generation logic remains the same) ...
if [ -z "$file" ] || [ -z "$title" ] || [ -z "$date" ]; then
continue
fi
local post_year post_month post_day
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
post_year="${BASH_REMATCH[1]}"
post_month=$(awk -v m="$((10#${BASH_REMATCH[2]}))" 'BEGIN { printf "%02d", m }')
post_day=$(awk -v d="$((10#${BASH_REMATCH[3]}))" 'BEGIN { printf "%02d", d }')
else
post_year=$(date +%Y); post_month=$(date +%m); post_day=$(date +%d)
fi
local formatted_path="${URL_SLUG_FORMAT//Year/$post_year}"
formatted_path="${formatted_path//Month/$post_month}"
formatted_path="${formatted_path//Day/$post_day}"
formatted_path="${formatted_path//slug/$slug}"
local display_date_format="$DATE_FORMAT"
if [ "${SHOW_TIMEZONE:-false}" = false ]; then
display_date_format=$(echo "$display_date_format" | sed -e 's/%[zZ]//g' -e 's/[[:space:]]*$//')
fi
local formatted_date=$(format_date "$date" "$display_date_format")
local post_link="/$formatted_path/"
cat >> "$output_file" << EOF
<article>
<h2><a href="$(fix_url "$post_link")">$title</a></h2>
<div class="meta">${MSG_PUBLISHED_ON:-"Published on"} $formatted_date${author_name:+" ${MSG_BY:-"by"} ${author_name:-$AUTHOR_NAME}"}</div>
EOF
if [ -n "$image" ]; then
local image_url="$image"
if [[ "$image" == /* ]]; then image_url="${SITE_URL}${image}"; fi
cat >> "$output_file" << EOF
<div class="featured-image index-image">
<a href="$(fix_url "$post_link")">
<img src="$image_url" alt="${image_caption:-$title}" title="${image_caption:-$title}" />
</a>
</div>
EOF
fi
# Show either full content or just description based on config
if [ "${INDEX_SHOW_FULL_CONTENT:-false}" = "true" ]; then
# Show full post content
local post_content=""
local content_cache_file="${CACHE_DIR:-.bssg_cache}/content/$(basename "$file")"
# Try RAM preload first
if [ "${BSSG_RAM_MODE:-false}" = true ] && declare -F ram_mode_has_file > /dev/null && ram_mode_has_file "$file"; then
local source_stream
source_stream=$(ram_mode_get_content "$file")
post_content=$(printf '%s\n' "$source_stream" | awk '
BEGIN { in_fm = 0; found_fm = 0; }
/^---$/ {
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
if (in_fm) { in_fm = 0; next; }
}
{ if (!in_fm) print; }
')
# Try to get content from cache first
elif [ -f "$content_cache_file" ]; then
post_content=$(cat "$content_cache_file")
else
# Extract content from source file if cache doesn't exist
local in_frontmatter=false
local found_frontmatter=false
{
while IFS= read -r line; do
if [[ "$line" == "---" ]]; then
if ! $in_frontmatter && ! $found_frontmatter; then
in_frontmatter=true
found_frontmatter=true
continue
elif $in_frontmatter; then
in_frontmatter=false
continue
fi
fi
if ! $in_frontmatter && $found_frontmatter; then
post_content+="$line"$'\n'
fi
done
} < "$file"
# If no frontmatter was found, use the whole file
if ! $found_frontmatter; then
post_content=$(cat "$file")
fi
fi
# Convert to HTML if it's a markdown file
local html_content=""
if [[ "$file" == *.md ]]; then
html_content=$(convert_markdown_to_html "$post_content")
elif [[ "$file" == *.html ]]; then
# For HTML files, content is already HTML
html_content=$(sed -n '/<body.*>/,/<\/body>/p' "$file" | sed '1d;$d')
# If body extraction failed, use content as-is
if [ -z "$html_content" ]; then
html_content="$post_content"
fi
else
html_content="$post_content"
fi
if [ -n "$html_content" ]; then
cat >> "$output_file" << EOF
<div class="post-content">
$html_content
</div>
EOF
fi
elif [ "${SHOW_INDEX_DESCRIPTIONS:-true}" = "true" ] && [ -n "$description" ]; then
# Show just the description/excerpt (default behavior)
cat >> "$output_file" << EOF
<div class="summary">
$description
</div>
EOF
fi
cat >> "$output_file" << EOF
</article>
EOF
done # End of while loop reading posts
# Close the posts list div
cat >> "$output_file" << EOF
</div> <!-- .posts-list -->
EOF
# Add featured image
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
# Add description
if [ -n "$description" ]; then
cat >> "$output_file" << EOF
<div class="summary">
<p>$description</p>
</div>
EOF
fi
# Add tags
if [ -n "$tags" ]; then
cat >> "$output_file" << EOF
<div class="tags">
EOF
local tag_slug
local OLD_IFS="$IFS"
IFS=','
for tag in $tags; do
tag=$(echo "$tag" | awk '{$1=$1};1')
tag_slug=$(generate_slug "$tag")
# Ensure tag link has trailing slash
echo " <a href=\"$(fix_url "/tags/$tag_slug/")\" class=\"tag\">$tag</a>" >> "$output_file"
done
IFS="$OLD_IFS"
cat >> "$output_file" << EOF
</div>
EOF
fi
cat >> "$output_file" << EOF
</article>
EOF
done
# Close the posts list
cat >> "$output_file" << EOF
</div>
# Pagination logic (Only needed if there were posts)
if [ "$total_pages" -gt 1 ]; then
cat >> "$output_file" << EOF
<!-- Pagination -->
<div class="pagination">
EOF
# Add pagination links
if [ $current_page -gt 1 ]; then
local prev_page=$((current_page - 1))
local prev_url
if [ $prev_page -eq 1 ]; then
prev_url="/"
else
prev_url="/page/$prev_page/"
fi
echo " <a href=\"$(fix_url "$prev_url")\" class=\"prev\">&laquo; ${MSG_NEWER_POSTS:-Newer}</a>" >> "$output_file"
fi
# Add page info element
if [ $total_pages -gt 1 ]; then
echo " <span class=\"page-info\">$(printf "${MSG_PAGE_INFO_TEMPLATE:-Page %d of %d}" "$current_page" "$total_pages")</span>" >> "$output_file"
fi
if [ $current_page -lt $total_pages ]; then
local next_page=$((current_page + 1))
# Ensure next page link has trailing slash
echo " <a href=\"$(fix_url "/page/$next_page/")\" class=\"next\">${MSG_OLDER_POSTS:-Older} &raquo;</a>" >> "$output_file"
fi
# Close pagination and add footer
cat >> "$output_file" << EOF
if [ "$current_page" -gt 1 ]; then
local prev_page=$((current_page - 1))
local prev_url="/"
if [ $prev_page -ne 1 ]; then prev_url="/page/$prev_page/"; fi
cat >> "$output_file" << PAG_EOF
<a href="$(fix_url "$prev_url")" class="prev">&laquo; ${MSG_NEWER_POSTS:-Newer}</a>
PAG_EOF
fi
cat >> "$output_file" << PAG_EOF
<span class="page-info">$(printf "${MSG_PAGE_INFO_TEMPLATE:-Page %d of %d}" "$current_page" "$total_pages")</span>
PAG_EOF
if [ "$current_page" -lt "$total_pages" ]; then
local next_page=$((current_page + 1))
cat >> "$output_file" << PAG_EOF
<a href="$(fix_url "/page/$next_page/")" class="next">${MSG_OLDER_POSTS:-Older} &raquo;</a>
PAG_EOF
fi
cat >> "$output_file" << EOF
</div>
EOF
fi # End pagination check
else
# No index.md and no posts - display a message or leave blank?
# Currently implies a blank content area between header/footer.
echo "No posts found and no custom index.md; homepage will be mostly empty."
fi # End of if total_posts_orig > 0
# Add footer (always needed in the 'else' case)
cat >> "$output_file" << EOF
$page_footer
EOF
fi # End of if [ -f "${PAGES_DIR}/index.md" ] ... else ...
# This message will now only be reached if index.md was NOT used.
echo -e "Generated index page ${GREEN}$current_page${NC} of ${GREEN}$total_pages${NC}"
}
# Use GNU parallel if available and beneficial
if [ "${HAS_PARALLEL:-false}" = true ] && [ $total_pages -gt 2 ]; then
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 jobs=$(( cores > 1 ? cores -1 : 1 ))
if [ $jobs -gt $total_pages ]; then jobs=$total_pages; fi
local cores
cores=$(get_parallel_jobs)
# Use all detected cores
local jobs=$cores
# Export required functions and variables
export OUTPUT_DIR URL_SLUG_FORMAT POSTS_PER_PAGE CACHE_DIR
export SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME DATE_FORMAT SITE_URL
export FORCE_REBUILD HEADER_TEMPLATE FOOTER_TEMPLATE SHOW_TIMEZONE
export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY
export INDEX_SHOW_FULL_CONTENT
export SHOW_INDEX_DESCRIPTIONS
export MSG_LATEST_POSTS MSG_HOME MSG_PAGINATION_TITLE MSG_PUBLISHED_ON MSG_BY
export MSG_NEWER_POSTS MSG_OLDER_POSTS MSG_PAGE_INFO_TEMPLATE
export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url
# Note: total_posts_orig is NOT exported, passed as argument now
export -f process_index_page file_needs_rebuild get_file_mtime format_date generate_slug fix_url convert_markdown_to_html
# Ensure templates are exported
if [ -z "$HEADER_TEMPLATE" ] || [ -z "$FOOTER_TEMPLATE" ]; then
@ -314,14 +686,14 @@ EOF
return 1
fi
# Process pages in parallel
seq 1 $total_pages | parallel --jobs $jobs process_index_page {} $total_pages "$file_index" || { echo -e "${RED}Parallel index page generation failed.${NC}"; exit 1; }
# Process pages in parallel, passing total_posts_orig as the 4th argument
seq 1 $total_pages | parallel --jobs $jobs --will-cite process_index_page {} $total_pages "$file_index" $total_posts_orig || { echo -e "${RED}Parallel index page generation failed.${NC}"; exit 1; }
else
# Sequential implementation
echo -e "${YELLOW}Using sequential processing${NC}"
local current_page=1
while [ $current_page -le $total_pages ]; do
process_index_page $current_page $total_pages "$file_index"
while [ "$current_page" -le "$total_pages" ]; do
process_index_page $current_page $total_pages "$file_index" $total_posts_orig
current_page=$((current_page + 1))
done
fi
@ -330,4 +702,4 @@ EOF
}
# Make the function available for sourcing
export -f generate_index
export -f generate_index

View file

@ -4,12 +4,6 @@
# Functions for converting markdown/HTML pages.
#
# Ensure necessary color variables are available if sourced independently
# RED='${RED:- [0;31m}' # Removed - Incorrect & should be inherited from main export
# GREEN='${GREEN:- [0;32m}' # Removed - Incorrect & should be inherited from main export
# YELLOW='${YELLOW:- [0;33m}' # Removed - Incorrect & should be inherited from main export
# NC='${NC:- [0m}' # Removed - Incorrect & should be inherited from main export
# Source dependencies
# shellcheck source=utils.sh disable=SC1091
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_pages.sh"; exit 1; }
@ -30,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
@ -51,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
@ -117,6 +125,12 @@ convert_page() {
# Handle image placeholders (remove for pages as they don't have featured images)
header_content=${header_content//\{\{og_image\}\}/}
header_content=${header_content//\{\{twitter_image\}\}/}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
# Add canonical link
local canonical_tag="<link rel=\"canonical\" href=\"${SITE_URL}${page_rel_url}\">"
header_content=${header_content//\{\{canonical\}\}/"$canonical_tag"}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# Assemble the final HTML
local final_html="${header_content}"
@ -184,10 +198,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
@ -196,14 +213,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,12 +259,13 @@ process_all_pages() {
export CONFIG_HASH_FILE # Export path to hash file
# Process page files in parallel using newline separation
printf '%s\n' "${page_files[@]}" | parallel --jobs "$cores" process_single_page_file {}
printf '%s\n' "${page_files[@]}" | parallel --jobs "$cores" --will-cite process_single_page_file {}
else
# Fallback to sequential processing
echo -e "${YELLOW}Using sequential processing for pages${NC}"
local file
for file in "${page_files[@]}"; do
[[ -z "$file" ]] && continue
process_single_page_file "$file"
done
fi
@ -229,4 +273,4 @@ process_all_pages() {
echo -e "${GREEN}Static page processing complete!${NC}"
}
# --- Page Generation Functions --- END ---
# --- Page Generation Functions --- END ---

View file

@ -4,12 +4,6 @@
# Functions for converting markdown posts to HTML.
#
# Ensure necessary color variables are available if sourced independently
# RED='${RED:-\\033[0;31m}' # Removed - Should be inherited from main export
# GREEN='${GREEN:-\\033[0;32m}' # Removed - Should be inherited from main export
# YELLOW='${YELLOW:-\\033[0;33m}' # Removed - Should be inherited from main export
# NC='${NC:-\\033[0m}' # Removed - Should be inherited from main export
# Source dependencies
# shellcheck source=utils.sh disable=SC1091
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from generate_posts.sh"; exit 1; }
@ -17,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"
@ -32,65 +85,91 @@ 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
reading_time=$(calculate_reading_time "$content")
local reading_time=0
if [ "${SHOW_READING_TIME:-true}" = "true" ]; then
reading_time=$(calculate_reading_time "$content")
fi
# Convert markdown content to HTML (No HTML caching here anymore)
local html_content
@ -124,7 +203,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>"
@ -174,68 +253,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=""
local iso_date=""
local iso_lastmod_date=""
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
@ -245,13 +290,17 @@ 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 author_json
author_json=$(printf '{\n "@type": "Person",\n "name": "%s"\n }' "$post_author_name")
schema_json_ld=$(printf '<script type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "BlogPosting",\n "headline": "%s",\n "datePublished": "%s",\n "dateModified": "%s",\n "author": %s,\n "publisher": {\n "@type": "Organization",\n "name": "%s",\n "logo": {\n "@type": "ImageObject",\n "url": "%s/logo.png"\n }\n },\n "description": "%s",\n "mainEntityOfPage": {\n "@type": "WebPage",\n "@id": "%s%s"\n }%s\n}\n</script>' \
"$(echo "$title" | sed 's/"/\"/g')" \
"$iso_date" \
"$iso_lastmod_date" \
"${AUTHOR_NAME:-Anonymous}" \
"${AUTHOR_EMAIL:-anonymous@example.com}" \
"$author_json" \
"$SITE_TITLE" \
"$SITE_URL" \
"$(echo "$meta_desc" | sed 's/"/\"/g')" \
@ -270,9 +319,22 @@ convert_markdown() {
local twitter_image_tag="<meta name=\"twitter:image\" content=\"$image_url\">"
header_content=${header_content//\{\{og_image\}\}/"$og_image_tag"}
header_content=${header_content//\{\{twitter_image\}\}/"$twitter_image_tag"}
header_content=${header_content//\{\{twitter_card\}\}/"summary_large_image"}
else
header_content=${header_content//\{\{og_image\}\}/}
header_content=${header_content//\{\{twitter_image\}\}/}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
fi
# Handle canonical link
local canonical_tag="<link rel=\"canonical\" href=\"${SITE_URL}${page_url}\">"
header_content=${header_content//\{\{canonical\}\}/"$canonical_tag"}
# Handle featured image preload for LCP optimization
if [ -n "$image_url" ]; then
header_content=${header_content//\{\{featured_image_preload\}\}/"<link rel=\"preload\" as=\"image\" href=\"$image_url\">"}
else
header_content=${header_content//\{\{featured_image_preload\}\}/}
fi
# Construct meta div (date, reading time, lastmod)
@ -285,29 +347,75 @@ convert_markdown() {
local formatted_date=$(format_date "$date" "$display_date_format")
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"
if [ "$formatted_date" != "$formatted_lastmod" ]; then
post_meta+=" &bull; ${MSG_UPDATED_ON:-Updated on}: $formatted_lastmod"
local post_meta="<div class=\"page-meta\">"
post_meta+="<p class=\"meta\">"
post_meta+="${MSG_PUBLISHED_ON:-Published on}: <time datetime=\"$iso_date\">$formatted_date</time> ${MSG_BY:-by} <strong>$display_author_name</strong>"
post_meta+="</p>"
if [ "${SHOW_READING_TIME:-true}" = "true" ]; then
local post_meta_reading_time
post_meta_reading_time=$(printf "${MSG_READING_TIME_TEMPLATE:-%d min read}" "$reading_time")
if [ "$formatted_date" != "$formatted_lastmod" ]; then
post_meta+="<p class=\"meta reading-time\">"
post_meta+="${MSG_UPDATED_ON:-Updated on}: <time datetime=\"$iso_lastmod_date\">$formatted_lastmod</time> &bull; $post_meta_reading_time"
post_meta+="</p>"
else
post_meta+="<p class=\"meta reading-time\">$post_meta_reading_time</p>"
fi
elif [ "$formatted_date" != "$formatted_lastmod" ]; then
post_meta+="<p class=\"meta\">"
post_meta+="${MSG_UPDATED_ON:-Updated on}: <time datetime=\"$iso_lastmod_date\">$formatted_lastmod</time>"
post_meta+="</p>"
fi
post_meta+=" &bull; $post_meta_reading_time</div>"
post_meta+="</div>"
# Construct featured image HTML
local image_html=""
if [ -n "$image" ]; then
local alt_text="${image_caption:-$title}"
image_html="<div class=\"featured-image\"><img src=\"$(fix_url "$image")\" alt=\"$alt_text\"><div class=\"image-caption\">${image_caption:-$title}</div></div>"
image_html="<div class=\"featured-image\"><img src=\"$(fix_url "$image")\" alt=\"$alt_text\" fetchpriority=\"high\"><div class=\"image-caption\">${image_caption:-$title}</div></div>"
fi
# Generate related posts if enabled and tags exist
local related_posts_html=""
if [ "${ENABLE_RELATED_POSTS:-true}" = true ] && [ -n "$tags" ]; then
# RAM fast path: direct map lookup avoids per-post command-substitution/function overhead.
if [ "${BSSG_RAM_MODE:-false}" = true ] && \
[ "${BSSG_RAM_RELATED_POSTS_READY:-false}" = true ] && \
[ "${BSSG_RAM_RELATED_POSTS_LIMIT:-}" = "${RELATED_POSTS_COUNT:-3}" ]; then
related_posts_html="${BSSG_RAM_RELATED_POSTS_HTML[$slug]-}"
if [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
echo -e "${BLUE}DEBUG: Generating related posts for $slug with tags: $tags${NC}"
fi
else
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
echo -e "${BLUE}DEBUG: Generating related posts for $slug with tags: $tags${NC}"
fi
related_posts_html=$(generate_related_posts "$slug" "$tags" "$date" "${RELATED_POSTS_COUNT:-3}")
fi
else
if [ "${BSSG_RAM_MODE:-false}" != true ] || [ "${RAM_MODE_VERBOSE:-false}" = true ]; then
echo -e "${BLUE}DEBUG: Skipping related posts for $slug - ENABLE_RELATED_POSTS=${ENABLE_RELATED_POSTS:-true}, tags=$tags${NC}"
fi
fi
# Construct article body
local final_html="${header_content}"
final_html+=$(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}"
@ -330,93 +438,301 @@ process_all_markdown_files() {
echo -e "${YELLOW}Processing markdown posts...${NC}"
local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt"
local modified_tags_list="${CACHE_DIR:-.bssg_cache}/modified_tags.list" # Define path for modified tags
local modified_authors_list="${CACHE_DIR:-.bssg_cache}/modified_authors.list" # Define path for modified authors
local file_index_prev="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt" # Path to previous index
local ram_mode_active=false
local file_index_data=""
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
ram_mode_active=true
file_index_data=$(ram_mode_get_dataset "file_index")
fi
if [ ! -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."
# --- 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))
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"
# Basic check if it looks like a post
if [ -z "$date" ] || [[ "$file" != "$SRC_DIR"* ]]; then
# echo -e "Skipping non-post file listed in index (pre-check): ${YELLOW}$file${NC}" >&2 # Too verbose
continue
fi
# Calculate expected output path (logic copied from process_single_file)
local output_path
local year month day
if [[ "$date" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}) ]]; then
year="${BASH_REMATCH[1]}"
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
else
year=$(date +%Y); month=$(date +%m); day=$(date +%d)
fi
local url_path="${URL_SLUG_FORMAT:-Year/Month/Day/slug}"
url_path="${url_path//Year/$year}"; url_path="${url_path//Month/$month}";
url_path="${url_path//Day/$day}"; url_path="${url_path//slug/$slug}"
local output_html_file="${OUTPUT_DIR:-output}/$url_path/index.html"
# Perform the rebuild check here
common_rebuild_check "$output_html_file"
local common_result=$?
local needs_rebuild=false
if [ $common_result -eq 0 ]; then
needs_rebuild=true # Common checks failed (config changed, template newer, output missing)
else # common_result is 2 (output exists and newer than templates/locale)
local input_time=$(get_file_mtime "$file")
local output_time=$(get_file_mtime "$output_html_file")
if (( input_time > output_time )); then
needs_rebuild=true # Input file is newer
fi
fi
# Check if this post needs rebuilding due to related posts cache invalidation
if ! $ram_mode_active && [ "$needs_rebuild" = false ] && [ -n "${RELATED_POSTS_INVALIDATED_LIST:-}" ] && [ -f "$RELATED_POSTS_INVALIDATED_LIST" ]; then
if grep -Fxq "$slug" "$RELATED_POSTS_INVALIDATED_LIST" 2>/dev/null; then
needs_rebuild=true # Related posts cache was invalidated
echo -e "Rebuilding ${GREEN}$(basename "$file")${NC} due to related posts cache invalidation"
fi
fi
if $needs_rebuild; then
files_to_process_list+=("$line")
files_to_process_count=$((files_to_process_count + 1))
else
# Only print skip message if not rebuilding
echo -e "Skipping unchanged file: ${YELLOW}$(basename "$file")${NC}"
skipped_count=$((skipped_count + 1))
fi
done < <(
if $ram_mode_active; then
printf '%s\n' "$file_index_data" | awk 'NF'
else
cat "$file_index"
fi
)
fi
# Check if any files need processing
if [ $files_to_process_count -eq 0 ]; then
@ -427,14 +743,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
@ -453,21 +772,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
@ -475,14 +832,18 @@ 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 SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME MARKDOWN_PROCESSOR MARKDOWN_PL_PATH DATE_FORMAT TIMEZONE SHOW_TIMEZONE SHOW_READING_TIME
export FEDIVERSE_CREATOR AUTHOR_FEDIVERSE_CREATORS_SERIALIZED
export 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" process_single_file_for_rebuild {} || { echo -e "${RED}Parallel post processing failed.${NC}"; exit 1; }
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; }
else
# Sequential processing for filtered list
echo -e "${YELLOW}Using sequential processing for $files_to_process_count posts${NC}"
@ -499,4 +860,4 @@ process_all_markdown_files() {
# Make the main function available for sourcing
export -f process_all_markdown_files convert_markdown # Export the main function and conversion
# Export helpers needed if sourced externally? Maybe not.
# Export helpers needed if sourced externally? Maybe not.

View file

@ -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)
@ -59,19 +65,6 @@ generate_pages_index() {
fi
# --- Read secondary pages from cache file --- END ---
# Access the exported array string and reconstruct the array safely (REMOVED)
# local temp_secondary_pages=()
# if [ -n "$SECONDARY_PAGES" ]; then
# # Read each entry separated by space/newline (default IFS)
# while read -r page_data; do
# # Trim leading/trailing whitespace just in case
# page_data=$(echo "$page_data" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# if [ -n "$page_data" ]; then
# temp_secondary_pages+=("$page_data")
# fi
# done <<< "$SECONDARY_PAGES"
# fi
# Skip if there are no secondary pages
if [ ${#temp_secondary_pages[@]} -eq 0 ]; then
echo -e "${YELLOW}No secondary pages found, skipping pages index${NC}"
@ -94,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",
@ -118,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"}
@ -131,6 +117,10 @@ EOF
# Remove image placeholders
header_content=${header_content//\{\{og_image\}\}/""}
header_content=${header_content//\{\{twitter_image\}\}/""}
header_content=${header_content//\{\{twitter_card\}\}/"summary"}
header_content=${header_content//\{\{fediverse_creator_meta\}\}/"${SITE_FEDIVERSE_CREATOR_META_TAG}"}
header_content=${header_content//\{\{canonical\}\}/}
header_content=${header_content//\{\{featured_image_preload\}\}/}
# Replace placeholders in the footer
footer_content=${footer_content//\{\{current_year\}\}/$(date +%Y)}
@ -148,7 +138,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
@ -163,4 +153,4 @@ EOF
}
# Make function available for sourcing
export -f generate_pages_index
export -f generate_pages_index

File diff suppressed because it is too large Load diff

View file

@ -4,12 +4,6 @@
# Functions for building intermediate file, tag, and archive indexes.
#
# Ensure necessary color variables are available if sourced independently
# RED='${RED:-\033[0;31m}' # Removed - Should be inherited from main export
# GREEN='${GREEN:-\033[0;32m}' # Removed - Should be inherited from main export
# YELLOW='${YELLOW:-\033[0;33m}' # Removed - Should be inherited from main export
# NC='${NC:-\033[0m}' # Removed - Should be inherited from main export
# Source Utilities and Content functions needed by indexing functions
# shellcheck source=utils.sh disable=SC1091
source "$(dirname "$0")/utils.sh" || { echo >&2 "Error: Failed to source utils.sh from indexing.sh"; exit 1; }
@ -23,171 +17,295 @@ declare -A file_index_data
# --- Indexing Functions --- START ---
# Optimized file index building with parallel processing and smarter caching
# Step 1: Build a raw index using centralized awk (fast extraction only)
_build_raw_file_index() {
local all_files_list="$1" # Input: file containing list of source files
local output_raw_index="$2" # Output: path for the raw index file
# Awk script to parse frontmatter - only extracts found fields
# No fallbacks handled here. Output includes filename and basename.
awk -f - $(<"$all_files_list") <<'EOF' > "$output_raw_index"
BEGIN {
FS="|"; OFS="|";
}
function reset_vars() {
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$/);
}
FNR == 1 {
if (NR > 1) {
# Print previous file raw data
print current_filename, current_basename, vars["title"], vars["date"], vars["lastmod"], \
vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"], \
vars["author_name"], vars["author_email"];
}
reset_vars();
current_filename = FILENAME;
current_basename = FILENAME;
sub(/.*\//, "", current_basename); # Get basename
}
# Markdown Parsing
is_md && /^---$/ {
if (!in_fm && !found_fm) { in_fm = 1; found_fm = 1; next; }
if (in_fm) { in_fm = 0; next; }
}
is_md && in_fm {
# Use compatible match for key-value extraction
if (match($0, /^([^:]+):[[:space:]]*(.*[^[:space:]])[[:space:]]*$/)) {
full_match = substr($0, RSTART, RLENGTH)
# Extract key part
key_start = match(full_match, /^[^:]+/)
key_str = substr(full_match, RSTART, RLENGTH)
# Extract value part (handle potential quotes)
value_start = match(full_match, /:[[:space:]]*(.*)$/)
value = substr(full_match, RSTART + 1, RLENGTH - 1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key_str);
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value);
key = tolower(key_str);
# Remove surrounding quotes from value if present
if ( (match(value, /^"(.*)"$/) || match(value, /^\'(.*)\'$/)) && length(value) > 1 ) {
value = substr(value, 2, length(value)-2);
}
if (key in vars) { vars[key] = value; }
}
next;
}
# HTML Parsing
is_html && match($0, /<title>([^<]*)<\/title>/) {
# Extract content within <title> tags
title_content = substr($0, RSTART + 7, RLENGTH - 15)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", title_content); # Trim whitespace
# Only set title if it wasn't already set by frontmatter (e.g., in markdown)
if (vars["title"] == "") {
vars["title"] = title_content;
}
}
is_html && match($0, /<meta[^>]+name="([^"]+)"[^>]+content="([^"]*)"[^>]*>/) {
# Extract key (name attribute)
key_match_start = RSTART + index($0, "name=") + 5 # Position after name="
key_match_len = index(substr($0, key_match_start), "\"") -1
key = tolower(substr($0, key_match_start, key_match_len));
# Extract value (content attribute)
content_match_start = RSTART + index($0, "content=") + 8 # Position after content="
content_match_len = index(substr($0, content_match_start), "\"") -1
value = substr($0, content_match_start, content_match_len);
# Only set if the key is one we care about and not already set
if (key in vars && vars[key] == "") { vars[key] = value; }
}
END {
if (NR > 0) {
# Print last file raw data
print current_filename, current_basename, vars["title"], vars["date"], vars["lastmod"], \
vars["tags"], vars["slug"], vars["image"], vars["image_caption"], vars["description"], \
vars["author_name"], vars["author_email"];
}
}
EOF
}
# Step 2: Process the raw index, applying fallbacks for missing essential fields
_process_raw_file_index() {
local input_raw_index="$1" # Input: path to the raw index file
local output_processed_index="$2" # Output: path for the final processed index
# Export functions needed in the loop
export -f get_file_mtime format_date_from_timestamp generate_slug generate_excerpt
export DATE_FORMAT # Needed by format_date_from_timestamp
export SRC_DIR # Needed indirectly by generate_excerpt if path is relative?
> "$output_processed_index" # Ensure output file is empty
local file filename title date lastmod tags slug image image_caption description author_name author_email
local file_mtime
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%.*}"
fi
# Fallback for Date (use file modification time)
if [ -z "$date" ]; then
file_mtime=$(get_file_mtime "$file")
date=$(format_date_from_timestamp "$file_mtime")
fi
# Fallback for Last Modified Date (use date if lastmod is missing)
if [ -z "$lastmod" ]; then
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
slug=$(generate_slug "$title")
fi
# Fallback for Description (generate excerpt)
# Check if description is empty or contains only whitespace
if [[ -z "$description" || "$description" =~ ^[[:space:]]*$ ]]; then
if [ "${GENERATE_EXCERPT:-true}" = "true" ]; then
description=$(generate_excerpt "$file")
fi
fi
# Apply fallback logic for author fields
if [ -z "$author_name" ]; then
author_name="${AUTHOR_NAME:-Anonymous}"
fi
if [ -z "$author_email" ] && [ -n "$author_name" ] && [ "$author_name" = "${AUTHOR_NAME:-Anonymous}" ]; then
# Only use default email if using default name
author_email="${AUTHOR_EMAIL:-}"
fi
# If author_name is specified but author_email is empty, leave email empty
# Output the fully processed line to the final index file
echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description|$author_name|$author_email" >> "$output_processed_index"
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 by comparing the newest file in src directory with our marker
# Check if rebuild is needed
if [ "${FORCE_REBUILD:-false}" = false ] && [ -f "$file_index" ] && [ -f "$index_marker" ]; then
local newest_file_time=0
# Use find -printf for efficiency if available (GNU find)
if find --version >/dev/null 2>&1 && grep -q GNU <<< "$(find --version)"; then
newest_file_time=$(find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" -printf '%T@\n' 2>/dev/null | sort -nr | head -n 1)
newest_file_time=${newest_file_time:-0} # Handle empty dir
# Convert float timestamp to integer
newest_file_time=$(printf "%.0f" "$newest_file_time")
else
# Fallback for non-GNU find (less efficient)
newest_file_time=${newest_file_time%.*} # Truncate to integer
else # POSIX/BSD find
local src_files
src_files=$(find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" 2>/dev/null)
for f in $src_files; do
local f_time=$(get_file_mtime "$f")
if (( f_time > newest_file_time )); then
newest_file_time=$f_time
fi
done
# Use -exec stat for better portability than parsing ls
# This might still be slow on very large sites compared to GNU find -printf
newest_file_time=$(find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" -exec stat -f %m {} \; 2>/dev/null | sort -nr | head -n 1)
newest_file_time=${newest_file_time:-0} # Handle empty dir
fi
local marker_time=$(get_file_mtime "$index_marker")
if [ "$newest_file_time" -le "$marker_time" ]; then
# Defensive check: if marker_time is 0 or invalid, force rebuild
[[ -z "$marker_time" || "$marker_time" -eq 0 ]] && marker_time=0
# Check if any source file is newer than the marker
if [[ "$newest_file_time" -gt 0 && "$marker_time" -gt 0 && "$newest_file_time" -le "$marker_time" ]]; then
echo -e "${GREEN}File index is up to date, skipping...${NC}"
return 0
else
echo -e "${YELLOW}File index rebuild needed (newest file: $newest_file_time, marker: $marker_time).${NC}"
fi
fi
lock_file "$file_index"
# Find all markdown/html files in the source directory, excluding hidden
local all_files_tmp="${CACHE_DIR:-.bssg_cache}/all_files.tmp.$$"
find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort > "$all_files_tmp"
# Find all markdown/html files
local all_files_list="${CACHE_DIR:-.bssg_cache}/all_files_list.$$"
find "${SRC_DIR:-src}" -type f \( -name "*.md" -o -name "*.html" \) -not -path "*/.*" | sort > "$all_files_list"
trap 'rm -f "$all_files_list"' EXIT # Ensure cleanup
local total_files=$(wc -l < "$all_files_tmp")
local total_files=$(wc -l < "$all_files_list")
if [ "$total_files" -eq 0 ]; then
echo -e "${YELLOW}No source files found in ${SRC_DIR:-src}. Skipping index build.${NC}"
> "$file_index" # Create empty index
touch "$index_marker"
unlock_file "$file_index"
rm -f "$all_files_tmp"
rm -f "$all_files_list"
trap - EXIT # Remove trap
return 0
fi
echo "Found $total_files files in source directory."
# Create temp directory for parallel processing
local temp_dir="${CACHE_DIR:-.bssg_cache}/temp_index_$$"
rm -rf "$temp_dir" # Clean up previous run just in case
mkdir -p "$temp_dir"
# Ensure metadata cache directory exists
mkdir -p "${CACHE_DIR:-.bssg_cache}/meta"
# Get number of available CPU 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
# Calculate batch size
local batch_size=$(( (total_files + cores - 1) / cores ))
[ "$batch_size" -lt 1 ] && batch_size=1
echo -e "${YELLOW}Processing $total_files files using $cores cores (batch size: $batch_size)...${NC}"
# Export required functions and variables
export DATE_FORMAT CACHE_DIR SRC_DIR FORCE_REBUILD # Add others as needed
export -f extract_metadata get_file_mtime format_date_from_timestamp generate_slug generate_excerpt lock_file unlock_file parse_metadata
# Split files into batches
split -l "$batch_size" "$all_files_tmp" "$temp_dir/batch_"
rm -f "$all_files_tmp" # Clean up original list
# Function to process a single batch file
process_batch() {
local batch_file="$1"
local output_batch="${batch_file}.out"
> "$output_batch" # Initialize empty file
while IFS= read -r file; do
# Get filename without extension
local filename=$(basename "$file" | sed 's/\\.[^.]*$//')
# Extract metadata from file
local metadata
metadata=$(extract_metadata "$file")
# Check for errors
if [[ $? -ne 0 || "$metadata" == "ERROR_FILE_NOT_FOUND" ]]; then
echo -e "${RED}Error processing metadata for $file, skipping.${NC}" >&2
continue
fi
local title date lastmod tags slug image image_caption description
IFS='|' read -r title date lastmod tags slug image image_caption description <<< "$metadata"
# Sanitize description: remove newlines
description=$(echo "$description" | tr '\n' ' ')
# Add to batch file
echo "$file|$filename|$title|$date|$lastmod|$tags|$slug|$image|$image_caption|$description" >> "$output_batch"
done < "$batch_file"
rm "$batch_file" # Remove processed input batch
}
export -f process_batch
# Process batches in parallel using GNU Parallel if available
if command -v parallel > /dev/null 2>&1 && [ "${HAS_PARALLEL:-false}" = true ]; then
find "$temp_dir" -name "batch_*" -not -name "*.out" | parallel --jobs "$cores" process_batch {}
else
# Fallback to sequential processing
echo -e "${YELLOW}GNU Parallel not found or disabled, processing batches sequentially...${NC}"
for batch_file in "$temp_dir"/batch_*; do
[[ "$batch_file" == *.out ]] && continue
process_batch "$batch_file"
done
fi
# Merge batch output files and ensure uniqueness
local file_index_tmp="${CACHE_DIR:-.bssg_cache}/file_index.tmp.$$"
find "$temp_dir" -name "*.out" -type f -exec cat {} + > "$file_index_tmp" 2>/dev/null || true
rm -rf "$temp_dir" # Clean up temp directory
# Filter by unique file path (first field)
local file_index_filtered="${CACHE_DIR:-.bssg_cache}/file_index.filtered.$$"
awk -F'|' '!seen[$1]++' "$file_index_tmp" > "$file_index_filtered"
rm -f "$file_index_tmp"
# Sort the filtered index by date (field 4) in reverse chronological order
# Define temporary file paths
local file_index_raw="${CACHE_DIR:-.bssg_cache}/file_index.raw.$$"
local file_index_processed="${CACHE_DIR:-.bssg_cache}/file_index.processed.$$"
local file_index_sorted="${CACHE_DIR:-.bssg_cache}/file_index.sorted.$$"
# Sort by date field (YYYY-MM-DD HH:MM:SS format). The default string sort works correctly in reverse.
sort -t '|' -k 4,4r "$file_index_filtered" > "$file_index_sorted"
rm -f "$file_index_filtered" # Remove the unsorted filtered file
trap 'rm -f "$all_files_list" "$file_index_raw" "$file_index_processed" "$file_index_sorted"' EXIT # Ensure cleanup
# Check if file_index has changed
# Step 1: Build raw index (fast awk extraction)
echo "Step 1: Processing $total_files files using centralized awk for raw data..."
_build_raw_file_index "$all_files_list" "$file_index_raw"
rm -f "$all_files_list" # Clean up file list immediately after use
# Step 2: Process raw index (apply fallbacks)
echo "Step 2: Applying fallbacks for missing fields..."
_process_raw_file_index "$file_index_raw" "$file_index_processed"
rm -f "$file_index_raw"
# Step 3: Sort the final processed index by date (field 4) reverse chronologically
echo "Step 3: Sorting processed index..."
sort -t '|' -k 4,4r -k 1,1 "$file_index_processed" > "$file_index_sorted" # Add secondary sort by filename
rm -f "$file_index_processed"
# Check if file_index content has changed
local index_content_changed=false
if [ -f "$file_index" ]; then
if ! cmp -s "$file_index" "$file_index_sorted"; then
# Check if the file content differs using cmp (portable)
if ! cmp -s "$file_index_sorted" "$file_index"; then
# cmp exits 1 if files differ, 0 if same
index_content_changed=true
echo -e "${YELLOW}File index content has changed.${NC}"
echo -e "${YELLOW}File index has changed.${NC}" >&2
fi
else
index_content_changed=true # No previous index exists
fi
mv "$file_index_sorted" "$file_index"
# Update frontmatter changes marker if content changed
if $index_content_changed ; then
# Move sorted index to final location if content changed
if [ "$index_content_changed" = true ]; then
echo -e "${YELLOW}Updating file index.${NC}" >&2
mv "$file_index_sorted" "$file_index"
# Update frontmatter changes marker if content changed
touch "$frontmatter_changes_marker"
echo -e "${YELLOW}File index changed, updating frontmatter marker.${NC}"
echo -e "${YELLOW}File index changed, updating frontmatter marker.${NC}" >&2
# Update the main index marker timestamp
touch "$index_marker"
else
echo -e "${GREEN}File index content unchanged, discarding sorted version.${NC}" >&2
rm -f "$file_index_sorted"
# Keep the old marker timestamp
fi
touch "$index_marker"
unlock_file "$file_index"
trap - EXIT # Remove trap upon successful completion
echo -e "${GREEN}File index built with $(wc -l < "$file_index") files!${NC}"
echo -e "${GREEN}File index built with $(wc -l < "$file_index") complete entries!${NC}"
}
# Build tags index from the file index
@ -198,6 +316,44 @@ build_tags_index() {
local tags_index_file="${CACHE_DIR:-.bssg_cache}/tags_index.txt"
local frontmatter_changes_marker="${CACHE_DIR:-.bssg_cache}/frontmatter_changes_marker"
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
local file_index_data tags_index_data
file_index_data=$(ram_mode_get_dataset "file_index")
if [ -z "$file_index_data" ]; then
ram_mode_set_dataset "tags_index" ""
ram_mode_clear_dataset "has_tags"
echo -e "${GREEN}Tags index built!${NC}"
return 0
fi
tags_index_data=$(printf '%s\n' "$file_index_data" | awk -F'|' -v OFS='|' '
{
if (length($6) > 0) {
split($6, tags_array, ",");
for (i in tags_array) {
tag = tags_array[i];
gsub(/^[[:space:]]+|[[:space:]]+$/, "", tag);
if (length(tag) == 0) continue;
tag_slug = tolower(tag);
gsub(/[^a-z0-9]+/, "-", tag_slug);
gsub(/^-+|-+$/, "", tag_slug);
if (length(tag_slug) == 0) tag_slug = "-";
print tag, tag_slug, $3, $4, $5, $2, $7, $8, $9, $10, $11, $12;
}
}
}')
ram_mode_set_dataset "tags_index" "$tags_index_data"
if [ -n "$tags_index_data" ]; then
ram_mode_set_dataset "has_tags" "1"
else
ram_mode_clear_dataset "has_tags"
fi
echo -e "${GREEN}Tags index built!${NC}"
return 0
fi
# --- Optimized Rebuild Check --- START ---
local rebuild_needed=false
local reason=""
@ -235,33 +391,233 @@ build_tags_index() {
lock_file "$tags_index_file"
> "$tags_index_file" # Clear the file
# > "$tags_index_file" # Clear the file - AWK will overwrite
# Read from file index and extract tags
local line file filename title date lastmod tags slug image image_caption description
while IFS= read -r line || [[ -n "$line" ]]; do
IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line"
# Use awk for efficient processing and slug generation
awk -F'|' -v OFS='|' '{
# $1=file, $2=filename, $3=title, $4=date, $5=lastmod,
# $6=tags, $7=slug, $8=image, $9=image_caption, $10=description, $11=author_name, $12=author_email
if (length($6) > 0) { # Check if tags field is not empty
split($6, tags_array, ","); # Split tags by comma
for (i in tags_array) {
tag = tags_array[i];
gsub(/^[[:space:]]+|[[:space:]]+$/, "", tag); # Trim whitespace
if (length(tag) == 0) continue; # Skip empty tags
if [ -n "$tags" ]; then
local tag_slug
echo "$tags" | tr ',' '\n' | while IFS= read -r tag; do
# Remove leading/trailing whitespace
tag=$(echo "$tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$tag" ]] && continue # Skip empty tags
# Generate slug within awk (replicating generate_slug logic)
tag_slug = tolower(tag);
gsub(/[^a-z0-9]+/, "-", tag_slug); # Replace non-alphanumeric with hyphens
gsub(/^-+|-+$/, "", tag_slug); # Trim leading/trailing hyphens
if (length(tag_slug) == 0) tag_slug = "-"; # Handle empty slugs
tag_slug=$(generate_slug "$tag")
# Output: TagName|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription
echo "$tag|$tag_slug|$title|$date|$lastmod|$filename|$slug|$image|$image_caption|$description" >> "$tags_index_file"
done
fi
done < "$file_index"
# Print: TagName|TagSlug|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription|AuthorName|AuthorEmail
print tag, tag_slug, $3, $4, $5, $2, $7, $8, $9, $10, $11, $12;
}
}
}' "$file_index" > "$tags_index_file"
unlock_file "$tags_index_file"
# Check if the generated index is not empty and create/remove flag file
local tags_flag_file="${CACHE_DIR:-.bssg_cache}/has_tags.flag"
if [ -s "$tags_index_file" ]; then
touch "$tags_flag_file"
else
rm -f "$tags_flag_file"
fi
echo -e "${GREEN}Tags index built!${NC}"
}
# Build authors index from the file index
build_authors_index() {
echo -e "${YELLOW}Building authors index...${NC}"
local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt"
local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt"
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
local file_index_data authors_index_data
file_index_data=$(ram_mode_get_dataset "file_index")
if [ -z "$file_index_data" ]; then
ram_mode_set_dataset "authors_index" ""
ram_mode_clear_dataset "has_authors"
echo -e "${GREEN}Authors index built!${NC}"
return 0
fi
authors_index_data=$(printf '%s\n' "$file_index_data" | awk -F'|' -v OFS='|' '
{
author_name = $11;
author_email = $12;
if (length(author_name) == 0) next;
author_slug = tolower(author_name);
gsub(/[^a-z0-9]+/, "-", author_slug);
gsub(/^-+|-+$/, "", author_slug);
if (length(author_slug) == 0) author_slug = "anonymous";
print author_name, author_slug, author_email, $3, $4, $5, $2, $7, $8, $9, $10;
}')
ram_mode_set_dataset "authors_index" "$authors_index_data"
if [ -n "$authors_index_data" ]; then
ram_mode_set_dataset "has_authors" "1"
else
ram_mode_clear_dataset "has_authors"
fi
echo -e "${GREEN}Authors index built!${NC}"
return 0
fi
# Check if rebuild is needed: missing cache or input/dependencies changed
local rebuild_needed=false
if [ ! -f "$authors_index_file" ]; then
rebuild_needed=true
elif file_needs_rebuild "$file_index" "$authors_index_file"; then
echo -e "${YELLOW}Authors index is outdated or dependencies changed, rebuilding authors...${NC}"
rebuild_needed=true
fi
if [ "$rebuild_needed" = false ]; then
echo -e "${GREEN}Authors index is up to date, skipping...${NC}"
return 0
fi
if [ ! -f "$file_index" ]; then
echo -e "${RED}Error: File index '$file_index' not found. Cannot build authors index.${NC}"
return 1
fi
lock_file "$authors_index_file"
> "$authors_index_file" # Clear the file
# Read from file index and extract author info
# Use awk for efficient processing and slug generation
awk -F'|' -v OFS='|' '{
# $1=file, $2=filename, $3=title, $4=date, $5=lastmod,
# $6=tags, $7=slug, $8=image, $9=image_caption, $10=description, $11=author_name, $12=author_email
author_name = $11;
author_email = $12;
# Skip if author_name is empty
if (length(author_name) == 0) next;
# Generate slug within awk (replicating generate_slug logic)
author_slug = tolower(author_name);
gsub(/[^a-z0-9]+/, "-", author_slug); # Replace non-alphanumeric with hyphens
gsub(/^-+|-+$/, "", author_slug); # Trim leading/trailing hyphens
if (length(author_slug) == 0) author_slug = "anonymous"; # Handle empty slugs
# Print: AuthorName|AuthorSlug|AuthorEmail|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription
print author_name, author_slug, author_email, $3, $4, $5, $2, $7, $8, $9, $10;
}' "$file_index" > "$authors_index_file"
unlock_file "$authors_index_file"
# Check if the generated index is not empty and create/remove flag file
local authors_flag_file="${CACHE_DIR:-.bssg_cache}/has_authors.flag"
if [ -s "$authors_index_file" ]; then
touch "$authors_flag_file"
else
rm -f "$authors_flag_file"
fi
echo -e "${GREEN}Authors index built!${NC}"
}
# Compare current and previous authors index to find affected authors and check if index needs rebuild
# Exports: AFFECTED_AUTHORS (space-separated list of author names)
# AUTHORS_INDEX_NEEDS_REBUILD ("true" or "false")
identify_affected_authors() {
local authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt"
local authors_index_prev_file="${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt"
export AFFECTED_AUTHORS=""
export AUTHORS_INDEX_NEEDS_REBUILD="false"
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
local authors_index_data
authors_index_data=$(ram_mode_get_dataset "authors_index")
if [ -n "$authors_index_data" ]; then
AFFECTED_AUTHORS=$(printf '%s\n' "$authors_index_data" | awk -F'|' 'NF { print $1 }' | sort -u | tr '\n' ' ')
AUTHORS_INDEX_NEEDS_REBUILD="true"
fi
export AFFECTED_AUTHORS
export AUTHORS_INDEX_NEEDS_REBUILD
return 0
fi
# If previous index doesn't exist, all authors in the current index are affected,
# and the main index needs rebuilding.
if [ ! -f "$authors_index_prev_file" ]; then
if [ -s "$authors_index_file" ]; then # Check if current index has content
echo "Previous authors index not found. Marking all authors as affected." >&2 # Debug
AFFECTED_AUTHORS=$(cut -d'|' -f1 "$authors_index_file" | sort -u | tr '\n' ' ')
AUTHORS_INDEX_NEEDS_REBUILD="true"
else
echo "Both previous and current authors indexes are missing or empty. No authors affected." >&2 # Debug
fi
export AFFECTED_AUTHORS
export AUTHORS_INDEX_NEEDS_REBUILD
return 0
fi
# If current index doesn't exist (but previous did), means all posts were deleted?
# Mark authors from previous index as affected, index needs rebuild.
if [ ! -f "$authors_index_file" ] || [ ! -s "$authors_index_file" ]; then
echo "Current authors index not found or empty. Marking all previous authors as affected." >&2 # Debug
AFFECTED_AUTHORS=$(cut -d'|' -f1 "$authors_index_prev_file" | sort -u | tr '\n' ' ')
AUTHORS_INDEX_NEEDS_REBUILD="true"
export AFFECTED_AUTHORS
export AUTHORS_INDEX_NEEDS_REBUILD
return 0
fi
# Extract AuthorName|Filename from both files for precise comparison
local current_entries="${CACHE_DIR:-.bssg_cache}/authors_curr_af.$$"
local prev_entries="${CACHE_DIR:-.bssg_cache}/authors_prev_af.$$"
trap 'rm -f "$current_entries" "$prev_entries"' RETURN
cut -d'|' -f1,7 "$authors_index_file" | sort > "$current_entries"
cut -d'|' -f1,7 "$authors_index_prev_file" | sort > "$prev_entries"
# Find differences (lines unique to current or previous)
local diff_output
diff_output=$(comm -3 "$current_entries" "$prev_entries")
# Extract unique author names from the differences
if [ -n "$diff_output" ]; then
AFFECTED_AUTHORS=$(echo "$diff_output" | sed 's/^[[:space:]]*//' | cut -d'|' -f1 | sort -u | tr '\n' ' ')
echo "Affected authors identified: $AFFECTED_AUTHORS" >&2 # Debug
else
echo "No difference in posts per author found." >&2 # Debug
AFFECTED_AUTHORS=""
fi
# Compare author counts (AuthorName|Count) to see if the main index needs rebuilding
local current_counts="${CACHE_DIR:-.bssg_cache}/authors_curr_counts.$$"
local prev_counts="${CACHE_DIR:-.bssg_cache}/authors_prev_counts.$$"
trap 'rm -f "$current_entries" "$prev_entries" "$current_counts" "$prev_counts"' RETURN
cut -d'|' -f1 "$authors_index_file" | sort | uniq -c | awk '{print $2"|"$1}' | sort > "$current_counts"
cut -d'|' -f1 "$authors_index_prev_file" | sort | uniq -c | awk '{print $2"|"$1}' | sort > "$prev_counts"
if ! cmp -s "$current_counts" "$prev_counts"; then
echo "Author counts differ. Main authors index needs rebuild." >&2 # Debug
AUTHORS_INDEX_NEEDS_REBUILD="true"
else
echo "Author counts are the same." >&2 # Debug
AUTHORS_INDEX_NEEDS_REBUILD="false"
fi
export AFFECTED_AUTHORS
export AUTHORS_INDEX_NEEDS_REBUILD
rm -f "$current_entries" "$prev_entries" "$current_counts" "$prev_counts"
trap - RETURN # Remove trap upon successful completion
}
# Build archive index by year and month from the file index
build_archive_index() {
echo -e "${YELLOW}Building archive index...${NC}"
@ -269,16 +625,53 @@ build_archive_index() {
local file_index="${CACHE_DIR:-.bssg_cache}/file_index.txt"
local archive_index_file="${CACHE_DIR:-.bssg_cache}/archive_index.txt"
# Check if rebuild is needed
local rebuild_needed=false
if indexes_need_rebuild; then rebuild_needed=true;
elif [ ! -f "$archive_index_file" ]; then rebuild_needed=true;
elif [ "$file_index" -nt "$archive_index_file" ]; then
echo -e "${YELLOW}File index is newer than archive index, rebuilding archives...${NC}";
rebuild_needed=true;
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
local file_index_data archive_index_data=""
file_index_data=$(ram_mode_get_dataset "file_index")
if [ -z "$file_index_data" ]; then
ram_mode_set_dataset "archive_index" ""
echo -e "${GREEN}Archive index built!${NC}"
return 0
fi
local line file filename title date lastmod tags slug image image_caption description author_name author_email
while IFS= read -r line; do
[ -z "$line" ] && continue
IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line"
[ -z "$date" ] && continue
local year month month_name
if [[ "$date" =~ ^([0-9]{4})[-/]([0-9]{1,2})[-/]([0-9]{1,2}) ]]; then
year="${BASH_REMATCH[1]}"
month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
else
continue
fi
local month_name_var="MSG_MONTH_${month}"
month_name="${!month_name_var}"
if [[ -z "$month_name" ]]; then
month_name="$month"
fi
archive_index_data+="$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description|$author_name|$author_email"$'\n'
done <<< "$file_index_data"
ram_mode_set_dataset "archive_index" "$archive_index_data"
echo -e "${GREEN}Archive index built!${NC}"
return 0
fi
if ! $rebuild_needed; then
# Check if rebuild is needed: missing cache or input/dependencies changed
local rebuild_needed=false
if [ ! -f "$archive_index_file" ]; then
rebuild_needed=true
elif file_needs_rebuild "$file_index" "$archive_index_file"; then
echo -e "${YELLOW}Archive index is outdated or dependencies changed, rebuilding archives...${NC}"
rebuild_needed=true
fi
if [ "$rebuild_needed" = false ]; then
echo -e "${GREEN}Archive index is up to date, skipping...${NC}"
return 0
fi
@ -293,9 +686,9 @@ build_archive_index() {
> "$archive_index_file" # Clear the file
# Read from file index and extract date info
local line file filename title date lastmod tags slug image image_caption description
local line file filename title date lastmod tags slug image image_caption description author_name author_email
while IFS= read -r line || [[ -n "$line" ]]; do
IFS='|' read -r file filename title date lastmod tags slug image image_caption description <<< "$line"
IFS='|' read -r file filename title date lastmod tags slug image image_caption description author_name author_email <<< "$line"
if [ -n "$date" ]; then
local year month month_name
@ -334,8 +727,8 @@ build_archive_index() {
[[ -z "$month_name" ]] && month_name="Unknown"
fi
# Output: Year|MonthNum|MonthName|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription
echo "$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description" >> "$archive_index_file"
# Output: Year|MonthNum|MonthName|PostTitle|PostDate|PostLastMod|PostFilename|PostSlug|PostImage|PostImageCaption|PostDescription|AuthorName|AuthorEmail
echo "$year|$month|$month_name|$title|$date|$lastmod|$filename.html|$slug|$image|$image_caption|$description|$author_name|$author_email" >> "$archive_index_file"
fi
done < "$file_index"
@ -344,4 +737,95 @@ build_archive_index() {
echo -e "${GREEN}Archive index built!${NC}"
}
# --- Indexing Functions --- END ---
# Compare current and previous archive index to find affected months and check if index needs rebuild
# Exports: AFFECTED_ARCHIVE_MONTHS (space-separated list of "YYYY|MM")
# ARCHIVE_INDEX_NEEDS_REBUILD ("true" or "false")
identify_affected_archive_months() {
local archive_index_file="${CACHE_DIR:-.bssg_cache}/archive_index.txt"
local archive_index_prev_file="${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt"
export AFFECTED_ARCHIVE_MONTHS=""
export ARCHIVE_INDEX_NEEDS_REBUILD="false"
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
local archive_index_data
archive_index_data=$(ram_mode_get_dataset "archive_index")
if [ -n "$archive_index_data" ]; then
AFFECTED_ARCHIVE_MONTHS=$(printf '%s\n' "$archive_index_data" | awk -F'|' 'NF { print $1 "|" $2 }' | sort -u | tr '\n' ' ')
ARCHIVE_INDEX_NEEDS_REBUILD="true"
fi
export AFFECTED_ARCHIVE_MONTHS
export ARCHIVE_INDEX_NEEDS_REBUILD
return 0
fi
# If previous index doesn't exist, all months in the current index are affected,
# and the main index needs rebuilding.
if [ ! -f "$archive_index_prev_file" ]; then
if [ -s "$archive_index_file" ]; then # Check if current index has content
echo "Previous archive index not found. Marking all months as affected." >&2 # Debug
AFFECTED_ARCHIVE_MONTHS=$(cut -d'|' -f1,2 "$archive_index_file" | sort -u | tr '\n' ' ')
ARCHIVE_INDEX_NEEDS_REBUILD="true"
else
echo "Both previous and current archive indexes are missing or empty. No months affected." >&2 # Debug
fi
export AFFECTED_ARCHIVE_MONTHS
export ARCHIVE_INDEX_NEEDS_REBUILD
return 0
fi
# If current index doesn't exist (but previous did), means all posts were deleted?
# Mark months from previous index as affected, index needs rebuild.
if [ ! -f "$archive_index_file" ] || [ ! -s "$archive_index_file" ]; then
echo "Current archive index not found or empty. Marking all previous months as affected." >&2 # Debug
AFFECTED_ARCHIVE_MONTHS=$(cut -d'|' -f1,2 "$archive_index_prev_file" | sort -u | tr '\n' ' ')
ARCHIVE_INDEX_NEEDS_REBUILD="true"
export AFFECTED_ARCHIVE_MONTHS
export ARCHIVE_INDEX_NEEDS_REBUILD
return 0
fi
# Extract YYYY|MM|Filename from both files for precise comparison
local current_entries="${CACHE_DIR:-.bssg_cache}/archive_curr_ymf.$$"
local prev_entries="${CACHE_DIR:-.bssg_cache}/archive_prev_ymf.$$"
trap 'rm -f "$current_entries" "$prev_entries"' RETURN
cut -d'|' -f1,2,7 "$archive_index_file" | sort > "$current_entries"
cut -d'|' -f1,2,7 "$archive_index_prev_file" | sort > "$prev_entries"
# Find differences (lines unique to current or previous)
local diff_output
diff_output=$(comm -3 "$current_entries" "$prev_entries")
# Extract unique YYYY|MM pairs from the differences
if [ -n "$diff_output" ]; then
AFFECTED_ARCHIVE_MONTHS=$(echo "$diff_output" | sed 's/^[[:space:]]*//' | cut -d'|' -f1,2 | sort -u | tr '\n' ' ')
echo "Affected months identified: $AFFECTED_ARCHIVE_MONTHS" >&2 # Debug
else
echo "No difference in posts per month found." >&2 # Debug
AFFECTED_ARCHIVE_MONTHS=""
fi
# Compare month counts (YYYY|MM|Count) to see if the main index needs rebuilding
local current_counts="${CACHE_DIR:-.bssg_cache}/archive_curr_counts.$$"
local prev_counts="${CACHE_DIR:-.bssg_cache}/archive_prev_counts.$$"
trap 'rm -f "$current_entries" "$prev_entries" "$current_counts" "$prev_counts"' RETURN
cut -d'|' -f1,2 "$archive_index_file" | sort | uniq -c | awk '{print $2"|"$1}' | sort > "$current_counts"
cut -d'|' -f1,2 "$archive_index_prev_file" | sort | uniq -c | awk '{print $2"|"$1}' | sort > "$prev_counts"
if ! cmp -s "$current_counts" "$prev_counts"; then
echo "Month counts differ. Main archive index needs rebuild." >&2 # Debug
ARCHIVE_INDEX_NEEDS_REBUILD="true"
else
echo "Month counts are the same." >&2 # Debug
ARCHIVE_INDEX_NEEDS_REBUILD="false"
fi
export AFFECTED_ARCHIVE_MONTHS
export ARCHIVE_INDEX_NEEDS_REBUILD
rm -f "$current_entries" "$prev_entries" "$current_counts" "$prev_counts"
trap - RETURN # Remove trap upon successful completion
}
# --- Indexing Functions --- END ---

View file

@ -13,6 +13,7 @@ BUILD_START_TIME=$(date +%s)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Determine the project root (one level up from the SCRIPT_DIR's parent)
PROJECT_ROOT="$( dirname "$( dirname "$SCRIPT_DIR" )" )"
export BSSG_PROJECT_ROOT="$PROJECT_ROOT"
# Check if PROJECT_ROOT is already the current directory to avoid unnecessary cd
if [ "$PWD" != "$PROJECT_ROOT" ]; then
echo "Changing directory to project root: $PROJECT_ROOT"
@ -22,22 +23,20 @@ else
fi
# --- Load Config First ---
# Source Config Loader (sets defaults using :-, loads config.sh, config.sh.local)
# This establishes the base config (Config File > Default)
# shellcheck source=config_loader.sh
source "${SCRIPT_DIR}/config_loader.sh" || { echo -e "${RED}Error: Failed to source config_loader.sh${NC}"; exit 1; }
echo "Loaded base configuration ('${CONFIG_FILE:-config.sh}') and locales."
# Configuration and arguments are now expected to be handled by the calling script (bssg.sh)
# and passed via exported environment variables. We assume variables like SRC_DIR,
# OUTPUT_DIR, THEME, FORCE_REBUILD, etc., are already set in the environment.
# --- Parse CLI Arguments ---
# Source CLI handler (defines parse_args, show_help)
# shellcheck source=cli.sh
source "${SCRIPT_DIR}/cli.sh" || { echo -e "${RED}Error: Failed to source cli.sh${NC}"; exit 1; }
# Argument parsing is now handled by bssg.sh. Specific build options might still
# be passed, but core config like --config is handled before this script runs.
# We may need to parse specific build flags later if needed.
# Now parse args. This applies CLI overrides (CLI > Config File > Default)
parse_args "$@"
echo "Parsed command line arguments (CLI overrides applied)."
# Placeholder for any potential build-specific argument parsing if necessary in the future
# --- Handle Random Theme (after CLI parsing) --- START ---
# Random theme logic might need adjustment if THEME is purely from env var
# Check if THEME is set to 'random' from the environment
if [[ "${THEME:-default}" == "random" ]]; then
echo -e "${YELLOW}Theme set to random, selecting a random theme...${NC}"
# Find available themes (directories in THEMES_DIR)
@ -67,55 +66,229 @@ if [[ "${THEME:-default}" == "random" ]]; then
fi
# --- Handle Random Theme --- END ---
# Print the theme being used for this build (final value after CLI override)
echo -e "${GREEN}Using theme: ${THEME}${NC}"
# Re-export variables potentially overridden by parse_args.
# This ensures subsequent scripts see the final values.
echo "Re-exporting variables potentially set by CLI..."
export SRC_DIR OUTPUT_DIR TEMPLATES_DIR THEMES_DIR STATIC_DIR THEME
export SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL
export POSTS_PER_PAGE CLEAN_OUTPUT FORCE_REBUILD CONFIG_FILE
export PAGES_DIR DRAFTS_DIR
# This is no longer strictly necessary as the calling script exports them,
# but keep it for now in case some build-specific args modify env vars locally.
# echo "Re-exporting variables potentially set by CLI..."
# export SRC_DIR OUTPUT_DIR TEMPLATES_DIR THEMES_DIR STATIC_DIR THEME
# export SITE_TITLE SITE_DESCRIPTION SITE_URL AUTHOR_NAME AUTHOR_EMAIL
# export POSTS_PER_PAGE CLEAN_OUTPUT FORCE_REBUILD CONFIG_FILE
# export PAGES_DIR DRAFTS_DIR
# Add any other variable that parse_args can change
# Source Utilities (needs sourced colors) AFTER config/cli
# Source Utilities - MUST BE SOURCED FIRST to get print_info etc.
# Needs to happen before any potential print statements.
# shellcheck source=utils.sh
source "${SCRIPT_DIR}/utils.sh" || { echo -e "${RED}Error: Failed to source utils.sh${NC}"; exit 1; }
source "${SCRIPT_DIR}/utils.sh" || { echo -e "\033[0;31mError: Failed to source utils.sh\033[0m"; exit 1; }
# Build mode validation and setup
BUILD_MODE="${BUILD_MODE:-normal}"
case "$BUILD_MODE" in
normal|ram) ;;
*)
echo -e "${RED}Error: Invalid BUILD_MODE '$BUILD_MODE'. Use 'normal' or 'ram'.${NC}" >&2
exit 1
;;
esac
export BUILD_MODE
export BSSG_RAM_MODE=false
# Print the theme being used for this build (final value after potential random selection)
echo -e "${GREEN}Using theme: ${THEME}${NC}"
echo "Loaded utilities."
# --- RAM Mode Stage Timing --- START ---
BSSG_RAM_TIMING_ENABLED=false
if [ "$BUILD_MODE" = "ram" ]; then
BSSG_RAM_TIMING_ENABLED=true
fi
declare -ga BSSG_RAM_TIMING_STAGE_KEYS=()
declare -ga BSSG_RAM_TIMING_STAGE_LABELS=()
declare -ga BSSG_RAM_TIMING_STAGE_MS=()
BSSG_RAM_TIMING_STAGE_ACTIVE=false
BSSG_RAM_TIMING_CURRENT_STAGE_KEY=""
BSSG_RAM_TIMING_CURRENT_STAGE_LABEL=""
BSSG_RAM_TIMING_CURRENT_STAGE_START_MS=0
_bssg_ram_timing_now_ms() {
if [ -n "${EPOCHREALTIME:-}" ]; then
local epoch_norm sec frac ms_part
# Some locales expose EPOCHREALTIME with ',' instead of '.' as decimal separator.
epoch_norm="${EPOCHREALTIME/,/.}"
if [[ "$epoch_norm" =~ ^([0-9]+)([.][0-9]+)?$ ]]; then
sec="${BASH_REMATCH[1]}"
frac="${BASH_REMATCH[2]#.}"
frac="${frac}000"
ms_part="${frac:0:3}"
printf '%s\n' $(( 10#$sec * 1000 + 10#$ms_part ))
return
fi
fi
if command -v perl >/dev/null 2>&1; then
perl -MTime::HiRes=time -e 'printf("%.0f\n", time()*1000)'
else
printf '%s\n' $(( $(date +%s) * 1000 ))
fi
}
_bssg_ram_timing_format_ms() {
local ms="$1"
printf '%d.%03ds' $((ms / 1000)) $((ms % 1000))
}
bssg_ram_timing_start() {
if [ "$BSSG_RAM_TIMING_ENABLED" != true ]; then
return
fi
if [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" = true ]; then
bssg_ram_timing_end
fi
BSSG_RAM_TIMING_CURRENT_STAGE_KEY="$1"
BSSG_RAM_TIMING_CURRENT_STAGE_LABEL="$2"
BSSG_RAM_TIMING_CURRENT_STAGE_START_MS="$(_bssg_ram_timing_now_ms)"
BSSG_RAM_TIMING_STAGE_ACTIVE=true
}
bssg_ram_timing_end() {
if [ "$BSSG_RAM_TIMING_ENABLED" != true ] || [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" != true ]; then
return
fi
local end_ms elapsed_ms
end_ms="$(_bssg_ram_timing_now_ms)"
elapsed_ms=$((end_ms - BSSG_RAM_TIMING_CURRENT_STAGE_START_MS))
if [ "$elapsed_ms" -lt 0 ]; then
elapsed_ms=0
fi
BSSG_RAM_TIMING_STAGE_KEYS+=("$BSSG_RAM_TIMING_CURRENT_STAGE_KEY")
BSSG_RAM_TIMING_STAGE_LABELS+=("$BSSG_RAM_TIMING_CURRENT_STAGE_LABEL")
BSSG_RAM_TIMING_STAGE_MS+=("$elapsed_ms")
BSSG_RAM_TIMING_STAGE_ACTIVE=false
BSSG_RAM_TIMING_CURRENT_STAGE_KEY=""
BSSG_RAM_TIMING_CURRENT_STAGE_LABEL=""
BSSG_RAM_TIMING_CURRENT_STAGE_START_MS=0
}
bssg_ram_timing_print_summary() {
if [ "$BSSG_RAM_TIMING_ENABLED" != true ]; then
return
fi
# Close any open stage (defensive; build flow should end stages explicitly).
if [ "$BSSG_RAM_TIMING_STAGE_ACTIVE" = true ]; then
bssg_ram_timing_end
fi
local count="${#BSSG_RAM_TIMING_STAGE_MS[@]}"
if [ "$count" -eq 0 ]; then
return
fi
local total_ms=0
local max_ms=0
local max_label=""
local i
for ((i = 0; i < count; i++)); do
local stage_ms="${BSSG_RAM_TIMING_STAGE_MS[$i]}"
total_ms=$((total_ms + stage_ms))
if [ "$stage_ms" -gt "$max_ms" ]; then
max_ms="$stage_ms"
max_label="${BSSG_RAM_TIMING_STAGE_LABELS[$i]}"
fi
done
echo "------------------------------------------------------"
echo -e "${GREEN}RAM mode timing summary:${NC}"
printf " %-26s %12s %10s\n" "Stage" "Duration" "Share"
for ((i = 0; i < count; i++)); do
local stage_label="${BSSG_RAM_TIMING_STAGE_LABELS[$i]}"
local stage_ms="${BSSG_RAM_TIMING_STAGE_MS[$i]}"
local pct_tenths=0
if [ "$total_ms" -gt 0 ]; then
pct_tenths=$(( (stage_ms * 1000 + total_ms / 2) / total_ms ))
fi
local formatted_ms
formatted_ms="$(_bssg_ram_timing_format_ms "$stage_ms")"
printf " %-26s %12s %6d.%d%%\n" "$stage_label" "$formatted_ms" $((pct_tenths / 10)) $((pct_tenths % 10))
done
echo -e " ${GREEN}Total (timed stages):$(_bssg_ram_timing_format_ms "$total_ms")${NC}"
if [ -n "$max_label" ]; then
echo -e " ${YELLOW}Slowest stage:${NC} ${max_label} ($(_bssg_ram_timing_format_ms "$max_ms"))"
fi
}
# --- RAM Mode Stage Timing --- END ---
# Check Dependencies
# shellcheck source=deps.sh
bssg_ram_timing_start "dependencies" "Dependencies"
source "${SCRIPT_DIR}/deps.sh" || { echo -e "${RED}Error: Failed to source deps.sh${NC}"; exit 1; }
check_dependencies # Call the function to perform checks and export HAS_PARALLEL
bssg_ram_timing_end
if [ "$BUILD_MODE" = "ram" ]; then
export BSSG_RAM_MODE=true
export FORCE_REBUILD=true
# shellcheck source=ram_mode.sh
source "${SCRIPT_DIR}/ram_mode.sh" || { echo -e "${RED}Error: Failed to source ram_mode.sh${NC}"; exit 1; }
print_info "RAM mode enabled: source/template files and build indexes are held in memory."
print_info "RAM mode parallel worker cap: ${RAM_MODE_MAX_JOBS:-6} (set RAM_MODE_MAX_JOBS to tune)."
fi
echo "Checked dependencies. Parallel available: ${HAS_PARALLEL:-false}"
# Source Cache Manager (defines cache functions)
# shellcheck source=cache.sh
bssg_ram_timing_start "cache_setup" "Cache Setup/Clean"
source "${SCRIPT_DIR}/cache.sh" || { echo -e "${RED}Error: Failed to source cache.sh${NC}"; exit 1; }
echo "Loaded cache manager."
# Check if config changed BEFORE updating the hash file, store status for later use
BSSG_CONFIG_CHANGED_STATUS=1 # Default to 1 (not changed)
if config_has_changed; then
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
# RAM mode is intentionally ephemeral, always rebuild from preloaded inputs.
BSSG_CONFIG_CHANGED_STATUS=0
elif config_has_changed; then
BSSG_CONFIG_CHANGED_STATUS=0 # Set to 0 (changed)
fi
export BSSG_CONFIG_CHANGED_STATUS
# --- Initial Cache Setup & Cleaning --- START
# IMPORTANT: CACHE_DIR must be defined (usually in config) and available
# --- Add check for CLEAN_OUTPUT influencing FORCE_REBUILD --- START ---
if [ "${CLEAN_OUTPUT:-false}" = true ]; then
if [ "${FORCE_REBUILD:-false}" != true ]; then
echo -e "${YELLOW}Clean output requested (--clean-output), forcing rebuild (--force-rebuild)...${NC}"
FORCE_REBUILD=true
export FORCE_REBUILD # Ensure the variable is exported if changed here
fi
fi
# --- Add check for CLEAN_OUTPUT influencing FORCE_REBUILD --- END ---
# Handle --force-rebuild first
if [ "${FORCE_REBUILD:-false}" = true ]; then
if [ "${BSSG_RAM_MODE:-false}" != true ] && [ "${FORCE_REBUILD:-false}" = true ]; then
echo -e "${YELLOW}Force rebuild enabled, deleting entire cache directory (${CACHE_DIR:-.bssg_cache})...${NC}"
rm -rf "${CACHE_DIR:-.bssg_cache}"
echo -e "${GREEN}Cache deleted!${NC}"
fi
echo "Ensuring cache directory structure exists... (${CACHE_DIR:-.bssg_cache})"
mkdir -p "${CACHE_DIR:-.bssg_cache}/meta" "${CACHE_DIR:-.bssg_cache}/content"
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
echo "Ensuring cache directory structure exists... (${CACHE_DIR:-.bssg_cache})"
mkdir -p "${CACHE_DIR:-.bssg_cache}/meta" "${CACHE_DIR:-.bssg_cache}/content"
# Create initial config hash *after* ensuring cache dir exists
create_config_hash
# Create initial config hash *after* ensuring cache dir exists
create_config_hash
else
echo "RAM mode: skipping cache directory creation and config hash persistence."
fi
# --- Initial Cache Setup & Cleaning --- END
# Handle --clean-output flag (using logic moved from original main/clean_output_directory)
@ -135,32 +308,154 @@ if [ "${CLEAN_OUTPUT:-false}" = true ]; then
echo -e "${YELLOW}Output directory (${OUTPUT_DIR:-output}) does not exist, no need to clean.${NC}"
fi
fi
# Load Templates (and generate dynamic menus, exports vars like HEADER_TEMPLATE)
# shellcheck source=templates.sh
source "${SCRIPT_DIR}/templates.sh" || { echo -e "${RED}Error: Failed to source templates.sh${NC}"; exit 1; }
preload_templates # Call the function
echo "Loaded and processed templates."
bssg_ram_timing_end
# Source Content Processor (defines functions like extract_metadata, convert_markdown_to_html)
# Moved up before indexing as indexing uses some content functions (e.g., generate_slug)
# shellcheck source=content.sh
bssg_ram_timing_start "index_build" "Index/Data Build"
source "${SCRIPT_DIR}/content.sh" || { echo -e "${RED}Error: Failed to source content.sh${NC}"; exit 1; }
echo "Loaded content processing functions."
# Source Indexing Script (defines index building functions)
# Moved up before preload_templates
# shellcheck source=indexing.sh
source "${SCRIPT_DIR}/indexing.sh" || { echo -e "${RED}Error: Failed to source indexing.sh${NC}"; exit 1; }
echo "Loaded indexing functions."
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
ram_mode_preload_inputs || { echo -e "${RED}Error: RAM preload failed.${NC}"; exit 1; }
fi
# --- Build Intermediate Indexes ---
# Moved up before preload_templates
# --- Start Change: Snapshot previous file index ---
file_index_file="${CACHE_DIR:-.bssg_cache}/file_index.txt"
file_index_prev_file="${CACHE_DIR:-.bssg_cache}/file_index_prev.txt"
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
if [ -f "$file_index_file" ]; then
echo "Snapshotting previous file index to $file_index_prev_file" >&2 # Debug
cp "$file_index_file" "$file_index_prev_file"
else
# Ensure previous file doesn't exist if current doesn't
rm -f "$file_index_prev_file"
fi
fi
# --- End Change ---
optimized_build_file_index || { echo -e "${RED}Error: Failed to build file index.${NC}"; exit 1; }
# --- Start Change: Snapshot previous tags index ---
tags_index_file="${CACHE_DIR:-.bssg_cache}/tags_index.txt"
tags_index_prev_file="${CACHE_DIR:-.bssg_cache}/tags_index_prev.txt"
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
if [ -f "$tags_index_file" ]; then
echo "Snapshotting previous tags index to $tags_index_prev_file" >&2 # Debug
cp "$tags_index_file" "$tags_index_prev_file"
else
# Ensure previous file doesn't exist if current doesn't
rm -f "$tags_index_prev_file"
fi
fi
# --- End Change ---
build_tags_index || { echo -e "${RED}Error: Failed to build tags index.${NC}"; exit 1; }
# --- Start Debug: Show tags_index.txt content ---
# echo "DEBUG: Content of $tags_index_file after build:" >&2
# cat "$tags_index_file" >&2
# echo "--- End $tags_index_file DEBUG ---" >&2
# --- End Debug ---
# --- Start Change: Snapshot previous authors index ---
authors_index_file="${CACHE_DIR:-.bssg_cache}/authors_index.txt"
authors_index_prev_file="${CACHE_DIR:-.bssg_cache}/authors_index_prev.txt"
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
if [ -f "$authors_index_file" ]; then
echo "Snapshotting previous authors index to $authors_index_prev_file" >&2 # Debug
cp "$authors_index_file" "$authors_index_prev_file"
else
# Ensure previous file doesn't exist if current doesn't
rm -f "$authors_index_prev_file"
fi
fi
# --- End Change ---
build_authors_index || { echo -e "${RED}Error: Failed to build authors index.${NC}"; exit 1; }
# --- Start Change: Identify affected authors ---
identify_affected_authors || { echo -e "${RED}Error: Failed to identify affected authors.${NC}"; exit 1; }
# --- End Change ---
if [ "${ENABLE_ARCHIVES:-false}" = true ]; then
# --- Start Change: Snapshot previous archive index ---
archive_index_file="${CACHE_DIR:-.bssg_cache}/archive_index.txt"
archive_index_prev_file="${CACHE_DIR:-.bssg_cache}/archive_index_prev.txt"
if [ "${BSSG_RAM_MODE:-false}" != true ]; then
if [ -f "$archive_index_file" ]; then
echo "Snapshotting previous archive index to $archive_index_prev_file" >&2 # Debug
cp "$archive_index_file" "$archive_index_prev_file"
else
# Ensure previous file doesn't exist if current doesn't
rm -f "$archive_index_prev_file"
fi
fi
# --- End Change ---
build_archive_index || { echo -e "${RED}Error: Failed to build archive index.${NC}"; exit 1; }
# --- Start Change: Identify affected archive months ---
identify_affected_archive_months || { echo -e "${RED}Error: Failed to identify affected archive months.${NC}"; exit 1; }
# --- End Change ---
fi
echo "Built intermediate cache indexes."
bssg_ram_timing_end
# Load Templates (and generate dynamic menus, exports vars like HEADER_TEMPLATE)
# Moved down after indexing
# shellcheck source=templates.sh
bssg_ram_timing_start "templates" "Template Prep"
source "${SCRIPT_DIR}/templates.sh" || { echo -e "${RED}Error: Failed to source templates.sh${NC}"; exit 1; }
preload_templates # Call the function
echo "Loaded and processed templates."
# --- Pre-calculate Max Template/Locale Time --- START ---
# Moved down, should happen after templates are loaded
echo "Pre-calculating latest template/locale modification time..."
# IMPORTANT: Requires TEMPLATES_DIR, THEMES_DIR, THEME, LOCALE_DIR, SITE_LANG, get_file_mtime to be available
latest_template_locale_time=0
# Determine active template directory
active_template_dir="${TEMPLATES_DIR:-templates}"
if [ -d "$active_template_dir/${THEME:-default}" ]; then
active_template_dir="$active_template_dir/${THEME:-default}"
fi
header_template_file="$active_template_dir/header.html"
footer_template_file="$active_template_dir/footer.html"
# Determine active locale file
active_locale_file=""
if [ -f "${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh" ]; then
active_locale_file="${LOCALE_DIR:-locales}/${SITE_LANG:-en}.sh"
elif [ -f "${LOCALE_DIR:-locales}/en.sh" ]; then # Fallback to en.sh
active_locale_file="${LOCALE_DIR:-locales}/en.sh"
fi
# Get timestamps (returns 0 if file doesn't exist)
header_time=$(get_file_mtime "$header_template_file")
footer_time=$(get_file_mtime "$footer_template_file")
locale_time=$(get_file_mtime "$active_locale_file")
# Find the maximum time
latest_template_locale_time=$header_time
if (( footer_time > latest_template_locale_time )); then
latest_template_locale_time=$footer_time
fi
if (( locale_time > latest_template_locale_time )); then
latest_template_locale_time=$locale_time
fi
export BSSG_MAX_TEMPLATE_LOCALE_TIME=$latest_template_locale_time
echo "Latest template/locale time: $BSSG_MAX_TEMPLATE_LOCALE_TIME (Header: $header_time, Footer: $footer_time, Locale: $locale_time)"
# --- Pre-calculate Max Template/Locale Time --- END ---
bssg_ram_timing_end
# --- Prepare for Parallel Processing ---
if [ "${HAS_PARALLEL:-false}" = true ]; then
@ -175,65 +470,143 @@ if [ "${HAS_PARALLEL:-false}" = true ]; then
export CACHE_DIR FORCE_REBUILD POSTS_DIR PAGES_DIR OUTPUT_DIR SITE_URL SITE_TITLE SITE_DESCRIPTION AUTHOR_NAME
export HEADER_TEMPLATE FOOTER_TEMPLATE POST_URL_FORMAT PAGE_URL_FORMAT BASE_URL PRETTY_URLS
export CACHE_READING_TIME READING_TIME_WPM MARKDOWN_PROCESSOR MARKDOWN_PL_PATH DATE_FORMAT TIMEZONE SHOW_TIMEZONE
export SHOW_HEADER_MENU SHOW_INDEX_DESCRIPTIONS GENERATE_EXCERPT SHOW_READING_TIME
# Export the pre-calculated time for parallel jobs
export BSSG_MAX_TEMPLATE_LOCALE_TIME
echo "Core parallel exports complete."
fi
# --- Related Posts Cache Invalidation --- START ---
# This will be handled during post processing for better timing
RELATED_POSTS_INVALIDATED_LIST=""
if [ "${ENABLE_RELATED_POSTS:-true}" = true ]; then
# Export the variable for use by generate_posts.sh
export RELATED_POSTS_INVALIDATED_LIST
fi
# --- Related Posts Cache Invalidation --- END ---
# --- Generate Content HTML ---
# Source and run Post Generator
# shellcheck source=generate_posts.sh
bssg_ram_timing_start "posts" "Posts"
source "${SCRIPT_DIR}/generate_posts.sh" || { echo -e "${RED}Error: Failed to source generate_posts.sh${NC}"; exit 1; }
process_all_markdown_files || { echo -e "${RED}Error: Post processing failed.${NC}"; exit 1; }
echo "Generated post HTML files."
bssg_ram_timing_end
# --- Post Generation --- END ---
# --- Page Generation --- START ---
# --- Page Generation --- START --
# Source the page generation script
# shellcheck source=generate_pages.sh disable=SC1091
bssg_ram_timing_start "pages" "Static Pages"
source "$SCRIPT_DIR/generate_pages.sh" || { echo -e "${RED}Error: Failed to source generate_pages.sh${NC}"; exit 1; }
# Call the main page processing function
process_all_pages || { echo -e "${RED}Error: Page processing failed.${NC}"; exit 1; }
bssg_ram_timing_end
# --- Page Generation --- END ---
# --- Tag Page Generation --- START ---
# Source and run Tag Page Generator
# shellcheck source=generate_tags.sh disable=SC1091
bssg_ram_timing_start "tags" "Tags"
source "$SCRIPT_DIR/generate_tags.sh" || { echo -e "${RED}Error: Failed to source generate_tags.sh${NC}"; exit 1; }
# Call the main function from the sourced script
generate_tag_pages || { echo -e "${RED}Error: Tag page generation failed.${NC}"; exit 1; }
echo "Generated tag list pages."
bssg_ram_timing_end
# --- Tag Page Generation --- END ---
# --- Author Page Generation --- START ---
# Source and run Author Page Generator (if enabled)
if [ "${ENABLE_AUTHOR_PAGES:-true}" = true ]; then
bssg_ram_timing_start "authors" "Authors"
# shellcheck source=generate_authors.sh disable=SC1091
source "$SCRIPT_DIR/generate_authors.sh" || { echo -e "${RED}Error: Failed to source generate_authors.sh${NC}"; exit 1; }
# Call the main generation function
# It will internally use AFFECTED_AUTHORS and AUTHORS_INDEX_NEEDS_REBUILD
generate_author_pages || { echo -e "${RED}Error: Author page generation failed.${NC}"; exit 1; }
echo "Generated author pages."
bssg_ram_timing_end
fi
# --- Author Page Generation --- END ---
# --- Archive Page Generation --- START ---
# Source and run Archive Page Generator (if enabled)
if [ "${ENABLE_ARCHIVES:-false}" = true ]; then
bssg_ram_timing_start "archives" "Archives"
# Source the script (loads functions)
# shellcheck source=generate_archives.sh disable=SC1091
source "$SCRIPT_DIR/generate_archives.sh" || { echo -e "${RED}Error: Failed to source generate_archives.sh${NC}"; exit 1; }
generate_archive_pages || { echo -e "${RED}Error: Archive page generation failed.${NC}"; exit 1; } # Call the main function
# Call the main generation function
# It will internally use AFFECTED_ARCHIVE_MONTHS and ARCHIVE_INDEX_NEEDS_REBUILD
generate_archive_pages || { echo -e "${RED}Error: Archive page generation failed.${NC}"; exit 1; }
echo "Generated archive pages."
else
echo "Archives disabled, skipping generation."
bssg_ram_timing_end
fi
# --- Archive Page Generation --- END ---
# --- Main Index Page Generation --- START ---
# Source and run Main Index Page Generator
# shellcheck source=generate_index.sh disable=SC1091
bssg_ram_timing_start "main_index" "Main Index"
source "$SCRIPT_DIR/generate_index.sh" || { echo -e "${RED}Error: Failed to source generate_index.sh${NC}"; exit 1; }
# Call the main function from the sourced script
generate_index || { echo -e "${RED}Error: Index page generation failed.${NC}"; exit 1; }
echo "Generated main index/pagination pages."
bssg_ram_timing_end
# --- Main Index Page Generation --- END ---
# --- Feed Generation --- START ---
# Source and run Feed Generator
# shellcheck source=generate_feeds.sh disable=SC1091
bssg_ram_timing_start "feeds" "Sitemap/RSS"
source "$SCRIPT_DIR/generate_feeds.sh" || { echo -e "${RED}Error: Failed to source generate_feeds.sh${NC}"; exit 1; }
# Call the functions from the sourced script
generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure
generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure
echo "Timing sitemap generation..."
if [ "${BSSG_RAM_MODE:-false}" = true ]; then
echo "Timing RSS feed generation..."
feed_jobs=0
feed_jobs=$(get_parallel_jobs)
if [ "$feed_jobs" -gt 1 ]; then
echo "RAM mode: generating sitemap and RSS in parallel..."
sitemap_failed=false
rss_failed=false
generate_sitemap &
sitemap_pid=$!
generate_rss &
rss_pid=$!
if ! wait "$sitemap_pid"; then
sitemap_failed=true
fi
if ! wait "$rss_pid"; then
rss_failed=true
fi
if $sitemap_failed; then
echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}"
fi
if $rss_failed; then
echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}"
fi
else
generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure
generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure
fi
else
generate_sitemap || echo -e "${YELLOW}Sitemap generation failed, continuing build...${NC}" # Allow failure
echo "Timing RSS feed generation..."
generate_rss || echo -e "${YELLOW}RSS feed generation failed, continuing build...${NC}" # Allow failure
fi
echo "Generated RSS feed and sitemap."
bssg_ram_timing_end
# --- Feed Generation --- END ---
# --- Secondary Pages Index Generation --- START ---
@ -243,36 +616,166 @@ echo "Generated RSS feed and sitemap."
# We attempt to reconstruct the array from the exported string.
# shellcheck disable=SC2154 # SECONDARY_PAGES is exported by templates.sh
if [ -n "$SECONDARY_PAGES" ] && [ "$SECONDARY_PAGES" != "()" ]; then
bssg_ram_timing_start "secondary_index" "Secondary Index"
# shellcheck source=generate_secondary_pages.sh disable=SC1091
source "$SCRIPT_DIR/generate_secondary_pages.sh" || { echo -e "${RED}Error: Failed to source generate_secondary_pages.sh${NC}"; exit 1; }
generate_pages_index || echo -e "${YELLOW}Secondary pages index generation failed, continuing build...${NC}" # Allow failure
echo "Generated secondary pages index."
bssg_ram_timing_end
else
echo "No secondary pages found, skipping secondary pages index."
echo "No secondary pages defined, skipping secondary index generation."
fi
# --- Secondary Pages Index Generation --- END ---
# --- Asset Handling --- START ---
# Source the asset handling script
# shellcheck source=assets.sh disable=SC1091
bssg_ram_timing_start "assets" "Assets/CSS"
source "$SCRIPT_DIR/assets.sh" || { echo -e "${RED}Error: Failed to source assets.sh${NC}"; exit 1; }
# Copy static assets
echo "Timing static files copy..."
copy_static_files || { echo -e "${RED}Error: Failed to copy static assets.${NC}"; exit 1; }
# Process CSS files
# Process CSS files (includes theme asset copy)
echo "Timing CSS/Theme processing..."