Casca Docs Home API SSG Themes Graph

Casca SSG

casca site is the native static-site generator used by the casca.style portal. make site runs Casca and writes the deploy artifact to site/_build/.

Quick Start

casca init my-site
cd my-site
casca site

For the portal build:

make site

The direct equivalent is:

bin/casca site --config site/casca.toml --root . --output site/_build

For local development:

bin/casca dev --config site/casca.toml --root . --port 8000

For serving an already-built output directory:

bin/casca preview --output site/_build --port 8001

For project validation:

bin/casca doctor --config site/casca.toml --root .

casca init writes casca.toml, content/index.md, layouts/default.html, and dist/casca.css. The default layout is the always-chrome minimal layout; --layout minimal selects the same template explicitly. The scaffold uses <body class="casca"> with one substantive body child, <casca-layout>, and fills brand, nav, footer, and markdown content through slots. The generated config sets [markdown].casca_css_url = "/dist/casca.css", [[layouts]].content_selector = "#casca-layout-main", and a matching [[promote]] rule so a fresh scaffold can be built immediately with casca site. Init refuses to overwrite scaffold files unless --force is passed, and it does not create the output directory or run a build.

Source Layout

The scaffolded layout is:

casca.toml
content/
layouts/
dist/

The portal uses site/pages/ for Phase 4.4.1 compatibility. Configure that with [site].content_dir = "pages". Page sources are .html, .htm, and .md. Files whose names start with _ are skipped. Extensions listed in [build].ignore_extensions are skipped.

HTML sources with a document-root <html> are treated as complete pages. Fragments and markdown are inserted into a selected layout. Layouts are valid HTML5; per-page metadata uses a <casca-meta> element and URLs are bare root-relative paths normalized at build time.

URL Handling

Layouts and fragment pages use bare root-relative URLs. The SSG normalizes them to page-relative form at build time via a lol_html attribute rewriting pass, so one build can be served from /, /casca/, or any other URL root.

<link rel="stylesheet" href="/dist/casca.css">
<a href="/docs/">Docs</a>
<img src="/img/hero.png">

The build-time normalization is depth-aware:

Current output fileSource URLRendered URL
index.html/dist/casca.cssdist/casca.css
charts/bar.html/dist/casca.css../dist/casca.css
docs/guides/start.html/dist/casca.css../../dist/casca.css
a/b/c/page.html/dist/casca.css../../../dist/casca.css
charts/bar.html/../
charts/bar.html/docs/../docs/
docs/guide.html/docs/../docs/

