# Architecture contract

> Repository assembly, configuration, diagnostics, localization, output, performance, security, CSS, accessibility, and release-state boundaries.

---

LLMS index: [llms.txt](/llms.txt)

---

> [!NOTE] OINK 1.2.0 contract
> This contract describes the v1.2.0 release. Its canonical bilingual sources
> are in `content/docs/design/`.

## Repository and assembly {#repository-and-assembly}

The repository root is a Hugo Module and complete theme, not a site or npm
workspace. Hugo Extended compiles SCSS and templates. Browser runtimes and
third-party assets are committed, so a normal build performs no network fetch.
Public bilingual documentation, examples, and browser tests live in the
sibling `oink.pgsty.com` repository; the theme repository keeps only narrow
internal regression fixtures under `tests/site/` and has no separate public
example surface.

Generated `public/` and `resources/` trees are never source. Vendored runtimes,
font families, and Font Awesome glyph definitions are supported distributions,
not dead-code candidates; `VENDOR.json` and `bin/check-vendor.py` pin their
integrity. OINK ships the complete supported Font Awesome distribution because
consumer-authored content may use icons that theme templates do not.

The official compiled Font Awesome CSS is one stable, fingerprinted vendor
stylesheet loaded before the fingerprinted `main.css` produced from theme and
consumer SCSS. A site-style edit therefore does not invalidate the icon
distribution, while ordinary cascade order still lets the site override it.
Capability styles such as KaTeX, DocSearch, Swagger, and Asciinema remain
separate and load only when used. Fingerprints make immutable URLs possible;
the deployment host, not the Hugo theme, owns their HTTP cache headers.

Hugo types `docs`, `book`, `blog`, and `swagger` select the reading shells;
`params.ui.shell_types` may add types. Landing is `layout: landing`. There is no
`article` type or second blog shell: immersive pages are a blog presentation
described in the [shell contract](/docs/design/shell/).

`layouts/_partials/shell/config.html` resolves shared shell facts. Layouts must
render through `content/render.html` before `scripts.html`, because render hooks
and shortcodes register capability flags in the Page Store. Override the
narrowest partial; superficially similar base templates remain separate where
merging would change Hugo lookup precedence.

## Configuration and diagnostics {#configuration-and-diagnostics}

Theme policy lives under `params.ui.*`; multi-setting integrations such as
`comments.giscus`, `plantuml`, and `drawio` stay top-level. Boolean features use
bare booleans unless they also have several settings. A page override drops the
`ui.` prefix: `params.ui.image_zoom` becomes `image_zoom`, never a front-matter
`ui` map. `hugo.yaml` declares published defaults; an owning resolver and its
checker define any optional configuration shape or range.

Invalid input follows one rule: warn with the value, allowed shape, and safe
fallback; then use that fallback or omit the unsafe feature. Ordinary
`hugo server` therefore remains usable, while every publishing gate uses
`--panicOnWarning`. The theme never calls `errorf`, and `check-params.py`
enforces that boundary. Do not add speculative validation for unreachable
states.

There is no generic renamed-key registry. A transition that still needs a
migration diagnostic uses a targeted warning in its owning resolver plus a
strict negative test; removed keys are never read as a compatibility path.

Network-capable features are explicit and degrade closed. PlantUML requires
`plantuml.svg_image_url`, Draw.io requires `drawio.drawio_server`, and Algolia
requires `appId`, `apiKey`, and `indexName`; incomplete configuration warns and
emits no request. Draw.io loads only when rendered content contains PNG or SVG
candidates, then inspects each distinct image URL once.
Diagram endpoints must be strings containing an HTTP(S) URL with a host or a
same-site path. Unsupported schemes, protocol-relative URLs, backslashes, whitespace, and
pathless same-site references warn and disable the integration before its runtime is selected.

## Interface localization {#interface-localization}

> [!NOTE] Available since OINK 1.1
> OINK 1.1.0 expands the native interface catalogs to the complete locale set
> below. Consumer-authored content still needs its own translations.

