-- 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) OpenPreviewInBrowserRequested(project) } 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 } rule OpenPreviewInBrowser { when: OpenPreviewInBrowserRequested(project) -- Starts or reuses the project's localhost preview server, then opens its -- base URL in the external system browser when no post editor is active. -- With an active post editor it opens that post's normal generated-site URL. -- This is separate from the post editor's internal preview pane. ensures: ExternalBrowserOpened( url: active_post_generated_url or preview_base_url(project) ) } -- Route resolution -- Preview renders all posts (published + draft) on-demand via Liquid templates. -- Draft content comes from the DB; published content comes from its .md file. -- 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). -- Draft bodies resolve from DB; published bodies resolve from .md files. -- Renders via Liquid template with full PageRenderer context. ensures: PreviewResponse(rendered_html) } rule ServeDraftPreview { when: PreviewDraftRequest(path, post_id) -- Editor preview uses the post's canonical generated route with draft and -- post_id query markers, so links continue through the normal route tree. -- Renders draft content from DB and published content from the 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 ServeGeneratedRuntimeData { when: PreviewRequest(path) requires: path = "/calendar.json" or is_feed_path(path) or is_pagefind_path(path) or is_image_path(path) -- Calendar and feed data use the preview post universe and normal list -- visibility rules. Pagefind and image files are read from the same project -- filesystem locations as the generated site. ensures: PreviewResponse(runtime_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 source (per post or translation): -- 1. status=draft → DB content, falling back to its last-published file -- 2. status=published → published .md file -- 3. No DB body or file → empty string -- -- 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 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 }