Skip to main content
Casca
Theme

Casca SSGLink to section

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 StartLink to section

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.html, layouts/default.html, and minimal assets. It refuses to overwrite those files unless --force is passed, and it does not create the output directory or run a build.

Source LayoutLink to section

The scaffolded layout is:

casca.toml
content/
layouts/
assets/

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 real HTML files with one intentionally small expression surface for URL helpers: {{ asset_url('...') }}, {{ page_url('...') }}, and {{ canonical_url() }}.

Layout URL HelpersLink to section

Layouts may call a small set of URL helpers inside {{ ... }} regions. These helpers run only in layout HTML. Markdown content is not evaluated.

<link rel="stylesheet" href="{{ asset_url('/dist/casca.css') }}">
<a href="{{ page_url('/docs/') }}">Docs</a>
<link rel="canonical" href="{{ canonical_url() }}">

asset_url(path) and page_url(path) both take one single-quoted output-tree path. A leading / is optional. The rendered value is depth-aware and root-traced from the current page's output depth, so one build can be served from /, /casca/, or another URL root.

Current output fileHelper callRendered URL
index.htmlasset_url('/dist/casca.css')dist/casca.css
charts/bar.htmlasset_url('/dist/casca.css')../dist/casca.css
docs/guides/start.htmlasset_url('/dist/casca.css')../../dist/casca.css
a/b/c/page.htmlasset_url('/dist/casca.css')../../../dist/casca.css
charts/bar.htmlpage_url('/')../
charts/bar.htmlpage_url('/docs/')../docs/
docs/guide.htmlpage_url('/docs/')../docs/

canonical_url() takes no arguments. It returns [site].base_url joined to the current page URL. With base_url = "https://example.test/casca/", a page rendered at /docs/SSG.html gets:

<https://example.test/casca/docs/SSG.html>

The expression surface is intentionally narrow. It supports only function calls with either one single-quoted string argument or no arguments. Unknown functions, malformed expressions, wrong argument counts, empty asset_url or page_url paths, .. path components, external URLs, query strings, fragments, and backslashes are build errors. Error messages include the layout path and source line.

External URLs do not need helper wrappers. Write them directly in layout HTML:

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

Inside asset_url(...) and page_url(...), any path with a scheme prefix such as https: or mailto:, or any scheme-relative path beginning with //, is a build error.

CompatibilityLink to section

asset_url(path), page_url(path), canonical_url(), and the {{ FN(ARG) }} layout syntax are public SSG API. Future Casca CLI releases must preserve them or make a semver-major SSG compatibility change.

Config SchemaLink to section

casca.toml supports:

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

Markdown to Casca primitive mappingLink to section

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 <div class="casca-prose-callout" data-callout="note" role="note" aria-label="Note">...</div>. The <strong> remains inside the blockquote.calloutsThe wrapper gets a note role and accessible name. 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 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.

Theme Picker WidgetLink to section

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 ServerLink to section

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 EventsLink to section

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 GraphLink to section

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 ServerLink to section

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.

DoctorLink to section

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 ParityLink to section

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.

TLSLink to section

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 CodesLink to section

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

IRON RULE 4.4.2Link to section

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 RoutingLink to section

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.

MarkdownLink to section

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.

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.

WidgetsLink to section

Built-in widgets are compiled into the binary.

AssetsLink to section

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]] copies a file from --root to an explicit output path. [[promote]] copies a file or directory from --root to an explicit output path. The portal uses this to copy dist/ into site/_build/dist/.

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

Feed-triggered rebuildLink to section

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 lockLink to section

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 cacheLink to section

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 layerLink to section

dist/casca-live.js is a strictly opt-in progressive enhancement for the analytics block. It is NOT bundled into dist/casca.css or dist/casca-core.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.

Reserved WorkLink to section

Named slots and casca-head-extras are reserved but not implemented.