This is the multi-page printable view of this section. .
Design and development
- 1: Architecture contract
- 2: Component contract
- 3: Shell and navigation contract
- 4: Landing contract
- 5: OINK migration boundary
- 6: Design decisions
- 7: Design research
-
8: Design proposals and PRDs
- 8.1: Backlinks and knowledge graph
- 8.2: Media convergence
- 8.3: Bulk agent indexes
- 8.4: Book publication pipeline
This section publishes the maintainer contracts released with OINK 1.0.0,
with Hugo Extended 0.160.1 as the compatibility floor. Continuous tests use
one pinned Hugo Extended toolchain, currently 0.165.0; the floor is not a
second matrix leg. The canonical bilingual sources live in this repository
under content/docs/design/.
This section is the durable design record for OINK. It complements the task-oriented guides elsewhere on the site: use those guides to build a site, and use this section to understand current invariants, the reasons behind them, the evidence used to evaluate alternatives, and work that is still only a proposal.
Reading this section
| Layer | Meaning |
|---|---|
| Contracts | Normative behavior that compatible implementations must preserve |
| Decisions | Accepted rationale and boundaries that explain current behavior |
| Research | Dated, non-normative evidence that may need to be refreshed |
| Proposals | Draft PRDs and RFCs; publication here is not proof of implementation |
Contract map
| Contract | Authority |
|---|---|
| Architecture | Build, configuration, diagnostics, localization, featured images, output, security, accessibility, and performance |
| Components | Component API, Book and release primitives, validation, and output degradation |
| Shell and navigation | Navigation, search, blog presentation, actions, taxonomies, and page-end composition |
| Landing pages | Landing data, the 22-section registry, runtime, accessibility, and outputs |
| Migration boundary | Supported 0.4-to-current content and configuration migrations |
Design records
| Collection | Contents |
|---|---|
| Decisions | Accepted diagnostic, configuration, and authoring rationale |
| Research | Goldmark probes and evidence from real OINK consumers |
| Proposals | Active PRDs for knowledge graphs, media convergence, and machine-readable indexes |
Create every new OINK PRD or RFC as an English and Chinese page pair under
content/docs/design/proposals/. Do not create another repository-local
plan/, plans/, or proposal/ tree. Once a proposal is accepted, update the
implementation, owning checker, and relevant contract; preserve the stable
rationale under Decisions and retire the draft through Git history and the
changelog.
Authority and maintenance
This directory owns the maintainer design prose in English and Chinese. The theme
repository owns executable facts: hugo.yaml owns published defaults; owning
resolvers and checkers define optional shapes; layouts/ and assets/ own
rendered behavior; check scripts and tests/goldens/ own validation; and
VENDOR.json owns bundled versions, licenses, files, and checksums.
Whenever public behavior changes, update the implementation, its owning checker, and both language versions of the relevant contract in the same delivery. Tests should exercise behavior and output rather than pinning prose.
1 - Architecture contract
This is the architecture contract released with OINK 1.0.0. This page is the
canonical English source; its Chinese peer is maintained beside it in
content/docs/design/.
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.
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
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.
Interface localization
This locale expansion describes the feature branch. It is not a published module capability until a later release tag resolves through the Go proxy.
OINK ships native interface catalogs for the 31 locale filenames present in
google/docsy@64f51c5,
plus generic zh as the Simplified Chinese default:
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 192-key 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 scalars. 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
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 bundled resource outranks an inherited cascade image. 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
Every base template sets Page.Store.tdOutputFormat:
| Output | Contract |
|---|---|
| HTML | Complete semantic content; local runtime only for used capabilities |
| 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 |
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.
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.Pagesper page when a site-level resource orpartialCachedresult can own the work; - render
.Contentonce 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: prefetchrequests, useful navigations, transferred bytes, and CSP impact with a reversiblemoderateexperiment; - validate reachable author input, not hypothetical internal states.
bin/measure-baseline.py measures build time, output weight, bundle count, and
shortcode density. bin/sites/build-all.py builds maintained consumers in
isolated snapshots.
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.
Font roles are ui, body, heading, code, display, meta, 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 is a fixed crimson pair so a page dense in
identifiers reads as code and prose rather than code and links. 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 theme’s own
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.
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.
2 - Component contract
This is the component contract released with OINK 1.0.0. This page is the
canonical English source; its Chinese peer is maintained beside it in
content/docs/design/.
Tutorials and exhaustive examples belong in the reader-facing Components section. This page defines the API and behavior that those guides rely on.
Authoring model
Use ordinary Markdown when one block plus attributes can express a component. Use shortcodes for compound bodies or facts Markdown cannot carry. There is no parallel component registry. Native forms require:
Only {{%/* steps */%}} uses percent delimiters because its body belongs to the
page outline; every other shortcode uses angle delimiters. Compound bodies pass
through content/render-block.html with a unique ID scope. Shortcode and
component parameter captions, labels, titles, and names are plain text;
Markdown belongs in bodies. Landing narrative fields follow their own contract.
An icon is one Font Awesome class pair. Components expose safe classes and
attributes, not arbitrary color or inline style.
Public API
OINK has 29 shortcodes:
- core:
tabs,tab,steps,cards,card,fields,field,include,kbd,badge,param,comment,contributors,asciinema; - Book:
fig,tbl,eq,eg,xref,book-toc,book-figures,book-tables,book-equations,book-examples; - release:
release-card,release-assets,download; - OpenAPI:
swagger,redoc.
| Component | Native form | Shortcode form | HTML runtime |
|---|---|---|---|
| Callout | > [!TYPE], fold, {icon=} |
none | none |
| Tabs | adjacent fences/tables with {tab= group= value=} |
tabs / tab |
tabs on used pages |
| Steps | ordered list + {.steps} |
steps |
none |
| Cards | link list + {.cards} |
cards / card |
none |
| Fields | table + {.fields} |
fields / field |
none |
| FileTree | filetree data fence |
none | divider only with comments |
| Gallery | gallery data fence |
none | shared Image Zoom when eligible |
| Image | Markdown image + block attributes | none | Image Zoom when eligible |
| Table | attributes, caption, number, or tabs | tbl for compound Book tables |
tabs when tabbed |
| Book target | image/table/passthrough/fence + {num=} |
fig, tbl, eq, eg |
none |
| Release assets | checksums data fence |
release-assets |
copy in HTML |
| Diagram/data | mermaid, plantuml, markmap, math, chem, echarts, infographic fences |
none | selected local runtime only |
Validation
Invalid author input follows the architecture contract:
warn, use the documented safe fallback or omit the component, and let
--panicOnWarning make the same diagnostic fatal at publication gates. Named
and positional forms are not mixed. Book target IDs match
[A-Za-z][A-Za-z0-9_.:-]*; Book numbers match [0-9A-Za-z.-]+; classes are
token-validated. Hook and shortcode targets share one page registry, so
collisions cannot produce duplicate output IDs.
URLs use content/url.html. Images resolve through page resources, section
resources, global assets, then static or explicit remote URLs. Local rasters
carry intrinsic dimensions; SVG, static, and remote sources remain valid but
cannot use Hugo image operations.
Component behavior
Callouts and tabs
Callout types are note, tip, important, warning, caution, success,
danger, question, example, quote, and details; - starts folded and
+ expanded. Unknown types remain visible as neutral callouts without JS.
Adjacent tabs group only when consecutive and of the same block kind. group
enables hash #<group>-<value> and storage td-tabs:v1:<group>; ungrouped tabs
use neither. HTML exposes every panel before JS, print expands them, Markdown
retains authored source, and RSS receives the rendered text summary. The full
form supports arbitrary Markdown; tab.label is required, value is required
exactly with a parent group, and an orphan tab warns and renders nothing.
Steps, cards, fields, and tables
Native steps accept ordinary block content. Use the shortcode only when a step
must contain a percent-delimited container. Native cards are link lists; the
full form adds bodies, badges, icons, and images. Native fields map the first
column to the name, the last to the description, and middle columns through
meta= or headings; the full form allows block descriptions. card and
field are valid only inside their parents.
Field anchors are field-<name> with lowercase punctuation runs collapsed to
hyphens, so params.ui.typography becomes field-params-ui-typography.
Duplicate anchors receive positional suffixes.
The table hook owns responsive wrapping and captions. .matrix makes the first
column row headers; .full-width widens normal or matrix tables. .fields
cannot combine with matrix, full-width, numbering, or tabs; numbering and tabs
are also mutually exclusive.
Images, Gallery, FileTree, and fences
The Markdown image hook is the ordinary image API. Inline images stay inline;
block images become figures with caption or num. Image processing belongs
to this native form alone: the full fig source form is a numbered container
whose parameter list deliberately excludes command/options, so a processed
numbered image is written as a native block image with num. Allowed image attributes
are id, num, caption, width, height, link, command, and options
plus shared safe attributes. command and options appear together and use
Hugo Fit, Resize, Fill, or Crop on processable local resources. A plain
linked image uses Markdown syntax; the link attribute therefore requires a
caption or number. Linked and decorative images do not load Zoom.
Gallery accepts one Markdown image per line with optional description, link,
and class. FileTree accepts indentation, - name, optional /, comments, and
validated icon/tone/open/type attributes. Markdown preserves authored source;
print renders expanded static figures and trees.
All code highlighting uses Chroma. Common fence attributes include title,
copy, wrap, collapse, label, id, line options, tabs, and Book
num/caption. Copy returns authored source. ECharts input is declarative
JSON/YAML; callbacks use $fn:<name> from window.OinkEchartsFunctions, never
embedded script execution.
Book
The book type extends the docs shell and follows the content tree or
data/docs_nav.json. book_number, book_part, book_kind, and book_status
are presentation metadata; they do not change Hugo publication state.
Numbered kinds are fig, tbl, eq, and eg, with default ID
<kind>-<num>. eg needs a caption; eq without num is an unnumbered display
formula. xref names exactly one kind plus optional page/anchor, or an
anchor with explicit text. A numbered example is one framed body and caption.
Footnotes belong to the page document. Native numbered tables and fences keep
them there. A shortcode body is a separate Goldmark document, so footnote
references in tbl, eg, fig, card, tab, field, or include warn and
remain literal; code-shaped text is ignored by that check.
book-toc follows navigation order at depth 1–3; the four book-* indexes
collect one target kind each. Single-page Print preserves the page’s ordinary
heading and footnote IDs exactly as regular HTML renders them. Multi-page
section Print and whole-Book Print rewrite cross-page links and namespace those
page-local headings and footnotes to avoid aggregate collisions, while
preserving explicit target IDs. Consumers opt into those potentially expensive
aggregate outputs.
Release and download
Release front matter is one release_url in the form
https://github.com/<owner>/<repo>/releases/tag/<tag>; owner, project, and tag
come from the URL and date from the page. No remote release state is fetched.
The removed release map, release_products, and
release_group_by_product warn with their replacement and are not compatibility
paths. The section index lists every page, using parsed project tag when
available and the page title otherwise.
Checksums accept canonical lines or one source resource, never both; filenames cannot be paths. HTML adds local copy, while static outputs expose full hashes.
Downloads use data/download/<key>.yaml. Channels are rolling or pinned;
only pinned URLs and commands interpolate ${version} and ${tag}. Before
publication, rolling channels remain usable and pinned channels show pending.
Markdown renders the complete channel list; RSS omits the component.
Verification
Shared output rules live in the architecture contract; exceptions are defined with their components above. Markdown and RSS set no browser runtime flags; Print retains only flags required by rendered print features. Source checks cover parameters, hook policy, runtime isolation, and migration; output checks compare HTML, print, Markdown, RSS, and LLMS goldens; browser tests cover interactive surfaces. Migration is documented in the migration contract.
3 - Shell and navigation contract
This is the shell and navigation contract released with OINK 1.0.0. This
page is the canonical English source; its Chinese peer is maintained beside
it in content/docs/design/.
Authorities and navigation
| Concern | Authority |
|---|---|
| Global navigation | Hugo menus.main |
| Docs / Book sidebar and pager | content tree or data/docs_nav.json |
| Root switcher | resolved top-level content roots |
| Discovery | per-language local search index |
| Page and Palette actions | shared action registry |
No feature introduces another menu or page tree. One menu child level is
interactive; deeper levels warn and flatten beneath linked group headings.
External links use target="_blank" rel="noopener noreferrer"; internal links
remain language- and subpath-aware.
Navbar desktop and drawer views project one tree, and every dropdown panel is
one moderate column of icon-and-title rows — the mega panel and its columns
menu parameter are retired, and a configured columns warns while keeping the
single column. Menu descriptions are configuration data only. The link tree
stays
true-centered at every width: text links from lg, icon links below. Between lg
and md the end edge keeps search, version, language, theme, and GitHub with no
menu button. Below md those utilities move to the footline dock, and Home or
explicit Landing pages add one drawer entry beside search that opens the full
labelled tree; no other width or surface renders a drawer entry. Language
links target the
page translation or that language’s home, stay relative when languages share a
host/base path, and become absolute only for language-specific baseURLs;
hreflang stays absolute. navbar_autohide applies to fine pointers from
768px, never touch or drawer widths, and the hidden bar keeps its slot: the
layout reserves the navbar band in both states, a pinned bar occupies exactly
that band with its rule inside it, revealing fades the bar in place without
covering resting content, and hero pages ignore the policy in favour of their
overlay bar. The home page owns the same soft boundary a hero page does: its
navbar carries no bottom rule and no scrolled shadow, resolving into a short
wash below the bar instead.
Sidebar and pager share root and order. manual_link, build.render: link,
dividers, hidden nodes, and placeholders retain their documented semantics.
sidebar_icon_policy is all (default), groups, or none; icons are one
Font Awesome class pair. Invalid policies follow the shared warning/fallback
contract.
Immersive blog presentation
There is no article type or second shell. Immersive reading is four independent keys on the ordinary blog shell, set on a page or section cascade; the section index repeats values it also needs:
The blog shell renders no breadcrumb by default—an article reads as a
standalone piece—so the recipe needs no key for it. breadcrumb remains an
ordinary key a page or cascade may still set either way, on any shell.
hero uses the shared featured image as a decorative full-bleed backdrop on
single pages and section indexes. With no image it renders the normal opening;
banner and wash remain single-page modes. The navbar overlays a hero on a
contrast scrim and scrolls with it.
toc_style is fixed or flow; flow places a wider rail beside the article
and pins it only after scrolling. Its resting place aligns with the article’s
info line, or its description where a page has no info line. docs-shell.js
measures the offset because a title wraps to an unknown number of lines;
without JavaScript the rail starts where the article starts.
toc_taxonomies: false removes term clouds; a rail with neither TOC nor clouds
renders nothing. notoc remains the page-level TOC opt-out. These switches do
not change bylines, tags, series, pager order, feeds, or page-end composition,
and the rail disappears below the xl breakpoint.
Search, actions, and runtime
params.offline_search opts into a local per-language index. When enabled it
also builds under hugo server by default; set offline_search_on_serve: false
for large edit loops. HTML search appears on Home, shell pages, and Landing when
landing_search is enabled. Other non-shell pages and Print omit the dialog,
Lunr, and Palette.
Search metadata is search_keywords, search_boost (default 1), and
search_exclude. The index carries URL, title, taxonomies, excerpt, headings,
description, body/summary, root, section, type, keywords, boost, breadcrumb,
and icon. Fixture budget is 2 MiB raw / 512 KiB gzip. Sites may return extra
strings from hooks/search-keywords-extra.html.
Built-in action IDs are copy_markdown, copy_link, open_chatgpt,
open_claude, view_markdown, view_history, edit_page,
create_child_page, create_issue, create_project_issue, print_section,
print, switch_theme, switch_language, switch_version, and
open_github. copy_link is Palette-only outside the share bar. Site commands
under languages.<lang>.params.ui.command_palette.commands may open a safe URL
or invoke a built-in ID, never inject JavaScript.
The Palette has empty, text-search, and > command modes; quick links derive
from navigation. It has no history, semantic search, personalization, or remote
fallback. Search queries stay in-browser and no default telemetry is sent.
OinkSurfaceCoordinator arbitrates Palette, drawer, root, language, and version
menus. Surfaces own focus restoration and Escape. Keyboard navigation ignores
editable controls and modals: /, \, f, c open search/commands; j/k
move headings; q/e move pages; h changes presentation; l/y, t, and
r open language, theme, and root choices. Sidebar WASD/Arrow navigation uses
real focus without rewriting Tab order.
The outline derives cursor and visible-heading range from one heading model and
the scroller’s computed scroll-padding-top; its SVG line and dot share the
same animated values so they cannot drift. No speculative DOM repair pass is
allowed.
Share
params.ui.share is empty by default and accepts any ordered subset of 16
targets: x, bluesky, mastodon, facebook, linkedin, reddit,
hackernews, telegram, whatsapp, line, pinterest, weibo, chatgpt,
claude, email, and copy. A page list replaces its inherited list;
share: false opts out. Unknown entries warn and are dropped. Only regular
pages render the bar; print, Markdown, and RSS omit it.
Targets are plain intent links carrying the page permalink/title, plus the
local copy_link button. Pinterest media comes from the shared featured-image
resolver. ChatGPT and Claude receive build-time permalink prompts and are
independent of page-menu assistant actions. Discord has no public intent target
and is deliberately absent.
The bar loads no platform SDK, iframe, script, stylesheet, counter, or campaign
parameter and makes no request until a reader activates a link. It is one
accessible labeled glyph row. share/items.html resolves targets and
share/bar.html renders them.
Annotation
Page annotation resolves descriptors in annotation-items.html and renders
them through page-meta-lastmod.html; either may be overridden narrowly. Lines
appear in this order:
| Line | Condition |
|---|---|
| Last modified | Lastmod is set |
| Upstream | front matter upstream_link is non-empty |
| Translation | configured authoritative language has a translation and this page has authored text |
upstream_link is per-page; a cascade counts, and upstream_link: "" opts out.
Other upstream facts resolve site params → data/upstreams[upstream_source] →
front matter: upstream_name, upstream_copyright, upstream_license,
upstream_notice, optional upstream_ref, and upstream_modified. The first
four are required with a link. Invalid or incomplete attribution warns and
emits no legal notice; unsupported URLs are refused. Publication gates reject
the warning with --panicOnWarning.
upstream_modified changes the credit verb and links commit history; it adds no
line. The notice page carries full license/warranty text. Translation notice is
opt-in through params.ui.translation_notice, cascades as the page key
translation_notice, skips generated or bodyless pages, and can be disabled on
a natively authored page with translation_notice: false.
Authors and series
A blog article head is title, info line, term badges, byline, then the series
strip; the description leads the body below them. The info line
(article-info.html) always carries the date; with reading_time on it adds
the word count and the minutes. Front matter upstream_link—the same per-page
fact the annotation attributes—adds a localized link to the original, gated by
the shared URL policy. Term rows are bare badge runs whose taxonomy name lives
on the group label, not as a visible prefix. At rest a term badge is a pale
neutral chip with muted ink, led by the taxonomy’s term glyph; a linked badge
picks up the current section’s accent wash, border, and ink on hover or focus.
taxonomy-icon.html owns the vocabulary—each taxonomy pairs a whole-taxonomy
glyph with a term glyph (folder-open/folder, tags/tag, cubes/cube,
users/user-pen, book-bookmark/book for series, generic shapes)—and
params.ui.taxonomy_icons overrides a pair with one string for both surfaces
or a taxonomy/term map; unusable input warns and keeps the built-in. The
right-rail cloud wears the whole-taxonomy glyph on its head alone: cloud chips
and the term-archive filter chips stay text plus count, because repeating the
glyph beside an announced taxonomy is noise. The byline carries the people
alone—portrait, name, and the profile’s one-line bio—with no label and no date.
List rows, cards, and term archives share one metadata line of the same shape:
date, one localized author-and-section phrase, then word count and minutes
behind the same reading_time switch. Under that sentence sits one wrapping
badge line with every taxonomy’s terms, taxonomies in alphabetical order, each
badge wearing its term glyph; cards leave out authors, whom their sentence
already names.
Authors activate only through taxonomies: {author: authors}. The profile term
page owns display name, summary, body, and featured-image avatar; an absent
profile falls back to link title, initial, and archive. authors-resolve.html
preserves front-matter order for article heads, list rows, and one RSS
dc:creator per author. Legacy author remains unchanged when authors is
absent; when both exist, authors wins without warning. Custom author taxonomy
plurals behave as ordinary taxonomies.
Series activate only through taxonomies: {series: series}. Term pages own the
introduction; no parameter, data file, cover model, or runtime is added. A page
uses series: [name] and optional series_weight. series-pages.html orders
weighted members first by weight, then unweighted members by ascending date,
with Path tie-breaks; strip and term page share it. The first named series gets
one HTML/print strip. The panel is translucent over a blur rather than an opaque
card, because a hero article paints its featured image behind this band and an
opaque ground would punch a hole through the picture; on a plain article the tint
resolves to the page’s own ground, so one treatment serves both. Its summary owns
the full bar and trailing caret, while the series name – its taxonomy icon
included – remains a sibling link laid over a hidden width reservation so the
summary never contains a nested interactive control. Opening the bar rules a
hairline under it and places the reading order in one adaptive grid on the same
surface, preserving DOM order. Every member link owns its ordinal, set at the end
of a fixed square track so the titles hold one edge at any list length; equal
cells stay one column when narrow and add columns only while each title retains a
readable measure, so a desktop panel uses its width without stretching one
selected row across it. Hover and the reader’s own place borrow the two grounds
sidebar navigation already uses for those states, and the current member adds a
filled ordinal and a heavier title, so the cue is never colour alone. Print shows the same list expanded in one column. Singleton
series and non-HTML outputs omit it. Numbering, cross-references, and aggregate
output remain Book concerns.
The default article taxonomy chips omit reserved authors and series because
their dedicated surfaces already carry them. Explicit
params.taxonomy.page_header restores either.
Blog indexes and page composition
Blog section indexes use params.ui.blog_index: list (default) and cards
are one flat run, newest first, sharing blog_index_size pagination—the
metadata line’s dates make year headings redundant; table shows the whole
section as date/title/tag rows without pagination. Cards use the shared lead
image, localized date/author/section metadata, tags, and a three-line summary.
Term and taxonomy pages stay row lists.
params.ui.blog_index_toggle renders all three forms for the current paginator
slice and lets readers cycle them. The configured form controls first paint and
hidden forms load no images. A reader’s stored choice is scoped to indexes that
publish all three forms: a section whose toggle is off publishes one form and
always shows it. A front-matter value or cascade overrides the site mode per
section. A table published without the toggle remains a complete, unpaginated
archive.
params.logo is always the brand mark; params.wordmark, or the site title, is
the text half hidden at compact widths. Docs, Book, Blog, and Swagger share one
shell model. Page-end order is Share, Feedback, Annotation, Pager, Comments.
Docs/Book pager follows sidebar preorder; Blog uses weight then reverse date;
pager: false opts out. Static outputs omit pager UI.
Every rendered footer style keeps an icon-only utility dock at the end of its
bottom line: version, language, theme, then keyboard help. Its menus open upward;
the version trigger never exposes the current branch or release label. The fat
footer’s collapse chevron follows the dock. Below lg the bottom line gives up
its copyright/center/dock columns and stacks them as three centered full-width
rows, the dock last. These global controls do not render in the sidebar footer,
and footer_style: none removes the whole bottom line.
There is no archive shell, arbitrary-depth flyout, second navigation authority,
query upload, or browser compatibility shim for removed config. Feedback emits
only docs_feedback through an existing gtag, stores the choice locally, and
does not replace Giscus.
Verification
bin/check-navigation-contract.py, bin/check-shell.py, JS tests, output
goldens, and the consumer browser suite cover navigation, language/subpath
links, blog variants, page-end order, keyboard behavior, accessibility, and
responsive layout.
4 - Landing contract
This is the landing-page contract released with OINK 1.0.0. This page is the
canonical English source; its Chinese peer is maintained beside it in
content/docs/design/.
Shared rules live in the architecture and component contracts; migration belongs in the migration contract.
Shell and data
Any regular page may declare layout: landing. It renders navbar, full-width
canvas, and footer without docs sidebars or TOC rails. The homepage keeps
data/home/<lang>.yaml as a compatible authoring path through the same renderer.
A non-home page resolves sections from inline front matter,
data/landing/<key>/<lang>.yaml, an exact-language entry in one
data/landing/<key>.yaml, then English or unsuffixed local data. Landing never
fetches mutable facts; stars, prices, screenshots, and avatars are committed or
generated before Hugo runs.
params.ui.landing_search defaults to true and enables the existing local
Palette only when offline_search is enabled. params.ui.github_stars and
params.ui.alt_site are optional local chrome facts.
Section registry
The registry has exactly 22 built-ins:
hero,metrics,capabilities,principles,cards,logo-wall,gallery,testimonials,contributors,faq,markdown,cta;pricing,pricing-compare,command-box,steps,timeline,code-plate,preview,case-study,download,bar-chart.
An entry is a type string or a map with type, key, id, enabled, inline
data, or a deliberate local partial. Authors provide unique IDs; OINK
normalizes them to anchor-safe values. Unknown types follow the shared
warn-and-safe-fallback policy; they never vanish silently, and
--panicOnWarning rejects them at publication. landing/ partials own
built-ins; removed home/ partial names are not an API.
preview places Markdown source beside RenderString output through the
site’s hooks, so its content registers the same runtimes as docs content. The
source pane uses Chroma and a file name, default page.md. Markdown output
uses a four-backtick markdown fence; RSS omits it. Pane labels are theme i18n.
hero.align is start or center. Center is text-only; combining it with an
image warns and falls back to start, preserving the image. download
consumes the same data/download/<key>.yaml schema as the shortcode and
introduces no second channel, version, publication, or interpolation model.
Language, runtime, and accessibility
Narrative files may be language-specific. Shared fact fields resolve
<field>_<exact language> with - normalized to _, then
<field>_<primary language>, then the unsuffixed field. camelCase aliases are
not accepted. Narrative fields render inline or block Markdown through the
site’s hooks; values reused as accessible names are plainified. Section copy is
site data; only theme controls use OINK i18n.
Interactive HTML sets hasLanding, which conditionally adds only landing.js.
The runtime reuses OinkSurfaceCoordinator and owns reveal, count-up, copy,
compact-menu, and theme-image enhancement. Server output remains complete
without JavaScript.
Marquee duplication is CSS-only; the duplicate is aria-hidden and inert,
and a localized checkbox persists pause without JS. Reduced motion disables
motion, forced colors preserves controls, and theme images follow the shared
theme event. The navbar mega panel and its columns parameter are retired: a
menu that still sets columns warns and keeps the single column. The compact menu uses real
links/buttons, traps no focus, and does not duplicate the desktop tree.
Outputs and compatibility
| Output | Contract |
|---|---|
| HTML | Full static sections plus progressive enhancement |
| Static grids and content; controls removed | |
| Markdown | Headings, prose, lists, tables, and code without theme classes |
| RSS | Landing sections omitted |
Non-HTML output sets no Landing flag or runtime. Root-relative links and assets honor deployment subpaths; normal builds download no images.
Removed 0.4 component forms belong to the migration toolkit, not parallel Landing implementations. OINK adds no pricing-period toggle, remote-fact API, hotspot editor, visual builder, or second registry. Existing homepage data and explicit custom section partials remain valid.
5 - OINK migration boundary
This is the migration contract released with OINK 1.0.0. This page is the
canonical English source; its Chinese peer is maintained beside it in
content/docs/design/.
This is source and configuration guidance, not a release ledger. Local source, commit, tag, push, consumer pin, deployment, and production parity remain separate states. For the reader-facing upgrade procedure, see Upgrade.
Toolkit scope
bin/migrations/oink06.py only scans and automatically rewrites Markdown files
under a site’s content directory, including supported YAML front matter. It
does not rewrite Hugo configuration, data files, layouts, assets, modules, or
generated output. TOML/JSON front matter and ambiguous Markdown are reported
with positions for manual review.
Dry-run is the default and a completed migration is idempotent:
Code fences are not rewritten. book_figures.py retains narrow TPME, DDIA
v1/v2, and pg-internal profiles; it is not a generic parser.
0.4 content to current forms
| Removed form | Current form | Toolkit key |
|---|---|---|
alert, details, pageinfo, raw disclosure |
> [!TYPE] callout |
callout |
tabpane, legacy tab, code-group, code-tab |
adjacent {tab=} blocks or tabs / tab |
tabs |
FileTree shortcodes or {.filetree} list |
filetree fence |
filetree |
Gallery shortcodes or {.gallery} list |
gallery fence |
gallery |
| ECharts / infographic shortcode | same-named data fence | datafence |
| Docsy card families | .cards list or cards / card |
cards |
imgproc, image |
Markdown image + attributes | image |
readfile |
include |
include |
fence filename= |
title= |
fencetitle |
badge outline= |
remove outline |
badge |
leaf example, book-figures kind= |
eg, explicit book-* index |
eg |
| percent-delimited fields | angle-delimited fields / field |
fieldsdelim |
Docsy _param placeholders and card header= highlights |
Font Awesome / badge / param or callout |
param_placeholders |
| unsupported legacy shortcodes | manual review with source position | reportonly |
Configuration and front matter
The following configuration changes are manual; the toolkit may report matching front-matter keys but never edits site configuration.
| Old | Current |
|---|---|
offlineSearch* |
offline_search* |
disable_click2copy_chroma |
ui.code_copy (inverted) |
content_width |
`reading_width: slim |
github_url |
github_repo |
ui.no_left_sidebar |
ui.sidebar_enabled (inverted) |
| breadcrumb aliases | ui.breadcrumb |
ui.scrollSpy |
ui.scroll_spy (inverted) |
ui.showLightDarkModeMenu |
ui.dark_mode.show_menu |
ui.readingtime |
ui.reading_time |
ui.ul_show |
ui.sidebar_expand_levels |
ui.docs_root |
ui.docs_sidebar_root |
ui.pager |
ui.pager_types |
{ enable: bool } annotation/zoom/keyboard/reading maps |
bare booleans |
ui.typography.preset |
ui.typography |
print.disable_toc |
print.toc (inverted) |
Prism, rss_sections, and algolia_docsearch are removed. Chroma is the only
highlighter; Algolia configuration is search.algolia. Page overrides drop the
ui. prefix. Legacy hide_feedback, hide_readingtime, exclude_search,
content_width, camelCase manual links, and nested front-matter ui maps are
reported with replacements.
0.5 to 0.6
- Replace
upstream_attributionwithupstream_linkplusupstream_name,upstream_copyright,upstream_license, andupstream_notice; renamedownstream_modifiedtoupstream_modified. - Replace the
releasemap with one GitHubrelease_url; removerelease_productsandrelease_group_by_productfrom release indexes. - Blog and default dates now default to ISO
2006-01-02; retain explicittime_format_blogortime_format_defaultfor prose dates.
Removed names warn and take the documented safe fallback or render nothing;
ordinary previews continue, while --panicOnWarning rejects them at a strict
gate. blog_index_toggle, featured_image: hero, toc_style, and
toc_taxonomies are additive opt-ins. They introduce no content type;
immersive reading stays on the ordinary blog shell.
Prerequisites and validation
Enable Goldmark unsafe rendering, block attributes, and standalone block images
as shown in the component contract. Enable
passthrough explicitly for \(...\), \[...\], or $$...$$; Hugo does not
merge theme markup config.
Run the smallest source and output checks for the changed contract with the pinned Hugo Extended 0.165.0 toolchain, JS tests when runtime changes, and strict root and subpath builds. For maintained sites, inspect representative EN/ZH Docs and Blog routes at desktop and narrow widths, then record pin, deployment, and hosted parity separately.
6 - Design decisions
A decision explains why OINK chose one compatible design over another. The five contracts above it remain the normative description of current behaviour; implementation and owning checkers remain the executable facts.
OINK used to keep reviews, PRDs, and execution notes in a local plan/
directory. That made useful reasoning hard to discover and allowed abandoned
designs to look authoritative. Accepted reasoning now lives here, in the same
bilingual, versioned site as the contracts it supports.
Decision map
| Decision | What it settles |
|---|---|
| Warnings and safe fallbacks | Why ordinary preview survives invalid input while publication remains strict |
| Configuration model | Where configuration belongs, how pages override it, and why OINK has no parallel configuration namespace |
| Markdown-first authoring | Why native Markdown is preferred and Docs, Blog, Book, and Landing extend shared systems |
| Generated configuration schema | Why the editor schemas are a generated projection, and how the drift gate keeps a third configuration authority from appearing |
Record format
An accepted decision records context, the choice, consequences, and the proof that makes the choice current. It does not reproduce a parameter reference or a tutorial. Every decision links to its owning contract and verification surface, and its English and Simplified Chinese pages change together.
When a decision changes, update the implementation, checker, affected contract, and decision record in one delivery. Preserve the old answer in Git history and the release changelog instead of leaving two active answers in the navigation tree.
Related
- Design contracts — current normative behaviour
- Research — dated evidence that informs decisions
- Proposals — ideas that have not been accepted
6.1 - Warnings and safe fallbacks
OINK does not call Hugo’s errorf. Invalid author or site input emits a
warning and either uses a documented safe fallback or omits the invalid
fragment. Release and deployment builds use --panicOnWarning, so the same
warning remains a hard publishing failure.
Context
Hugo builds the whole site as one transaction. An errorf raised while one
page is being edited makes every URL served by that rebuild return an error,
including unrelated pages and the home page. The server process survives and
recovers after the input is fixed, but collaborative preview is unavailable in
the meantime.
A warning has a different development cost. The affected value can fall back,
the rest of the site remains inspectable, and the author receives a precise
message. A publication build still fails because OINK’s CI and integration
gates add --panicOnWarning.
Decision
Validation follows four rules:
- Name the invalid key and value, the allowed shape, and the fallback.
- Include a page position when the value came from page front matter; avoid repeating one site-wide warning for every page.
- Never pass an invalid value into a later operation. Validate first, then render from the normalized value.
- Where no honest fallback exists, warn and render nothing. Do not invent content, make a network request, or emit an unsafe URL merely to keep going.
The shared enum, boolean, CSS-length, and number shapes live in
layouts/_partials/validate.html. Domain resolvers may add narrower checks,
but they preserve the same warning/fallback contract.
Safety boundary
Continuing a build never means continuing with unsafe output. A rejected CSS length falls back before it reaches a style attribute. An incomplete remote service configuration omits the component before the browser can make a request. An unsafe action URL is dropped. The protection is the absence of the bad output, not the act of terminating Hugo.
This also separates editing from publication cleanly:
| Stage | Invalid input |
|---|---|
hugo server or an ordinary local build |
Warn, fall back or omit, keep other pages available |
| CI, release validation, deployment | The same warning becomes a non-zero build under --panicOnWarning |
Consequences
- Every fallback is part of the public contract and must match the default declared by the theme.
- A change from failure to fallback also changes its tests. A negative test proves ordinary build survival, the warning text, the rendered fallback, and strict-build failure.
- Checkers must test the rejected output directly. A URL security test, for example, asserts that the unsafe URL is absent instead of treating any build failure as sufficient proof.
- Rendered markup owns DOM, attribute, ordering, and emitted-token assertions; the browser suite owns computed color, size, spacing, breakpoint, and interaction results. A checker does not freeze a Sass spelling when the public result can be observed directly.
- Source-level checks remain for forbidden constructs such as
errorfand for narrow topology invariants that output cannot prove, such as one authority, one resolver, or an intentionally restricted caller set.
Verification
The owning references are the
architecture contract,
bin/check-params.py, and strict builds of both the theme fixture and this
integration site.
6.2 - Configuration model
OINK keeps Hugo’s native keys and useful Docsy-compatible keys in place,
places theme presentation and behaviour under params.ui.*, and exposes a
matching top-level front-matter key for a page override. It does not add a
params.oink.* tree or a registry that shadows Hugo’s configuration model.
Context
OINK inherits a mature configuration surface and adds shells, content output, and local interaction. Earlier designs attempted to move every theme-owned key under a new namespace and resolve a complete configuration dictionary once per page. That produced a second language beside Hugo’s own keys, complicated section cascades, and made migration larger than the behaviour it was meant to control.
The current model keeps ownership visible instead:
| Layer | Responsibility | Examples |
|---|---|---|
| Hugo | Site identity, languages, menus, outputs, taxonomies, markup, modules | baseURL, languages, outputs |
| Site facts and integrations | Repository, version, author, local search, comments, external services | params.github_repo, params.version, params.comments |
| OINK interface | Shell, navigation, presentation, and local interaction | params.ui.sidebar_*, params.ui.typography, params.ui.share |
| Page or section | A narrow override of an eligible site default | sidebar_enabled, featured_image, share |
| Data files | Structured facts and ordered content that are not switches | data/landing, data/download, data/docs_nav.json |
Decision
The configuration API follows these rules:
- Site facts remain at the established top level. Interface choices belong
under
params.ui.*. - A page override drops the
ui.prefix and otherwise keeps the same name. A sectioncascadecan apply that top-level key to its descendants. - Boolean features use a scalar where that is the complete policy. A map is reserved for features with real subordinate settings; an established map may accept a boolean shorthand.
- Names are positive, snake_case, and grouped by function. Closely related settings share a prefix instead of growing another nested resolver.
- Theme defaults are declared in the theme’s
hugo.yaml. Templates may add a derived default only when one static value would erase a deliberate shell-specific distinction. - Each feature family owns its normalization and validation. A shared helper supplies common shapes, but there is no global compatibility registry that silently rewrites arbitrary old keys.
The complete current key list, types, and defaults live in the configuration reference. This decision records the placement rules; it is not a second parameter catalogue.
Compatibility
Public renames receive a targeted warning from the owning resolver, a migration note, and a negative test. Removed or misspelled keys do not justify a permanent alias layer. Hugo and third-party camelCase keys remain camelCase where changing them would break their native API; OINK-owned additions use snake_case.
Page values resolve through Hugo’s ordinary front-matter and cascade model.
OINK does not ask authors to put a nested ui: tree in front matter and does
not promise to merge arbitrary nested page maps.
Consequences
- Adding a public setting requires a declared default or an explicitly derived default, an owning resolver, documentation, and a positive and negative test.
- Configuration guides link to the one reference table instead of repeating types and defaults.
- A new data structure is justified by ordered or repeated facts, not merely by a desire to avoid adding a parameter.
- Invalid scalar values follow the warning and fallback decision.
Verification
bin/check-params.py audits declared defaults, page aliases, warning
behaviour, and the no-errorf invariant. The public reference and its Chinese
peer are checked in the integration site’s bilingual and rendered-link suites.
6.3 - Markdown-first authoring
Prefer a native Markdown form when Goldmark can preserve the intended semantics. Keep a shortcode only when it provides a capability the native form cannot express. Add a content scenario by extending an existing shell and data model, not by creating a parallel rendering system.
Context
OINK serves short manuals, large references, release archives, landing pages, and books. A survey of eleven consumer sites covered more than five thousand Markdown files and exposed both extremes: pages with almost no theme syntax and pages assembled from many nested shortcodes and local layout overrides.
A component API optimized only for the second group becomes a private DSL. An API optimized only for plain Markdown leaves books, rich figures, tab groups, and structured releases to site-local HTML. The useful boundary is capability, not novelty.
Decision
OINK applies the following order:
- Native Markdown first. Lists become Steps, Cards, or FileTree markers; tables become Fields or matrices; blockquotes become callouts; fenced code, images, and passthrough blocks carry attributes through render hooks.
- Shortcodes for missing capability. A full-form shortcode remains where CommonMark indentation, nested containers, processing options, or cross-page registration cannot express the same result safely.
- One semantic implementation. Native and full forms normalize into the same partials and output contract. They are not two components that merely look alike.
- One extension line. A new Landing section joins the section registry; a new Blog presentation remains a Blog variant; Book numbering joins the content primitive and navigation systems. OINK does not add a second card, landing, navigation, or article shell for one feature.
- Facts stay outside presentation strings. Versions, repositories, dates, and ordered records come from front matter, site parameters, or data files. A shortcode argument is not a second source of truth.
Output contract
An authoring form is complete only when its semantic content has a deliberate result in every enabled output:
| Output | Requirement |
|---|---|
| HTML | Semantic server-rendered content; JavaScript only enhances it |
| Static, expanded, and free of controls that require interaction | |
| Markdown / LLMS | Source-shaped prose, links, lists, tables, and fences; no component HTML |
| RSS | Safe static content or an explicit omission |
This requirement prevents an attractive HTML-only component from silently damaging agent output, feeds, or a printable book.
Trust and presentation
Render hooks and shortcodes consume explicit allowlists. Unsafe URL schemes, inline event handlers, and arbitrary style input are dropped. Author-provided classes are accepted only on the documented surfaces where downstream site CSS is part of the established authoring contract. Icons use one Font Awesome class pair; OINK does not invent a second icon-ID language.
Consequences
- A proposed component must first show why Markdown plus an existing hook is insufficient.
- Keeping a full-form shortcode requires a named capability and tests for both forms reaching the same normalized output.
- Shell variants use independent presentation keys so opting into a hero or a flow outline does not change taxonomies, feeds, pager order, or content type.
- Consumer evidence is dated research, not a permanent excuse to freeze an accidental syntax. The current public surface remains defined by the component contract and shell contract.
Verification
The authoring contract is exercised by theme component, Book, output, and golden checkers, then by this site’s bilingual examples and browser suites. The Goldmark facts behind the native forms are recorded in block-attribute research.
6.4 - Generated configuration schema
The two JSON Schemas under schema/ are projected by
bin/generate-config-schema.py from the theme’s hugo.yaml and the
template read-point scan; hand edits cannot survive CI. The schema is a
read-only projection of the existing authorities, never a third one.
Context
The theme already has two configuration authorities: hugo.yaml, which
declares every default beside a comment explaining it, and
check-params.py, whose read-point scan knows every key the templates
actually consume. Editors know neither, so authors type params.ui.* keys
and front matter from memory.
A JSON Schema gives editors completion and hover documentation. The danger is the schema quietly becoming a third authority that drifts from the other two. A hand-maintained schema always ends up out of step with the implementation, and stale completion is worse than none.
Decision
bin/generate-config-schema.py generates two files under schema/:
site-params.schema.json validates a site’s hugo.yaml (types and defaults
from the theme’s own hugo.yaml, descriptions from its comment blocks), and
front-matter.schema.json validates page front matter (every key the
templates read as authoring surface, descriptions inherited from the matching
site key). Keys read only to warn that they were renamed or removed are
excluded by name.
Two deliberate restraints are part of the decision:
- The front-matter schema carries no type constraints. Several keys
accept a bare-boolean opt-out beside their site type (
share: false,theme_color: false); a wrong red squiggle under valid input would be worse than no squiggle at all. - The
hugo.yamlreader is a small parser for exactly the shapes that file uses – nested maps, scalars, inline lists. Anything it cannot read is a hard error, so outgrowing it breaks the drift gate loudly instead of mis-generating.
Consequences
The only way to change a schema is to change hugo.yaml or the templates the
scan reads: when the public configuration surface moves, the schemas
regenerate in the same commit, and there is no second inventory anyone must
remember to maintain. The cost is that the generator and the read-point scan
become an implicit gate on the public surface – a new parameter key must be
something they can understand, or CI fails outright.
Verification
python3 bin/generate-config-schema.py --check regenerates in memory and
fails when schema/ is stale or missing; the theme’s CI runs it beside the
parameter contract checker. Editor wiring and the behaviour itself are
documented normatively in Configuration.
7 - Design research
Research records what was measured, with which inputs and tool versions. Results may explain a decision, but they do not override the current contracts or implementation.
Research belongs in the public Design tree when another maintainer can inspect its method, understand its limits, and repeat the relevant check. Raw agent transcripts, temporary build logs, and local absolute paths do not meet that standard.
Research map
| Record | Evidence |
|---|---|
| Goldmark block attributes | Render-hook visibility and CommonMark container limits on the supported Hugo floor |
| Consumer and migration evidence | A dated corpus survey plus deterministic Book migration results |
| Comprehensive review, 2026-08-26 | Implementation, configuration, output, security, test, performance, and doc audit |
Publication rules
A research record states its date, inputs, relevant versions, method, result, and known limits. Volatile counts are labeled as snapshots. External framework comparisons are refreshed from primary sources before publication and distilled into OINK-relevant conclusions rather than copied as a competitor catalogue.
When a result becomes a stable product choice, link it from an accepted decision. When it proposes behaviour that does not exist, move the design question to Proposals.
7.1 - Goldmark block-attribute evidence
These probes produced byte-identical relevant output on Hugo Extended 0.160.1 and 0.164.0. They explain OINK’s native component forms; the current component contract remains authoritative.
Method
The probe used a minimal Hugo site without OINK templates. Render hooks printed
their context fields and .Attributes as visible markers. The site enabled
Goldmark block attributes, passthrough delimiters for inline and block math,
unsafe rendering for the deliberately inspected raw HTML, and
wrapStandAloneImageWithinParagraph: false.
Each source shape was rendered with the compatibility-floor Hugo and the then current Hugo version. Relevant output was compared byte for byte. The findings below record platform behaviour, not visual styling.
Findings
| Source shape | Hook result | Design consequence |
|---|---|---|
Ordered list with paragraphs, fences, callouts, nested lists, and {.steps} |
The class attaches to the outer <ol> and rich list-item blocks survive |
A Markdown list is the native Steps form |
| Heading inside a list item | The heading remains inside <li> and enters .TableOfContents |
Native Steps can carry navigable headings |
Nested list with {.filetree} |
The class attaches to the outer <ul> |
FileTree needs no wrapper merely to preserve hierarchy |
Standalone image plus {#id num= caption= .class} |
render-image receives IsBlock=true and all attributes |
A Book figure can have a native image form |
| Inline image inside a paragraph | IsBlock=false; the image receives no block attributes |
Inline images cannot use the block-figure contract |
Block math plus {#id num=} |
render-passthrough receives block type and attributes |
A numbered equation can use the native passthrough form |
Table plus {.fields #id num= caption=} |
render-table receives the class and named attributes |
Field tables, matrix markers, captions, and Book numbering can share one hook |
Fenced code plus {#id num= caption=} |
The code-block hook receives the attributes | A numbered example can be the fence itself |
Callout plus {icon= tab=} |
The blockquote hook receives callout metadata and attributes | Folding, inline title markup, icon, and tab metadata can coexist |
| Attribute line separated from its block by a blank line | The attribute silently disappears | Source checks must reject orphan attribute lines |
Adjacent tables with tab= |
Each table hook receives its own tab label | Adjacent-block tabs can extend beyond code fences |
Container boundary
Hugo’s % shortcode delimiter renders .Inner as Markdown, but its template
must put a blank line before and after that inner Markdown. Without both blank
lines, a following list may be treated as literal HTML-block content instead of
Markdown.
A multi-line % container inside a CommonMark list item has a harder limit:
the generated HTML is not indented as list content, so the list closes before
the container and restarts afterwards. This is why OINK keeps a full Steps form
for steps that must contain another full container. Ordinary rich blocks,
fences, and < shortcodes do not have that limitation.
Nested % shortcodes also receive already rendered inner HTML in the relevant
collector shape. A collector that requires the child’s original Markdown uses
< delimiters and renders the captured body through the shared scoped block
renderer.
Attribute ownership
An available attribute is not automatically a public attribute. Every hook
owns a documented allowlist. style and inline on* handlers are rejected;
URL-bearing values pass the shared URL policy. A site class is retained only on
the surfaces where downstream CSS is an established extension mechanism.
The experiment also showed that gallery images inside list items can be block images while still receiving no knowledge of their parent list’s marker. A runtime may therefore need either a theme-emitted marker or a narrow structural fallback; it cannot assume the image hook sees arbitrary ancestors.
Limits and verification
These results cover Hugo 0.160.1 and 0.164.0 with the stated Goldmark settings. They do not promise identical behaviour for a site that changes those settings or for a later Hugo release. A Hugo-floor change reruns the focused component, Book, table, gallery, and Markdown-output checks before this snapshot is updated.
7.2 - Consumer and migration evidence
These counts describe the repositories inspected in August 2026. They are evidence for design choices, not live product metrics or compatibility promises.
Corpus
The authoring survey scanned the content/ trees of eleven OINK consumer sites:
5,325 Markdown files, of which 5,293 had YAML front matter. The set included
single-language English and Chinese references, bilingual product sites,
release archives, custom landing pages, and separate Book consumers.
The survey deliberately measured source Markdown rather than generated HTML. It counted shortcode calls, fenced-code attributes, callouts, table markers, raw HTML, front-matter keys, content types, and site-local layouts. A later Book-focused pass added five long-form consumers.
Findings that changed the design
| Evidence | Resulting choice |
|---|---|
| Content ranged from nearly plain Markdown to pages with many nested components | Native Markdown is the default form; a full form survives only for a named capability gap |
| Documentation, Blog, Landing, releases, and books repeatedly reimplemented navigation or cards locally | Extend the shared shell, registry, and primitive rather than adding a parallel system |
| Site-specific table classes were common, while canonical Field-table headings were rare | Hook attributes use an allowlist but preserve documented site-class extension points; Fields cannot be inferred from arbitrary two-column tables |
| Book sites carried private figure, table, equation, example, and cross-reference conventions | Numbered primitives and migration profiles need deterministic classification, stable IDs, and rendered-target verification |
| Sites mixed single-language, peer-file bilingual, and generated-language content | Language authority and generation boundaries must be explicit; a migration never treats an untracked generated tree as source |
| Rich HTML pages still needed print, Markdown, feeds, and agent output | Every component declares its output degradation before its interactive HTML is accepted |
The evidence also rejected several attractive additions. Documentation sites did not justify a second Landing system; Book sites did not need a new cover component; a serial archive did not justify a new shell type; and remote API collection belonged to site-side CI rather than a Hugo theme that promises local builds.
Block and table evidence
A focused pass over eleven sites plus Book consumers found 11,484 pipe tables.
Only eleven already matched the strict Field-table heading vocabulary, while
roughly 874 were reference-style tables and about 1,300 were compatibility
matrices. The result was explicit .fields and .matrix markers rather than
shape guessing.
The same pass found eighteen Steps blocks in the eleven-site corpus. They all
used the full form with headings and rich content. Platform probes showed that
a native ordered list could carry most of that content, while another full %
container inside a list item could not. OINK therefore keeps both forms for a
technical capability boundary, not merely for stylistic preference.
Deterministic Book migration
Three dated dry-run profiles tested whether the migration rules could account for every recognized source without inventing semantics:
| Profile snapshot | Classified result | Manual boundary |
|---|---|---|
| DDIA v2 | 106 figures, 3 tables, 22 code examples, and all 304 relevant links accounted for | One caption link flattened to visible text; no unaccounted skip |
| DDIA v1 | 90 numbered figures and 203 matching references | 14 decorative or unnumbered images deliberately left alone |
| TPME | 31 figures, 10 tables, 44 numbered references, and 1,018 generic stable references | No skipped recognized item |
| Private Book profile | 119 figures, 5 tables, and 136 numbered references | 3 ambiguous images retained for manual review |
Each profile was dry-run first, wrote only after its ambiguity boundary was understood, produced zero changes on a second run, built with warnings fatal, and passed rendered kind/number/anchor checks. The public migration toolkit and current profile boundaries are documented in Writing a book and the migration contract.
Limits
These counts should not be copied into product marketing or used as a current site inventory. Repeating the research requires a fresh repository list and a new dated report. Paths, uncommitted content, private repository names, raw agent transcripts, and generated build artifacts are intentionally excluded from this public record.
7.3 - OINK comprehensive review, 2026-08-26
This page records evidence collected against github.com/pgsty/oink and its integration site on 2026-08-26.
It changes no API and does not mean that any recommendation below is implemented. Current Design contracts,
implementation, and owning checkers remain authoritative.
Superseded in part by OINK 0.7.1. The code findings F01–F06 were fixed in that release — see the 0.7.1 release notes. Read the findings below as the evidence that motivated the fix, not as the current state of the theme.
Review verdict
OINK’s main-line quality is substantially above that of a typical Hugo theme. The default path builds, bilingual coverage is strong, component tests are broad, and the project treats output and trust boundaries seriously. The real site showed no general breakage across desktop, mobile, light/dark, and the primary accessibility paths. Theme and site worktrees were clean, their current remote checks were green, and every locally rerun first-party suite passed.
Green checks do not prove that every published invariant holds. This review found 4 P1, 9 P2, and 5 P3 findings. The recurring pattern is that OINK has a strong modern contract, while several early or peripheral surfaces have not joined it; the current gates are excellent at preserving selected positive scenarios but do not systematically cover configuration space, static-output degradation, or the semantic accuracy of public documentation.
Before the next release tag, at minimum:
- disable Swagger UI’s default online validator and lock zero implicit egress with a non-localhost browser test;
- place all public configuration and Landing data behind common type, range, URL, and CSS-value validation;
- redesign Swagger, Redoc, and Asciinema output degradation and runtime gates; and
- repair generated schemas and bring the public configuration/front-matter references back to current behavior.
Baseline and method
Review baseline
| Item | Snapshot |
|---|---|
| Theme repository | clean main at fe439fdb1d7c2df745088c9bfcbb8c350403ee63, equal to origin/main |
| Current stable tag | v0.7.0 at cbb6f4e0bfe47e17ba7aa41d04b8651c943cf858 |
| Documentation site | clean main at fd5fcde, publicly pinned to github.com/pgsty/oink v0.7.0 |
| Local tools | Hugo Extended 0.164.0, Python 3.14.6, Node 26.4.0, npm 11.17.0 |
| Remote CI | theme HEAD GitHub Actions run 32792753866 succeeded |
Validation executed
- all 31 theme checkers passed;
- all 85 migration unit tests passed;
- all 38 theme browser-runtime unit tests passed;
- all 40 HTML/Print/Markdown/RSS/LLMS golden surfaces passed;
- the strict
tests/siteHugo build passed; - the real bilingual site’s
npm testpassed: 121/121 page pairs, 886 heading IDs, 24,860 internal links, and 3,172 fragments; - the full real-site Playwright suite passed: sitemap-wide axe, 29 accessibility cases, 45 responsive/navigation cases, 16 keyboard cases, 10 content-component cases, 18 code-block cases, 4 PRD5 cases, and 5 theme-color cases;
- extra visual review at 320 CSS px covered the EN home, ZH configuration, ZH Book, and OpenAPI/Redoc pages with no page-level horizontal overflow;
npm auditreported no advisory among the site’s 79 npm dependencies; an OSV Query API batch for the 26 exact versions inVENDOR.jsonreturned no known advisory;measure-baseline.py assets --fixture-sitepassed its isolated strict build.
Severity
| Level | Meaning |
|---|---|
| P1 | Breaks a core product, security/privacy, or ordinary-editing invariant; fix before the next tag |
| P2 | Material behavior, contract, or compatibility defect; fix soon with a behavior gate |
| P3 | Maintainability, performance, process, or documentation-governance debt |
Finding summary
| ID | Level | Finding | Default-site impact |
|---|---|---|---|
| F01 | P1 | Swagger UI enables its online validator on production URLs | Pages using swagger only |
| F02 | P1 | Invalid configuration can crash ordinary Hugo or silently emit bad output | Depends on authored configuration |
| F03 | P1 | Swagger/Redoc/Asciinema violate static-output and runtime-isolation contracts | Pages using those shortcodes |
| F04 | P1 | Landing sends unvalidated data to safeCSS and lets other bad values pass silently |
Related Landing fields |
| F05 | P2 | Custom page-action and archived-version URLs bypass the shared URL policy | Sites configuring those options |
| F06 | P2 | Generated JSON Schemas contain wrong defaults, types, descriptions, and candidate keys | Authors using editor schemas |
| F07 | P2 | The supposedly complete configuration/front-matter references lag v0.7 behavior | All maintainers and consumers |
| F08 | P2 | Design contracts and proposal lifecycle present conflicting authorities | Maintainers |
| F09 | P2 | OpenAPI accessibility defects are excluded while Redoc is presented as an alternative | OpenAPI readers |
| F10 | P2 | Strict-CSP guidance omits theme-owned inline script and style | Strict-CSP consumers |
| F11 | P2 | No browser support baseline; automation is Chromium-only | Firefox, Safari, RTL, forced-color users |
| F12 | P2 | Output-security and rendered-Markdown gates have systematic blind spots | Consumers relying on those verdicts |
| F13 | P2 | Real cross-repository candidate integration is manual and non-atomic | Every public behavior change |
| F14 | P3 | Checker duplication and source-string coupling are high | Maintainers and isolated worktrees |
| F15 | P3 | Baseline CSS and fonts remain the main first-visit payload | Every HTML page |
| F16 | P3 | Vendor integrity is strong, but vulnerability/SBOM and CI supply-chain gates are manual | Release maintainers |
| F17 | P3 | Changelog, implemented proposals, and behaviorless metadata reduce signal | Maintainers and upgraders |
| F18 | P3 | The Print isHTML FIXME no longer explains the real dependency |
Print-template maintainers |
Detailed findings
F01 — Swagger UI implicitly contacts the online validator (P1)
layouts/_shortcodes/swagger.html initializes SwaggerUIBundle without validatorUrl: null. The vendored
swagger-ui-bundle.js defaults that option to https://validator.swagger.io/validator and suppresses the badge only when
the specification URL contains localhost or 127.0.0.1. On a deployed host it creates an online-validator badge whose
request includes the specification URL.
This violates the promises that theme-owned network features are off by default, that same-origin specifications remain local, and that OINK is local-first. An intranet deployment can disclose its internal hostname/specification URL. The localhost exemption is also why every current local browser test misses the request.
Set validatorUrl: null explicitly. Any future online validator should be an explicit opt-in URL, pass the shared URL policy,
and be documented as a privacy/CSP integration. Test a production-like non-localhost origin while intercepting every request
and require same-origin specifications to fetch first-party resources only.
F02 — Invalid configuration does not consistently warn and fall back (P1)
ui-param.html says callers validate types; several do not. Minimal builds produced the following results:
| Input | Actual result |
|---|---|
ui.blog_index_size: nope |
ordinary build fails because .Paginate requires a positive integer |
ui.sidebar_expand_levels: nope |
ordinary build fails in add |
ui.sidebar_menu_truncate: nope |
ordinary build fails while first casts the value |
offline_search_summary_length: nope |
ordinary build fails while truncate casts the value |
ui.sidebar_width_min: "1; color: red" |
warning-free build emits --td-shell-sidebar-min: ZgotmplZpx |
ui.sidebar_width_min: -50 |
warning-free build emits -50px |
blog_index_columns: 2.5 / section_index_columns: 2.5 |
warning-free build feeds 2.5 to CSS repeat() |
ui.sidebar_item_overflow: clip |
warning-free build silently behaves as ellipsis |
ui.sidebar_menu_foldable: definitely |
the non-boolean string is truthy and enables folding |
ui.blog_index_size: 0 |
Hugo default silently converts it back to 12 |
Landing marquee.rows/capabilities.columns and Asciinema numeric parameters also call int/float directly. Other bad
types, such as print.toc or offline_search_max_results, silently change behavior.
This directly contradicts the Diagnostics decision: ordinary hugo server may become unusable, while some bad input reaches
a strict publishing gate without any warning. Add shared integer, positive-integer, range, paired-range, and grid-count
validators. Normalize before arithmetic or output. Every public key needs legal site and page cases plus illegal ordinary
(warn/fallback) and strict (failure) cases. Cross-field invariants such as min <= max, pager size >= 1, and integer grid
counts belong in domain resolvers.
F03 — OpenAPI and Asciinema remain HTML-only islands (P1)
Architecture and Components require Markdown/LLMS without theme component markup, static Print, and safe static RSS or explicit omission. Current behavior disagrees:
- Redoc emits
<style>,<div class="td-redoc">, and<redoc spec-url=...>into generated.md; - Swagger places an executable inline initializer in its shortcode;
- Asciinema
.mdcontains the fulltd-asciinematree and JSON script; - Asciinema Print loads about 185 KB of player JS/CSS and can print only an incidental frame;
- Swagger/Redoc leave empty Print containers and can still select 1–2 MB runtimes; and
- these shortcodes are absent from the Markdown/RSS/Print golden matrix.
Agent output contains theme HTML, paper/EPUB readers receive empty shells, Print carries useless runtime, and Swagger breaks a strict CSP. Reader-facing guides currently document these defects as output behavior, contradicting the normative contracts.
Make all three branch on tdOutputFormat: full behavior in interactive HTML; a titled static link and spec/cast address in
Print/Markdown/RSS, or explicit omission. Only interactive HTML should set capability flags. Move Swagger initialization into
a stable chunk and Redoc styles into a stylesheet; add four-output goldens and runtime-absence assertions.
F04 — Landing CSS, URL, and numeric inputs do not share one trust boundary (P1)
hero.html validates title_size but concatenates media.ratio and media.max_width verbatim before marking the complete
string safeCSS. This input:
builds strictly with no warning and emits:
Landing permits inline sections in page front matter, so this is not merely an internal repository constant. Other section
columns, rules, dimensions, styles, icons, and URLs are handled ad hoc. A javascript: URL often becomes #ZgotmplZ
without warning; bad columns become ZgotmplZ; some direct integer casts abort the build.
Add a section normalization layer with common class, icon, URL, CSS-length, grid-count, boolean, and enum handling.
hero.media.ratio should be two constrained track values rather than arbitrary CSS; max_width should use the length
validator. All Landing actions should reuse content/url.html, and each built-in section needs negative tests.
F05 — Two configuration URL surfaces bypass shared policy (P2)
params.ui.page_context_menu.links passes through url-template.html and directly into safeURL; url_latest_version is
also treated as trusted configuration and marked safeURL. Neither path validates scheme, host, whitespace, or
protocol-relative URLs. A warning-free build can produce:
Clicking executes JavaScript. Site configuration is high-trust input, so this is not a default remote exploit, but it violates the published safe-URL model and gives copied configuration unnecessary execution power.
Allow only HTTP(S) and explicitly supported first-party relative URLs, using the shared resolver. Validate archived-version URLs too. The browser action registry’s second check is good defense, but the progressive-enhancement anchor must not bypass it.
F06 — Generated schemas disagree with actual YAML (P2)
The small parser in generate-config-schema.py does not strip inline comments. At least 11 defaults become strings, including
print.toc ("true # ..." instead of boolean), print.section_break_wordcount, both index column counts, and enum defaults
such as footer_style, blog_index, and typography.
Comment association also drifts: breadcrumb commentary is attached to section_index; quick-link commentary to
sidebar_icon_policy; taxonomy-icon commentary to pager_types; and local-chrome commentary to image_zoom.
The front-matter schema advertises removed detector keys (release, upstream_attribution, downstream_modified) and
misclassifies navbar-menu Params.columns as page front matter. The drift check compares the same buggy generator with its
committed output, so it reliably preserves the error.
Use a real comment-preserving parser or explicit machine metadata markers rather than extending the ad-hoc parser. The scanner must distinguish page, menu, shortcode, and legacy-detector contexts. Tests should compare each schema default to Hugo’s actual parsed value and keep removed keys out of completion.
F07 — Public configuration and front-matter references are not current (P2)
Both reference pages claim to list every key the theme reads. Material drift includes:
- long English date defaults where
hugo.yamlnow uses ISO2006-01-02; - Blog docs missing
hero,table, toggle, size,toc_style, andtoc_taxonomies; - the removed
releasemap and release filters presented as current, whilerelease_urlis absent; images: []described as disabling featured images even though bundle discovery continues;upstream_modifieddescribed as adding a line, while current behavior changes the attribution verb;- inconsistent claims that invalid input directly fails versus warns in ordinary preview and fails only at a strict gate;
- the Book guide saying OINK stops at Print HTML after v0.7 shipped BookManifest/EPUB/PDF tooling;
- Asciinema/OpenAPI guides turning static-output defects into product contracts; and
- Features saying 28 vendor dependencies when the authoritative manifest has 26.
English and Chinese usually agree on the stale answer, so translation parity cannot detect the error. Treat the two references as a focused contract migration. Derive a comparable key inventory from implementation/schema, keep semantics reviewed by hand, and gate current-key coverage, removed-key placement, enums, and defaults.
F08 — The Design tree contains conflicting authorities and unretired proposals (P2)
The clearest contradiction is that Shell retires navbar columns/mega panels and promises a warning plus one column, while
Landing still says navbar mega-menu columns accept 1–4. Implementation and tests follow Shell.
Lifecycle is also incomplete. config-schema is marked implemented but remains an Active proposal. Book publication has shipped
manifest, EPUB, PDF, and most CI work while a Draft proposal duplicates the Architecture contract. Media convergence retains
implemented milestones and the open M4 in one original design record.
Correct Landing, move stable config-schema facts into Architecture/Decision and retire the proposal, and reduce Book publication to the remaining consumer-migration question or replace it with a narrow follow-up. Active proposals should not contain a second current API.
F09 — OpenAPI accessibility claims conflict with test exclusions (P2)
The axe suite excludes both .td-swagger-ui and .td-redoc. Its comments name Swagger’s unnamed server selector and
non-keyboard scrollable version stamp, plus Redoc operation-description contrast. The guide discloses only Swagger’s defects and
presents the rendered Redoc as the alternative, implying that Redoc meets the site’s zero-violation gate.
Publish the real boundary in both languages. Fix Redoc contrast in theme CSS where possible; use a narrow post-render adapter for fixable Swagger DOM. Remaining upstream defects should have versioned waivers, upstream issue links, and a separate axe report instead of excluding the whole supported surface while claiming a site-wide zero.
F10 — The current theme does not directly support a strict CSP (P2)
Deployment guidance says strict CSP is workable but lists only author scripts, ECharts callbacks, analytics, remote specs or diagram services, and Giscus. A normal Docs page already emits two theme-owned executable inline scripts (theme first paint and shell prepaint) plus inline style. Markmap, Swagger, Algolia, and Google CSE add more theme-owned inline initializers. There is no nonce API, hash manifest, or complete sample policy.
script-src 'self' blocks theme first paint and shell-state restoration; style-src 'self' blocks theme color, font roles,
Landing, and several inline custom properties. Consumers must add 'unsafe-inline', maintain hashes, or override templates,
none of which the guide states.
Move stable initializers into same-origin chunks with data/JSON configuration. For unavoidable inline content, provide a
generated hash manifest or one nonce hook. Publish minimal-core, Markmap/OpenAPI, and third-party-integration policies and state
the style-src requirements.
F11 — Browser compatibility has no baseline or cross-engine proof (P2)
CI installs Chromium only, and product documentation names no minimum Chrome, Firefox, or Safari version. The implementation
uses or enhances with :has(), dialog, inert, color-mix(), @property, logical properties, and discrete display
transitions. Some paths have fallbacks, but there is no engine matrix.
RTL assurance is mostly source markers, small JS tests, and one element-level geometry mutation rather than a full RTL-language site. Most forced-color assurance only checks that strings exist in SCSS rather than computed behavior.
Publish a small support matrix and run core shell/navigation/content/dialog cases on Chromium, Firefox, and WebKit. Add a real
languageDirection: rtl integration configuration plus forced-colors, reduced-motion, 320 px, and 200% zoom scenarios.
F12 — Output-security and Markdown gates do not inspect every claimed surface (P2)
For .md, check-output-security.py scans only Markdown-link syntax; it does not feed raw HTML through the HTML scanner, so
Redoc/Asciinema scripts, spec-url, and raw href are invisible. It also ignores URLs in CSS and JSON configuration, while the
fixture runs with a broad --third-party allowance.
check-rendered-markdown.mjs is also misleadingly named: it scans generated HTML text nodes for leftover Markdown syntax; it
does not read generated .md. The actual Markdown golden set covers 15 pages and omits OpenAPI/Asciinema.
Separate HTML trust, machine-output purity, and rendered-text residue into clearly named gates. Give generated Markdown a very narrow raw-HTML allowlist; parse CSS URLs, form actions, JSON URLs, and non-executable JSON scripts deliberately. Every public shortcode should enter at least one Markdown/Print/RSS behavior case.
F13 — Candidate integration across the two repositories is manual (P2)
Theme CI tests only synthetic tests/site; documentation-site CI tests only the public tag pinned by go.mod. Real EN/ZH and
Playwright validation of a theme PR depends on a maintainer’s local HUGO_MODULE_REPLACEMENTS, and changes in the two
repositories cannot be committed atomically.
Both repositories can therefore be green while public references drift from implementation, as this review demonstrates. The written release-state separation is correct, but automation does not enforce the same-delivery rule for implementation, owning checker, and paired contract.
Add a read-only candidate workflow that checks out a theme PR SHA and a declared documentation-site SHA, applies a temporary
module replacement, and runs npm test plus the critical browser suites. Allow a Design-contract PR to identify the candidate
theme SHA too. Tag, pin, and deployment remain distinct, but the candidate pair gains one traceable joint verdict.
F14 — Checker maintenance cost and source coupling are high (P3)
The coverage is valuable, but 34 check-*.py files contain 546 read_text() calls. Many repeat require, temporary-site
creation, file writes, Hugo invocation, and error aggregation. Numerous assertions freeze template/SCSS spelling, nearby
comments, or whole-file equality instead of observable behavior.
Some helpers hard-code theme: oink with --themesDir <repo-parent>, making the checkout/worktree basename an implicit
precondition. There is no unified Python lint/type gate. This makes checkers quick to add but encourages shared blind spots.
Create a common fixture builder and assertion library; move negative cases into table-driven data. Keep source checks for true topology invariants only and move the rest to parsed output or computed styles. Load the theme through an explicit symlink or module replacement rather than repository basename.
F15 — Runtime splitting succeeded, but baseline CSS/fonts dominate first visit (P3)
The isolated strict fixture baseline was:
| Metric | Value |
|---|---|
| Cold/warm build | 1.256 s / 1.273 s |
| Pages | 249 |
| Stable JS chunks | 18 |
| Main + Font Awesome CSS | 549.8 KB raw / 91.1 KB gzip |
| Fonts total (FA portion) | 999.7 KB raw / 248.5 KB gzip |
| Median Docs-page JS | 176.9 KB raw / 55.3 KB gzip |
| Generated public | 26.2 MB |
| v0.7.0 Go module zip | 7.8 MB (about 20.5 MB and 1,140 files expanded) |
Stable first-party capability chunks correctly removed combinatorial bundles, and large third-party runtimes are page-local. The remaining common cost is Bootstrap/theme/Landing CSS and the complete Font Awesome distribution.
Do not prune Font Awesome by observed template usage; that would violate the authoring contract. Instead measure whether Landing, Book, or Swagger CSS can become independently cached/surface-local, inspect fonts actually requested on first visit, and maintain a trend report rather than an arbitrary hard threshold.
F16 — Vendor builds are reproducible, but advisory and CI supply-chain gates remain manual (P3)
Positive evidence: VENDOR.json pins 26 packages, 56 artifacts, 31 license files, and tree hashes; check-vendor.py passed;
OSV and npm audit reported no known advisory in this snapshot.
The custom manifest is not part of a common SBOM/advisory gate, and npm audit cannot see vendored browser packages.
Two documentation-site workflows download a Hugo .deb and immediately install it with sudo dpkg -i without a checksum.
Actions use movable major tags, and theme CI floats Python at 3.x.
Generate CycloneDX/SPDX from VENDOR.json, schedule OSV scanning, pin Hugo archive/deb SHA-256, pin high-trust release actions
to commit SHAs, and choose a specific Python version or matrix.
F17 — Design and release records have lost signal (P3)
CHANGELOG.md has 1,768 lines; the v0.7.0 section alone is about 300 lines, and Unreleased spends about 20 lines on one checker
retry. The narratives are useful engineering history but make breaking changes, migrations, and observable behavior harder for
upgraders to find.
book_kind and book_part are acknowledged by contract and repeated in content front matter while templates explicitly do not
read them. They impose API-like authoring cost without behavior. Implemented proposals remaining active add another duplicate
answer.
Keep the changelog to observable changes, breaking/migration notes, and concise fixes; move long design stories to Blog/Research and link them. Give behaviorless metadata a consumer/schema or demote it to site-owned fields.
F18 — The Print isHTML FIXME is no longer accurate (P3)
hugo.yaml says to leave isHTML unset until Hugo fixes issue #14381. Hugo closed that issue on 2026-01-17, and the fix shipped
before OINK’s 0.160.1 floor. Simply enabling isHTML: true still produces missing page/section/landing Print-layout warnings in
the current theme, causing a strict build to fail.
The actual dependency has shifted from “waiting for an alias fix” to “the current Print template names rely on non-HTML lookup rules.” Do not simply delete the workaround. First complete the HTML-classified Print lookup matrix and alias/subpath tests; if false remains intentional, update the comment to the real reason and add a test that prevents cleanup based on a closed issue.
Strengths
- Source, local validation, commit, tag, public module, consumer pin, and deployment are explicitly separated.
- The Hugo 0.160.1 floor plus 0.164/0.165 theme matrix is strong.
- Most newer components follow warning/fallback, four-output, shared URL/attribute, and capability-flag contracts.
- The 32 locale schemas match, with strong real EN/ZH page, heading-ID, link, and narrow-navigation gates.
- Search, keyboard behavior, surface coordination, page actions, and theme color have both unit and browser behavior tests.
- Vendor license/hash checks and EPUB/PDF path, loopback, CSP, and overwrite boundaries are thoughtfully designed.
- Manual 320 px review found no page-level overflow; current core visual quality is good.
- Builds are fast, and first-party JS now uses stable capability chunks.
Recommended remediation roadmap
Phase 0: before the next tag
- Set Swagger
validatorUrl: nulland add a production-origin no-network test. - Build the public-parameter inventory and validate every F02/F04 field with negative cases.
- Redesign four-output behavior and runtime gates for Swagger, Redoc, and Asciinema.
- Validate custom action and archived-version URLs.
- Repair the schema parser/scanner and regenerate both schemas.
- Synchronize paired Config, Front matter, OpenAPI, Asciinema, Book, Features, and Landing-contract pages.
Phase 1: contract gates
- Create a minimum HTML/Print/Markdown/RSS coverage map for all 29 shortcodes.
- Split and strengthen output-trust and machine-output-purity gates.
- Normalize all Landing section input centrally.
- Externalize theme-owned inline initializers and publish CSP guidance.
- Add a cross-repository candidate workflow.
Phase 2: compatibility and structure
- Add Firefox/WebKit, real RTL, forced colors, and 200% zoom.
- Consolidate the Python checker harness and source-string assertions.
- Evaluate surface-specific CSS and actual font requests.
- Generate an SBOM, schedule OSV, and pin CI download digests.
- Retire implemented proposals and reduce changelog volume.
Acceptance criteria
- A same-origin Swagger specification on a production-like origin makes no third-party request.
- Every invalid public configuration warns and falls back/omits in ordinary builds, fails strictly, and emits no
ZgotmplZ. - Generated
.mdcontains notd-*, theme script/style, or empty interactive container. - Print loads no Swagger/Redoc/Asciinema runtime and provides an understandable static alternative.
- Schema default types exactly match Hugo parsing, and removed keys are absent from completion.
- EN/ZH configuration and front-matter key/enum/default inventories match implementation.
- Core Playwright passes on Chromium, Firefox, and WebKit, with real RTL and forced-color behavior assertions.
- Every candidate theme SHA has a traceable joint validation against the real documentation site.
Review limits
This pass did not individually audit every consumer repository, production response headers/CDN caches, real Firefox/Safari, or screen readers, and it did not manually reverse-engineer 13 MB of minified third-party source. Advisory checks are a 2026-08-26 snapshot and may change. Existing CI/contract evidence was used for DDIA/TPME EPUB/PDF consumers; no site was republished or deployed during this review.
8 - Design proposals and PRDs
A proposal describes behaviour that may not exist. Current behaviour is defined by the contracts, accepted decisions, implementation, and owning checkers. Never use a proposal as a configuration reference.
This section is the canonical home for OINK product requirement documents,
RFC-style designs, and unresolved maintainer proposals. Do not create a local
plan/, plans/, proposal/, or parallel design tree in the theme repository
or the documentation repository.
Active proposals
| Proposal | Current boundary |
|---|---|
| Backlinks and knowledge graph | G1 (static backlinks) is accepted, implemented on the theme’s main branch, and ships with OINK 0.8.0; the local and global graphs (G2/G3) remain draft |
| Media convergence | Partially implemented; the media-result contract and Landing resource metadata shipped, M3 resolved for native-image processing, retirement (M4) open |
| Bulk agent indexes | Accepted (2026-08-27); both outputs are implemented on the theme’s main branch and ship with OINK 0.8.0, after which this proposal retires |
| Book publication pipeline | Manifest and EPUB/PDF tooling are released — see Architecture; only consumer migration is still open here |
The generated-configuration-schema proposal has been retired through the lifecycle: the behaviour is documented normatively in Configuration, the long-lived rationale moved to the generated configuration schema decision, and the draft text is preserved by Git history.
Where a new PRD goes
Create one English-primary page and its Simplified Chinese peer:
Use explicit, stable English heading IDs in both files. Keep code, keys, paths, versions, and API names unchanged in Chinese. A proposal begins with visible draft status and includes:
- status, owner, date, and affected contract surface;
- context and evidence;
- goals and explicit non-goals;
- proposed behaviour and output/accessibility/security boundaries;
- compatibility and migration impact;
- implementation and owning-checker plan;
- acceptance criteria and open decisions;
- a decision log for later changes to the proposal itself.
Large experiments may add a dated page under
../research/, but temporary logs and generated
artifacts stay outside Hugo content and outside Git.
Lifecycle
Acceptance does not turn the PRD into a second contract. Move stable behaviour into the owning contract, stable rationale into Decisions, and user steps into the relevant guide. Then retire the proposal from active navigation. A local build, commit, tag, public module, consumer pin, and deployment remain separate completion states.
Review gate
Before implementation, reviewers confirm that the proposal does not duplicate an existing shell, resolver, component family, or data authority. During implementation, a changed design updates this bilingual proposal before code silently diverges. Acceptance requires the narrow theme checker, the real documentation site, rendered EN/ZH, relevant outputs, accessibility, and responsive review.
8.1 - Backlinks and knowledge graph
On 2026-08-27 every G1 open decision was resolved and G1 (static backlinks) was accepted. It is implemented on the theme’s main branch and ships with OINK 0.8.0. The local and global graphs (G2/G3) stay draft pending real-world evidence from G1; their names and configuration are not public API until accepted.
Premise
Reverse navigation and a view of connected pages are properties of the link
graph, not of [[wikilink]] spelling. Hugo already accepts ordinary Markdown
links and ref / relref. OINK can derive a graph from content authors already
write, without adding a parser, Goldmark extension, or parallel authoring
syntax.
The first value is backlinks, not visualization. A static inbound-link list is useful without JavaScript and can degrade into print and Markdown. An interactive graph remains an optional enhancement over that complete list.
Goals and non-goals
Goals:
- derive one language-local link index per build;
- show deterministic inbound links on a page;
- optionally show a bounded local neighbourhood;
- optionally publish a whole-site view and a machine-readable graph;
- preserve ordinary preview when an edited link is stale or incomplete.
Non-goals:
- introducing
[[wikilink]]syntax; - indexing external,
mailto:, same-page anchor, or self links; - executing JavaScript to discover links already present in content;
- turning a visualization into the only way to navigate;
- promising perfect extraction from arbitrary shortcode parameters or raw HTML.
Delivery stages
| Stage | Deliverable | Runtime | Independent value |
|---|---|---|---|
| G1 | Language-local link index and backlink list | None | Reverse navigation in HTML, Print, and Markdown |
| G2 | Local graph around the current page | Existing ECharts plus a small local runtime | Spatial view with G1 as the accessible fallback |
| G3 | Global graph page and graph data output | Same runtime | Whole-site exploration and machine-readable edges |
Each stage is accepted separately. G1 does not wait for G2, and G2 does not force every page to load graph code.
Extraction contract
The proposed index scans source content once per language and records one edge
per source/target pair. It strips fenced code and inline code before extracting
ordinary Markdown links and ref / relref; then it resolves only internal
pages, removes fragments for page identity, drops self-links, and deduplicates
repeated references.
The implementation must test at least:
- duplicate links collapse to one edge;
- fenced and inline code produce no edge;
- external, protocol-relative, mail, same-page anchor, and self links are excluded;
refandrelrefare included;- each language produces an independent graph;
- an unresolved derived edge warns or is reported by the focused checker
without making ordinary
hugo serverunusable.
Raw source scanning has known omissions. A URL stored in a custom shortcode
parameter or raw <a href> may not appear. Those omissions must be documented
instead of hidden behind a claim of a complete semantic graph.
Backlink output
G1 renders an aside group in the right rail, a sibling of the table of contents
and the taxonomy clouds: what is on this page beside what points at this page.
The group is expanded by default and shows the first eight entries; the rest
fold behind a native disclosure so a heavily referenced page cannot swallow the
rail. The switch is the site key params.ui.backlinks (bare boolean, default
off); a page overrides it with the prefix-free front matter key backlinks,
and a section can cascade it. Order is deterministic: the stable page path —
language-independent, naturally grouped with navigation, and needing no second
ordering authority. The group uses ordinary links and is omitted when there are
no inbound pages.
Unresolvable derived edges are dropped silently and recorded as a known gap: G1 is a local navigation enhancement, not a link checker, and having it report broken links for the site would only duplicate warnings.
Print and Markdown keep the readable list. RSS omits it unless feed-level research demonstrates that backlinks improve an article feed rather than creating noisy site navigation.
Interactive graph boundary
G2 reuses the locally vendored ECharts graph series. The current page is the centre; direct inbound and outbound neighbours form the default depth. A hard node cap prevents unreadable or expensive views. Keyboard focus, text alternatives, reduced motion, forced colours, narrow screens, and print are acceptance requirements, not later polish.
If JavaScript or ECharts is unavailable, G1 remains complete and visible. The runtime is loaded only on pages that render a graph and must join the existing feature-bundle key so unlike pages cannot collide in the asset cache.
Global output
G3 may add a dedicated graph page and an opt-in JSON output. The JSON schema would contain a version, language, nodes, and directed edges with stable URLs; it would not expose local file paths or unpublished pages. The output must be derived from the same index as G1 and G2 so three representations cannot drift.
Compatibility and migration
Ordinary Markdown remains unchanged, so content migration is unnecessary. Configuration names remain undecided until a prototype proves the smallest surface. The default for every interactive or global output is off; a static backlink list may be considered separately because it is local navigation with no network or browser state.
Acceptance criteria
Acceptance requires a focused graph checker, extraction fixtures, HTML/Print/ Markdown goldens, strict-build negative cases, browser accessibility and responsive tests, and a real bilingual-site build. Performance is measured on a representative large site, but a dated prototype timing is not a permanent budget.
Open decisions
Every G1 question is resolved (see the decision log). Still open, and owned by G2/G3:
- Does the local graph expose one depth or a tightly capped second depth?
- Which page metadata, if any, is useful enough to enter graph JSON?
- Is G3 useful enough to justify a new output format before G1 and G2 have production evidence?
Decision log
- 2026-08-19: Drafted the three-stage design.
- 2026-08-27: Resolved and accepted G1, scheduled for OINK 0.8.0. G1 is
opt-in: the site key
params.ui.backlinksis a bare boolean defaulting to off, pages override withbacklinks, and no shell-type gating — policy belongs to the site and the page, not the shell. Ordering simplifies to a single stable-page-path sort, dropping the section → weight → title chain: one deterministic authority is enough for reverse navigation, and a multi-level sort would be a second navigation authority. Unresolvable edges drop silently and are recorded as a known gap, never warned. G2/G3 and the graph data output keep waiting for production evidence. - 2026-08-27: Design review moved the block from the page end to the right rail. Backlinks are page metadata and pair with the table of contents, while the page end is the reader’s completion zone — share, feedback, provenance, pager, comments. The rail group also adds the eight-entry cap, with the rest behind a native disclosure.
8.2 - Media convergence
M1 (the shared media-result contract) and M2 (Landing resource metadata)
are implemented on the theme’s main branch, and M3 is resolved as option 2:
processing stays exclusively on native Markdown images, and the full fig
source form remains a container whose parameter list deliberately excludes
command/options. M4 (compatibility retirement) stays open pending a
consumer inventory. The sections below are the original design record.
Current baseline
The content image hook, numbered fig, cards, and galleries resolve local page
resources, section resources, global assets, static files, and explicit remote
URLs through content/image-resolve.html. Raster resources can contribute
intrinsic dimensions and processing derivatives. HTML Zoom eligibility is
marked with data-td-image-zoom; the build-time detector only checks that
theme-emitted marker.
Standalone Markdown images can already combine caption or Book numbering with
processing and a link. Numbered image figures share td-figure and
td-book-figure semantics. Landing media passes the shared URL trust policy,
while featured images intentionally use a ranking resolver because their job
is to select a representative image rather than render one explicit source.
Remaining problem
The shared safety boundary is stronger than the shared media model. Landing
media still does not obtain the same page-resource metadata and processing
result as body images. Featured-image selection and explicit image resolution
have separate result shapes. Some compatibility class names remain in markup,
and Book’s full fig form cannot express every processing option available to
the native image hook.
The design question is therefore no longer “replace seven image entry points.” It is whether the remaining surfaces can share a small result contract without erasing their different semantics.
Goals and non-goals
Goals:
- define one normalized media-result shape for URL, source URL, dimensions, alternative text, attribution, processability, and external status;
- let explicit content images, Landing media, and representative images reuse that shape where their source semantics overlap;
- keep figure markup and Zoom eligibility single-owned;
- decide whether the full
figform needs processing or whether authors should use the native image form for processed numbered images; - retire compatibility markup only after consumer evidence and a release note.
Non-goals:
- adding a third-party lightbox or remote image service;
- changing image Zoom from opt-in to site policy by accident;
- giving galleries a new caption, sequence, or carousel model;
- merging non-image Book targets such as tables, equations, and examples into an image-only base class;
- making featured-image ranking identical to explicit image resolution.
Proposed phases
M1 — Result contract
Document the fields returned by the content and representative-image resolvers, then extract the intersection into one internal media-result contract. Keep source ranking in the featured resolver and source resolution in the content resolver. This is an internal refactor with byte-stable output.
M2 — Landing resource metadata
Allow Landing items to resolve eligible local resources through the media contract, gaining intrinsic dimensions and the same URL/security decision. Explicit width and height in Landing data continue to win. Remote and static sources remain valid but cannot pretend to have processable-resource metadata.
M3 — Full figure capability decision
Choose one of two answers:
- add processing arguments to the full
figsource form and normalize them through the same processing helper; or - keep processing exclusively on native Markdown images and document full
figas the container for arbitrary numbered block content.
No implementation should leave both answers half-supported. Markdown/LLMS must link to the documented source or derivative consistently in both forms.
M4 — Compatibility retirement
Inventory downstream CSS and JavaScript before removing old image-element classes or attributes. If a compatibility name is still used, retain it for a documented release window or migrate the owning site in the same release train.
Safety, output, and accessibility
- Image URLs keep the shared scheme and remote-host policy.
- Missing required alternative text warns and renders a decorative fallback only where the current contract permits it.
- Width and height never claim metadata that an SVG, static file, or remote source did not provide.
- Linked images are not Zoom targets; the runtime preserves dialog focus, keyboard close, reduced motion, and narrow-screen containment.
- Print, Markdown, RSS, and LLMS strip interaction markers while retaining the intended image, caption, attribution, number, and link.
Acceptance criteria
Each phase owns byte-level HTML and Markdown evidence, content and Landing resolver tests, URL/security checks, image-processing tests, Book targets, gallery/Zoom browser tests, and real-site EN/ZH narrow-screen review. The proposal is accepted only after the M3 capability choice is explicit.
Open decisions
- Is one shared result struct enough, or would a common lower-level URL/resource record keep resolver ownership clearer?
- Should Landing consume resource attribution, or only dimensions and URL?
- Does full
figprocessing solve a real consumer need now that native images support numbering, captions, links, and processing together? - Which emitted compatibility names are still used by real consumers?
8.3 - Bulk agent indexes
Accepted on 2026-08-27 after resolving every open decision. Both outputs –
the per-top-level-section LLMSFULL bundles and the NAVJSON navigation
tree at the language root – are implemented on the theme’s main branch and
ship with OINK 0.8.0, after which this proposal retires; released behaviour is
owned by the
Architecture contract.
OINK already shipped per-page Markdown, the in-language llms.txt, the HTML
discovery link, and Copy Markdown; this page covers only the two new outputs.
Current baseline
A site can enable Hugo’s Markdown output for pages and sections, and the LLMS home output that
generates llms.txt. OINK renders shortcodes as semantic Markdown, keeps source URLs and the
in-language LLMS index discoverable, and Copy Markdown reads the same alternative output URL. The
theme declares output formats but never forces a site’s outputs choice.
Navigation already has an authority chain: an explicit data/docs_nav.json tree when present,
otherwise the content tree and weights. The sidebar, the pager, and declared section indexes share
that authority. Machine navigation output must derive from the same tree rather than invent ordering.
Goals and non-goals
Goals:
- Optionally assemble an in-language full-text bundle for each explicitly enabled top-level section;
- Optionally publish a versioned navigation JSON for agents and external tools;
- Reuse the human site’s page renderer, page-inclusion rules, and navigation authority;
- Keep every output opt-in through Hugo output configuration;
- Validate links, language isolation, media types, and deterministic order.
Non-goals:
- Replacing per-page Markdown or
llms.txt; - A new
params.oink.*configuration tree; - Scraping generated
public/files during the Hugo build; - Embedding private source paths, draft pages, or cross-language fallback;
- Promising that one giant full-text file fits every model context.
Full-text bundle
The llms-full.txt output concatenates the same semantic Markdown used for per-page output, with a
stable, visible separator and source URL between pages. Version 1 implements per-top-level-section
bundles only: each top-level section that explicitly enables the format in its _index front matter
outputs gets one file per language. The whole-site single-file shape is deferred until real-site
evidence shows per-section bundling is not enough — a giant single file both outgrows model contexts
and couples every section’s updates to one artifact.
Hugo output configuration decides which sections receive the format, never a theme parameter. The theme ships a checker that reports mismatches between intent and actual output, but must not modify the site’s output set.
The bundle is assembled inside Hugo through the shared page-render partial; it never reads sibling
artifacts from public/ and never depends on output build order. File size is reported as evidence;
no arbitrary threshold may let --panicOnWarning reject an otherwise legal publish.
Navigation JSON
The navigation JSON is a home output beside llms.txt: one file per language at the language root.
It carries a schema version, the language, the root node, and recursive ordered nodes. Page nodes
carry a stable ID (the in-language permalink path), title, HTML URL, Markdown URL when enabled,
kind, and children — plus the description when one exists. Explicit external navigation nodes carry
only a label, a URL, and the external kind.
Nodes do not serialize weight: array order is the contract, weight is the private mechanism
that derives it, and publishing it would invite consumers to re-sort. The output follows the same
visibility and ordering rules as the rendered sidebar, excluding drafts, headless resources, hidden
navigation items, and pages unavailable in the current language, and never serializes local
filenames.
The format owns its JSON Schema (schema/nav.v1.schema.json, a hand-authored, versioned contract
artifact outside the generated configuration schema’s drift gate) and golden fixtures, and is marked
notAlternative so Hugo never advertises it as a page-level alternate.
Discovery and output boundaries
llms.txt lists the enabled bundles and the navigation JSON by default — discovery belongs in the
index file; that is what it is for. The HTML head keeps discovering per-page Markdown and the
in-language LLMS index without stuffing every bulk artifact into every page.
Shortcodes, Landing sections, Book targets, and interactive components keep their current Markdown degradation. The new outputs may not add component HTML, scripts, comments, feedback controls, or navigation chrome.
Compatibility and migration
Both outputs default off; a site that does not enable them is byte-identical. Enabling is a
site-side Hugo outputs choice — no new params keys, no renames, no migration steps. Disabling an
output is a complete exit with no residue.
Implementation and owning-checker plan
- Output formats:
LLMSFULL(text/plain,baseName: llms-full,notAlternative, section level) andNAVJSON(application/json,notAlternative, home level), declared beside the existingMARKDOWN/LLMSdefinitions. - Templates: the section
llms-fulllayout concatenates the shared per-page Markdown render partial in navigation order; the home navigation JSON layout walks the existing navigation authority partial rather than introducing a second tree traversal. - Owning checker: a new
bin/check-agent-indexes.pyvalidates, on thetests/sitefixture, language isolation, link resolvability, sidebar-consistent order, schema compliance, and byte-stable rebuilds, and reports each bundle’s bytes and page count (report only, no ceiling gate). - Goldens: the
check-goldens.pymatrix gains llms-full and navigation JSON fixtures. - Documentation: the site gains a bilingual agent-index guide;
llms.txtdiscovery behaviour folds into the existing LLMS documentation; this proposal retires through the lifecycle.
Acceptance criteria
- EN and ZH outputs contain only their own language’s pages and URLs.
- Every listed Markdown URL exists; every navigation URL resolves or is explicitly external.
- Order under the same root matches the rendered sidebar and pager.
- The navigation JSON validates against
schema/nav.v1.schema.json. - With a pinned Hugo version and inputs, rebuilding the same sources is byte-stable.
- With the new formats off, HTML, Markdown, Print, RSS, and LLMS goldens show no regression.
- A large-site fixture proves per-top-level-section bundling rather than a file for every nested section.
Decision log
- 2026-08-20: Drafted; the bundle listed whole-site and per-section shapes, and the navigation JSON’s location was undecided.
- 2026-08-27: Resolved the five open decisions and accepted the proposal. Version 1 builds
per-top-level-section bundles only, deferring the whole-site file until real evidence; the
navigation JSON is a home output; schema v1 node metadata is the minimal set (stable ID, title,
HTML URL, Markdown URL, kind, children, optional description) with
weightnever serialized;llms.txtlists both enabled artifacts by default; the checker reports size evidence without enforcing any model-context ceiling.
8.4 - Book publication pipeline
The opt-in BookManifest, the generic EPUB and PDF runners and their artifact
validation are released, and the normative account of them is in
Architecture. No build publishes either file on
its own. What remains open here, and only here, is consumer migration.
Context and evidence
OINK already owns Book navigation order, numbered figures, tables, equations, examples, cross-references, whole-Book Print HTML, heading and footnote namespacing, and per-page Markdown degradation. The missing boundary is a machine-readable whole-Book handoff that a generic packager can consume.
DDIA currently carries a substantial EPUB preprocessor that follows OINK’s numbered primitives, cross-page links, footnotes, image paths, and Book order. TPME retains an older exporter whose historical root-file inputs no longer match its current Hugo content tree. The first proves that publication is real; the second proves that a consumer-local recipe can quietly become stale.
An EPUB is not one rendered template. It is a ZIP container with publication metadata, a resource manifest, a spine, navigation, content documents, styles, and media. Hugo can render an intermediate output, but a packaging tool must produce and validate the final file.
Goals
- Give every Book primitive one theme-owned semantic result for publication.
- Publish an opt-in whole-Book intermediate with deterministic page order, stable targets, and cross-references; resolve local resources from the semantic Print document only during explicit packaging.
- Provide a generic, versioned EPUB packager and a version-pinned Print-to-PDF runner that consumers invoke with their own publication facts.
- Prove the boundary on two structurally different public Book consumers.
Non-goals
- Do not enable an expensive aggregate output for every site or section.
- Do not call a Markdown, JSON, or HTML intermediate an EPUB.
- Do not infer title, author, cover, ISBN, edition, rights, or release policy.
- Do not fetch remote images or services during an ordinary Hugo build.
- Do not add a second Book shell, duplicate navigation authority, or a generic publication-configuration namespace.
- Do not promise pixel-identical PDF pagination across browser engines.
Ownership boundary
| OINK owns | The consumer owns |
|---|---|
| Book order derived from the existing navigation authority | Which language, edition, and Book root to publish |
Semantic lowering for fig, tbl, eq, eg, xrefs, headings, and footnotes |
Title, authors, identifier, cover, rights, and imprint |
| Stable intermediate schema and generic packager behavior | Optional chapter exclusions and publication-specific front/back matter |
| EPUB structure/link validation and Print-to-PDF runner | Release automation, signing, distribution, and legal approval |
| Theme fixtures and compatibility checks | Content correctness and final artifact approval |
The consumer passes facts; it does not patch OINK markup. OINK supplies semantics; it does not decide whether a book may be distributed.
Proposed behavior
The first implementation is an opt-in Book manifest, not a final ebook. It references the already published per-page Markdown and records only facts the theme can derive honestly:
- schema version and language;
- Book root and flattened page order;
- page title, optional Book number, HTML URL, and Markdown URL;
- stable heading and numbered-object targets;
- cross-page references.
The manifest is emitted only for a Book root that explicitly enables the output. The normal HTML, Print, Markdown, RSS, search, and navigation builds remain byte-for-byte independent of that opt-in.
The OINK EPUB packager consumes the manifest and the existing whole-Book Print HTML, whose semantic output already contains namespaced headings and footnotes, numbered targets, raw authored anchors, MathML, and static interaction fallbacks. It rewrites only publication URLs, invokes pinned Pandoc 3.10, and validates the result with EPUBCheck plus OINK’s internal-target checker. A consumer supplies a small metadata file and cover. Per-page Markdown remains in the manifest as an auditable source-shaped output, not as a second semantic conversion path.
Local resources must exist under the generated public/ tree and are packaged
without network access. A consumer that knowingly retains a remote image must
opt in with --allow-remote-resources; this permits passive HTTP(S) media only,
never remote scripts or local-file schemes. The tool refuses to replace an
existing EPUB unless --force is explicit.
PDF derives from the same whole-Book Print HTML. The runner exposes the build on
a temporary loopback server with script-src 'none', blocks external resources
by default, invokes an explicit Chrome/Chromium binary, and refuses implicit
replacement. Print CSS
owns A4 size, paper-safe code wrapping, full-width numbered equations, and page
numbers. The checker uses Poppler to verify the PDF structure, A4 geometry,
extractable Book titles, and sampled page-number margins; final approval still
includes rendered-page review.
The reference workflow remains four explicit steps, not a new Hugo mode:
Output, accessibility, and security
- HTML and existing outputs do not load an exporter or gain browser runtime.
- The intermediate preserves document language, heading hierarchy, alternative text, table headers, link text, and source order.
- Interactive controls degrade to their existing static Markdown/Print forms.
- Resource paths must resolve inside the build output or be explicit outbound links; packaging never follows an arbitrary local path from authored content.
- No consumer value becomes raw HTML, CSS, command arguments, or a filesystem path without the same validation and normalization used by current outputs.
Compatibility and migration
This is additive and opt-in. Existing Book sites keep their current outputs and scripts. The DDIA pilot removes consumer-side transformations only after the theme intermediate accounts for every currently validated chapter, numbered object, footnote, image, and internal link. TPME is the second-consumer gate; no DDIA-specific route, label, or chapter list may enter the generic schema.
Prototype evidence
An isolated opt-in build against the first manifest prototype produced these results without modifying either consumer checkout:
| Consumer | Ordered pages | Headings | Raw Markdown anchors | Numbered targets | Xrefs | Unresolved |
|---|---|---|---|---|---|---|
| DDIA | 23 | 597 | 33 | 131 (106 figures, 3 tables, 22 examples) | 292 | 0 |
| TPME | 18 | 295 | 946 | 41 (31 figures, 10 tables) | 1,062 | 0 |
Neither manifest contained a duplicate numbered target ID. Strict builds added about 0.28 seconds for DDIA and 0.22 seconds for TPME in this local sample. The large TPME raw-anchor count is decisive: the packager must consume a rendered output where those explicit anchors survive. Whole-Book Print HTML already does so while also carrying namespaced headings, footnotes, numbered targets, and MathML; the manifest must not duplicate the whole document tree.
The same isolated snapshots were then packaged through one generic command:
| Consumer | EPUB chapters | Typed targets | Package size | OINK package/link check | EPUBCheck 5.3.0 |
|---|---|---|---|---|---|
| DDIA | 23 | 131 | 22.9 MB | 0 errors | 0 errors, 0 warnings |
| TPME | 18 | 41 | 2.2 MB | 0 errors | 0 errors, 0 warnings |
DDIA’s one remote poster required the explicit network-resource opt-in; TPME
packaged entirely from local output. The generic checker verifies every page
anchor and each target’s kind and num against BookManifest, rather than
depending on the old DDIA preprocessor’s wrapper classes. The theme fixture
also passes after a minified Hugo build. Publication CI pins Pandoc 3.10 and
EPUBCheck 5.3.0 by version and archive digest, in a separate job from the Hugo
compatibility matrix.
The Print-to-PDF pilot uses Chrome for Testing headless shell 151.0.7922.34, pinned by archive digest in the same publication CI job:
| Consumer | Book pages | PDF pages | Package size | Structural/text/page-number check | Rendered review |
|---|---|---|---|---|---|
| Theme fixture | 5 | 23 | 1.1 MB | 0 errors | cover, tables, code, equations, footnotes |
| DDIA | 23 | 527 | 60.3 MB | 0 errors | CJK, tables, figures, code, references, end matter |
| TPME | 18 | 197 | 8.4 MB | 0 errors | CJK, wide tables, code, callouts, end matter |
All three PDFs are tagged, unencrypted A4 documents. The real-consumer checker
found every manifest page title and the first, middle, and last CSS page number.
Visual review exposed and fixed three pre-existing Print defects: child math did
not propagate the KaTeX stylesheet to the Book aggregate; a broad Bootstrap
column reset matched KaTeX col-align-* internals; and pre > code defeated
paper-edge wrapping. These are narrow print fixes, not exporter-specific DOM
rewrites.
Implementation plan
- Done: extract the Print page sequence into one shared Book partial without changing rendered Print output.
- Done: add the disabled-by-default manifest and fixture checker.
- Done: package isolated DDIA and TPME snapshots with one generic EPUB path, then validate typed targets, internal links, and EPUB 3.3 conformance.
- Done: render the theme fixture plus isolated DDIA and TPME snapshots with one pinned Chrome runner; validate and visually review representative pages.
- Next consumer migration: replace DDIA’s semantic preprocessor with metadata plus one invocation only after its repository independently accepts the new publication gate.
Acceptance criteria
- Default sites publish no new aggregate file and incur no material build cost.
- The pinned Hugo Extended 0.165.0 toolchain builds the opt-in fixture with warnings fatal.
- Existing HTML, Print, Markdown, RSS, navigation, search, and browser tests pass.
- The DDIA pilot retains 23 chapters and all 106 figures, 3 tables, 22 examples, and internal links as typed semantic targets with zero unresolved target.
- TPME produces an artifact through the same schema and packager.
- No consumer script contains OINK primitive-specific regular expressions.
- EPUBCheck and OINK’s package/link checker pass; the PDF structure/text/page checker and representative rendered-page review pass.
Open decisions
- Should consumer migration happen before or after the next OINK release tag?
Decision log
- 2026-08-24: Drafted the theme/consumer ownership boundary. Chose an opt-in semantic intermediate before any final EPUB API or implementation.
- 2026-08-24: The DDIA/TPME isolated pilot resolved the first format decision: keep one JSON manifest and consume existing whole-Book Print HTML; do not add a generated whole-Book Markdown output or another semantic lowering path.
- 2026-08-24: The packager consumes whole-Book Print HTML, so the manifest uses
the existing
no_printexclusion too. This keeps one publication sequence and avoids a second output-specific exclusion key. - 2026-08-24: Implemented the generic EPUB path and pinned Pandoc 3.10 plus EPUBCheck 5.3.0 in publication CI. Isolated DDIA and TPME packages pass both the typed-target/internal-link checker and official EPUB 3.3 validation.
- 2026-08-24: Remote publication resources remain rejected by default. DDIA’s historical remote poster exercises an explicit opt-in instead of weakening the default or adding a DDIA-specific rewrite.
- 2026-08-24: Added the loopback Print-to-PDF runner and pinned Chrome for Testing headless shell 151.0.7922.34 by archive digest. Theme, DDIA, and TPME PDFs pass structural, text, A4, and page-number checks plus rendered review.
- 2026-08-24: PDF review fixed only the owning Print contracts: aggregate math capability propagation, Bootstrap column-selector scope, code wrapping, single-column numbered equations, and CSS page margins.