give a page the assets its own content asked for, and write down the rule

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>
This commit is contained in:
2026-08-02 19:22:56 +06:00
co-authored by Claude Opus 5
parent 69f43e5791
commit 64e53f28c8
20 changed files with 397 additions and 88 deletions
@@ -37,3 +37,12 @@ clickable, and nothing is stored (ADR-0078):
- [x] Draw icons from Unicode rather than shipping an icon font
- [x] Serve every panel above as plain `<details>`
- [ ] Ship a single byte of JavaScript on a page like this one
## Assets arrive only when asked for
`::tally` is a site-defined shortcode whose theme fragment declares a stylesheet. Calling it twice pulls
that stylesheet in once, and a page that never calls it carries no stylesheet at all (ADR-0079):
::tally{label="Shortcodes the engine hardcodes" count=3}
::tally{label="Shortcodes the theme adds without a rebuild" count="as many as you like"}
@@ -0,0 +1,13 @@
---
title: A Page That Carries Its Own Assets
styles: [sandbox.css]
scripts: [sandbox.js]
---
Every other page on this site ships no JavaScript. This one does, because its own frontmatter names a file
sitting beside it — the single exception in ADR-0080, and the only way a script reaches a page here.
The reference theme would not emit the `<script>` tag at all; this site's `templates/page.html` redefines
the `head` block to add it. The stylesheet needs no override, since a stylesheet is not the exception.
Both files are resolved relative to this bundle. A name reaching outside it is dropped before it is ever
an address.
@@ -0,0 +1 @@
.sandbox { outline: 1px dashed #8a7f6a; padding: 0.5rem; }
@@ -0,0 +1 @@
document.title = document.title; /* the smallest honest demo */
+2
View File
@@ -0,0 +1,2 @@
/* Pulled in only by pages that call ::tally (ADR-0079). */
.tally { border-left: 3px solid #8a7f6a; padding-left: 0.75rem; }
+15
View File
@@ -0,0 +1,15 @@
{{/* The site's own page template: it inherits the whole document from the binary and redefines one block.
This is where the JavaScript half of ADR-0080's exception is demonstrated, deliberately *not* in the
reference theme — that ships no <script> at all and verify.sh enforces it (ADR-0026). A real site
that wants demos, games or runnable embeds opts in here, once, and every ordinary page still emits
nothing because .Scripts is empty unless a bundle's own frontmatter named a file. */}}
{{define "head" -}}
{{.Assets}}
{{- range .Styles}}
<link rel="stylesheet" href="{{.}}">
{{- end}}
{{- range .Scripts}}
<script src="{{.}}" defer></script>
{{- end}}
{{- end}}
@@ -0,0 +1,6 @@
{{/* A shortcode whose markup needs a stylesheet, demonstrating that the theme — not the engine — decides
which assets a call pulls in (ADR-0079). The engine renders `assets:tally` once however many times
::tally is called, and not at all on a page that never calls it. */}}
{{define "tally"}}<p class="tally">{{.Args.label}}: <strong>{{.Args.count}}</strong></p>{{end}}
{{define "assets:tally"}}<link rel="stylesheet" href="/static/tally.css">
{{end}}