A bundle nested under another bundle is a member of that series (ADR-0033), so
`Site.Sequence` walks up to the nearest bundle ancestor and back down to its
members: ordered by `order` where set, then by name. Members resolve through the
language fallback, so a chapter with no Bengali variant still holds its place in
Bengali reading order instead of breaking prev/next.
One `.Sequence` field carries both shapes a theme needs. A landing page renders
`.Members` as an archive; a chapter renders `.Prev`/`.Next`, which are pointers
into `.Members` so `{{with}}` yields nothing at the ends. `Index == 0` is what
tells the two apart.
`Query` was deliberately not extended. A series ascends where `Run` descends, and
an order knob on `Query` is the config knob rule 6 bans; instead `Site.keys()`
came out so both iterate the index one way, deleting `Run`'s own dedupe map.
`draft` is not honoured: no bundle carries the field and nothing else excludes
drafts, so entry 19 adds it in both places at once. Recorded in content-model.md
rather than left implied.
state.md also corrects six inventory rows that had drifted before this change —
three LOC figures, the test total, `go.mod`, and two lines that were flatly wrong
("Dependencies: none", "goldmark is not yet imported"). The coupling gate proves
state.md changed with the code; it cannot prove the numbers are right.
137 lines
7.6 KiB
Markdown
137 lines
7.6 KiB
Markdown
# 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` |
|
|
|
|
## 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:
|
|
|
|
1. the contract extension — new fields, new named blocks, new queries, new URLs;
|
|
2. a note stating what a theme must do to use it, precise enough to act on without this conversation;
|
|
3. 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.
|