# 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 | | `.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 | | `.Alternates` | every language this key exists in, as `.Lang` and `.URL`, for `hreflang` | | `.Sequence` | the series this page sits in, absent when it sits in none (ADR-0033) | `.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). A listing page receives `.Title`, `.Lang`, `.Canonical`, `.Style` as above, plus: | Field | Contents | |---|---| | `.Items` | entries on this page: `.Title`, `.Key`, `.URL`, `.Date` | | `.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` | set instead of `.Items` when entries are grouped — a tag listing groups by section, each `.Name` and `.Items` | ## 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). 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 `templates/shortcodes.html` holds one named template per shortcode, and that is where a shortcode's markup lives — the engine parses the call and supplies its arguments, never any HTML (ADR-0036). | Shortcode | Template | Receives | |---|---|---| | `{{< figure src="…" alt="…" caption="…" >}}` | `figure` | `.src`, `.alt`, `.caption` — every argument as written, escaped on output | Arguments arrive as strings and are escaped by `html/template` like any other data, 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. The set is overlaid the same way as the page kinds: a site's `templates/shortcodes.html` is parsed after the embedded one, so redefining `figure` replaces it and any fragment left alone is inherited. Argument names are contract, added but never renamed. ## 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 | the `styles` / `scripts` frontmatter lists, resolved relative to the bundle | | chrome strings | looked up by key and language, never hardcoded English in a template — live, see above | | validity windows | a template that renders time-dependent output declares one | ## Extras view For a request under a bundle's extras directory (`ideas/deferred-decisions.md`) the engine additionally provides: | Field | Contents | |---|---| | `.Extras.Entries` | the tree: `Name`, `Path`, `URL`, `RawURL`, `Kind`, `Size`, `IsDir`, `ModTime` | | `.Extras.Selected` | nil on the bare listing; otherwise the chosen entry | | `.Extras.Selected.HTML` | rendered output for `markdown` and `text` kinds; empty otherwise | | `.Extras.Selected.RawURL` | always present — for ``, ``, or a download link | `.Bundle` is the parent, so a theme has its title, language and breadcrumb. Selecting an entry is an ordinary link and a full re-render, so a sidebar-plus-pane layout needs no JavaScript; swapping the pane client-side is a later enhancement over working markup, never a requirement. ## What the engine does not provide Layout, class names, CSS, client-side behaviour, and any choice about how something *looks* — including how media is embedded, how a listing is arranged, and whether something appears in a sidebar. Those are theme decisions, and a request touching them is split: a contract extension here, a change there. The engine also does not provide a component library, a CSS build step, or a JavaScript runtime. `conventions.md` holds the output floor the engine itself meets — semantic HTML, zero-JS, every image carrying width, height and alt — and a theme is expected to stay inside it, but the engine does not enforce a theme's markup. ## The reference theme The binary embeds a reference theme — templates plus one small stylesheet — so a bare site root renders (ADR-0026). It exists to make this document executable: it implements every field and block named here and nothing else, and a golden-file test through it catches contract regressions before a real theme does. It is not a design. Legibility only, no branding, no visual opinions, no JavaScript — `verify.sh` fails on a `