Two decisions and one mechanism. The human wants demos, games and runnable
embeds to carry real CSS and JS while every ordinary page stays scriptless, and
wants adding an asset to be theme work rather than a rebuild.
The mechanism reuses what already had that property. A theme defines
`assets:<name>` beside its other fragments; shortcodes record their own name as
they are opened; after conversion the engine renders each matching fragment once
into Page.Assets. So a gallery calling one shortcode forty times carries its
stylesheet once, and a page that called nothing carries nothing. Frontmatter
`use:` reaches the same fragments without a call.
Considered and rejected: templates/assets.yaml, which reads more declaratively
and buys a parser, a contract shape and a rebuild for conditional markup; and a
table in Go mapping shortcode to files, which would hardcode exactly what was
deliberately made data-driven.
Collection is parse-phase, so no transform counter moves — goldmark's extender
list is already the ordered pipeline for parse work, which state.md's counter
says in its "does not count" column.
Separately, styles/scripts are lifted at last. They sat in content-model.md's
table unread, and the theme contract listed them under "what the engine
provides", which was aspirational rather than true. Both are bundle-relative: a
name with .. or a leading / is dropped and logged, the refusal ::include and a
code block's file= already make. The engine builds the URLs because a theme must
not construct an address.
ADR-0080 writes the antifeature list down, with its single exception inside it.
An antifeature nobody recorded does not bind anything, and each of these dies to
one reasonable-looking request at a time. The exception is author-invoked and
cannot fire by accident.
The reference theme emits the stylesheets and no script element at all. That was
the human's correction to a first attempt which had page.html emitting the tag
and verify.sh narrowed to permit it — narrowing the gate to fit the code was
backwards, and the narrowing was also wrong, passing a probe with a hardcoded src
because it filtered whole lines and every line carries {{define}}. verify.sh is
untouched. examples/demo-site redefines the head block instead, so the JavaScript
half is demonstrated by a site rather than built into the binary, which is a
better demonstration and a stronger property.
Evidence, against the demo site with a freshly built binary: the sandbox page
carries its own css and js at bundle-relative URLs; colophon calls ::tally twice
and carries tally.css once with zero scripts; about calls it never and carries
neither; listings unaffected. Plus a table test for the escape refusal, which
until now had only the running server behind it.
19 files, +355/-86. No counter moves. Demo is 31 bundles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
examples/
demo-site/ is a complete khosra site, kept here to be read and served — the demonstration a person walks
through to see what the engine does, and the fixture the engine tests itself against (ADR-0051).
make demo # serves it at localhost:8080
khosra check -site examples/demo-site
It is content, not a fixture in disguise: every file is what an author would write. Two things keep it honest,
both in verify.sh:
TestTheExampleSiteExercisesEveryFeature(ininternal/web) serves this directory through the real handler and asserts one case per feature. A feature that changes shape breaks it.khosra checkruns over it and must report nothing fatal.
Adding a feature means adding it here, with a case in that test. A feature the example does not show is a feature nobody can see working, and the gates are what stop that from being optional.
What each part is for
| Path | Demonstrates |
|---|---|
site.yaml |
the two site-level settings: base and title |
content/posts/day-01…11 |
pagination — eleven dated posts against a page size of ten |
content/posts/first-light/ |
two languages, a directory bundle owning a picture, a figure with a caption |
content/posts/only-english |
the fallback chain, and a canonical link naming the variant actually served |
content/posts/renamed-thing/ |
slug moving an address, aliases keeping the old one working |
content/posts/unfinished |
a draft: invisible without -dev on |
content/posts/scheduled |
a future date: published by the clock, with nothing to run |
content/comics/the-long-monsoon/ |
a series — structural membership, sparse order, prev/next/first/last, an archive, and a landing page in two languages |
content/art/monsoon-studies/ |
a gallery: three JPEGs sized into a srcset, and an SVG left exactly as drawn |
content/writing/notes-on-water/ |
an include whose fragment has no URL, and an extras/ tree with a nested directory |
content/writing/typography |
what the engine does to prose, and what it deliberately does not |
content/pages/* |
undated bundles: reachable, and absent from every feed |
content/pages/bengali-only |
a bundle that exists in one non-default language only |
content/status/* |
a titleless bundle — legal, and the one check warning this site expects |
templates/list.html |
overriding a single named block and inheriting the document |
static/note.txt |
files served verbatim |
The one expected warning
khosra check reports the titleless status note. That is deliberate: titleless is legal, the theme falls back
to the bundle's key, and the warning exists so an author who did not mean it finds out. A fatal finding here
would fail verify.sh.