-- allium: 1 -- bDS Local Preview Server -- Scope: core (Wave 4) -- Distilled from: src/main/engine/PreviewServer.ts, PageRenderer.ts use "./template.allium" as template use "./generation.allium" as generation entity PreviewServer { host: String -- 127.0.0.1 port: Integer -- 4123 is_running: Boolean } config { preview_host: String = "127.0.0.1" preview_port: Integer = 4123 } surface PreviewControlSurface { facing _: PreviewOperator provides: StartPreviewRequested(project) StopPreviewRequested(server) } surface PreviewHttpSurface { facing _: PreviewClient provides: PreviewRequest(path) PreviewDraftRequest(path, post_id) } rule StartPreview { when: StartPreviewRequested(project) ensures: PreviewServer.created( host: config.preview_host, port: config.preview_port, is_running: true ) } rule StopPreview { when: StopPreviewRequested(server) -- Graceful shutdown with inflight request tracking ensures: server.is_running = false } -- Route resolution -- Preview renders all posts (published + draft) on-demand via Liquid templates. -- Content priority: DB content (draft edits) over published .md file content. -- See invariant PreviewDraftOverlay below. rule ServePostPreview { when: PreviewRequest(path) requires: is_post_path(path) -- path matches "/{yyyy}/{mm}/{dd}/{slug}" -- Finds post by slug+date regardless of status (published or draft). -- Content resolved via editor_body: DB content if present, else .md file. -- Renders via Liquid template with full PageRenderer context. ensures: PreviewResponse(rendered_html) } rule ServeDraftPreview { when: PreviewDraftRequest(path, post_id) -- Explicit draft preview by post_id (used by editor preview pane). -- Renders draft content (from DB, not filesystem). ensures: PreviewResponse(rendered_html) } rule ServeArchivePreview { when: PreviewRequest(path) requires: is_archive_path(path) -- Category, tag, date archives with pagination -- Includes both published and draft posts in listings. ensures: PreviewResponse(rendered_html) } rule ServeMediaFile { when: PreviewRequest(path) requires: is_media_path(path) -- Path-traversal protection: validates path stays within media directory ensures: PreviewResponse(media_file) } rule ServeAssets { when: PreviewRequest(path) requires: is_asset_path(path) ensures: PreviewResponse(asset_file) } rule ServeLanguagePrefixedRoute { when: PreviewRequest(path) requires: is_language_prefixed(path) -- Detects language prefix from supported languages -- Renders with translation overlay for that language ensures: PreviewResponse(translated_html) } invariant PreviewDraftOverlay { -- Preview is the draft workspace: it shows what the blog *will* look like, -- not what it currently looks like on the published site. -- -- Post universe: all posts with status in {published, draft}. -- Archived posts are excluded. -- -- Content priority (per post): -- 1. DB content field (draft edits not yet published) → used when non-nil -- 2. Published .md file (last-published snapshot) → used when DB content is nil -- 3. Empty string → fallback if neither exists -- -- This means: -- - A purely draft post (never published) renders from DB content. -- - A published-then-edited post renders from DB content (the draft edits). -- - A published post with no pending edits renders from its .md file. -- -- Contrast with generation (see generation.allium GenerationPublishedOnly): -- Generation uses *only* published .md file content, never DB draft content, -- and excludes posts that have never been published. } invariant ThemeSwitching { -- Preview supports live theme/mode switching via query params -- ?theme=amber&mode=dark etc. -- Uses Pico CSS with configurable themes } invariant PreviewServerBinding { for server in PreviewServers where server.is_running: server.host = config.preview_host and server.port = config.preview_port } invariant LocalhostOnly { -- Preview server binds to 127.0.0.1 only, never 0.0.0.0 }