The rewriter recognises URL-bearing attributes on <a>, <link>, <script>, <img>, <source>, <audio>, <video>, <iframe>, <form>, <use>, <object>, <embed>, and OpenGraph / Twitter / refresh <meta> content. Root-relative paths (/foo) are rewritten; protocol-relative (//host), scheme URIs (https:, mailto:, tel:, data:), in-page fragments (#anchor), and already-relative paths (./foo, foo, ../foo) are left untouched.

External URLs do not need any wrapper. Write them directly:

<a href="https://codeberg.org/skell/casca">Code</a>
<link rel="preconnect" href="//cdn.example.test">

Per-page metadata via <casca-meta>

Fragment pages declare per-page <head> content with a top-level <casca-meta> element. At build time the SSG extracts its children into the layout's <head> and removes the <casca-meta> element from the output.

<casca-meta>
  <title>My Page</title>
  <meta name="description" content="...">
</casca-meta>

<section>
  Page content here.
</section>

A page's <title>, <meta name="...">, and <link rel="canonical"> from <casca-meta> win over the corresponding declarations in the layout's <head>. The layout's stylesheets, preloads, icons, and other always-on chrome render unchanged. Duplicate <casca-meta> at top level emits a build warning; a <casca-meta> nested inside other content is a build error.

Compatibility

Bare root-relative URLs and the <casca-meta> element are the public SSG contract. The build-time normalization pass (Phase 110a) replaced the prior {{ asset_url(...) }} / {{ page_url(...) }} / {{ canonical_url() }} layout helpers. Future Casca CLI releases preserve the bare-URL and <casca-meta> contracts or document a semver-major SSG compatibility change.

Config Schema

casca.toml supports:

Fresh init does not scaffold a theme-picker widget, theme manifest, or theme source files. The generated CSS bundle still contains the built-in Casca moods; sites that opt into the theme picker configure that widget and its theme sources explicitly.

When [markdown].wrap_in_casca_layout = true, a selected layout should either contain the intended <casca-layout> shell or provide only the insertion target where the SSG will insert that wrapper. Layouts that author their own light-DOM header, nav, skip link, or footer should disable wrapping for those routes.

Unknown keys warn by default and become errors in strict mode.

JSON config bridge

casca config get and casca config set expose the panel-editable config sections as JSON. The editable sections are site, build, markdown, and favicons; summary is get-only and reports counts for TOML-only sections.

casca config get --section site --format json
casca config set --section site --input - --format json
casca config get --section summary --format json

set rewrites only known keys in the targeted section, validates the edited document with the normal SSG parser, and preserves comments, unknown keys, and non-edited TOML sections.

Markdown to Casca primitive mapping

Markdown pages render through CommonMark/GFM first, then casca site maps a small set of rendered HTML primitives to Casca-classed HTML by default. Raw HTML written inside markdown is included because the pass operates on the final rendered HTML fragment.

Input patternOutput patternOpt-out flagAccessibility implicationsDependency
<table>Add class="casca-data" while preserving existing classes.tablesNative table semantics are unchanged.Casca table primitive and Phase 088 prose context.
<thead>Add class="casca-data-head" while preserving existing classes.tablesHeader grouping semantics are unchanged.Casca table primitive.
<tbody>Add class="casca-data-body" while preserving existing classes.tablesBody grouping semantics are unchanged.Casca table primitive.
<pre><code class="language-X"><pre class="casca-code" data-language="X"><code>. The language-X token is removed from <code>. Other code classes remain.code_blocks<pre><code> semantics remain. data-language is metadata only.Phase 088 prose code styles and future highlighter hook.
<pre><code> with no language<pre class="casca-code"><code>. No data-language.code_blocks<pre><code> semantics remain.Phase 088 prose code styles.
<blockquote> whose first element child is <p> and that paragraph's first element child is <strong>Wrap the whole blockquote in <aside class="casca-prose-callout" data-callout="note" aria-label="Note">...</aside>. The <strong> remains inside the blockquote.calloutsThe wrapper gets a complementary landmark and accessible name from native <aside> plus the label. The original quoted content remains in document order.Phase 088 callout CSS.
<details>, <summary>, <hr>, GFM task listsNo SSG-side change.NoneExisting element semantics and pulldown task list checkbox output remain.Phase 088 styles these through elements or existing GFM classes.

The [markdown] table controls the pass:

[markdown]
extensions = ["tables", "strikethrough", "autolinks", "footnotes", "tasklists"]
unsafe_html_passthrough = true
heading_anchor_class = ""
smart_punctuation = false
casca_primitive_mapping = true
wrap_in_casca_layout = true
default_variant = "doc"

[markdown.opt_out]
tables = false
code_blocks = false
callouts = false

Defaults are filled even when [markdown] or [markdown.opt_out] is omitted. Set casca_primitive_mapping = false to leave rendered markdown primitives in their pre-v1.5.0 form. Set individual [markdown.opt_out] flags to keep only that primitive unchanged.

For callouts, a blockquote with any explicit class attribute is treated as an author opt-out. The value is not interpreted, so class="quote", class="my-custom-quote", and even an empty class attribute preserve the blockquote as plain quoted content.

Markdown pages are also wrapped in <casca-layout> by default when the selected layout shell does not already contain a real <casca-layout> element and the rendered markdown fragment does not provide a top-level <casca-layout>. Generated wrappers use:

<casca-layout class="casca" data-variant="doc">
  <template shadowrootmode="open">...</template>
  <article id="casca-layout-main" tabindex="-1" class="casca-prose">
    ...
  </article>
</casca-layout>

Variant precedence is frontmatter casca.variant, then the matched [[layouts]] rule's variant, then [markdown].default_variant, then literal doc. Valid variants are doc, demo, landing, and article. When [markdown].wrap_in_casca_layout = false, route variants are ignored and the existing layout content insertion path is used unchanged.

The generated shadow template comes from tools/release-templates/casca-layout-shell.html. Modern engines that support Declarative Shadow DOM paint the shadow chrome. Older engines render the light-DOM markdown article directly without the header, skip link, or footer chrome. Casca ships no JavaScript polyfill.

Casca Custom Elements in Markdown

Markdown pages can invoke any Casca primitive through two syntaxes: fenced TOML code blocks for data-heavy charts and inline custom elements for controls and layout primitives.

Fenced Block Charts

Data-heavy chart primitives use fenced code blocks with a casca-* language tag. The body is TOML:

```casca-bar
title = "Monthly Sales"
labels = ["Jan", "Feb", "Mar"]
[[series]]
label = "Revenue"
values = [65, 80, 45]
```

The SSG generates both the visual chart and a screen-reader-accessible .casca-data table.

Supported chart fenced blocks: casca-bar, casca-line, casca-area, casca-pie, casca-heatmap, casca-scatter, casca-waterfall, casca-radar, casca-candlestick.

Inline Custom Elements

Simple controls and layout primitives use HTML custom elements with attributes or <template slot="..."> children:

<casca-progress value="65" label="Development"></casca-progress>

<casca-card>
  <template slot="title">Getting Started</template>
  <template slot="body">Content here.</template>
</casca-card>

Attribute-driven inline elements: casca-progress, casca-gauge, casca-divider-pixel, casca-theme-image, casca-range, casca-date, casca-color, casca-toggles/casca-toggle, casca-slot-grid, casca-anchor-nav.

Slot-driven inline elements: casca-card/casca-card-grid, casca-thumb, casca-toolbar/casca-toolbar-group, casca-stat/casca-stat-grid, casca-switch, casca-disclosure, casca-filter, casca-segment, casca-modal, casca-analytics, casca-site-header, casca-hero, casca-site-footer.

Obsidian Callout Syntax

Blockquotes starting with [!TYPE] are detected as prose callouts:

> [!note] Optional title
> Callout body paragraph.

Supported callout types: note, warning, tip, danger, info, example, question, abstract, success, failure, bug, todo, quote.

The existing > **Type:** Body syntax continues to work unchanged.

Opt-Out

Individual primitives can be disabled via casca.toml:

[markdown.opt_out]
primitives = ["bar", "pie"]

Frontmatter

Markdown pages may declare per-page metadata in a leading YAML or TOML frontmatter block. The SSG parses the block and uses its values to override page titles, control layout wrapping, select variants, and carry structured metadata for feeds and sitemaps.

Block format

A frontmatter block is a fenced metadata region at the very top of a markdown file. Two fence styles are supported:

YAML (--- opener and closer):

---
title: My Page
date: 2026-06-01
draft: false
---

TOML (+++ opener and closer):

+++
title = "My Page"
date = "2026-06-01"
draft = false
+++

The fence must start at column 0 on the first line. The opener and closer markers must appear on their own lines. Trailing whitespace after the closer marker is accepted. A missing closer is treated as body text: the entire file is rendered as markdown and no frontmatter is parsed.

Canonical keys

Six keys carry cross-platform semantics. They mean the same thing in Hugo, Zola, Obsidian, and Jekyll:

KeyTypePurpose
titlestringPage title override. Takes precedence over the first ATX heading (# Title) in the markdown body.
descriptionstringPage description. Stored in page metadata for feed and sitemap consumers.
datestringPublication date in ISO-8601 format (e.g., 2026-06-01). Stored as an opaque string; no date parsing or validation is performed.
draftbooleanWhen true, the page is excluded from sitemap and Atom feed. The page is still rendered for preview.
tagsarray of stringsMulti-axis classification. Parsed and stored for future tag-page rendering.
slugstringURL slug override. Parsed and stored for future URL-rewriting phases.

All six keys are optional. Omitted keys are treated as if they were not declared.

Casca namespace

SSG-specific per-page overrides live under a casca key (a sub-table in TOML, a nested mapping in YAML):

KeyTypeDefaultPurpose
casca.variantstringconfig defaultLayout variant override. Must be one of doc, demo, landing, article. Invalid values produce a warning (non-strict) or error (strict).
casca.wrap_in_casca_layoutbooleantrueWhen false, this page's rendered markdown body is not wrapped in <casca-layout>. Overrides the route-level wrap_in_casca_layout setting for this file only.
casca.primitive_mappingbooleantrueWhen false, Casca-class mappings (casca-data, casca-code, casca-prose-callout) are not applied to this page's markdown output. Overrides the global [markdown].casca_primitive_mapping setting for this file only.
casca.canonicalstringnoneExplicit canonical URL for this page. Stored for future use.

Example TOML frontmatter with Casca namespace:

+++
title = "API Reference"
date = "2026-06-01"
draft = false

[casca]
variant = "doc"
canonical = "https://casca.style/docs/api/"
+++

Example YAML:

---
title: API Reference
date: 2026-06-01
draft: false
casca:
  variant: doc
  canonical: <https://casca.style/docs/api/>
---

Unknown keys

Keys not in the canonical six or casca namespace are silently ignored in normal mode. This preserves compatibility with Obsidian vaults, Hugo params.*, and other tools' metadata conventions.

In strict mode ([site] strict = true), unknown keys produce a build warning with the key names and file path. Strict mode does not fail the build on unknown keys because Obsidian vaults require their plugin metadata to remain in the frontmatter block.

Title precedence

The page title is resolved in this order:

  1. Frontmatter title key (if present)
  2. First ATX heading (# Title) in the markdown body (if present)
  3. Filename-based fallback (e.g., my-page.md becomes "My Page")

Variant precedence

The <casca-layout data-variant="..."> attribute is resolved in this order:

  1. Frontmatter casca.variant (per-page override)
  2. Route-level [[layouts]].variant from casca.toml
  3. [markdown].default_variant from casca.toml
  4. Literal doc

Theme Picker Widget

The portal uses the theme-picker widget to render one static picker form from site/casca-themes.toml instead of duplicating mood markup in each layout:

[[widgets]]
widget = "theme-picker"
manifest = "casca-themes.toml"
selector = "[data-casca-theme-picker-widget]"

Layouts place a normal HTML placeholder:

<div data-casca-theme-picker-widget></div>

The widget replaces the placeholder with <form class="casca-theme-picker" data-casca-theme-picker method="get" action="/_casca/theme">. Output remains static HTML. casca dev and casca preview serve /_casca/theme when this manifest is configured, then transform HTML responses at request time so body[data-casca-theme], the checked radio, and hidden return_to reflect the resolved mood. casca site still writes static bytes and does not bake per-theme variants into site/_build.

Development Server

casca dev builds the same SSG output as casca site, keeps it in memory, serves it over HTTP by default, watches source files, and broadcasts HMR events over /_casca/ws. It accepts --config, --root, --host, --port, --format, --fail-on-warning, --strict, and --quiet.

The [dev] table supports host, port, open, watch_css, and inject_shim. open is parsed but remains inactive in this phase. watch_css = true watches repo-root src/**/*.css when that directory exists. inject_shim = true allows the dev server to append:

<script src="[dev-script]"></script>

The actual dev script path is the reserved /_casca/ path plus dev.js. The shim is served only by casca dev. It is not written to dist/, not served by casca preview, and not injected by casca site. Casca library features remain HTML and CSS only.

Dev responses use:

Cache-Control: no-cache, must-revalidate

HMR Events

The browser shim is receive-only and handles these server events:

{"kind":"css-update","bundle":"/dist/casca.css"}
{"kind":"html-update","page":"<resolved-url>"}
{"kind":"full-reload"}
{"kind":"error","source":"<path>","line":<num>,"column":<num>,"code":"<code>","message":"<msg>","fix":"<optional>"}
{"kind":"error-clear"}
EventPayloadBrowser behavior
css-updatebundleReplaces matching stylesheet links after the new link loads.
html-updatepageReloads only when the current path matches page.
full-reloadnoneReloads the current page.
errorsource, location, code, message, fixShows the scoped dev overlay.
error-clearnoneHides the overlay and resets dismissal.

The overlay is created by the shim only after an error or connection failure. Its classes use the casca-dev-overlay prefix followed by double underscore and its inline CSS avoids global selectors such as html, body, :root, and universal selectors.

Watcher and Incremental Graph

casca dev watches configured content, layouts, assets, casca.toml, and repo-root CSS when enabled. It ignores build outputs, target/, bin/, dependency directories, VCS directories, and common editor temporary files.

The incremental graph tracks source pages, layouts, assets, and built pages. A page edit targets one built page, a layout edit targets pages using that layout, an asset edit copies only the asset state, a repo CSS edit rebuilds /dist/casca.css, a config edit performs a full rebuild, and a collection-split source edit refreshes the generated collection pages.

CSS HMR preserves form state and scroll position because it swaps stylesheet links. HTML changes reload the page and rely on normal browser scroll restoration.

Theme manifest changes in the config directory use the same rebuild path as casca.toml changes. A successful rebuild swaps the in-memory output and theme serving context together. A failed manifest parse or invalid default keeps the previous valid serving context and broadcasts the existing HMR error event.

Preview Server

casca preview serves an existing output directory. It does not rebuild, watch files, expose /_casca/ws, serve the reserved dev script path, or inject the shim. When --output is omitted, it resolves [site].output_dir from casca.toml. It uses the same no-cache response header as dev so local smoke checks do not hide stale files behind browser cache. When a config is available and includes a theme-picker manifest, preview loads that manifest once at startup and serves the same local theme setter and HTML transform as dev. With --output and no config, preview keeps literal static serving and /_casca/theme is inactive.

Doctor

casca doctor validates a project without writing output files. With no arguments it discovers casca.toml by walking up from the current directory. It also accepts --config, --root, --format human|json|brief, --json, --quiet, and --fail-on-warning.

Doctor always validates the config in strict mode. It checks known config keys and value types, widget names and widget-specific schemas, layout files, layout insertion selectors, layout section paths, collection sources, favicon and promote sources, configured content, layout, and asset directories, markdown front matter, and duplicate output paths. It warns for orphan layouts.

Exit codes are:

JSON output contains the same diagnostic codes, severity, paths, messages, and summary counts as human output.

Snapshot Parity

casca check-parity <BASELINE_DIR> <CURRENT_DIR> compares two rendered output snapshots. The command accepts --format human|json|brief, --json, --include-ignored, --fail-on-allowed, and --quiet.

The parity oracle builds a normalized DOM for every matching HTML page. It compares element order, tag names, parser-decoded text, attributes as maps, heading ids, local link targets, classes as sets, and whitespace-sensitive text inside pre, code, textarea, script, and style.

Diff classes are:

The Makefile parity targets from the parallel-run window are retired. check-parity remains available as a local snapshot utility when a developer needs to compare two output directories.

TLS

casca dev and casca preview use HTTP by default. HTTPS is opt-in:

bin/casca dev --tls --tls-cert /tmp/casca-dev.crt --tls-key /tmp/casca-dev.key
bin/casca preview --tls --tls-cert /tmp/casca-dev.crt --tls-key /tmp/casca-dev.key

Both certificate and key are required. Casca does not generate certificates in Phase 4.4.2. Use a local certificate from a tool such as mkcert or openssl. When TLS is enabled, the browser shim connects to wss://<host>:<port>/_casca/ws.

Exit Codes

casca dev and casca preview use the same local-serving exit contract:

IRON RULE 4.4.2

Phase 4.4.2 requires all dev, preview, HMR, TLS, inherited 4.4.1 parallel-run, inherited 4.4.0 byte-identity, lint, site, no-shim-leak, and CLI grammar gates to pass before the implementation is considered complete.

Layout Routing

Each [[layouts]] entry has name, file, match, content_selector, and content_action. Rules are evaluated in declaration order. The first match wins. If none match, [site].default_layout is used.

Supported match fields are page, section, include_subsections, extensions, and not. Phase 4.4.1 supports replace_content. The replace_element action is parsed but rejected with a clear error.

The content selector must match exactly one element for fragment and markdown pages. The portal uses main and main > article.

Markdown

Markdown is rendered with pulldown-cmark using configured extensions: tables, strikethrough, autolinks, footnotes, and task lists. Casca normalizes the structural details needed by the portal contract: table align attributes, bare www. autolinks, footnote wrappers and backlinks, and task-list checkbox structure. Raw HTML passes through by default because Casca assumes trusted source content. The SSG is not a sanitizer.

Task-list items render as <li class="task-list-item"> containing a disabled <input type="checkbox"> with checked="" on completed items, and the parent list renders as <ul class="task-list">. The checkbox input has no id and is decorative rather than form-submittable.

Wikilink Graph

When [markdown.wikilinks] enabled = true, Casca generates a static graph page and per-page mini-graphs by default. The graph page route defaults to /graph and follows the normal clean-URL rules, so the default clean build writes graph/index.html. Disable only the generated page with:

[markdown.wikilinks]
enabled = true
graph = false

Set graph_page = "/network" to publish it at another absolute site path. The graph contains published markdown pages only; drafts, HTML pages, assets, and generated pages are excluded. backlinks = false disables per-page backlink sections but does not disable graph edge collection while graph = true.

Per-page mini-graphs are appended to published markdown pages as static SVG figures. Set per_page_graph = false to disable only those page-local graphs. Set graph_hops = 2 to include a wider ego-centric neighborhood; the effective value is clamped to 1..=5.

The graph page ships responsive static SVG and no runtime JavaScript. Public styling hooks are scoped to .casca-graph-shell and .casca-graph*, including .casca-graph, .casca-graph-edge, .casca-graph-node, .casca-graph-node-visual, .casca-graph-circle, .casca-graph-label, .casca-graph-focus, .casca-graph-focus-edge, .casca-graph-focus-neighbor, .casca-graph-focus-circle, .casca-graph-focus-label, .casca-graph-mini, .casca-graph-caption, .casca-graph-node-ego, .casca-graph-ego-ring, .casca-graph-empty, and .casca-graph-summary.

Front matter may be TOML between +++ fences or YAML between --- fences. Reserved keys are title, layout, date, slug, draft, tags, and aliases. YAML support is intentionally narrow: block mappings, sequences, and scalars are accepted. Flow style, anchors, aliases, merge keys, custom tags, and duplicate keys are rejected.

Widgets

Built-in widgets are compiled into the binary.

Assets

Assets under [site].assets_dir are copied into the output with the asset root stripped. For example, assets/css/site.css becomes css/site.css.

Favicons

Casca can generate and discover a conventional favicon directory. From one raster source image, run:

bin/casca favicon generate --root site --source site/assets/logo.png

The command writes deterministic files under <root>/_casca/favicons/:

_casca/favicons/favicon.ico
_casca/favicons/favicon-16.png
_casca/favicons/favicon-32.png
_casca/favicons/favicon-48.png
_casca/favicons/apple-touch-icon.png

On casca site and casca dev, recognized files in _casca/favicons/ are copied to the output root. The SSG injects missing favicon links into every rendered page head:

<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">

Generated hrefs are emitted page-relative in the final HTML, matching the rest of the SSG URL normalization. favicon-48.png is copied for completeness but is not auto-linked.

Existing layout or <casca-meta> links suppress equivalent generated links. Matching uses exact rel tokens, so mask-icon does not count as icon. Explicit outputs also take precedence: if a page, graph page, asset, markdown embed, [[favicons]], or [[promote]] already claims favicon.ico, the generated copy and generated link for that output are skipped.

[[favicons]] remains supported for manual setups. It copies a file from --root to an explicit output path:

[[favicons]]
repo = "assets/favicon.ico"
output = "favicon.ico"

[[favicons]]
repo = "assets/apple-touch-icon.png"
output = "apple-touch-icon.png"

If you use manual rules, keep the corresponding links in your layout or <casca-meta>:

<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">

[[promote]] copies a file or directory from --root to an explicit output path. The portal uses this to copy dist/ into site/_build/dist/.

PWA manifest

Casca does not generate a Web App Manifest yet (planned). To add one today, author manifest.webmanifest in your assets directory and wire it with [[promote]] or [[favicons]], then add the <link> to your layout:

<link rel="manifest" href="/manifest.webmanifest">

Output paths must be relative and must not contain ...

Feed-triggered rebuild

casca rebuild re-renders only the pages whose preprocessor dependency edges name a changed feed. It is the publisher-side counterpart to casca site: the publisher writes new feed bytes (typically write -> fsync -> rename) and then invokes the rebuild as a post-publish hook.

bin/casca rebuild --feed <path>...           # one-shot, named feed paths
bin/casca rebuild --feed-key <key>...        # one-shot, named feed cache keys
bin/casca rebuild                            # one-shot, every indexed page
bin/casca rebuild --watch                    # long-running watcher

--feed PATH resolves against the config directory and accepts absolute, config-relative, or cwd-relative paths; unknown feeds exit with code 2. --feed-key KEY is an exact string match against the preprocessor entry's cache_key. The two flags may be supplied together; the rebuild covers the union of resolved pages.

Without --strict, a required = true preprocessor entry whose feed is missing renders the designed never-published state plus a warning and the rebuild exits 0. With --strict, the rebuild aborts with code 1 the same way casca site does. --fail-on-warning elevates any warning to a non-zero exit but does not change the render.

The rebuild path runs write_file_atomic per page, so partial writes are not observable to a polling consumer (browsers or the optional live layer sidecar). The link checker is NOT re-run by default; pass --link-check if the host wants a full check after the rebuild.

Exit codes:

Watch mode and the active-watcher lock

casca rebuild --watch watches each preprocessor entry's resolved config.source AND its parent directory (catching write -> rename publishes). On startup it acquires a sentinel at <output_dir>/_casca/active-watcher.lock containing the watcher's PID and a heartbeat timestamp. The watcher refreshes the timestamp every 5 seconds and removes the file on clean exit.

casca site checks this lock on startup. If the heartbeat is within the last 30 seconds, casca site exits with code 2 and a clear error naming the lock path. The convention is: stop the watcher before running a full site build. The lock guards against the directory-wipe race where casca site's default clean_output = true would otherwise erase the watcher's atomic writes.

casca dev and casca rebuild --watch should not be run simultaneously against the same output root. Both writers share the same atomic-write contract per file, so file-level corruption is not possible, but both watchers would observe the same feed change and emit duplicate log lines. This case is documented rather than locked: the orchestrator workflow is one or the other.

casca dev's watch set was extended in this phase to include each preprocessor's resolved config.source AND its parent directory. A developer who updates a local feed fixture sees the HMR update without needing a separate casca rebuild invocation. The dev path defaults to strict_required = false: a missing required feed during the dev loop renders the designed never-published state rather than crashing the loop, matching the dev iteration expectation that fixtures may be missing.

Disk cache

casca rebuild and casca site write a small _casca/state.json cache containing the preprocessor dependency edges. The cache is purely an optimization; the canonical path is to re-derive the edge list from the source tree on every invocation. The cache is never authoritative, and the cache file may be deleted at any time by an operator without losing correctness.

Optional live layer

dist/casca-live.js is a strictly opt-in progressive enhancement for the analytics block. It is NOT bundled into dist/casca.css; hosts that want it ship it as a separate <script src="/dist/casca-live.js" defer></script> tag. The asset is built by make build-live, which is not a dependency of make build.

Hosts opt in at two layers:

  1. Per-site (asset shipping): set [build] live_layer = true in casca.toml. When true, the symmetrics preprocessor emits per-block JSON sidecars at _casca/preprocessors/symmetrics/<cache_key>.json, and casca site promotes dist/casca-live.js into the site output tree (e.g. site/_build/dist/casca-live.js under the standard [[promote]] repo = "dist" rule). When false (the default), the sidecars are NOT emitted AND the casca-live.js asset is NOT promoted, even if the library bundle is present in dist/. The library always produces dist/casca-live.js so it is available to hosts that opt in; the site builder gates whether it actually ships.
  2. Per-block (script activation): the host page template adds data-casca-live-source="/_casca/preprocessors/symmetrics/<cache_key>.json" on the <section class="casca casca-analytics"> element. The optional data-casca-live-interval="N" overrides the 60-second default poll cadence.

The script applies field-level text swaps only. It never sets .innerHTML, never calls Node.replaceWith() / Node.replaceChildren() / sets Element.outerHTML, never moves focus, never adds role="alert" or aria-live. Element identity is preserved across every transition.

See docs/API.md for the JSON sidecar shape and the per-state field-clearing rule the script consumes.

Partial Includes

<casca-include src="..."> expands a partial HTML file at build time (Pass D in the SSG pipeline). The element is replaced with the resolved partial's contents before any asset URL normalization or <casca-meta> merging occurs.

Syntax

<casca-include src="partials/site-footer.html">
</casca-include>

The src attribute is required. A missing src is a build error.

Path resolution

Relative paths are resolved against the directory of the file containing the <casca-include> element:

src="partials/foo.html"       -> <source_dir>/partials/foo.html
src="/partials/foo.html"      -> <layouts_root>/partials/foo.html

layouts_root is the parent directory of the configured layouts directory (site/ for the portal).

Recursion and depth cap

Partials may themselves contain <casca-include> elements, which are resolved recursively before the parent partial is inserted. The maximum include depth is 10 nested includes. Exceeding it is a build error:

include depth exceeded (max 10): a.html -> b.html -> ... -> k.html

Cycle detection

A path_stack tracks the include chain. If any file is included a second time within the same resolution, the build errors:

circular include: a.html -> b.html -> a.html

Attribute pass-through

Attributes on <casca-include> (except src and unwrap) are passed through to the included partial's root element as CSS custom properties:

<casca-include src="partials/card.html" variant="featured" columns="3">
</casca-include>

produces:

<div class="casca-card" style="--variant: featured; --columns: 3">
  ...
</div>

Values containing special characters are CSS-escaped to prevent injection. Values consisting only of [a-zA-Z0-9_-]+ are emitted unquoted.

When unwrap is present, pass-through attributes are silently ignored (there is no single root element to receive them).

Unwrap

The unwrap attribute is a boolean HTML attribute. When present, the outermost element of the partial is stripped and only its children are inserted:

<casca-include src="partials/items.html" unwrap>
</casca-include>

If partials/items.html contains <ul class="item-list"><li>One</li><li>Two</li></ul>, the result is <li>One</li><li>Two</li>.

Unwrapping a void element (<br>, <img>, <hr>, etc.) is a build error. Unwrapping an empty partial is a build error.

Inline slot fills

<casca-include> children may include <template slot="X"> elements that fill matching <slot name="X"> declarations inside the partial. These inline fills are resolved during Pass D, before the partial is inserted. See the Slot Filling section for the full contract; the same conflict rules apply.

<casca-include src="partials/card.html">
  <template slot="extra">
    <p>Inline content specific to this include</p>
  </template>
</casca-include>

File-not-found

Missing partials are a build error with file:line context:

missing include: site/partials/missing.html
  from site/layouts/landing.html:45 (<casca-include src="partials/missing.html">)

Page-fragment includes (deferred)

<casca-include> elements inside page fragments (files under site/pages/) are out of scope for Phase 110b and emit a build warning. They are left unresolved.

Slot Filling

Layout shells declare named insertion points with standard HTML5 <slot> elements. Pages fill them with <template slot="..."> elements. Resolution is build-time only (Pass E in the SSG pipeline); browsers receive fully resolved HTML with no custom elements.

Slot declaration

<slot name="hero">
  <header class="casca-doc-hero">
    <h1>Casca docs</h1>
  </header>
</slot>

The name attribute is required. A <slot> without name is a build error. Children of the <slot> serve as fallback content when no template fills the slot.

Template fill

<template slot="hero">
  <header class="casca-doc-hero">
    <h1>Custom Docs Landing</h1>
    <p>Extended description</p>
  </header>
</template>

At build time, the <template slot="hero"> element is removed from output and its children replace the fallback content of <slot name="hero">.

Conflict rules

Nesting

Slot resolution proceeds outside-in: outer slots fill before inner slots. A template's content may itself contain <slot> elements, which are filled in subsequent iterations. The maximum nesting depth is 10 iterations; exceeding it is a build error:

slot nesting exceeds depth cap (10 iterations)

Shadow-DOM scoping

<slot> and <template slot="..."> elements inside declarative shadow DOM (<template shadowrootmode="open"> or <template shadowrootmode="closed">) are skipped by Pass E. The browser owns slot resolution inside shadow roots.

Empty fills

An empty template (<template slot="X"></template>) replaces the slot's content with nothing. The fill exists, so it wins over fallback. Authors who want fallback to survive should omit the template rather than writing an empty one.

Worked example

Layout (site/layouts/doc.html):

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Casca docs</title>
</head>
<body class="casca">
  <header class="casca-site-header">
    <nav>...</nav>
    <casca-include src="partials/codeberg-icon.html"></casca-include>
  </header>
  <main>
    <slot name="hero">
      <header class="casca-doc-hero">
        <h1>Casca docs</h1>
      </header>
    </slot>
    <!-- page content inserted here by content_selector -->
  </main>
  <casca-include src="partials/site-footer.html"></casca-include>
</body>
</html>

Page fragment (site/pages/docs/index.html):

<template slot="hero">
  <header class="casca-doc-hero">
    <h1>Custom Docs Landing</h1>
    <p>Extended documentation landing page</p>
  </header>
</template>

<section>
  <h2>Getting Started</h2>
  <p>...</p>
</section>

Build output:

Pass F: Casca Custom Element Resolution

Pass F runs after Pass E (slot filling) and before the asset URL resolution pass. It rewrites all <casca-*> custom elements to their final CSS-contract HTML, handling four modes:

Pattern A: Template Attribute Capture

For Mode 2 primitives, each logical slot is a single <template> element. Extra attributes on the template carry metadata (e.g., trend="up", view="1"). The <template>'s slot attribute names the slot; all other attributes are captured as slot metadata and serialized to data-slot-*-attrs attributes for handler consumption.

Multiple templates with the same slot name are supported (e.g., multiple <template slot="view" view="N"> elements for a switch component). Each gets an indexed attribute (data-slot-name-0, data-slot-name-1, ...) plus corresponding -attrs attributes.

Body content (non-template children of the custom element) is preserved in data-inner-html and read by container-element handlers.

Markdown opt-out

Configure [markdown.opt_out].primitives to skip specific primitives during Pass F resolution. Opted-out elements pass through unchanged so the author can handle them downstream:

[markdown.opt_out]
primitives = ["analytics", "switch"]

Known Limitations

Reserved Work

This section tracks features currently reserved but not yet implemented. No features are currently reserved. Named slots and partial includes shipped in Phase 110b (see the Partial Includes and Slot Filling sections below).

Page graph for Casca SSG: 2 pages, 1 link within 1 hop.
Page graph for Casca SSGEgo-centric graph showing Casca SSG and connected published markdown pages within 1 hop.CascaCasca SSG