# Theme contract What the engine promises a theme, and the only thing this repository is bound to (ADR-0023). A theme's markup, layout and styling are not the engine's business; a theme's *inputs* are. **STATUS: partly live.** The fields under *Live today* exist and are gated; everything else is the shape the contract takes when the feature arrives. All of it is engine obligation, not theme instruction — a theme may ignore any of it. ## Live today A bundle page receives: | Field | Contents | |---|---| | `.Title` | may be empty; a template falls back to `.Key` rather than failing | | `.Lang` | the locale of this variant, always set | | `.Key` | the bundle's identity, without language or extension. **Not its address** — a `slug` moves the address and leaves the key alone, so link with `.URL` or `.Canonical` and never by assembling a key (ADR-0035) | | `.HTML` | the rendered body, already escaped | | `.Extra` | every frontmatter key the parser does not name (ADR-0002) | | `.Style` | the reference theme's stylesheet, inlined so a bare site root needs no asset route | | `.Canonical` | the permalink of the variant actually served — not the URL requested, which differs when the fallback chain supplied another language. **Absolute** when the site declares `base`, since a canonical link is resolved by machines rather than by the page (ADR-0039) | | `.Alternates` | every language this key exists in: `.Lang`, `.URL` (absolute, for `hreflang`) and `.Path` (root-relative, for a *visible* language link — `.URL` would send a reader to the canonical host) | | `.Site` | what the site declared about itself: `.Site.Base` and `.Site.Title`, either possibly empty | | `.Sections` | every section that holds something, for navigation. Only `.Title` and `.URL` are set, and the list is empty on a bare site (ADR-0049) | | `.Sequence` | the series this page sits in, absent when it sits in none (ADR-0033) | | `.Tags` | this bundle's own terms, each with the URL of its listing; empty when it carries none (ADR-0049) | | `.ExtrasURL` | this bundle's supporting files, empty when it has none — so a theme can offer them without guessing (ADR-0047) | `.Sequence` carries the reading order and this page's place in it: | Field | Contents | |---|---| | `.Sequence.Title`, `.Sequence.URL` | the series' title (may be empty) and its permalink | | `.Sequence.Members` | every entry in reading order, each as an `.Items` entry — *ascending*, unlike a dated listing | | `.Sequence.Index`, `.Sequence.Count` | this page's 1-based position and the total; `Index` is 0 when this page is the series landing itself | | `.Sequence.Prev`, `.Sequence.Next` | the neighbours, absent at the ends and on a landing page. `Prev` is the **earlier** entry — the opposite sense of a listing's `.PrevURL` | | `.Sequence.First`, `.Sequence.Last` | the ends of the series, present whenever it has members | A landing page therefore renders an archive from `.Members` and a chapter renders navigation from `.Prev`/`.Next`, both from one field. Membership and order are the engine's business (`content-model.md`); a theme never sorts. Two named templates: `base` is executed for every page; `main` is the block each kind of page defines and a theme redefines. There is one parsed set per kind — bundle and listing today — so two kinds may both define `main` without colliding (ADR-0019). Every kind of page receives `.Title`, `.Lang`, `.Canonical`, `.Style`, `.Site`, `.Sections` and `.Alternates` — the document shell. A listing page adds: | Field | Contents | |---|---| | `.Items` | entries on this page: `.Title`, `.Key`, `.URL`, `.Date`, `.Section` | | `.Page`, `.Pages` | 1-based position and total, `Pages` at least 1 | | `.PrevURL`, `.NextURL` | empty at the ends; *newer* is `prev`, because the order is newest first | | `.Groups` | the same entries partitioned by section, each with `.Name` and `.Items`. A tag listing supplies **both** shapes and the theme picks: templates cannot group, so the engine offers the partition, but whether a listing looks grouped is markup (ADR-0046) | ## Chrome text and formatting Three functions, available in every template including a site root's own blocks. They exist so no template hardcodes English: the words the engine supplies are the engine's to localise (ADR-0034). | Call | Gives | |---|---| | `{{t .Lang "newer"}}` | one phrase in that language. Extra arguments fill `%s` placeholders in order | | `{{num .Lang .Page}}` | an integer in that language's digits — `12`, `১২` | | `{{day .Lang .Date}}` | a date as that language reads it — `8 March 2026`, `৮ মার্চ ২০২৬`; empty for a zero date | Phrase keys today: `newer`, `older`, `empty`, `page-of` (two arguments), `position` (two arguments), `first`, `last`, `extras`, `back-to-page`, `contents`, `note`, `warn`, `tip`, `details`. An unknown language falls back to the default locale and an unknown key returns itself, so a missing translation can never blank a page or fail a render. Three rules a theme must keep: put the machine-readable form in the attribute and the localised form in the text — ``; never localise `.Title`, `.HTML` or anything else the author wrote; and never pass a URL, key or path segment through these functions. An address is not chrome (`content-model.md`). A site root cannot add or override a phrase yet. A theme needing its own words writes them in its own block; site-supplied strings wait for the settings cascade (`ideas/deferred-decisions.md`). ## Shortcode fragments Fragments live in `templates/shortcodes.html`, or in `templates/shortcodes/*.html`, or both — one named template per shortcode, and that is where a shortcode's markup lives. The engine parses the call and supplies its data, never any HTML (ADR-0036). Parse order is embedded file, embedded directory, site file, site directory, and the last definition of a name wins (ADR-0071). So **the directory overrides the file** within one source, and a site overrides the binary either way. The reference theme ships the directory: `figure`, `gallery`, `icon`, `admonitions` (note, warn, tip), `details`, `aside`, `toc`. Override one by defining that name anywhere the engine looks; everything you leave alone is inherited. ### Assets a fragment needs A shortcode that needs CSS or JS declares it as a second fragment named `assets:` (ADR-0079): ``` {{define "lightbox"}}{{end}} {{define "assets:lightbox"}}{{end}} ``` The engine renders `assets:lightbox` **once** into `Page.Assets` when the page called `::lightbox` at all — however many times — and renders nothing when it did not. A shortcode with no `assets:` fragment is the normal case and costs nothing to declare. Frontmatter `use: [lightbox]` pulls the same fragment in without a call, for a page whose script is not tied to one shortcode. The fragment receives an empty `Fragment`: it emits fixed markup, not per-call markup. Two calls with different arguments still share one asset, which is what makes deduplication meaningful. Order is first-call, and nothing promises an order *between* two assets. Where the files live is yours. `/static/` is served verbatim and needs no engine change, which is why the reference theme's examples point there. The reference theme itself defines no `assets:` fragment and ships no asset (ADR-0063) — this is a mechanism, not an invitation. Templates in the same set may call each other — `{{template "picture" .}}` from `figure.html` reaches a block defined in `gallery.html`, because both are parsed into the partials set. The pipeline is all that passes, and there is no way to build a value in a template, so inclusion is useful where the callee needs exactly what the caller has. Across sets it does not work: `base`, `page`, `list`, `extras` and the fragments are five separate sets, which is what stops a listing's `main` leaking into a bundle page (ADR-0019). Every fragment receives the same two fields (ADR-0037): | Field | Contents | |---|---| | `.Args` | the call's `key=value` pairs, exactly as the author wrote them | | `.Pictures` | images the *engine* gathered: one for a figure, many for a gallery, none when the call names nothing it recognises (ADR-0042) | | `.Lang` | the language being served, so a fragment can localise words of its own through `t` (ADR-0067) | | `.Body` | a container call's content, already rendered to HTML. Empty for every leaf call (ADR-0064) | | `.Headings` | the document's headings for a `::toc` call: `.Level`, `.Text`, `.ID`. Empty for every other call (ADR-0065) | Each picture carries: | Field | Contents | |---|---| | `.Src` | the author's own file, relative to the bundle — always usable on its own | | `.Srcset` | the generated widths, closed by the original at its own width; empty when nothing was worth generating, or when the format has no decoder | | `.Width`, `.Height` | the original's intrinsic size, for reserving the box; zero when the file could not be read | **A theme emitting `.Srcset` must emit `sizes` too.** The engine cannot supply it: `sizes` says how wide the picture will *be*, which is a fact about the layout and therefore the theme's (ADR-0068). Without it a browser assumes `100vw` and fetches the widest variant for a thumbnail, which costs more bandwidth than serving no derivatives at all. The reference theme states its own measure — 32rem for a figure, 16rem for a gallery column above 36rem — and adds `loading="lazy"` to gallery entries but never to a figure, which is often the first thing on the page. | Shortcode | Template | Receives | |---|---|---| | `::figure{src=… alt="…" caption=…}` | `figure` | `.Args.src`, `.Args.alt`, `.Args.caption` | | `::gallery` | `gallery` | `.Pictures` — every picture beside the bundle, in filename order | | `:name:` | `icon` | `.Args.name` — the name as written, nothing else | | `:::note{title=…}` … `:::` | `note` | `.Args.title`, `.Body` — the rendered content | | a fenced code block | `code` | `.Args.lang`, `.Args.title`, and `.Body` — chroma's token spans, already classed (ADR-0075) | | `::toc{depth=N}` | `toc` | `.Headings` — the document's headings in order, no deeper than `depth` if given | | `:::details{summary=… group=… open=…}` … `:::` | `details` | `.Args`, `.Body`. Siblings sharing a `group` open one at a time, through `
` and no script | | `:::aside{title=…}` … `:::` | `aside` | `.Args.title`, `.Body`. Beside the text where there is room, in the flow where there is not | **A fragment supplying its own words must localise them.** `.Lang` is there for exactly that: the reference theme labels an untitled admonition with `{{t .Lang "warn"}}` and a contents list with `{{t .Lang "contents"}}`, so a Bengali page reads সতর্কতা and সূচিপত্র rather than English (ADR-0067). Words the *author* wrote — `.Args.title`, `.Body` — are never touched. **A code block is highlighted by the engine and framed by the theme.** `.Body` is `
` with
token classes — `k` keyword, `s` string, `c` comment, `m` number, `nf` function, `ln` line number, and a
`line hl` for a highlighted row. The palette is a stylesheet's, never inline, so dark mode is the theme's as
usual. The reference theme frames it in a `
` with the title as a `
`, and stops there: a copy button needs a script, and the reference theme is gated script-free (ADR-0026), so that belongs in a theme of your own. A **container** call wraps content: `:::name{…}`, a body, then `:::`. It renders through the fragment of that name with `.Body` already HTML, and one level only — a `:::` inside closes the one it is in. A theme with no template for the kind renders nothing, and the engine writes the body out unwrapped, so an unknown kind costs an author styling and never paragraphs (ADR-0064). The reference theme defines `note`, `warn` and `tip`. The `icon` fragment is one template for the whole set: **which names exist is the theme's decision**, and the engine holds no list of them (ADR-0063). Render nothing for a name you do not know — the engine then writes the author's original `:name:` text back, so an unrecognised icon is never dropped out of a sentence. The reference theme maps six names to Unicode characters and ships no sprite, font or asset; a theme wanting drawn icons defines a sprite in its own `base.html` and emits `` from this fragment. `::include{file=…}` has **no fragment**: an included file is content, so it renders as Markdown in place and a theme has nothing to style about it (ADR-0038). Authored and engine-supplied data are kept apart so a `src` argument can never be confused with a `src` the engine found. Arguments are escaped by `html/template` like any other data, in whichever context the template puts them — which is what keeps an author's text out of the markup. A call whose template is missing renders nothing and logs; it never fails the page. A gallery entry has no alt text: nothing in a filename supplies one, and inventing it would be worse than an empty `alt` on a picture the page has already introduced. A figure's alt is the author's, from `.Args.alt`. Argument names are contract, added but never renamed. ## Feed discovery The engine serves Atom at `/feed.xml`, per section and per tag (`content-model.md`). A theme advertises it from the document head, which is what a reader looks for: ``` ``` Only with a declared `base`: without one there is no feed to point at. ## What is yours and what is the engine's The engine supplies facts it alone can produce — which bundles exist, what a picture measures, where a permalink points, what a month is called in Bengali. Everything about how those facts *look* is yours, and the engine is audited against that line (ADR-0046, `harness/architecture.md`). Two consequences worth knowing: - Where a listing offers more than one shape of the same data — `.Items` and `.Groups` — pick one. The engine is not telling you which. - If something you want is purely visual, it is CSS or a template, and asking the engine for it is the wrong request. Widow control used to be an engine feature and is now one line of stylesheet (ADR-0045). ## Typography is the theme's The engine transforms text where no stylesheet could — smart quotes, dashes, ellipses — and stops there. How text *lays out* is the theme's, done in CSS: the reference stylesheet sets `text-wrap: pretty` on body copy and `text-wrap: balance` on headings, which is where widow and orphan control belongs (ADR-0045). The engine will not insert characters into an author's prose to influence line breaking. ## Markup a body may now contain `.HTML` is Markdown output, so the dialect decides which elements a theme has to be ready to style (`content-model.md`). Beyond CommonMark's own, since ADR-0058: `` with ``/``, `
`/`
`/`
`, ``, ``, ``, ``, ``, and goldmark's footnote markup — `` in the text and a `
` carrying an `
    ` of notes. Headings arrive with an `id`. Since ADR-0078, `` opening a list item — a task list, static and unclickable; the reference stylesheet unbullets those lists with `:has`, since goldmark adds no class to find them by. None of it is optional and none of it is configurable: a theme that styles none of these still renders a correct page. The reference stylesheet does the minimum — a rule under each row, an indent for definitions, smaller footnotes — and nothing more (ADR-0026). ## The stability rule Fields and names are **added, never renamed or removed**. Absence is always legal: a template reading a field that does not exist gets the zero value and must not crash, and the engine must not make a missing field fatal at request time (ADR-0002, invariant 1). This is the View-layer freeze from `architecture.md`, arriving as soon as a theme exists rather than at Arc 2. Breaking the contract is not a feature — it is a new contract version, and it needs an ADR. ## What the engine provides | Provides | Detail | |---|---| | the page object | known fields plus an `Extra` bag carrying unknown frontmatter (ADR-0002) | | the resolved cascade | settings after site → section → bundle resolution (`ideas/deferred-decisions.md`) | | queries the page needs | its sequence neighbours, its taxonomy terms, its section's members | | named template lookup | per-type sets; a theme redefines a named block and inherits the rest (ADR-0019) | | URLs | every path the engine emits, so a theme never constructs one by hand | | per-page assets | `.Styles` and `.Scripts` — the bundle's own files as finished URLs, from the frontmatter lists, with anything climbing out of the bundle already dropped (ADR-0079) | | theme assets in use | `.Assets` — the `assets:` fragments this page's shortcode calls and `use:` list asked for, rendered once each. Emit it in the `head` block | | a `head` block | `base.html` calls `{{block "head" .}}`, empty by default; `page.html` fills it with `.Assets` and `.Styles`, because only a bundle carries assets. **`.Scripts` is not emitted by the reference theme** — it ships no `