OINK ships native interface catalogs for the 31 locale filenames present in
[`google/docsy@64f51c5`](https://github.com/google/docsy/tree/64f51c5bde2abd2e8a001cb31b32656f5800ca56/theme/i18n),
plus generic `zh` as the Simplified Chinese default:

```text
ar az bg bn de en es et fa fi fr he hi hu it ja ko nl no oc pl pt-br ro ru
sr-cyrl sr-latn sv tr uk zh-cn zh-tw
```

That is a compatibility scope, not a runtime dependency on Docsy and not a
claim that a consumer's authored content has been translated. A new Docsy
locale does not enter OINK automatically: it needs a complete OINK catalog and
the same review as every existing locale.

`i18n/en.yaml` owns the 194-message schema. Every one of the 32 OINK bundles has
exactly that key set and native UI text; an English value may remain only when
it is a reviewed product name, punctuation token, conventional abbreviation,
or genuine word shared by the target language. There are no generated English
fallback blocks. `zh` and `zh-cn` carry Simplified Chinese, while `zh-tw`
carries Traditional Chinese.

On the Hugo 0.160.x compatibility floor, a non-default generic `zh` language
key must set the concrete `locale: zh-CN` value when the regional Chinese
catalogs are also present. Bare `locale: zh` resolves in that configuration
from Hugo 0.161 onward. This affects language configuration, not the
`i18n/zh.yaml` catalog name.

Runtime placeholders such as `%s`, `{count}`, and `{{ .Count }}` may move to a
grammatically natural position but must remain byte-for-byte identical. Values
are strings or Hugo plural-message maps. Plural maps use the locale's supported
categories (`zero`, `one`, `two`, `few`, `many`, `other`); `other` is required,
and every form is a string with the same placeholders. Catalogs contain no hidden bidirectional controls; Arabic,
Persian, and Hebrew direction still comes from the consumer language setting
(`direction: rtl`), not from characters injected into translations.
`bin/check-i18n.py` enforces the locale set, schema, value shape, placeholders,
directional controls, and the small reviewed set of English-identical terms.
Adding a visible string therefore means translating it in every bundle in the
same change, not running a fallback generator.

## Featured images {#featured-images}

Hugo's `images` is the single authored API; `params.images` is only the
site-wide social fallback.

| Source | Reader thumbnail | Social card |
| --- | --- | --- |
| Page `images`, or bundled `**featured*`, `*feature*`, `{*cover*,*thumbnail*}` | yes | yes |
| Section `cascade.images` | yes | yes |
| Site `params.images` | no | yes |

`images: []` clears an explicit or cascaded value but does not disable bundled
resource discovery. Only the first resolved image is representative. Local
processable rasters may be cropped; SVG, static, and remote resources remain
valid without Hugo image operations.

`featured-image-resolve.html` owns source ranking and relative/absolute URLs.
A page's explicit `images` outranks its bundled resource, which outranks an
inherited cascade image, even when explicit and inherited values are identical.
For file-backed pages, authored presence is read from the source front matter;
Hugo parses its YAML, TOML, or JSON. For generated pages without a source file,
resolved `images` is treated as explicit. List thumbnails,
Open Graph/Twitter/schema helpers, author avatars, Pinterest media, and blog
presentation all consume that decision.

`params.ui.featured_image` is blog-only and defaults to `none`; front matter
overrides it per page or cascade. `banner` renders a figure above a single-page
title, `wash` colors its header, and `hero` paints the shell backdrop on single
pages and section indexes. Missing images and non-HTML output render no image.

## Outputs and runtime {#outputs-and-runtime}

Every base template sets `Page.Store.tdOutputFormat`:

| Output | Contract |
| --- | --- |
| HTML | Complete semantic content; local runtime only for used capabilities |
| Print | Expanded content; no shell navigation, search, or zoom runtime; the shared action layer supports explicit print controls |
| Markdown / LLMS | Source-shaped Markdown without `td-` component markup |
| LLMSFULL | Opt-in per top-level section: one `llms-full.txt` per enabled section per language, that same Markdown concatenated in reading order |
| RSS | Safe static summary or explicit omission |
| NAVJSON | Opt-in per site: one `navigation.json` per language, serializing the navigation authority the sidebar and pager already read |
| BookManifest | Opt-in ordered JSON handoff for a publication packager; never presented as an EPUB or PDF |

Output formats run in their defined order; the mutable-format concern is not a
cross-format race. Within Print, however, Hugo may render a Book page and
overlapping aggregates in parallel. One per-page cached coordinator therefore
produces the plain and Book variants in a fixed order, and each caller selects
the form it needs. Plain Print keeps page-local heading and routed xref URLs;
Book aggregates keep namespaced headings and in-document xrefs.

Consumers opt into custom outputs; OINK does not force expensive Book
aggregates. HTML gets the shared action and core layers plus stable first-party
capability chunks selected by the page flags. Templated capabilities publish at
most one chunk per language; flags choose script tags and never create a new
combination bundle. Print keeps the action layer and only runtimes required by
rendered print features. Large third-party UMD files stay separate; unused
feature runtimes stay absent.

`LLMSFULL` is enabled by a top-level section listing it in its `_index` front
matter `outputs`; the theme never adds it to a site's output set. One shared
renderer produces the per-page Markdown and the bundle, so a bundle is that same
semantic Markdown -- no `td-` component markup -- concatenated in the sidebar and
pager reading order. Enabling it below the top level warns and emits nothing, so
an ordinary build stays usable while `--panicOnWarning` blocks publication.

`NAVJSON` is enabled by the site's `outputs.home` and publishes one
`navigation.json` per language at the language root. It serializes the same
authority chain the sidebar and pager read: an explicit `data/docs_nav.json`
tree when present, the weighted content tree otherwise. Array order is the
contract and `weight` is never serialized, and the output is `notAlternative`.
`schema/nav.v1.schema.json` versions the format as a hand-authored contract
artifact, edited with its templates and checker rather than by the generated
configuration schemas' drift gate. Both outputs default off, so a site that
enables neither builds byte-identically; `bin/check-agent-indexes.py` owns them.

`BookManifest` is disabled unless a Book root explicitly lists it in `outputs`.
It references that Book's existing per-page Markdown and records derived page
order, headings, numbered targets, and xrefs. It contains no publication
metadata guessed by the theme and is not a distributable ebook.

The theme repository ships `bin/book-epub.py` and `bin/book-pdf.py` as explicit
publication steps, with `bin/check-book-epub.py` and `bin/check-book-pdf.py` as
their artifact gates. The EPUB packager combines `BookManifest` with the same
whole-Book Print HTML and accepts consumer metadata separately. The PDF runner
serves that Print output only on a temporary loopback address, invokes an
explicit Chrome/Chromium binary behind a `script-src 'none'` Content Security
Policy, and emits A4 pages with CSS page numbers.
The PDF server also applies a CSP sandbox, rejects meta-refresh navigation,
and refuses symlinks escaping the build tree. Without the network opt-in,
image and media requests are limited to the loopback origin and data URLs,
including requests initiated by CSS or SVG.
Both tools refuse missing or out-of-tree resources; network resources and
output replacement each require a separate explicit flag. The network opt-in
allows passive HTTP(S) media only; remote scripts and local-file schemes remain
invalid. Relative assets in the EPUB metadata file resolve from that file's
directory, not from the caller's working directory. No publication work runs
during an ordinary Hugo build, and PDF remains Print-derived rather than
another template output.

Performance rules:

- do not walk `.Site.Pages` per page when a site-level resource or
  `partialCached` result can own the work;
- render `.Content` once and read Page Store flags only after it;
- emit correct markup instead of scanning the DOM to repair it;
- group browser work by resource URL, not DOM instance;
- keep ordinary outputs opt-in when their aggregate cost is material;
- emit no Speculation Rules by default: a named production consumer must first
  measure `Sec-Purpose: prefetch` requests, useful navigations, transferred
  bytes, and CSP impact with a reversible `moderate` experiment;
- validate reachable author input, not hypothetical internal states.

`bin/measure-baseline.py` measures build time, output weight, bundle count, and
shortcode density.

## Trust, CSS, and accessibility {#trust-css-and-accessibility}

Authors may enable Goldmark `unsafe`; configuration and component parameters
are not raw HTML. The shared attribute policy consumes an allowlist, validates
class tokens, passes `data-*` and `aria-*`, and warns while dropping `style`,
`srcdoc`, `on*`, reserved, and unknown attributes. URL helpers reject dangerous
schemes and protocol-relative URLs where local or explicit absolute URLs are
required. Promised remote URLs remain supported but are never fetched at build
time.

Theme output uses `td-` classes, `data-td-*` attributes, and `--td-*` custom
properties; author markers such as `.steps`, `.cards`, and `.full-width` stay
unprefixed. CSS supports RTL, print, forced colors, reduced motion, long tokens,
and narrow viewports. Theme-owned decorative icons carry `aria-hidden`; pages
with task lists or raw authored Font Awesome elements alone load the authored
accessibility repair.

Reading-container focus distinguishes pointer origin from keyboard navigation.
A pointer-focused main region, table viewport, or code `pre` does not acquire
an outline merely because the reader presses another key. Tab, blur, or a new
non-pointer focus clears that exemption. Controls retain their own focus
styles, scrollable containers retain `tabindex`, and the skip-link destination
shows a local outline around its title instead of the entire article. Forced
colors preserve the keyboard indication; no global focus-outline reset is used.

Font roles are `ui`, `body`, `heading`, `code`, `display`, `meta`, `brand`, and
`print`, exposed as `--td-*-font-family`. `ui` is the main face: `body`
resolves through it, and `heading` through `body`, so one assignment moves
chrome, prose, and headings together. `params.ui.typography` is `technical` or
`system`; both compile into one stylesheet with no runtime. Legacy
Bootstrap/Docsy Sass variables continue to seed these roles.

`params.ui.fonts` reaches the same roles from configuration, for a site that
would rather not mount SCSS or add a stylesheet. It names faces and never
loads them: a family must be one the reader has or one the site declared in an
`@font-face` of its own, which keeps the key outside the network contract.
Values are gated to plain font family syntax and the emitted `:root` block is
rebuilt from the matched parts; an unknown role or an unsafe value warns and is
dropped alone. The block renders after the stylesheet, which is what lets an
authored face outrank the preset at equal specificity. A shell reads in the
site's faces and owns none of its own: a Book sets its numbers and captions in
the prose face, not in a technical one.

The accent family splits by role. Accent *text* -- links, external URLs, inline
code -- follows the Bootstrap link family and `--bs-code-color`, which a theme
color never redeclares. Inline code follows the visual preset: Slate retains
the crimson pair, while Paper uses ink text on a quiet chip. Accent
*grounds* -- selected rows, the greyed ground a navigation row takes under the
pointer, hover washes, the outline pill, rail and dot, chip hovers, a card's
hovered edge, a share button's hover fill, selection, focus rings -- follow
`--td-accent`, `--td-accent-rgb` and `--td-accent-hover`, which default to the
link family and are the only properties `params.ui.theme_color` emits. Ink that
belongs to the shell rather than to the prose follows them too: the outline
anchors the viewport is standing over, and a Book chapter's headings under the
pointer or keyboard focus, light in the section's color, not in the link blue.
`theme_color` and `theme_color_dark` take `#rgb`/`#rrggbb`; front matter and
section cascades override the site value. An unconfigured site emits nothing.
An unparseable value warns and keeps the default palette. A resolved color below 4.5:1 against the site default preset
canvas warns with a suppressible id and still ships: the check is advisory, and
only a parse failure drops a color. The light color is the key: a
`theme_color_dark` with no valid `theme_color` warns and is ignored, so a page
is colored in both modes or in neither. An omitted dark half lightens toward
white in 4% steps until it clears 4.5:1 on the dark canvas. Every emitted byte is
formatted from parsed integer channels, never from author text. One resolver
answers "what color is this page" for the head block and the sidebar root
switcher alike.

## Visual presets {#visual-presets}

OINK 1.2.0 defaults to Paper. `params.ui.preset` accepts
`paper`, `slate`, and the explicit experimental presets `ink`, `terminal`.
Invalid and reserved names (`folio`, `canvas`) warn and fall back to `paper`.
`params.ui.preset_menu` defaults to `false`; `true` offers Paper, Slate and the
site default. A list selects available choices, including experiments. A list must include the site default; missing it
warns and adds it. There is no page-level preset override.

Hugo renders `data-td-preset` and `data-td-site-preset` on every document root,
including 404 and print output. With reader choice enabled, an inline head
script validates `td-preset` before CSS loads. Selecting the default removes
that storage key. Invalid saved values are removed; blocked storage leaves
in-page controls usable and shows a non-persistence note. The `storage` event
synchronizes tabs. `td-preset-change` carries `{preset, previous, stored}`.
Mode remains independent: `data-bs-theme`, `td-color-theme`, and
`td-theme-change` retain their meaning. The browser chrome color follows the
resolved mode and preset. Without JavaScript, the site default light palette
renders; appearance controls require JavaScript.

All four presets compile into one stylesheet. Slate retains the v1.1.0 base
palette selectors and values. Paper changes palette and selected component
rules without changing shell columns, breakpoints, or global spacing. Dark
Paper redeclares every light palette token, including nested dark islands.
Font roles have equal selector specificity: preset, then `typography: system`,
then head-emitted `params.ui.fonts`. Site `_styles_project.scss` remains last.
`brand` controls the wordmark independently from display headings. Paper uses
local IBM Plex Sans; Slate keeps Inter; both retain Chakra Petch for the
wordmark and IBM Plex Mono for code. System typography requests no bundled
text face unless the site explicitly overrides a role. No external font is
introduced. Ink uses Inter throughout, with red markers, underlined prose
links, square geometry and no shadows. Terminal uses mono chrome/headings,
Plex Sans prose, teal links, amber accents, 2 px corners and no shadows. Its
navigation density changes only on desktop; prose measure and mobile targets
remain unchanged. CSS heading marks have empty accessible alternatives and
are omitted in unsupported browsers. Explicit `fonts.ui` still supplies the
main face unless a valid `fonts.body` overrides it. See the
[experiment record](/docs/design/research/2026-10-05-ink-terminal-experiment/).

Giscus auto palettes follow both dimensions; explicit Giscus theme or
light/dark stylesheet overrides remain authoritative. Print uses a light
preset palette on white paper even when the screen is dark. Mermaid and
ECharts continue to follow mode only; API widgets retain vendor palettes.
See the [accepted decision](/docs/design/decisions/visual-presets/) and
[local acceptance record](/docs/design/research/2026-10-05-visual-presets-acceptance/).

## Release states {#release-states}

Source complete, locally validated, committed, tagged, pushed, pinned by a
consumer, deployed, and production-identical are distinct states. A local Hugo
build proves only local validation.

---

Backlinks:

- [Upgrade](/docs/admin/upgrade/)
- [Languages](/docs/customize/i18n/)
- [Design](/docs/design/)
- [Components](/docs/design/components/)
- [CLI contract](/docs/design/decisions/cli/)
- [Diagnostics](/docs/design/decisions/diagnostics/)
- [Visual presets](/docs/design/decisions/visual-presets/)
- [Landing pages](/docs/design/landing/)
- [Proposals](/docs/design/proposals/)
- [CLI and roadmap](/docs/design/proposals/oink-cli-roadmap/)
- [Visual presets](/docs/design/proposals/visual-presets/)
- [2026-09-19 community review](/docs/design/research/2026-09-19-upstream-review/)
