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 file | Helper call | Rendered URL |
|---|---|---|
index.html | asset_url('/dist/casca.css') | dist/casca.css |
charts/bar.html | asset_url('/dist/casca.css') | ../dist/casca.css |
docs/guides/start.html | asset_url('/dist/casca.css') | ../../dist/casca.css |
a/b/c/page.html | asset_url('/dist/casca.css') | ../../../dist/casca.css |
charts/bar.html | page_url('/') | ../ |
charts/bar.html | page_url('/docs/') | ../docs/ |
docs/guide.html | page_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:
[site]:name,base_url,default_layout,content_dir,layouts_dir,assets_dir,output_dir, andstrict.base_urlis also used by the layout helpercanonical_url().[markdown]:extensions,unsafe_html_passthrough,heading_anchor_class, andsmart_punctuation.[build]:parallelism,clean_output,clean_urls,ignore_extensions,doctype,fail_on_warning,cache_dir, andlink_check.[dev]: local server defaults forcasca devandcasca preview.[[layouts]]: ordered routing rules.[[widgets]]: built-in widget configuration.[[favicons]]: copy explicit files to root output paths.[[promote]]: copy files or directories from--rootinto the output.
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 pattern | Output pattern | Opt-out flag | Accessibility implications | Dependency |
|---|---|---|---|---|
<table> | Add class="casca-data" while preserving existing classes. | tables | Native table semantics are unchanged. | Casca table primitive and Phase 088 prose context. |
<thead> | Add class="casca-data-head" while preserving existing classes. | tables | Header grouping semantics are unchanged. | Casca table primitive. |
<tbody> | Add class="casca-data-body" while preserving existing classes. | tables | Body 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. | callouts | The 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 lists | No SSG-side change. | None | Existing 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"}
| Event | Payload | Browser behavior |
|---|---|---|
css-update | bundle | Replaces matching stylesheet links after the new link loads. |
html-update | page | Reloads only when the current path matches page. |
full-reload | none | Reloads the current page. |
error | source, location, code, message, fix | Shows the scoped dev overlay. |
error-clear | none | Hides 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:
0: no errors, and no warnings when--fail-on-warningis active.1: validation errors, or warnings promoted by--fail-on-warning.2: usage or config discovery failure before a project can be validated.
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:
IGNORED: differences that do not affect the semantic contract, such as block-only whitespace or attribute ordering.ALLOWED: intentional generated artifacts that still need validation. The portal allows current-onlysitemap.xmlandblog/feed.xmlafter XML shape checks.FAILING: missing files, extra HTML files, node changes, text changes, heading id drift, class drift, local link drift, invalid generated XML, and other semantic differences.
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:
0: server startup followed by graceful shutdown.1: runtime failure after valid usage, including build, watcher, bind, TLS file, TLS parse, preview output, or server errors.2: usage or config-discovery failure, including missing required config, malformed config, invalid host, invalid port, or invalid TLS flag shape.
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.
page-title: sets<title>from the first matching heading, with optional prepend, append, default, andkeep_existing.anchor-headings: addsidattributes to markdown headings that lack one, preserving authored ids. For headings inside a.casca-proseancestor, also emits a child permalink anchor (<a class="casca-prose-anchor" href="#<id>">) with an accessible label, rendered by the prose stylesheet as a#glyph beside the heading on hover or focus.collection-split: splitsCHANGELOG.mdshaped release sections into virtual markdown pages plus an index. Release dates become deterministic page metadata for feeds and sitemaps.feed: writes an Atom 1.0 feed. The portal uses it forblog/feed.xml, with deterministic entry timestamps from release dates.sitemap: writessitemap.xmlwith<loc>, deterministic<priority>for every URL, and<lastmod>only when page metadata provides a deterministic date.link-check: validates local links in the generated output tree. External URLs,mailto,tel, query-only links, and configured ignores are skipped.
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:
0: rebuilt zero or more pages, optionally with stderr warnings.1: runtime error (IO failure, corrupt config, unknown preprocessor kind).2: usage error (unknown feed, unknown feed-key, conflicting flags, active-watcher lock detected when runningcasca site).
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:
- Per-site (asset shipping): set
[build] live_layer = trueincasca.toml. Whentrue, the symmetrics preprocessor emits per-block JSON sidecars at_casca/preprocessors/symmetrics/<cache_key>.json, andcasca sitepromotesdist/casca-live.jsinto the site output tree (e.g.site/_build/dist/casca-live.jsunder the standard[[promote]] repo = "dist"rule). Whenfalse(the default), the sidecars are NOT emitted AND thecasca-live.jsasset is NOT promoted, even if the library bundle is present indist/. The library always producesdist/casca-live.jsso it is available to hosts that opt in; the site builder gates whether it actually ships. - 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 optionaldata-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.