231
specs/generation.allium
Normal file
231
specs/generation.allium
Normal file
@@ -0,0 +1,231 @@
|
||||
-- allium: 1
|
||||
-- bDS Static Site Generation
|
||||
-- Scope: core (Wave 4)
|
||||
-- Distilled from: src/main/engine/BlogGenerationEngine.ts,
|
||||
-- PageRenderer.ts, GenerationWorkerPool, RoutePageGenerationService
|
||||
|
||||
use "./post.allium" as post
|
||||
use "./template.allium" as template
|
||||
use "./metadata.allium" as meta
|
||||
use "./menu.allium" as menu
|
||||
use "./translation.allium" as translation
|
||||
|
||||
surface GenerationControlSurface {
|
||||
facing _: GenerationOperator
|
||||
|
||||
provides:
|
||||
GenerateSiteRequested(generation)
|
||||
ValidateSiteRequested(project)
|
||||
ApplyValidationRequested(project_id, sections)
|
||||
}
|
||||
|
||||
surface GenerationRuntimeSurface {
|
||||
facing _: GenerationRuntime
|
||||
|
||||
provides:
|
||||
PageRenderRequested(template, context)
|
||||
GenerateSiteCompleted(generation)
|
||||
}
|
||||
|
||||
value GenerationSection {
|
||||
kind: core | single | category | tag | date
|
||||
}
|
||||
|
||||
value GeneratedFile {
|
||||
relative_path: String
|
||||
content_hash: String
|
||||
}
|
||||
|
||||
entity SiteGeneration {
|
||||
project_id: String
|
||||
base_url: String
|
||||
language: String -- main language
|
||||
blog_languages: Set<String>
|
||||
max_posts_per_page: Integer
|
||||
pico_theme: String?
|
||||
sections: Set<GenerationSection>
|
||||
|
||||
-- Output tracking
|
||||
generated_files: GeneratedFile with project_id = this.project_id
|
||||
}
|
||||
|
||||
surface GenerationStatusSurface {
|
||||
context generation: SiteGeneration
|
||||
|
||||
exposes:
|
||||
generation.project_id
|
||||
generation.base_url
|
||||
generation.language
|
||||
generation.blog_languages
|
||||
generation.max_posts_per_page
|
||||
generation.pico_theme
|
||||
generation.sections
|
||||
generation.generated_files.count
|
||||
}
|
||||
|
||||
invariant IncrementalByContentHash {
|
||||
-- Files are only written when content_hash changes
|
||||
-- generatedFileHashes table tracks (projectId, relativePath, contentHash)
|
||||
-- A file with unchanged hash is skipped on regeneration
|
||||
}
|
||||
|
||||
invariant MultiLanguageRoutes {
|
||||
-- Main language: flat routes (/{yyyy}/{mm}/{dd}/{slug})
|
||||
-- Additional languages: prefixed (/{lang}/{yyyy}/{mm}/{dd}/{slug})
|
||||
-- Each language subtree gets its own feeds and archives
|
||||
}
|
||||
|
||||
invariant CanonicalBaseUrlConfigured {
|
||||
for generation in SiteGenerations:
|
||||
generation.base_url != ""
|
||||
}
|
||||
|
||||
invariant NamedPicoTheme {
|
||||
for generation in SiteGenerations where generation.pico_theme != null:
|
||||
generation.pico_theme != ""
|
||||
}
|
||||
|
||||
invariant GeneratedFilesTracked {
|
||||
for generation in SiteGenerations:
|
||||
generation.generated_files.count >= 0
|
||||
}
|
||||
|
||||
-- Core section: root pages, sitemap, RSS, Atom, calendar.json
|
||||
|
||||
rule GenerateCoreSectionPages {
|
||||
when: GenerateSiteRequested(generation)
|
||||
requires: core in generation.sections
|
||||
ensures: FileGenerated("index.html")
|
||||
ensures: FileGenerated("sitemap.xml")
|
||||
-- Multi-language sitemap with hreflang alternates
|
||||
ensures: FileGenerated("feed.xml")
|
||||
-- RSS 2.0 feed
|
||||
ensures: FileGenerated("atom.xml")
|
||||
-- Atom feed
|
||||
ensures: FileGenerated("calendar.json")
|
||||
-- Post dates for calendar widget
|
||||
for lang in generation.blog_languages - {generation.language}:
|
||||
ensures: FileGenerated(format("{lang}/index.html", lang: lang))
|
||||
ensures: FileGenerated(format("{lang}/feed.xml", lang: lang))
|
||||
ensures: FileGenerated(format("{lang}/atom.xml", lang: lang))
|
||||
}
|
||||
|
||||
-- Single section: one HTML page per published post
|
||||
|
||||
rule GenerateSinglePostPages {
|
||||
when: GenerateSiteRequested(generation)
|
||||
requires: single in generation.sections
|
||||
for p in Posts where status = published:
|
||||
let url = post_canonical_url(p)
|
||||
ensures: FileGenerated(format("{url}/index.html", url: url))
|
||||
for lang in generation.blog_languages - {generation.language}:
|
||||
if p.translations.any(t => t.language.code = lang):
|
||||
ensures: FileGenerated(format("{lang}/{url}/index.html",
|
||||
lang: lang, url: url))
|
||||
}
|
||||
|
||||
-- Category section: paginated archive per category
|
||||
|
||||
rule GenerateCategoryPages {
|
||||
when: GenerateSiteRequested(generation)
|
||||
requires: category in generation.sections
|
||||
for cat in generation.categories:
|
||||
let page_count = ceil(posts_in_category(cat).count / generation.max_posts_per_page)
|
||||
ensures: FileGenerated(format("category/{cat}/index.html", cat: cat))
|
||||
for page in page_range(2, page_count):
|
||||
ensures: FileGenerated(format("category/{cat}/page/{page}/index.html",
|
||||
cat: cat, page: page))
|
||||
}
|
||||
|
||||
-- Tag section: paginated archive per tag
|
||||
|
||||
rule GenerateTagPages {
|
||||
when: GenerateSiteRequested(generation)
|
||||
requires: tag in generation.sections
|
||||
for t in Tags where post_count > 0:
|
||||
ensures: FileGenerated(format("tag/{slug}/index.html", slug: slugify(t.name)))
|
||||
}
|
||||
|
||||
-- Date section: year and month archives
|
||||
|
||||
rule GenerateDateArchivePages {
|
||||
when: GenerateSiteRequested(generation)
|
||||
requires: date in generation.sections
|
||||
for year in distinct_years(Posts):
|
||||
ensures: FileGenerated(format("{year}/index.html", year: year))
|
||||
for month in distinct_months(Posts, year):
|
||||
ensures: FileGenerated(format("{year}/{month}/index.html",
|
||||
year: year, month: month))
|
||||
}
|
||||
|
||||
-- Template rendering context
|
||||
|
||||
rule RenderPage {
|
||||
when: PageRenderRequested(template, context)
|
||||
-- Template rendering with full context:
|
||||
-- posts, pagination, menus, tags, categories,
|
||||
-- project metadata, i18n translations, theme settings
|
||||
-- Macro expansion: [[slug param1=value1 ...]] in post content
|
||||
-- HTML rewriting for canonical post/media paths
|
||||
ensures: RenderedHtml(template, context, output)
|
||||
}
|
||||
|
||||
-- Validation
|
||||
|
||||
rule ValidateSite {
|
||||
when: ValidateSiteRequested(project)
|
||||
-- Compares sitemap URLs to HTML files on disk
|
||||
-- Detects: missing pages, extra (stale) pages, sitemap/file mismatches
|
||||
ensures: ValidationReport(missing_pages, extra_pages, stale_pages)
|
||||
}
|
||||
|
||||
rule ApplyValidation {
|
||||
when: ApplyValidationRequested(project_id, sections)
|
||||
-- Targeted re-rendering for affected sections only
|
||||
ensures: GenerateSiteRequested(plan_generation(project_id, sections))
|
||||
}
|
||||
|
||||
-- Day-block grouping for archives
|
||||
invariant ArchiveDayBlocks {
|
||||
-- Archive/list pages group posts by day
|
||||
-- Each day block has a date header and the posts for that day
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- SEARCH INDEX: PAGEFIND
|
||||
-- ============================================================================
|
||||
|
||||
-- Pagefind builds a client-side full-text search index from generated HTML.
|
||||
-- Uses an embedded Pagefind library integration rather than a CLI subprocess.
|
||||
-- Runs as the final step of the generation pipeline, after all HTML is written.
|
||||
|
||||
rule BuildSearchIndex {
|
||||
when: GenerateSiteCompleted(generation)
|
||||
-- Only runs if any pages were rendered or deleted in this generation pass.
|
||||
-- Separate index per language:
|
||||
-- Main language: source = {html}/, output = {html}/pagefind/
|
||||
-- Each additional language: source = {html}/{lang}/, output = {html}/{lang}/pagefind/
|
||||
-- Each index built with force_language set to that language code.
|
||||
for lang in {generation.language} + (generation.blog_languages - {generation.language}):
|
||||
ensures: PagefindIndexBuilt(lang)
|
||||
}
|
||||
|
||||
invariant PagefindHtmlMarking {
|
||||
-- Single-post templates must include data-pagefind-body attribute
|
||||
-- on the <article> element to scope indexing to post content only.
|
||||
-- Pagefind ignores elements without this attribute.
|
||||
}
|
||||
|
||||
invariant PagefindAssets {
|
||||
-- Generated output includes Pagefind UI assets per language:
|
||||
-- {prefix}/pagefind/pagefind-ui.css
|
||||
-- {prefix}/pagefind/pagefind-ui.js
|
||||
-- where prefix is "" for main language, "{lang}" for additional languages.
|
||||
-- Frontend templates reference these via language_prefix variable.
|
||||
-- Assets are bundled locally — no external CDN references.
|
||||
}
|
||||
|
||||
config {
|
||||
pagefind_threshold: Integer = 0
|
||||
-- Minimum pages to trigger indexing (0 = always if any rendered/deleted)
|
||||
}
|
||||
Reference in New Issue
Block a user