One global namespace (ADR-0018): /tags/{term}/ spans every section and
/{section}/tags/{term}/ narrows it. Listings group by section so one busy term
stays readable, which needed List.Groups alongside Items — list.html renders
whichever is set.
This is Query's second use, so it gained a Tag field rather than being generalised
on speculation: one filter, two callers. Tag slugs lowercase and hyphenate,
preserving script, so "Long Monsoon" and "long monsoon" are one term while Bengali
passes through unchanged. Hand-chosen slugs per term still wait for the type
declaration that owns overrides.
`tags` is reserved at the top level and inside every section, alongside `page` and
the language prefixes. A tag listing redirects to its canonical URL only once it is
known to exist, matching the rule bundles already followed — otherwise a canonical
URL for nothing confirms what is not there.
One stale test expectation fixed rather than worked around: it asserted tags land
in Extra, which stopped being true when tags became a named field.
Evidence: /tags/monsoon/ lists Hello World under posts and First Rain under comics;
/comics/tags/monsoon/ shows one; /tags/monsoon 301s; /tags/nothing/ and /tags/ 404.
6.5 KiB
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 |
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 |
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 |
| 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 <img src>, <object data>, 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 <script tag in it. If it starts accumulating taste, it has stopped being a reference.
It demonstrates the contract; it is not the contract. Changing it does not change what a theme may rely on
— which is why verify.sh also fails when it changes without this document changing, since in practice the
two drift together.
Overriding it
A site root's templates/ is parsed after the embedded set, and the last definition of a name wins, so
a theme redefines one named block and inherits the document (ADR-0019). Per kind, exactly two files are
overlaid — base.html and that kind's block file (page.html or list.html). Overlaying every site
template into every set would let a listing's main leak into bundle pages, which is the collision
per-kind sets exist to prevent.
templates/theme.css in the site root replaces the reference stylesheet entirely; there is no merging.
static/ in the site root is served verbatim under /static/. Directory paths answer 404 rather than
listing their contents.
Splitting a request
When a feature spans engine and theme, this repo delivers:
- the contract extension — new fields, new named blocks, new queries, new URLs;
- a note stating what a theme must do to use it, precise enough to act on without this conversation;
- embedded defaults updated far enough to prove the extension works.
It does not deliver the theme. Claiming otherwise is the same error as claiming to have migrated content in a site root this repo cannot see.