Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

OINK Documentation

Build a technical content site. Use shared navigation, multilingual support, and search for documentation, blogs, books, and API references.

For a new OINK site, Start with the Starter. Preview the working site, then replace its sample content. Write in Markdown and build with Hugo Extended with its built-in Sass compiler. Hugo Modules also require Go to resolve the theme. Bundled theme assets require no CDN or npm build step.

The current release is v1.5.1. See the 1.5.1 release notes or the upgrade guide for an existing site.

Five ways in

  • Get started — Create an OINK Starter repository. Verify the local site, customize one part at a time, then deploy it.
  • Components — Each component has one page. The source appears first, followed by the rendered result.
  • Write Beautiful Docs — Follow a six-chapter hands-on walkthrough from cloning the Starter to publishing a polished bilingual site.
  • Case studies — Learn reusable design and migration patterns from production sites.
  • Design and development — Read contracts, accepted decisions, research evidence, and active proposals for OINK maintainers.

Find it by task

What you want to do Where to go
Decide whether it fits What is OINK
Install and preview Get started
Write a documentation page Writing pages
Turn a directory tree into a sidebar Organizing content
Look up a component’s syntax Components
Build an image album or browse the live gallery Expo albums · Live Expo
Change the name, logo, colors, and fonts Brand and appearance
Look up a configuration key’s default Configuration
Run a bilingual or multilingual site Languages
Practice building and writing Write Beautiful Docs
Study a production implementation Case studies
Deploy Deploy
Upgrade, or migrate from Docsy Upgrade
Maintain the theme, review a contract, or write a PRD Design and development

The first six sections in the sidebar (Introduction, Get started, Authoring, Components, Customization, and Operations) are for site authors and operators. Design and CLI at the bottom are reference material for OINK theme contributors.

1 - What is OINK

A local-first Hugo documentation framework that evolved from Docsy. Its components remain readable in Markdown. The theme includes its assets. Fifteen production sites use it.

OINK is a standalone Hugo theme for medium and large technical documentation sites. It evolved from Docsy, retaining the content model and multilingual behavior while replacing the shell, navigation, search, and content components.

The current release is OINK v1.5.1, built with Hugo Extended 0.168.0 (with its built-in Sass compiler), plus Go when resolving the theme as a Hugo Module. No Node.js, npm, PostCSS, or CDN requests are needed: Bootstrap, Font Awesome, fonts, local search, diagram runtimes, and API reference runtimes all ship in the theme repository and load only on pages that use them.

Components are written in clean Markdown rather than a custom template language: > [!NOTE] creates a callout, a table followed by {.fields} renders as a parameter list, and an image followed by {caption="..."} becomes a captioned figure. Fifteen production sites run on OINK today, including this one.

OINK turns Markdown content, configuration and local assets into one static documentation site
One Hugo build produces a static site ready to host

What the theme provides

  • The documentation and blog shell provides navigation, a sidebar tree, a table of contents, breadcrumbs, a pager, dark mode, print view, and accessible interaction.
  • The multilingual structure provides translation routing, fallback for untranslated pages, language weighting, RTL, 32 complete interface catalogs, and an opt-in duo view (params.ui.compare: true) for side-by-side reading.
  • Local browser features include Mermaid, Markmap, Swagger UI, Redoc, Asciinema, ECharts, Infographic, and full-text search. Mathematics is rendered by Hugo at build time and uses local KaTeX styles.
  • Content components include callouts, tabs, steps, cards, field lists, file trees, galleries, badges, keys, and more. Most have a native Markdown form.
  • Additional content types provide built-in book numbering and cross-references, release and download pages, data-driven landing pages, and OpenAPI reference pages.

The theme does not handle source hosting or deployment. A site can use GitHub, GitLab, or a private Git server. You can publish Hugo’s static output anywhere. The site owns its content, brand, and business components. The theme supplies the shell and reusable components.

Is OINK for me

A good fit when A poor fit when
The site has many pages and combines documentation, a blog, a book, release pages, and an API reference The site has one or two pages and needs no structured navigation. A README or a lighter Hugo theme is simpler
You need full multilingual support rather than only a translation link on an English site The site is mostly application UI. OINK can provide its documentation. Business components remain at the site layer
Reproducible builds and network isolation matter, and the build machine has no outbound access You need interactive components inside the prose (React / MDX)
Several sites need a shared shell without copies of layouts and shortcodes You want one switch to change the appearance. The theme has no brand switch. Appearance changes require CSS tokens and partial overrides
The team has no front-end engineers and maintains no Node toolchain You need a built-in CMS or a WYSIWYG editor

How it differs from other documentation systems

Choose a toolchain and authoring model before comparing individual features. Different systems can suit the same project. The choice depends on who maintains it:

Your priority What to evaluate
Keep an existing Hugo content workflow Compare OINK with Docsy and Hextra using your own content tree, overrides, and language needs
Build without a Node toolchain OINK includes browser assets in the theme. Hugo Module installation still needs Go to resolve modules
Write React components inside documentation Evaluate an MDX-based system such as Docusaurus. OINK’s main authoring model uses Markdown, attributes, and shortcodes
Publish books, downloads, or data-driven landing pages Try OINK’s built-in patterns on one representative page before adopting them site-wide

Check each candidate’s current installation and extension documentation. OINK’s search, image zoom, comments, and feedback require explicit configuration. The site also selects Markdown and agent outputs under outputs.

OINK forked from Docsy and evolved as a separate theme. It retains Docsy’s source history, Apache-2.0 obligations, and attribution. For details, see License and acknowledgements.

Start here

  • Get started — Use the official Starter. Customize one part at a time, then publish it.
  • Components — Each component has one page. The source appears first, followed by the rendered result.
  • Showcase — See fifteen production sites and the OINK features each site uses.

Highlights lists each theme capability. Each entry links to its guide.

To report a problem or submit a correction, follow the contribution guide. The 1.5.1 release notes cover Duo View repairs, output correctness, responsive reading, and the current build requirements.

1.1 - Highlights

Compare OINK with an ordinary Hugo theme. Each feature links to its guide.

This page lists the differences between OINK and an ordinary Hugo theme. Each item links to its guide. To install OINK now, see Quick start.

Components are written in Markdown

A callout is a > [!NOTE] blockquote (ten semantic types plus one neutral disclosure). A field list is a table with a {.fields} line. Steps and cards are lists with {.steps} / {.cards}. A caption is a {caption="…"} line under an image. Tabs use adjacent fences, each with a {tab="…"} attribute. File trees, galleries, Mermaid, and ECharts use data fences named after their language. On GitHub or in a plain Markdown reader, these appear as blockquotes, tables, lists, and code blocks. They retain all content.

29 shortcodes provide forms that native Markdown cannot express. These include cards with icons and images, and field entries with several Markdown paragraphs.

→ Components

Build with Hugo

Hugo Extended 0.168.0 or newer compiles the site’s assets. SCSS is compiled by Hugo’s embedded Sass transpiler. The theme never invokes postCSS. It requires neither npm nor webpack. Installing the theme as a Hugo Module also needs Go and access to the module dependencies, either over the network or from a local cache. With an offline archive or a submodule already prepared, Hugo Extended builds the site without Go or an external Sass compiler.

The interface runs JavaScript in the browser: search, the command palette, diagrams and tabs are page scripts. The theme includes those scripts. Each page receives only the scripts it uses.

→ Quick start

Local-first

Everything the browser needs is committed to the theme repository: Bootstrap, Font Awesome, four fonts, Lunr, Mermaid, KaTeX, Markmap, Swagger UI, Redoc, Asciinema, ECharts, Infographic, and the local PlantUML engine. VENDOR.json records the version, source, license file and SHA-256 checksum of each dependency. When you update a runtime, update its artifact, license, and checksum together.

Features that could make network requests require explicit configuration. OINK 1.4.0 renders PlantUML in the browser when enabled without an endpoint; an explicit endpoint keeps server mode. Without params.drawio.drawio_server, Diagrams.net warns and stays disabled. Without appId / apiKey / indexName, Algolia warns and stays disabled.

Local-first does not cover content that an author adds. Explicit network choices include external links, remote images and video, iframes, and remote API specifications. They also include hosted search, such as Algolia or Google Programmable Search, and analytics, comments, or other SaaS integrations. PlantUML and Diagrams.net use the network if the author configures a remote renderer. Pages that enable these features require network access at runtime.

→ License and acknowledgements · Configuration

One source, four outputs

Every component has a defined form in four outputs: interactive HTML, print, plain Markdown, and RSS. Print pages omit zoom and copy controls and fully expand disclosures. The print view is generated per section (this one is /_print/docs/about/), and the Markdown version is the same page address plus index.md.

Select the site’s output formats under outputs. The theme does not select them for the site.

→ Print · AI-agent support

Two languages and 32 interface locales

Multilingual support uses Hugo’s own mechanism: translation routing, a language picker ordered by weight, fallback for untranslated pages, RTL, and canonical and alternate metadata. Interface strings come in 32 language packs sharing one 235-message schema. These include all 31 locale filenames that Docsy supports, plus generic zh. Every pack contains native OINK interface text instead of English placeholder blocks. zh and zh-cn use Simplified Chinese. zh-tw uses Traditional Chinese. Setting params.ui.compare: true adds an opt-in duo view so readers can open an existing translation beside the current page and scroll both versions together.

→ Languages · Duo view

With params.offline_search on, Hugo generates one index per language. The browser searches Latin text with a local Lunr index and falls back to substring matching for CJK text. It sends no query to a third party. A page can adjust its weight with search_boost and add synonyms with search_keywords.

→ Search

Command palette

Cmd/Ctrl + K opens the command palette. A bare / enters search mode. A bare \ enters command-only mode. The palette contains pages, commands, and page actions, such as switching language, switching theme, and copying Markdown. Use the same entry point to search and run actions.

→ Command palette

Keyboard navigation

Keyboard navigation is on by default. You can turn it off per site or section. w and s move up and down the sidebar tree. a and d collapse and expand it. q and e go to the previous and next page. j and k move through the page’s table of contents. t toggles light and dark mode. l switches language. b opens or closes the duo view when a translation is available. h hides the reading shell. Single-key shortcuts are inactive while an input or textarea has focus, or while an input method is composing. The question-mark button in the footer’s bottom bar opens the cheatsheet.

→ Keyboard navigation

Turn on params.ui.backlinks to list the pages that link to each page. Hugo derives the list at build time from ordinary links. This requires no new syntax and no JavaScript. This site enables backlinks site-wide. See the “Linked from” group in this page’s right rail. More references produce a longer list. The list collapses when it has more than eight entries.

→ Backlinks

Four content types beyond documentation

The theme also has four kinds of page that need extra structure:

  • Books provide chapter numbering. Use {#id num=} to number figures / tables / equations / examples and xref to reference them. book-toc, book-figures, and related shortcodes generate indexes. You can print the whole book.
  • Release and download pages use data/download/*.yaml to produce release cards, asset tables, and checksums. The data controls publication state.
  • Landing pages use data/home/<lang>.yaml to assemble home-page sections. Any page with layout: landing can use data under data/landing/.
  • API references use the local Swagger UI and Redoc runtimes. The specification can reside on the site.

→ Books · Releases and downloads · Home and landing pages · API reference pages

Output for AI assistants

Add markdown to outputs. Each page then has a .md version. Its HTML <head> has a rel="alternate" reference to that version. Page actions include “Copy Markdown” and “View source”. The LLMS output format writes an llms.txt inventory at the site root (this site’s is https://oink.pgsty.com/llms.txt).

0.8.0 adds two more outputs. A section that enables LLMSFULL produces one llms-full.txt file. An agent can fetch it in a single request. A site that enables NAVJSON publishes navigation.json per language. This file represents the sidebar tree as data. Both are live on this site: https://oink.pgsty.com/docs/llms-full.txt and https://oink.pgsty.com/navigation.json are the real artifacts.

“Open in ChatGPT / Claude” is off by default. Clicking it sends the current URL to a third party. The site must turn on params.ui.page_context_menu.assistant_links explicitly.

→ AI-agent support

Versions

Configure params.versions to add a version menu to the navbar. An archived version shows a banner that links to the current version. The site chooses whether the menu links to corresponding pages. Each version is a separately built and deployed static site. No runtime service is required.

→ Versions

See for yourself

This site enables most of these features. Use these four checks:

  1. Press Cmd/Ctrl + K on any page. Type postgres to see local search results. Press \ for command-only mode.
  2. Append index.md to the current page address to get this page’s Markdown version.
  3. Open https://oink.pgsty.com/llms.txt, the site inventory for AI assistants. It links to the docs section’s llms-full.txt and to navigation.json.
  4. Look at this page’s right rail. “Backlinks” lists the pages that link here.

1.2 - Case Guide

Find the OINK production case closest to your documentation, book, landing page, or interactive tool.

The Case library describes fifteen OINK site projects and their implementation patterns, including this documentation site. Case pages link to the live result or project source. Where a pattern needs custom implementation, they distinguish site-owned code from theme features.

If you know the type of site you want to build, use this guide. Read a case to understand its architecture and trade-offs. Then use the linked documentation for exact configuration. Counts in each case reflect the audited snapshot.

Distribution documentation

pigsty.io

This very large English site combines a distribution manual, editorial blog, extension catalog, taxonomies, version navigation, and pricing landing pages.

pigsty.cc

This Chinese peer is deployed as an independent single-language site. This approach is useful when each language corpus has become a separate product.

pgsty.pro

This bilingual version archive renders many release pages from reusable, structured release data.

Product documentation

PIG

This compact bilingual CLI manual has a data-driven home page and a much larger companion blog.

SOW

This bilingual operations manual has a dedicated download content type that uses release metadata.

SILO

This large upstream migration uses a checked manifest to generate bilingual documentation navigation.

PG Exporter

This metrics manual combines generated navigation, a structured catalog, and system fonts.

Books

Designing Data-Intensive Applications

This multilingual, multi-edition book is the strongest example of numbered figures, cross-references, chapter navigation, and indexes.

The Product-Minded Engineer

This focused bilingual publication needs only OINK’s Book shell.

PG Internal

This finished Chinese translation is deliberately published as a single-language Book. It has no documentation tree or other language to select.

Aggregate, landing, and custom sites

PostgreSQL ecosystem library

This aggregate operations library combines several upstream manuals and partially translated language trees. They share one search and visual system.

pgsty.com

This small bilingual corporate site shows how OINK can primarily serve as a data-driven landing-page system.

Capslock

This project has two pages per language. Its custom shell hosts an interactive, data-driven configuration generator.

oink.pgsty.com

This full reference site contains public documentation, live component examples, design contracts, multiple content shells, and regression coverage in one repository.

pgext.cloud

This PostgreSQL extension catalog uses a queryable dataset as its primary object. It indexes 2,241 extensions and 576 packaged builds across 16 platforms.

Choosing a starting point

The theme repository’s tests/site/ is an internal CI fixture. Do not use it as a starter template. Its pages test rendering behavior. Use the production cases above as design references.

→ Browse all cases · Quick start · Repository tour

1.3 - License and acknowledgements

The theme uses Apache-2.0. The documentation uses CC BY 4.0. Each bundled third-party runtime keeps its own license terms.

OINK contains three types of material: theme source, documentation content, and bundled third-party assets. These materials are not relicensed as a single combined work. Each table below links to the authoritative repository file. If a summary disagrees with the license text, the file is authoritative.

Which licence covers what

Scope Licence Authoritative file
OINK theme source (layouts, partials, shortcodes, SCSS, JS, i18n) Apache License 2.0 Theme LICENSE, NOTICE
This site’s own code, build scripts and material derived from Docsy Apache License 2.0 Site LICENSE, NOTICE
This site’s original documentation content, except where stated otherwise Creative Commons Attribution 4.0 International Site LICENSE-CC-BY-4.0
Browser libraries, fonts and icons shipped with the theme Each component’s own licence Theme VENDOR.json and the licence files beside each asset

CC BY 4.0 covers only the original documentation content. It does not cover theme code, trademarks, screenshots, or third-party assets. The theme’s Apache-2.0 license does not change the licenses of its bundled dependencies.

Community contributors

OINK is maintained by PGSTY. Thank you to @imbajin for reporting and contributing the following reading repairs, with fixtures, regression tests, and bilingual docs. Introduced in 1.3.1, these fixes remain included in 1.5.1.

Contribution Pull request
Markmap node text across light/dark modes and visual presets Theme #48
Fixed TOC clearance above the footer Theme #50
Complete FileTree comments, integrated behind wrap=true Theme #52
Readable technical tables with an opt-in layout Theme #54
Bilingual contracts, release documentation, and companion regression coverage Documentation #11

The release notes record the integration details. To contribute a correction, translation, or fix, follow the contribution guide.

Upstream: Docsy

The theme’s NOTICE records these facts:

  • OINK is derived from Docsy, Copyright 2018 Google LLC and Docsy contributors.
  • OINK’s own theme work is Copyright 2026 PGSTY contributors.
  • The project and its upstream both use Apache License 2.0. VENDOR.json records each third-party browser dependency’s license, source, version, and checksum. Each required NOTICE file accompanies its asset.
  • The Docsy name and Google’s trademarks belong to their respective holders. These names identify the upstream project and imply no endorsement.

This site also derives from the Docsy project website. The site’s NOTICE records that origin. Docsy is OINK’s only code upstream. OINK retains its source history, Apache-2.0 obligations, and copyright notices. Modified files carry a modification notice, as Apache-2.0 requires.

Third-party runtimes shipped with the theme

The theme commits everything the browser needs to the repository (assets/third_party/, assets/js/third_party/, static/webfonts/). These assets need no npm step or build-time download. Hugo Module resolution still needs Go and an available module source. VENDOR.json is the machine-readable manifest for these assets. Each entry records the name, pinned version, source URL, license file path, and SHA-256 of every selected artifact. It also records an aggregate checksum for each of the three asset trees.

The table below matches the OINK v1.5.1 manifest (VENDOR.json generated 2026-10-25, schema 1, 29 entries). Versions change with each theme release. Use the manifest from your pinned theme version. Every source is the npm registry (https://registry.npmjs.org/…).

Package Version Licence What it does in the theme
bootstrap 5.3.8 MIT Grid, components and the RTL stylesheet
@popperjs/core 2.11.8 MIT Overlay positioning for Bootstrap
@fortawesome/fontawesome-free 7.3.1 CC-BY-4.0 AND OFL-1.1 AND MIT Icons throughout the site
@fontsource-variable/inter 5.3.0 OFL-1.1 Interface and body font
@fontsource-variable/ibm-plex-sans 5.3.0 OFL-1.1 Body font for Paper and Terminal
@fontsource/chakra-petch 5.3.0 OFL-1.1 Brand display font
@fontsource/ibm-plex-mono 5.3.0 OFL-1.1 Code font
lunr 2.3.9 MIT Local full-text search
@docsearch/js 5.0.4 MIT The optional Algolia DocSearch front end
@docsearch/css 5.0.4 MIT Its stylesheet
mermaid 11.17.2 MIT Mermaid diagrams
katex 0.18.4 MIT Mathematics
markmap-autoloader 0.18.12 MIT Mind maps
markmap-lib 0.18.12 MIT Mind maps
markmap-view 0.18.12 MIT Mind maps
markmap-toolbar 0.18.12 MIT Mind map toolbar
d3 7.9.0 ISC Markmap dependency
@highlightjs/cdn-assets 11.12.0 BSD-3-Clause Markmap dependency
webfontloader 1.6.28 Apache-2.0 Markmap dependency
swagger-ui-dist 5.32.14 Apache-2.0 OpenAPI reference pages
redoc 2.5.3 MIT OpenAPI reference pages
asciinema-player 3.17.0 Apache-2.0 Terminal recording playback
echarts 6.1.0 Apache-2.0 Charts
@antv/infographic 0.2.19 MIT Infographics
pako 3.0.1 MIT AND Zlib Decompression (diagram data)
external-svg-loader 1.7.1 MIT Inlining external SVG
idb-keyval 6.2.0 Apache-2.0 Browser-side caching

License texts accompany their assets. Examples include assets/third_party/bootstrap/LICENSE and assets/third_party/katex/LICENSE. Swagger UI, Redoc, and ECharts also include their own NOTICE or bundled-declaration files. Lunr is the only exception. Its code is in assets/js/third_party/. Its license is at assets/third_party/lunr/LICENSE.

The OINK 1.4.0 local PlantUML integration adds @plantuml/core 1.2026.9 (MIT) and Viz.js 3.24.0 (MIT), including Graphviz 14.1.1 (EPL-1.0) and Expat (MIT). Their notices and upstream source links accompany the two scripts in assets/third_party/plantuml/; VENDOR.json records the exact artifacts.

When you redistribute the theme, include all this license and notice material. When you update a runtime, update its artifact, license file, source, and checksum in the same change.

Fonts and icons

Inter, IBM Plex Sans, Chakra Petch, and IBM Plex Mono use the SIL Open Font License 1.1. OINK 1.5.1 includes 33 files under static/webfonts/: fourteen Inter subsets, twelve IBM Plex Sans subsets, four brand-font files, and three Font Awesome files. Font Awesome Free 7.3.1 uses a composite license. Its icon artwork uses CC BY 4.0, its font files use SIL OFL 1.1, and its code uses MIT. The text is in assets/third_party/Font-Awesome/LICENSE.txt.

The theme makes no request to a remote font service. The repository has no Google Fonts link. The site always serves fonts from its own baseURL. To change fonts or switch to the platform stack, see Brand and appearance.

Design references

Docsy is the only code upstream. The projects below are references for the design language. They supply no code or runtime dependencies. OINK has ported no code from them:

Project What was learned from it
Fumadocs Content-first presentation, information hierarchy, and writing components such as file trees and field lists (the theme’s NOTICE records this acknowledgement)
Nextra A simple documentation shell, filename labels and copy controls on code blocks, and per-page layout switches
Hextra A Hugo-native approach to implementation, file trees, badges, tabs
Mintlify Layered navigation structure, synchronized code groups, the reading experience of an API reference

Hugo is the build platform, and Go resolves modules when the theme is installed as a Hugo Module. Both are prerequisites, and the theme redistributes neither binary.

These names describe origin, dependency, or design references. They imply no endorsement by these projects. Project and product names belong to their respective holders.

Reusing this documentation

CC BY 4.0 permits sharing and adaptation for any purpose under these conditions. Give attribution, link to the license, and state whether you made changes. Do not imply endorsement by OINK, PGSTY, or any upstream project. This attribution is sufficient:

Adapted from the OINK documentation by PGSTY contributors, licensed under CC BY 4.0, with modifications.

Images or quotations with their own attribution keep their own credit and license. Removing the footer does not remove the attribution obligation.

Reusing the theme

Apache-2.0 permits using, modifying, and distributing the theme source and build output under its terms. Keep the license, copyright, and attribution notices. Keep the contents of NOTICE. When you distribute modified source, state which files you changed. A theme distribution should include LICENSE, NOTICE, VENDOR.json, and every third-party licence file the manifest references.

Apache-2.0 grants no trademark rights. It does not change third-party assets into Apache-licensed works.

1.4 - Contributing to OINK

Where to report bugs and contribute theme changes, documentation, translations, and regression tests for OINK 1.5.1.

Corrections, translations, bug reports, and focused pull requests are welcome. OINK v1.5.1 is the current theme release. Its release notes describe Duo View repairs, publishing correctness, responsive reading, and accessibility.

Where to contribute

Change Repository
Templates, styles, browser runtimes, defaults, and focused theme checkers pgsty/oink
Documentation, translations, examples, integration tests, and browser regression coverage pgsty/oink.pgsty.com
Optional CLI commands and native site checks pgsty/oink-cli

The CLI has its own version and release cycle. Ordinary Hugo consumers do not need it. Report a problem in the repository that owns the affected behavior.

Report a reproducible problem

Include the OINK version, hugo version, Go version for module builds, relevant configuration, and a minimal content example. For display problems, include the page, viewport width, browser, visual preset, and light/dark setting. Describe what you expected and what happened. Remove credentials and private content from reports and reproductions.

Build and check

The maintained baseline is OINK v1.5.1, Hugo Extended 0.168.0, and Go 1.27.3; the module’s Go minimum is 1.27.3. Hugo uses its built-in LibSass. Ordinary site builds require no external Sass compiler, Node.js, or npm.

Never enable --panicOnWarning or HUGO_PANICONWARNING. Keep warnings visible and review them; the upstream LibSass deprecation warning is accepted. Development and CI use the pinned Hugo baseline, without a legacy-version matrix.

For theme work, clone the theme and this site as siblings. Read each repository’s AGENTS.md, this site’s TRANSLATION.md, and the relevant Design contract. This site’s regression tests need Node >= 24 and npm >= 11.16.0:

npm ci
make check
make browser

Run the smallest owning checker first. make check runs the non-browser suite against the sibling theme; use make browser for changed rendering or interaction. Review affected English and Chinese pages at desktop and mobile widths in light and dark mode. The repository README also explains how to verify the published pin separately.

Prepare a pull request

Keep the change focused and describe the problem, resulting behavior, and validation. Update English and Chinese peers together and preserve stable heading IDs. Public theme changes need the implementation, owning checker, and both language versions of the affected contract. New design proposals belong in the bilingual Design proposals section.

Preserve unrelated changes in shared worktrees. Commit source and reviewed test expectations; keep generated output, caches, binaries, and filesystem module replacements out of Git. Keep historical release notes and dated research faithful to their original evidence.

Credit and licences

The acknowledgements link community fixes to their authors and pull requests. The 1.3.1 notes record the recent reading repairs; 1.5.1 keeps those fixes and the Hugo-only build restored in 1.3.2.

Theme code and site tooling use Apache-2.0; original documentation uses CC BY 4.0. Retain third-party licence and notice files. See License and acknowledgements for the full scope.

2 - Get started

Start from the official OINK Starter. Verify the local site, then customize content, language, brand, integrations, and deployment in that order.

For a new site, start from pgsty/oink-starter, a public GitHub template that pins a published OINK release, builds out of the box, and includes neutral project content and deployment workflows without this repository’s regression test harness.

Requirements

OINK sites build with Git, Go 1.27.3 or newer, and Hugo Extended 0.168.0 or newer (using Hugo’s built-in Sass compiler — no Node.js, npm, or external Sass installation required).

Choose a path

Starting point Path Result
New documentation or project site OINK Starter A small three-language Docs, Blog, and Book site with two deployment workflows
Existing Hugo site From scratch Add the OINK module and required Goldmark settings without replacing content
Existing Docsy or older OINK site Upgrade Preserve content, migrate supported syntax, and review site overrides

Five-minute baseline

  1. Install the tools

    Install Git, Go 1.27.3 or newer, and Hugo Extended 0.168.0 or newer. The Hugo output must contain extended:

    $ go version
    go version go1.27.3 darwin/arm64
    $ hugo version
    hugo v0.168.0+extended+withdeploy darwin/arm64
    

    On macOS, brew install git go hugo supplies them. On Linux and Windows, use the official Hugo installation guide and Go downloads. Choose Hugo Extended.

  2. Create or clone the site

    If you intend to keep the repository, open the Starter and select Use this template. Then clone the repository that GitHub creates. To evaluate the original without changes, use these commands:

    git clone https://github.com/pgsty/oink-starter.git my-docs
    cd my-docs
    hugo server
  3. Open the baseline

    Open http://localhost:1313/. The default Starter also publishes Chinese at /zh/ and French at /fr/. Confirm that Docs, Blog, Book, search, language switching, and light/dark mode all work before editing anything.

  4. Make one visible change

    Change the title and canonical URL at the top of hugo.yaml, then edit one sentence in data/home/en.yaml. Reload the browser. If both changes appear, configuration, content, and the pinned theme are connected correctly.

Customize from shallow to deep

  • Use OINK Starter — choose languages first, then identity, home page, content, navigation, brand, integrations, and deployment.
  • Starter repository tour — which file owns each part of the site, what to replace, and what can be removed.
  • Writing pages — front matter, headings, links, images, drafts, and the page-end controls.
  • Components — add expression only after the content tree is stable.
  • Brand and appearance — logo, accent, typography, width, and CSS extension points.
  • Deploy — use the supplied GitHub Pages or Cloudflare Pages workflow, then verify the real public routes.

Verify the build and content tree before customizing languages, navigation, CSS, analytics, and hosting; changing one layer at a time makes failures easy to isolate.

Publication gate

Before your first push, run the same production build command used by the Starter workflows:

hugo --cleanDestinationDir --gc --minify --environment production \
  --printPathWarnings

Confirm that the build exits cleanly with Total in … and that public/ contains the language roots along with representative Docs, Blog, and Book routes. Keep warnings visible for review and do not enable --panicOnWarning or HUGO_PANICONWARNING (Hugo’s built-in LibSass deprecation notice is expected). After pushing, check the CI workflow and the live URL as well.

Build with an AI agent

OINK publishes an agent skill, Oink 建站 (oink-site): one Markdown file that packages the site-building workflow, the review standards of OINK’s own sites, and lessons from the case library. It defers to this documentation for syntax and asks the agent to verify its work in a browser.

Save it as a skill, then start a new agent session:

Claude Code
mkdir -p ~/.claude/skills/oink-site
curl -fsSL https://oink.pgsty.com/skill.md -o ~/.claude/skills/oink-site/SKILL.md
Codex
mkdir -p ~/.codex/skills/oink-site
curl -fsSL https://oink.pgsty.com/skill.md -o ~/.codex/skills/oink-site/SKILL.md

Then describe the site you want, for example “Use the oink-site skill to build a bilingual documentation site for this project.” An agent without a skills directory can follow the file directly when you give it the URL. Run the download again to update; the file states the OINK release it was written for.

Next

Start with the complete Starter tutorial. If the template has more structure than your project needs, use the repository tour to remove it safely. Use From scratch only when adding OINK to an existing site or when you explicitly want to assemble every file yourself.

2.1 - Use OINK Starter

Create your project site from the official Starter. Change one part at a time: languages, identity, home page, content, navigation, brand, integrations, and deployment.

pgsty/oink-starter is the official template for new OINK sites. It stays lean and clean, without this site’s theme reference docs, analytics IDs, comment repo settings, browser test harness, or PGSTY branding.

You only need Git, Go 1.27.3+, and Hugo Extended 0.168.0+ (which includes its built-in Sass compiler — no Node.js or external Sass installation required).

What the template contains

Surface Included baseline First decision
Languages English, Simplified Chinese, French Keep all three, or select a supplied single/bilingual profile
Content Docs, Blog, and a short Book tutorial Rewrite the examples. Delete a whole surface only if you do not need it
Home One compact data/home/<lang>.yaml per language Replace the project promise and destinations
Brand Neutral logo and favicon Keep them until real project artwork exists
Integrations Repository, Giscus, analytics, share, and feedback examples are commented Enable only complete configurations you intend to operate
Deployment GitHub Pages and Cloudflare Pages Direct Upload workflows Choose one production path and verify its real URL

The Starter’s own Book at /book/ is a four-chapter tour from preview to deployment. This page gives maintainers more detail. It explains the order of changes, their boundaries, and the checks after each group of changes.

Create your repository

GitHub template, recommended

Open the Starter repository, select Use this template → Create a new repository, then clone the repository created under your account or organization:

git clone https://github.com/OWNER/PROJECT-DOCS.git
cd PROJECT-DOCS
hugo server

This creates your site’s own Git history. The original Starter remains an upstream reference rather than a remote for pushes.

Clone the original to evaluate it

For a disposable local evaluation:

git clone https://github.com/pgsty/oink-starter.git
cd oink-starter
hugo server

Do not start a real project by deleting this clone’s .git directory. GitHub’s template operation already creates a separate project and preserves an auditable first commit.

Preview before changing anything

Open these routes:

  • /, /zh/, /fr/ — the three home pages;
  • /docs/, /blog/, /book/ — the three content surfaces;
  • one translated page, then the language switcher;
  • search and the light/dark control at a narrow viewport.

Also record the resolved module:

hugo mod graph | grep github.com/pgsty/oink

Check the version supplied by your Starter snapshot in go.mod. To use the current release, update it to github.com/pgsty/[email protected] before customizing content:

hugo mod get github.com/pgsty/[email protected]
hugo mod tidy

Customize in layers

Layer 1: language profile

The root configuration enables English, Chinese, and French. Before making other configuration edits, choose one of the supplied profiles when that is not your intended language set. Choose one of these commands:

cp examples/hugo.single.yaml hugo.yaml     # English only
cp examples/hugo.bilingual.yaml hugo.yaml  # English + Chinese

These are complete minimal configurations. Copying one replaces the commented integration examples in the root file. Copy it before other changes. If hugo.yaml already contains project changes, merge the languages and disableLanguages sections. Do not overwrite the file.

If you already changed the title and URL in Get started, keep that file. To change the default three-language setup to English and Chinese, add only disableLanguages: [fr] at the top level. For English only, use [zh, fr]. This preserves the identity and integration settings you have already changed.

Disabled languages stay declared so Hugo recognizes .zh.md and .fr.md as translations and safely ignores them. If you remove a language permanently, remove its content and home data only after the selected profile builds.

Layer 2: identity

Change the two marked values at the top of hugo.yaml:

hugo.yaml
title: &siteTitle Project Name
baseURL: https://example.org/

The YAML anchor carries the title to all enabled languages. Then change the copyright holder and, after the new repository exists, uncomment its links:

hugo.yaml
params:
  copyright:
    authors: '[Project contributors](https://example.org/community/)'
    from_year: 2026
  github_repo: https://github.com/OWNER/PROJECT-DOCS
  github_branch: main

Run hugo server again and check the browser title, footer, edit/history links, and canonical URL. Change the logo only if the project has final artwork. Review the text identity first.

Layer 3: home page

Home-page content lives in language data files under data/home/:

data/home/en.yaml
data/home/zh.yaml
data/home/fr.yaml

Edit one language first. In each file, sections sets the section order while hero, cards, and cta supply the content. Replace the tagline, destination URLs, and sample card text while keeping the structure intact, then translate the updated copy into each enabled language.

For additional section types, see Home and landing pages. The theme renders these data files directly—no layout override is needed.

Layer 4: content and navigation

Rewrite or remove sample leaf pages under content/. Keep section roots until you decide whether that whole surface belongs in your project:

content/docs/  reference and task documentation
content/blog/  posts, design records, and release announcements
content/book/  a sequential long-form guide

The content tree becomes the sidebar. menus.main on the translated _index roots controls top navigation. Rename Docs, Blog, or Book beside its content. There is no second global menu tree. Keep translated files side by side and give corresponding headings the same explicit IDs:

page.md
page.zh.md
page.fr.md

Follow Organizing content before creating a custom navigation data file. The generated tree is enough for most sites.

Layer 5: brand and reader features

Replace assets/icons/logo.svg and static/favicon.svg when real assets are ready. Then enable the smallest useful configuration changes, one at a time:

hugo.yaml
params:
  ui:
    theme_color: '#245f94'
    typography: system
    image_zoom: true
    share: [mastodon, linkedin, email, copy]

For custom local fonts, use params.ui.fonts for family names or declare font files in site CSS. For layout, sidebar, search, and component settings, consult the Configuration reference rather than copying the much larger configuration of oink.pgsty.com.

Layer 6: integrations

The Starter leaves repository actions, Giscus, Google Analytics, feedback, and sharing off or commented. Enable an integration only after all of its required facts are known:

  • Repository links need the real owner, repository, and branch.
  • Giscus needs its repository/category names and immutable IDs.
  • Google Analytics needs a project-owned measurement ID.
  • Feedback records structured gtag events only if analytics is present.
  • Assistant links send the current URL to a third party. They require an explicit policy choice.

An incomplete optional block should remain commented. See Comments, Analytics and SEO, and Repository links for the operating boundary of each integration.

Build and deploy

Local production build

Before enabling a hosting workflow:

hugo --cleanDestinationDir --gc --minify --environment production \
  --printPathWarnings

Keep build warnings visible for review and do not enable --panicOnWarning or HUGO_PANICONWARNING (the upstream LibSass deprecation warning is expected). Commit hugo.yaml, go.mod, and go.sum. Never commit generated public/, resources/, module caches, or a local module replacement.

GitHub Pages

The Starter already contains .github/workflows/github-pages.yaml. In Settings → Pages, select GitHub Actions as the source. A push to main builds with the pinned toolchain, asks GitHub for the correct project subpath, and publishes public/ through the Pages deployment API.

Cloudflare Pages

The supplied .github/workflows/cloudflare-pages.yaml uses Direct Upload. Create a Pages Direct Upload project, add CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, then run the workflow manually once. Set the repository variable CLOUDFLARE_PAGES_ENABLED=true for automatic deploys, and CLOUDFLARE_SITE_URL when the canonical address is not the default pages.dev domain.

For one project, choose either Direct Upload or Cloudflare Git integration. Do not use both. The complete host comparison and baseURL rules are in Deploy.

Verify and remove samples

Before you declare the site ready, complete these checks:

  1. Search for placeholders such as Project Name, example.org, OWNER, and PROJECT, then decide whether each remaining occurrence is intentional.
  2. Open every enabled language root and representative Docs, Blog, and Book pages on desktop and mobile.
  3. Confirm that language switching opens peer pages rather than the home page.
  4. Test search, dark mode, one component, Markdown output, print, 404, canonical URLs, and repository actions.
  5. Check the deployed workflow and the public URL separately from the local build.

Delete the sample Book or Blog only after removing its top-menu root and any home-page card that links to it. After you remove each whole surface, rebuild and review the diagnostics. This lets you identify which change caused a failure.

Next

Use the Starter repository tour as a file-level map, then continue with Writing pages and Configuration. For an existing site that should not inherit the Starter’s content model, use From scratch.

2.2 - Starter repository tour

Find the oink-starter files that control identity, languages, home, content, navigation, brand, deployment, and the pinned theme.

This page walks through the repository layout created from pgsty/oink-starter. go.mod pins the OINK theme as a Hugo Module, and Hugo caches the resolved theme source in the Go module cache rather than copying it into your repository.

Top-level map

oink-starter/

  • oink-starter/
    • hugo.yamlidentity, languages, outputs, parameters, module import
    • go.modsite module and exact OINK release
    • go.summodule checksums
    • examples/
      • hugo.single.yamlEnglish-only complete profile
      • hugo.bilingual.yamlEnglish + Chinese complete profile
    • data/
      • home/
        • en.yamlone compact landing page per language
        • zh.yaml
        • fr.yaml
    • content/
      • _index.mdlanguage home roots
      • _index.zh.md
      • _index.fr.md
      • docs/Introduction, Get Started, Tutorial, Reference
      • blog/posts, design records, release announcements
      • book/sequential tutorial about the Starter
    • assets/
      • icons/logo.svgprocessed project logo
    • static/
      • favicon.svgcopied unchanged to the site root
    • i18n/
      • fr.yamlStarter-specific French interface overrides
    • .github/workflows/
      • github-pages.yamlproduction build and GitHub Pages deployment
      • cloudflare-pages.yamlproduction build and Cloudflare Direct Upload
    • README.mdoperating summary for repository maintainers
    • LICENSEtemplate source license

Generated public/, resources/, .hugo_build.lock, and module caches contain build state. Git ignores them. They are not source files.

What to change first

Path Responsibility Initial action
hugo.yaml Identity, canonical URL, languages, outputs, theme features, optional integrations Change the two marked values. Choose a language profile before other edits
data/home/ Home-page promise, cards, calls to action Rewrite every enabled language after one language is approved
content/ All reader-facing material Replace example leaf pages. Keep each section root until you decide to remove the whole surface
assets/icons/logo.svg Processed logo Replace only with final artwork
static/favicon.svg Browser icon Replace together with the logo review
params.github_* in hugo.yaml Edit/history/new-page/issue links Uncomment only after the destination repository exists

What to keep

  • go.mod and go.sum pin and verify the template’s selected OINK release. Commit both. Record a later upgrade separately from this baseline.
  • The three Goldmark settings in hugo.yaml: native Steps, Cards, Fields, image attributes, and Book targets depend on them.
  • outputs: if you remove markdown, LLMS, or print, you remove the corresponding Markdown, agent-index, or print output.
  • fetch-depth: 0 in workflows if enableGitInfo stays on. Last-modified and contributor facts require repository history.
  • GOWORK: off and HUGO_MODULE_WORKSPACE: off in CI. A developer’s local workspace must not replace the published release under verification.

Optional surfaces

Docs, Blog, and Book are independent top-level surfaces. To remove one safely:

  1. Delete its content/<surface>/ tree.
  2. Remove any home-page card or link that targets it.
  3. Confirm that no other page links to it.
  4. Run a build that keeps warnings visible for review. Inspect the remaining top navigation.

Do not delete only translated section roots. This creates language-specific navigation and fallback behavior that can appear to be an error. Remove a surface in all enabled languages or document the asymmetry.

The two configuration profiles under examples/ are optional after the language decision. They are useful references, but the root hugo.yaml is the only active site configuration.

Content and navigation

Under Docs and Book, directory structure and weight form the sidebar and pager sequence. Top navigation comes from menus.main on section roots. A translated root repeats the same identifier, parent, and weight while translating visible labels.

The Starter intentionally demonstrates the Documentation System model:

  • Introduction explains what the project does and why.
  • Get Started helps a new user achieve a result.
  • Tutorial teaches a complete task.
  • Reference records exact supported behavior.

Rename or restructure these sections for your project. Keep the learning paths separate. Do not combine every type of answer in one tree.

Language model

English source files end in .md; Chinese and French peers end in .zh.md and .fr.md. Home data uses language keys under data/home/. The root profile declares the languages, their locale, order, and site description.

The single and bilingual profiles keep disabled languages declared. Hugo then recognizes unused suffixes as translations. It does not render several files to one English URL. Copy a profile only before you start project-specific configuration. After that, merge changes manually.

Where OINK lives

Two files establish the module boundary:

hugo.yaml
module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: '0.168.0'
go.mod
module github.com/OWNER/PROJECT-DOCS

go 1.27.3

require github.com/pgsty/oink v1.5.1

This example pins the current release, v1.5.1. Check the pin in your Starter snapshot and follow the upgrade guide if it is older.

hugo mod graph displays the resolved version. Production builds use the exact tag in go.mod, while HUGO_MODULE_REPLACEMENTS can point to a local checkout during theme development. Keep local module replacements out of Git.

Deployment files

The GitHub Pages workflow runs automatically on pushes to main. In repository settings, select GitHub Actions as the Pages source. The Cloudflare workflow runs manually, or automatically only after the repository variable CLOUDFLARE_PAGES_ENABLED=true is set. Its required account ID and API token remain repository secrets.

Keep only the workflows for deployment paths you use. For one project, choose either Cloudflare Direct Upload or Cloudflare Git integration. Do not run both as deployment checks.

Safe customization order

  1. Prove the untouched preview.
  2. Select languages, then change identity.
  3. Replace one home page and then its translations.
  4. Replace content and verify navigation.
  5. Change brand and reader features one group at a time.
  6. Enable complete external integrations.
  7. Run the production build.
  8. Deploy, then verify production independently.

If you own the repository, commit after each group of changes. This lets you identify the decision that caused a regression or needs a rollback.

Verify

hugo mod graph | grep github.com/pgsty/oink
hugo --cleanDestinationDir --gc --minify --environment production \
  --printPathWarnings
git status --short

Confirm that the module graph names the pinned release. The build must succeed and its diagnostics must be reviewed; the upstream LibSass warning is accepted. Git status must contain source edits but no public/ or cache files. Before deployment, open each enabled language root and one route from each of Docs, Blog, and Book.

2.3 - From scratch and other install methods

Build a minimal OINK site in an empty directory. Compare four install methods: Module, submodule, offline archive, and pinned source copy.

This is the manual alternative to the recommended OINK Starter. It builds a minimal site in an empty directory. A small hugo.yml and one hugo mod get command create a single-language site you can preview. You must assemble the home page, example content, deployment workflow, and all component usage yourself.

For an existing Hugo site, use the short integration path below. For an existing Docsy site, see Upgrade.

The second half compares four install methods: Hugo Module, Git submodule, offline archive, and pinned source copy. Current development and builds use Go 1.27.3 and Hugo Extended 0.168.0 (with built-in Sass — no external Sass needed).

Add OINK to an existing site

Work on a branch with the site’s current configuration and content preserved. Skip hugo new site and keep the existing configuration filename.

  1. If the site has no go.mod, run hugo mod init with your repository’s module path. Otherwise keep the existing module declaration.
  2. Run hugo mod get github.com/pgsty/[email protected].
  3. Replace the old theme selection with the OINK module.imports entry shown below. Preserve unrelated imports and configuration. Merge the three markup.goldmark settings and markup.highlight.noClasses: false from the example. Do not replace your whole configuration with it.
  4. Review site-owned layouts/ and assets, old theme shortcodes, and page type/layout values. These overrides and conventions may still select the previous theme’s behavior. Keep content. Make only the necessary adaptations.
  5. Run hugo, then open an existing representative page with hugo server. Check its navigation, images, and code blocks before applying optional OINK features. Continue with verification.

From an empty directory to the first page

  1. Create the skeleton and fetch the theme

    hugo new site --format yaml my-docs
    cd my-docs
    git init
    hugo mod init github.com/example/my-docs
    hugo mod get github.com/pgsty/[email protected]

    After hugo mod init, specify your site’s module path. This is usually its repository address. hugo mod get writes go.mod and go.sum. Commit both.

    Before building, create .gitignore so generated files stay out of Git. Leave enableGitInfo off until you have made the first commit:

    .gitignore
    /public/
    /resources/
    /.hugo_build.lock
    /.hugo_cache/

    Find the latest release tag on GitHub Releases; v1.5.1 on this page reflects this site’s current pin. Production sites should pin an explicit release tag in go.mod rather than tracking main.

  2. Writing hugo.yml

    For this new site only, rename the generated hugo.yaml to hugo.yml (Hugo accepts both) and replace its contents with the following. Existing sites should merge the relevant settings instead:

    hugo.yml
    title: Product Docs
    baseURL: https://docs.example.com/
    defaultContentLanguage: en
    # enableGitInfo: true        # the "last modified" time comes from git; make the first Git commit before enabling
    
    languages:
      en:
        label: English
        locale: en-US
        weight: 1
        title: Product Docs
        params:
          description: Everything about running Product in production
        menus:
          main:
            - { name: Docs, pageRef: /docs, weight: 20 }
            - { name: Blog, pageRef: /blog, weight: 50 }
    
    # The three Goldmark prerequisites: OINK's native Markdown components depend on them
    markup:
      goldmark:
        renderer:
          unsafe: true # allow inline HTML in content
        parser:
          attribute:
            block: true # attribute lines such as {.steps} {.cards} {caption=}
          wrapStandAloneImageWithinParagraph: false # only a block-level image can carry an attribute line
      highlight:
        noClasses: false # code colours follow light and dark mode
    
    params:
      offline_search: true
      github_repo: https://github.com/example/product-docs
      copyright:
        authors: '[Example Inc.](https://example.com/)'
        from_year: 2026
      ui:
        dark_mode: true
        sidebar_menu_foldable: true
        section_index: cards
    
    outputs:
      home: [HTML, markdown, LLMS]
      page: [HTML, markdown]
      section: [HTML, RSS, print, markdown]
    
    module:
      imports:
        - path: github.com/pgsty/oink
      hugoVersion:
        extended: true
        min: '0.168.0'

    These five blocks control different parts of the site:

    Block Governs Consequence of omitting it
    Top level + languages Site name, domain, languages and navbar menu An incorrect baseURL makes every absolute production link incorrect
    markup.goldmark The three component prerequisites An attribute line becomes a literal {.steps} in the prose
    params Search, repository links, shell switches Interactive features stay off. The theme does not select them for the site
    outputs The per-page .md, llms.txt and print pages No “Copy as Markdown” in the page menu, and no print view
    module References the theme and declares the Hugo floor The build cannot find the theme

    Mathematics additionally needs Goldmark’s passthrough extension; see Math. For each key’s meaning and default, see Configuration.

  3. Write the first page

    Every top-level directory under content/ is a section. The directory structure determines the sidebar structure. A documentation section needs at least an _index.md:

    content/docs/_index.md
    ---
    title: Docs
    linkTitle: Docs
    description: Everything about running Product in production.
    weight: 20
    ---
    
    Start with [Install](/docs/install/).
    content/docs/install.md
    ---
    title: Install
    description: Install Product on a fresh machine.
    weight: 10
    ---
    
    ## Prerequisites {#prerequisites}
    
    > [!IMPORTANT]
    > Product needs PostgreSQL 18 or newer.
    
    ## Install {#install}
    
    ```bash
    curl -fsSL https://get.example.com | bash
    ```

    Write explicit {#id} anchors on headings. When you add a translation, the anchors in both languages must correspond. For page-writing instructions, see Writing pages.

  4. Preview

    hugo server

    Open http://localhost:1313/docs/. The Docs section lists Install. The home page is still empty until you add home content. Edit the Install page and confirm that the preview updates.

Other install methods

The steps above use a Hugo Module. The other three address specific constraints. These include network isolation, platforms that require the full theme tree as build input, and organizations that review their own theme copy. Apart from hugo mod vendor, none of them creates a Go module, and the site references the theme with theme: oink rather than module.imports. For these methods, you must manage version resolution and integrity checks.

Hugo Module (recommended)

hugo mod init github.com/example/product-docs
hugo mod get github.com/pgsty/[email protected]
hugo.yml
module:
  imports:
    - path: github.com/pgsty/oink

This is the only method in which Hugo resolves the version, verifies the checksum, and records it in go.sum. hugo mod graph shows the resolved version. hugo mod get -u upgrades it. This method requires Go on the machine.

Git submodule

Record an exact theme commit in the site repository:

git submodule add https://github.com/pgsty/oink.git themes/oink
git -C themes/oink fetch --tags
git -C themes/oink checkout v1.5.1
git add .gitmodules themes/oink
hugo.yml
theme: oink

Before CI runs Hugo, it must initialize the submodule. Otherwise, themes/oink is an empty directory:

git submodule update --init --recursive

Offline archive

For network-isolated environments, choose one of two paths. Prepare either archive on a connected machine. Transfer the complete archive.

With hugo mod vendor, the resolved theme source is frozen into the site directory, and later builds need neither the network nor Go.

hugo mod vendor          # writes _vendor/, holding the theme's full source tree
tar czf ../my-docs.tgz . # put the archive outside the directory being archived

If _vendor/ exists, Hugo prefers it. hugo mod graph prints +vendor. Keep module.imports in hugo.yml unchanged. This step requires Go. Later builds do not. Upgrading the theme means returning to a connected environment and running hugo mod get and hugo mod vendor again.

_vendor/ collects only the directories the theme mounts (assets, data, i18n, layouts, static) plus hugo.yaml and theme.toml. It does not include LICENSE, NOTICE or VENDOR.json. To redistribute that archive, take those three files from the theme repository as well.

With a tag source archive, no Go module is created. Unpack the selected theme version into themes/oink/.

curl -L -o oink.tar.gz \
  https://github.com/pgsty/oink/archive/refs/tags/v1.5.1.tar.gz
mkdir -p themes/oink
tar xzf oink.tar.gz -C themes/oink --strip-components=1
hugo.yml
theme: oink

The theme repository’s root is the module root. Unpacking places layouts/, assets/, i18n/, and static/ directly under the target. There is no additional directory level. Redistribution must keep LICENSE, NOTICE, and VENDOR.json. The manifest records each third-party runtime’s version, source, license path, and SHA-256. Use it for an offline audit.

When moving between machines, generate the archive and its checksum from an immutable tag on the connected side:

git clone --branch v1.5.1 --depth 1 \
  https://github.com/pgsty/oink.git oink
git -C oink archive --format=tar.gz --prefix=oink/ \
  --output=../oink-v1.5.1.tar.gz v1.5.1
shasum -a 256 oink-v1.5.1.tar.gz \
  > oink-v1.5.1.tar.gz.sha256

Carry the archive and its .sha256 into the isolated environment, verify the checksum, and unpack:

shasum -a 256 -c oink-v1.5.1.tar.gz.sha256
mkdir -p themes
tar -xzf oink-v1.5.1.tar.gz -C themes

Before building offline, confirm that all eleven top-level entries are present:

themes/oink/

  • oink/
    • go.modmodule path declaration, used when resolving as a Hugo Module
    • hugo.yamltheme default parameters and the Hugo version floor
    • theme.tomltheme metadata, required by the theme: oink method
    • LICENSEApache-2.0
    • NOTICEupstream attribution; must be kept on redistribution
    • VENDOR.jsonthird-party runtime manifest: version, source, licence path, SHA-256
    • assets/SCSS, JS and the third-party runtimes shipped with the theme
    • layouts/templates, partials, shortcodes, render hooks
    • static/font files, published as is
    • i18n/32 interface language files
    • data/the SPDX licence table behind the page-end attribution line

Pinned source copy

When a hosting platform needs the theme files in the site repository, use the tag archive procedure above and unpack it into themes/oink/. Set theme: oink and commit the extracted files together with the tag and checksum you verified.

A plain git clone ... themes/oink leaves a nested .git directory. Adding it to the parent repository records a Git link rather than the theme files. This does not provide a self-contained source copy. Use a submodule if you want Git to track the theme by reference.

The four methods compared

Method Needs Go Version auditable Theme source in your repository Use when
Hugo Module Yes go.sum verifies automatically No The default
Git submodule No The repository records the commit By reference The theme source has to be in the repository
Offline archive No Checksums verified by hand Yes Network isolation
Pinned source copy No Record the tag and checksum Yes The platform requires a complete tree
A consuming site needs no front-end toolchain

Bootstrap, Font Awesome, the fonts, and the search and diagram runtimes all ship with the theme. A site needs no node_modules, no PostCSS, no RTLCSS and no CDN. Tutorials that install npm dependencies for a Docsy site describe upstream Docsy’s process and do not apply to OINK.

Developing against a local theme checkout

This section applies only when changing the theme and the site together. Clone the two repositories as siblings:

sibling directory layout
~/pgsty/
├── oink/            # the theme
└── product-docs/    # your site

Use the HUGO_MODULE_REPLACEMENTS environment variable to substitute the local checkout temporarily, leaving go.mod untouched:

cd ~/pgsty/product-docs
HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> ../oink' hugo server

The documentation site’s Makefile runs these commands. make dev and make check expect the theme checkout at the sibling ../oink:

Makefile: as the documentation site writes it
build:
	hugo --cleanDestinationDir --minify

check:
	HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> $(abspath ../oink)' npm test

dev:
	HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> $(abspath ../oink)' hugo server --renderToMemory

A Go workspace (go work init plus HUGO_MODULE_WORKSPACE=go.work) is an equivalent alternative. Both apply only to the local machine. CI and production builds use the version in go.mod. Never commit go.work.

Verify

hugo mod graph                                       # which theme version actually resolved
hugo --gc --minify --printPathWarnings

It passes when the build ends with Total in … and no unexpected WARN or ERROR (the upstream LibSass deprecation warning is expected; keep warnings visible for review and do not enable --panicOnWarning or HUGO_PANICONWARNING). Then confirm:

  • /docs/ opens and the sidebar holds the page you wrote
  • The navbar has a search box that finds the heading you just wrote
  • The light/dark toggle is present. Code block colors change with it, which confirms markup.highlight.noClasses: false took effect
  • git status --short lists only source changes. Git ignores generated output. For the Module path, commit both go.mod and go.sum. Other install methods keep their own theme source or submodule record.

3 - Authoring

Write documentation pages, blog posts, books, release pages, and API references. Learn the page structure and content organization.

This section covers all content types supported by OINK: documentation pages, blog posts, books, Expo image albums, release and download pages, and OpenAPI references. They share a single Markdown dialect and front matter schema, while each content type adds its own layout conventions.

What a documentation page is made of

Every documentation page is a single Markdown file. YAML front matter between the opening --- lines sets the page metadata (title, linkTitle, description, and weight), followed by standard Markdown and OINK’s native components:

content/docs/install.md
---
title: Install Pigsty
linkTitle: Install
description: Get a working PostgreSQL cluster onto a clean EL 9 machine.
weight: 20
---

## Prerequisites {#prerequisites}

A Linux machine you can reach over SSH, passwordless `sudo`, and Python 3.11 or
newer.

> [!IMPORTANT]
> The installer rewrites `/etc/yum.repos.d/`. Back it up first.

Save the file as content/docs/install.md and run hugo server. The page is served at /docs/install/ with an Install entry in the left sidebar.

Common Markdown patterns

Most everyday components use standard Markdown plus a Goldmark attribute block rather than custom shortcodes:

Pattern Syntax Guide
Explicit heading anchor ## Prerequisites {#prerequisites} Writing pages
Callout / admonition > [!NOTE] (TIP, IMPORTANT, WARNING, CAUTION, DETAILS) Callouts
Code tabs Adjacent code fences with ```bash {tab="macOS"} Tabs
Numbered steps An ordered list followed by {.steps} on the next line Steps
Navigation cards A bullet list of links followed by {.cards} on the next line Cards
Parameter / field table A Markdown table followed by {.fields} on the next line Fields
Captioned or sized image ![alt](/img.webp) followed by {width="800" caption="..."} Images
File tree A ```filetree fenced block using a YAML-like list FileTree

Content types and where they are covered

What you are writing Where to go
A documentation page: front matter, heading anchors, links, images, drafts Writing pages
The tree and the sidebar: _index.md, weight, icons, folding, multiple sidebar roots Organizing content
Looking up what a front matter key means Page parameters
A blog post, a release announcement, RSS Blog posts
A book: chapter numbering, figures and tables, cross-references, whole-book print Books
An image album: file structure, metadata, layouts, and the image viewer Expo albums
A release and download page: version cards, asset tables, checksums Releases and downloads
An OpenAPI reference page API reference pages
Writing in two languages: paired files, aligned anchors, fallback for untranslated pages Languages
A component’s syntax and parameters Components

3.1 - Writing pages

Create a documentation page. Learn where to save it, how to set front matter and anchors, and how links, images, and page-end controls work.

This page covers the complete process for writing a documentation page. It explains file location, front matter, heading anchors, links, images, drafts, and page-end controls. The site must already build locally. If it does not, start with Quick start.

Creating a page

A page is a Markdown file under content/. Its location determines its URL. For example, content/docs/install.md is published as /docs/install/. The Chinese translation is a .zh.md file of the same name in the same directory, sharing one logical path with the English page.

Use a single file for a page without attached resources. If a page has images, cast files, or example configuration, create a directory. Name the page file index.md and put the resources beside it. Hugo calls this a page bundle:

the two page shapes inside content/

  • content/
    • docs/
      • _index.mdsection index, English
      • _index.zh.mdsection index, Chinese
      • install.mdsingle-file page → /docs/install/
      • install.zh.mdits Chinese translation
      • anatomy/page bundle → /docs/anatomy/
        • index.md
        • index.zh.md
        • shell.webppage resource, shared by both languages

hugo new content docs/install.md generates an empty file with front matter from an archetype. See the Hugo documentation. You can also write the file manually.

Important

When a Chinese page has no English counterpart, Hugo does not give it resources without a language suffix. In that case the resource filename needs the .zh. infix (shell.zh.webp) while the body still writes shell.webp.

The front matter you need

Between the two --- lines at the top of the file is YAML front matter. Four keys belong on every page:

content/docs/install.md
---
title: Install Pigsty       # page heading, browser title, search result title
linkTitle: Install          # short name in the sidebar and breadcrumbs; falls back to title
description: Get a working PostgreSQL cluster onto a clean EL 9 machine.
weight: 20                  # ordering among siblings; use multiples of 10 to leave room
---

Use description to state in one sentence what the reader can accomplish. It appears on section index cards, in search results, and on social cards. weight determines sidebar order. Only equal weights use alphabetical order.

Other keys are optional. These include the icon, draft status, search weight, comment switch, and page shell. For the full table, see Page parameters.

Heading levels and stable anchors

Start body sections at ##. Reserve # for title. The theme renders the page heading. Another # in the body creates a second top-level heading. The right column outline starts at ##. Hugo’s markup.tableOfContents determines its maximum depth. This site uses ####.

Write an explicit English anchor {#id} on every ## and ###:

Source
## Prerequisites {#prerequisites}

### Disk and memory {#disk-and-memory}

There are two reasons:

  • Cross-language alignment. Hugo derives an ID from heading text. A Chinese heading produces a Chinese ID. /docs/install/#prerequisites and /zh/docs/install/#前提条件 identify equivalent places with different anchors. A translation audit cannot compare them. Give the translated heading the English page’s ID. Both pages then share one fragment.
  • Link stability. Revisions can change heading text. They should not break public links. After publication, an explicit ID is a public route. If you must rename it, leave an empty anchor for the old ID:
Source: leaving a target behind for the old anchor
## Getting started <a id="get-started"></a> {#quickstart}

Use lowercase English with hyphens, unique within the page. This site’s translation audit compares the heading IDs rendered by the English and Chinese pages and fails on a mismatch.

Use one of these three forms, according to its purpose:

Form Example When to use it
Absolute site path [Configuration](/docs/customize/config/) The default. It points at a published route, is easy to audit and replace site-wide, and survives source files moving
Relative path [another page](../organize/), ![diagram](shell.webp) Resources inside the same page bundle, or a neighboring page that should follow the source directory
The ref / relref shortcode [Configuration]({{</* ref "/docs/configure/overview" */>}}) If the target must exist at build time. A missing target fails the build instead of leaving a broken link

All three carry a trailing slash and point at directory-style routes (/docs/write/pages/), matching Hugo’s default permalinks.

The theme has no link render hook. It passes links to Goldmark unchanged. External links do not automatically receive target="_blank". If you need a new tab, write HTML or use the site’s layouts/_markup/render-link.html.

Plain Markdown links are not checked for target existence. Use these rules:

  • Prefer absolute paths for internal links. After restructuring the site, use grep to replace them site-wide.
  • When you move a page, add aliases for the old path. Update internal links to the new route. Do not use an alias for navigation indefinitely.
  • If you are unsure that a target exists, use ref. The build then checks it.

In a bilingual site, link to the logical page (/docs/write/pages/) rather than to a .zh.md filename, and keep fragment IDs language-neutral.

Where images go

Put a page’s screenshots in its page bundle. Put shared images in assets/images/. Put large files that require no processing in static/. Use ![alt text](source) for all three. An attribute line controls caption, size, zoom, and numbering. See Images.

Drafts and publishing

A page with draft: true never reaches the build output:

front matter
---
title: Migration guide, not yet final
draft: true
---

Preview with hugo server -D to show drafts (-D is --buildDrafts). A page whose date is in the future is also excluded. -F shows those pages. A production build uses neither switch, and plain hugo publishes only finished content.

OINK’s Markdown extensions

The body is standard Markdown (Goldmark) plus the native forms below. Each is ordinary Markdown syntax with one attribute line, and each stays readable as source on GitHub:

Component Shortest syntax Page
Callouts > [!NOTE] on the first line of a blockquote Callouts
Tabs Two adjacent fences each carrying {tab="Homebrew"} Tabs
Steps An ordered list followed by a {.steps} line Steps
Cards A list of links followed by a {.cards} line Cards
Field lists A table followed by {.fields meta="type default"} Fields
Table extras A table followed by {.matrix} or {caption="…"} Tables
Code blocks {title="hugo.yml" copy=false} on the fence info line Code Blocks
Images A standalone image followed by {caption="…" width="600"} Images
File trees A filetree fence, one - name/ # comment per line FileTree
Mathematics A math fence, or display math wrapped in $$ Math
Diagrams A mermaid fence (also plantuml, markmap, echarts) Mermaid

The remaining components use shortcodes. These include badges, keys, file includes, terminal recordings, and the Book figure and table family. For syntax and parameters, see Components.

A combined example: code fences and a callout inside steps.

Source
1. Install Hugo Extended, 0.168.0 at the oldest:
   ```bash
   brew install hugo
   ```
1. Clone OINK Starter and preview it:
   ```bash
   git clone https://github.com/pgsty/oink-starter my-docs
   cd my-docs && hugo server
   ```
   > [!TIP]
   > Add `-D` to preview drafts as well.
{.steps}
  1. Install Hugo Extended, 0.168.0 at the oldest:
    brew install hugo
  2. Clone OINK Starter and preview it:
    git clone https://github.com/pgsty/oink-starter my-docs
    cd my-docs && hugo server
    Tip

    Add -D to preview drafts as well.

What appears at the end of a page

The theme generates these four blocks in a fixed order. Do not write them in the body. An optional Share bar appears before them:

Position What it is Default Where to configure
1 Feedback: the two “Was this page helpful?” buttons Off Repository links and page info
2 Last modified: the time and the most recent commit subject, linked to GitHub On when Git information is available Repository links and page info
3 Pager: previous and next, in sidebar tree order On for docs / book / blog Navigation and menus
4 Comments: giscus When configured and enabled Comments

The theme also generates the action menu beside the title. It includes copy Markdown, edit this page, view history, open an issue, and print. Configure it in Repository links and page info.

To turn off a control for one page, set its front matter: feedback: false, annotation: false, pager: false, comments: false. The keys are described in Page parameters.

Verify

After writing a page, run a production build:

hugo --printPathWarnings
  • The build must exit with Total in … and no ERROR. An invalid attribute key or component parameter logs a warning with a source position and renders a safe fallback, whereas a ref pointing to a missing target fails the build.
  • --printPathWarnings flags any two pages that resolve to the same output path, which is most common in multilingual sites or after editing permalinks.

Then confirm three things in the browser:

  1. The page appears in the sidebar at the position determined by weight.
  2. The right-hand outline lists your ## headings. Clicking a heading adds an English anchor to the URL.
  3. The English and Chinese versions of the same heading share an anchor (this site audits that with node scripts/check-doc-translations.mjs --public public).

3.2 - Organizing content

Use the directory structure to create the sidebar tree. Configure _index.md, weight, index styles, icons, folding, hidden pages, and documentation paths.

OINK requires no separate navigation configuration. The directory structure under content/ creates the sidebar tree. This page covers how directories and files are arranged, section indexes, ordering, icons, folding, hiding, and multiple sidebar roots.

Directories are the sidebar

Hugo calls a directory a section. Its Markdown files are pages. A nested directory is a subsection. The sidebar renders this tree level by level. weight determines order. linkTitle supplies labels, with title as fallback. The tree on the left comes from this source:

the first two levels of content/docs/

  • content/
    • docs/
      • _index.mdsection root: type: docs + cascade
      • about/Introduction
        • _index.md
        • features.md
      • start/Get started
        • _index.md
      • write/Authoring (this section)
        • _index.mdweight: 30
        • pages.mdweight: 10
        • organize.mdweight: 20
        • frontmatter.mdweight: 30
      • components/Components
        • _index.md

Every directory needs an _index.md

A section index is the _index.md inside the directory (_index.zh.md for Chinese). Without this file, Hugo still creates the section. It has no title, description, icon, or weight. The sidebar shows the directory name. You cannot control its order.

content/docs/deploy/_index.md
---
title: Deploy
linkTitle: Deploy
description: Publish the site to GitHub Pages, Cloudflare Pages or your own Nginx.
weight: 50
icon: fa-solid fa-cloud-arrow-up
---

A section _index.md also supports cascade. It applies shared settings to the whole subtree. You do not need to repeat them on each page.

content/docs/reference/_index.md
---
title: Reference
weight: 90
cascade:
  pager: false        # no previous / next on any page in this subtree
  search_boost: 0.8   # reference pages rank slightly lower in search
---

Ordering: use multiples of 10 for weight

Pages in a section use ascending weight order. Only equal weights use date and linkTitle. Always use multiples of 10, such as 10, 20, and 30. You can then insert a page between others without changing their weights. A section’s own weight decides its position among its siblings.

A page with no weight counts as 0, and Hugo places those after every page that does have one, ordered among themselves by date and title. That order changes as content changes. Give every page a weight.

Single file or page bundle

A page with no resources of its own is a single slug.md. For a page with images, cast files, or example files, use a directory. Put index.md and the resources in that directory. The two shapes look identical in the sidebar and produce the same URL. See Writing pages.

List or cards on a section index

After the body of an _index.md, the theme appends an index of the child pages in one of two styles:

hugo.yml: the site-wide default
params:
  ui:
    section_index: cards # list | cards

list is the theme default. It shows one line per child page with its title and description. cards is a grid of link cards reading each child’s icon, linkTitle and description. This site uses cards, and this section’s index page is the example. Override it in a single section’s front matter when that section needs the other style:

content/docs/reference/_index.md
section_index: list
cascade:
  section_index: list   # and its descendant sections too

Two page-level switches work independently of the style. simple_list: true renders a compact bulleted list. no_list: true generates no index. Use the latter when the page body supplies its own navigation.

Tip

In the card style, description is the card body. Keep it to one sentence that fits on a single line.

Sidebar icons

Write one Font Awesome class pair in a page’s or section’s front matter:

content/docs/deploy/_index.md
icon: fa-solid fa-cloud-arrow-up

Icon density is a site-level policy, so that leaf pages do not all carry icons:

hugo.yml
params:
  ui:
    sidebar_icon_policy: groups # all | groups | none
Value Effect
all Every entry that declares an icon shows it (the compatibility default when unset)
groups Only the root and nodes with children show icons. Ordinary leaf pages do not
none No entry icons in the sidebar

For a new site, set groups explicitly. Groups then keep their identifying icons, and leaf pages omit icons. This site uses that setting. Only the six sections on the left have icons.

Expanding and folding

A section with children carries a fold arrow in the sidebar. OINK saves the whole sidebar’s collapse, width, and scroll state. To persist individual branch choices, add optional site code using the sidebar runtime API. By default, the path containing the current page is expanded. Other paths are collapsed. Blog-type sections are expanded by default.

content/docs/reference/_index.md
sidebar_expanded: true   # this section is always expanded by default

Site-level folding, compact mode, initial expansion depth, width and truncation are configured in Layouts and page types. The full key definitions are in Configuration.

Hiding from the sidebar

Front matter Effect
toc_hide: true The page is absent from the sidebar tree (it is still published, and links to it still work)
hide_summary: true The page is absent from the section index
sidebar_divider: true The entry stops being a link and becomes a group heading in the sidebar
manual_link: https://… The sidebar row points elsewhere. Pair it with manual_link_title and manual_link_target: _blank

toc_hide and hide_summary control two different entry points, so set both only when the page should appear in neither.

Groups without a landing page

Since OINK 1.1, a divider section keeps its children while its title has no link. This fixes the missing children in v1.0.0. Use this _index.md when the directory should only organize child pages:

---
title: Reference
sidebar_divider: true
build:
  render: never
---

The title is a group label. If sidebar folding is enabled, its button folds the children. The children remain in the pager, search, navigation JSON, and Print. Without JavaScript, the group stays expanded. Omit build to keep publishing the section page while still showing a non-link sidebar label. Leaf dividers keep their old separator appearance.

toc_hide hides a section’s entire subtree. no_list only removes the child list from a section’s body, and hide_summary removes an entry from its parent page. None of these is a replacement for a group-only section. build.render: link retains a permalink without publishing a page. For an unpublished group, prefer never. This prevents other navigation from creating a destination.

The shell follows type, not the path

The documentation shell (sidebar, table of contents, breadcrumbs, pager) does not depend on the directory name. It depends only on whether the page’s type is listed in params.ui.shell_types:

hugo.yml: the theme default
params:
  ui:
    shell_types: [docs, book, blog, swagger]

Documentation can therefore live at any path, with type assigned by a cascade. To put a handbook at content/handbook/, the section root reads:

content/handbook/_index.md
---
title: Operations handbook
type: docs
sidebar_root_for: self      # the sidebar tree roots here rather than falling back to /docs
cascade:
  type: docs                # the whole subtree uses the documentation shell
---
Important

When the documentation directory is not called docs, sidebar_root_for: self is needed alongside type: docs. Otherwise the sidebar looks for its root at params.ui.docs_section (default docs), and a reader under /handbook/ sees the /docs/ tree.

Multiple sidebar roots

By default, the current top-level section is the sidebar root. A row above the tree names that root. A large subtree can have its own root. For example, use a separate root for a versioned API reference or a self-contained handbook:

content/docs/api/v2/_index.md
---
title: API reference v2
sidebar_root_for: self   # self | children
---
Value Meaning
self The section’s index page and all its descendants take it as their sidebar root
children The index page stays in the parent tree. Only descendants use this root

The switcher above the root is site-wide. It lists every top-level section and every section that declares sidebar_root_for: self. With one entry, it shows a plain link. With two or more entries, it shows a dropdown. To keep a top-level section or nested self-root out of the global choices, write sidebar_root_menu: false in its _index.md. The current root remains as a location marker when browsing that section.

Below the switcher, the section index remains the tree’s first link. The switcher selects a tree. The root link opens a document. sidebar_root_link_self: false makes that row point at the parent section instead.

Verify

hugo --printPathWarnings

The build must complete with Total in … and no ERROR. --printPathWarnings flags any two pages that resolve to the same output path when restructuring directories.

Then confirm each of these in the browser:

  1. The sidebar order matches the weight values you wrote, and a new section appears where expected.
  2. The section index lists every child (a missing one comes from hide_summary or a missing _index.md).
  3. Breadcrumbs and the pager follow the same order as the sidebar, because the pager reads the same tree.
  4. The tree has the same shape after switching language (every _index.md needs a .zh.md counterpart).

When sidebar entries exceed params.ui.sidebar_menu_truncate, the build warns and says what to raise it to. Do not ignore that warning. Truncated entries never appear in the sidebar.

Controlling a branch from site code

The window.OinkSidebar API is available since OINK 1.1. v1.0.0 does not provide it. Load site code after the theme scripts, for example through layouts/_partials/hooks/body-end.html, and wait for ready before reading or restoring branch state. This example expands the first sidebar group:

const sidebar = window.OinkSidebar;
if (sidebar) {
  sidebar.ready.then(() => {
    const button = document.querySelector(
      '#td-shell-sidebar [data-td-shell-tree-toggle]'
    );
    if (!button) return;
    const id = button.getAttribute('aria-controls');
    sidebar.setExpanded(id, true);
  });
}

Use the button’s existing aria-controls value as the region ID. getState(id) returns {id, expanded} or null. A change sends one oink:sidebar-disclosure event on document, after the button, region and accessibility state agree. Its detail is {id, expanded, source}. Repeated writes of the same value send no event. Restoring through the API keeps the current page’s ancestor groups open, while a reader can still fold them.

The theme does not save individual branch preferences. A site that adds this should own the storage key’s language and navigation-version scope, tolerate unavailable storage, and ignore IDs absent from the current page. The sidebar contract defines the lifecycle.

3.3 - Page parameters

The full front matter table — active page keys and retained 1.x compatibility no-ops, grouped by sidebar, shell, search, output, page end, Book, Expo, landing and release pages.

This page lists all page-level parameters that OINK reads, including explicit 1.x compatibility no-ops. It omits keys that the theme reads only to warn about a rename or removal. Those keys are in Migration, and they are also kept out of the generated editor schema. Hugo’s own front matter fields (slug, url, build, sitemap, expiryDate and others) work as usual. For their meaning, see the Hugo documentation. Site parameters (params.* in hugo.yml) are in Configuration.

How to read the tables

Precedence, highest first:

  1. The page’s own front matter.
  2. The nearest cascade. If several cascade layers set the same key, the closest layer takes precedence.
  3. The site parameter in hugo.yml.

Keys whose Default column says “site value” fall back to the site parameter of the same name when unset.

Write page keys at the top level of front matter, without the site key’s ui. prefix. For example, the site’s params.ui.section_index becomes the page’s section_index. Do not nest these keys in a ui: block: the theme neither reads nor reports a ui: block there. If a setting has no effect, check its key name against this page.

content/docs/wide-reference.md
---
title: Compatibility matrix
weight: 40
page_width: wide
footer_style: slim
image_zoom: true
section_index: list
---

Inside a cascade, use the same key names one level deeper:

content/docs/reference/_index.md
cascade:
  pager: false
  section_index: list

An invalid value does not abort the build: the theme logs a warning naming the key, supplied value, and fallback, then renders the page with the default listed below so a single typo does not return HTTP 500 across hugo server.

When continuing with a default would publish misleading metadata—such as an incomplete upstream attribution—the theme logs a warning and omits the block entirely. Unresolved content references (ref / relref) still fail the build through Hugo.

Basics

title , string , default—
Page heading, browser title, search result title. Required on every page
linkTitle , string , defaulttitle
Short name in the sidebar, breadcrumbs, pager and cards
description , string , default—
One-sentence summary for section cards, search snippets, and meta description. Blog pages also show it as an introduction above the body
weight , integer , default0
Order among siblings. Use multiples of 10. 0 (unset) sorts after every page with a weight. See Organizing content
draft , boolean , defaultfalse
A draft never reaches build output. Use hugo server -D to preview it. See Writing pages
date , date , default—
Blog date and release-page sort key. A future date excludes the page by default
lastmod , date , defaultGit commit time
The page-end “last modified” time. No manual value is needed if the site enables enableGitInfo
aliases , string array , default—
Redirects an old path to this page. Use for page migration rather than everyday navigation
type , string , defaulttop-level directory name
Decides the template and the shell: docs, book, blog, swagger — see Organizing content
layout , string , default—
Picks a layout for one page: landing, releases
cascade , map , default—
Applies the keys below to the whole subtree

Sidebar and navigation

The guide is Organizing content.

icon , Font Awesome class pair , default—
Icon in the sidebar, section cards and search results, e.g. fa-solid fa-rocket
toc_hide , boolean , defaultfalse
Excludes this page and its subtree from sidebar navigation and the pager sequence, in both content-derived and explicit trees
hide_summary , boolean , defaultfalse
Absent from the section index
sidebar_divider , boolean , defaultfalse
A non-link group heading, excluded from pager destinations. The 1.1 implementation retains a section’s children. Use build.render: never for a group without its own page
sidebar_expanded , boolean , defaulttrue for blog sections, false otherwise
This section is expanded by default in the sidebar
sidebar_root_for , self / children , default—
Makes this section a sidebar tree root. self includes the section index. children covers descendants only. Any other value warns and is ignored
sidebar_root_link_self , boolean , defaulttrue
The root row links to itself. false links to the parent section instead. A non-boolean warns and uses true
sidebar_root_menu , boolean , defaulttrue
Includes a top-level section or nested self-root in global switcher choices. An excluded current root remains a location marker. The nested-root exclusion is fixed in 1.1
toc_root , boolean , defaultfalse
When the sidebar root is the site home, excludes this whole top-level section from the tree and the pager sequence
manual_link , URL , default—
The sidebar and section index row points elsewhere
manual_link_relref , content reference , default—
The same link, resolved with relref. A missing target fails the build
manual_link_title , string , defaulttitle
Hover title for the manual link
manual_link_target , string , default—
For example _blank. The theme adds noopener
no_list , boolean , defaultfalse
The section index generates no child list
simple_list , boolean , defaultfalse
The child index renders as a compact bulleted list
section_index , list / cards , defaultsite value (list)
Style of the child index. An invalid value warns and falls back
section_index_columns , integer , default2
Column count in the card style
notoc , boolean , defaultfalse
Hides the right-hand page outline
pager , boolean , defaultdecided by params.ui.pager_types
false turns off previous / next for this page. A non-boolean warns and is ignored
navbar_enabled , boolean , defaultsite value (true)
Whether this page renders the navbar
navbar_autohide , boolean , defaultsite value (false)
The navbar hides itself on pointer devices
breadcrumb , boolean , defaultshell default
Whether this page renders breadcrumbs. Docs/Book default on. Blog defaults off
theme_color , string , defaultsite value
#rgb/#rrggbb hex value for this page’s accent backgrounds. In a section root’s cascade, it gives the whole section an identity. See Brand and appearance
theme_color_dark , string , defaultderived
The dark-mode accent. If a page overrides theme_color under a cascade that sets this key, it inherits that dark value. Override both. theme_color: false opts the page out of an inherited section color entirely
page_context_menu , boolean , defaultsite value (true)
The page action menu on the title row (copy Markdown, edit this page, print, …)
page_context_menu.assistant_links , boolean , defaultsite value (false)
The ChatGPT / Claude link items, written page_context_menu: { assistant_links: false }. A page may only restrict the site policy. It may never enable these links alone

Page shell

Site-level defaults and what they do are in Layouts and page types.

page_width , normal / wide / full , defaultnormal
Width of the content column. An invalid value warns and falls back
reading_width , slim / normal / wide , defaultnormal
Reading width on Book pages. Applies only to type: book
footer_style , fat / slim / none , defaultsite value (fat)
Footer shape. An invalid value warns and falls back
body_class , string , default—
A class appended to <body> for the site’s own CSS
reading_time , boolean , defaultsite value
Whether this page shows a reading time. false hides it
sidebar_enabled , boolean , defaulttrue
Whether this page shows the left sidebar. false hides it
scroll_spy , boolean , defaultsite value
Silent 1.x compatibility no-op. The normal shell runtime always provides active-heading tracking
keyboard_nav , boolean , defaultsite value (true)
Single-key keyboard navigation — see Keyboard navigation. A non-boolean warns and falls back
lastmod_commit , subject / hash / none , defaultsubject
How the commit is shown after “last modified”. An invalid value warns and falls back
sidebar_expand_levels, sidebar_menu_compact, sidebar_menu_foldable, sidebar_item_overflow , as the site parameter , defaultsite value
Each page can override sidebar behavior. The values are in Configuration
sidebar_width_min, sidebar_width_max , positive integer , defaultsite value (220 / 480)
Per-page minimum and maximum for desktop sidebar resizing. A minimum above its maximum warns and restores the site pair
code_copy , boolean , defaultsite value (true)
Default copy control for code blocks on this page. An explicit fence copy= takes precedence
toc_style , fixed / flow , defaultsite value (fixed)
Fixed right-rail panel or a wider rail beginning in the content flow
toc_taxonomies , boolean , defaultsite value (true)
Whether taxonomy clouds join the right-rail outline
taxonomy_icons , map , defaultsite value
Per-taxonomy icon overrides for this page or section cascade

The guide is Search.

search_keywords , string or string array , default—
Extra search terms, including synonyms and other languages
search_boost , positive number , default1.0
Ranking multiplier. The final score is the text match score times this value. A non-numeric, non-finite, zero or negative value warns and falls back to 1.0
search_exclude , boolean , defaultfalse
Keeps the page out of the local index

Output formats

The guides are AI-agent support (.md and llms.txt) and Print.

outputs , string array , defaultsite outputs
The output formats this page generates. [HTML] disables the .md version
no_print , boolean , defaultfalse
Excluded from the whole-chapter and whole-book print aggregate

Page end: comments, feedback and provenance

The order is fixed: feedback → provenance → pager → comments. See Writing pages.

comments , boolean , defaultsite params.comments.enable (false)
Whether this page shows the giscus comment section — see Comments
feedback , boolean or map , defaultsite params.ui.feedback (off)
The map form takes enable and reasons. Anything else warns and falls back
annotation , boolean , defaultsite params.ui.annotation (on)
The “last modified / provenance” block at the page end. Only a boolean is accepted. Other values warn and fall back
backlinks , boolean , defaultsite params.ui.backlinks (off)
Whether the right rail shows the “Backlinks” group beside the table of contents. A section can cascade it. Only a boolean is accepted; anything else warns and falls back — see Navigation and menus
translation_notice , language code or false , defaultsite params.ui.translation_notice (off)
The language code of the authoritative version, so a translation can say so and link back. Write false on a page authored natively in this language

Upstream attribution

When a page is derived from material elsewhere, upstream_link declares the source and the page-end provenance line gives the work, the copyright holder, the license, and a link to the full notice. This family resolves site parameters → the data/upstreams entry named by upstream_source → this page’s front matter. The most specific declaration takes precedence.

upstream_link is read from front matter only (a cascade counts, site parameters do not). A site-wide value would make every page claim the same source. A companion key without upstream_link warns and the attribution is omitted.

upstream_link , URL , default—
The address of the material this page is derived from. An empty string opts out of an inherited cascade value
upstream_name , string , default—
The upstream work, as the attribution names it. Required once upstream_link is set
upstream_copyright , string , default—
The copyright notice, retained as upstream wrote it. Required
upstream_license , SPDX identifier , default—
Must be found in data/licenses, or it warns and the attribution is omitted. Required
upstream_notice , site path or URL , default—
The page carrying the full notice (licence text, warranty disclaimer, upstream NOTICE, snapshot pin). Required
upstream_ref , string , default—
The tag or commit the snapshot pins, shown in parentheses after the work
upstream_source , string , defaultsite parameter
The entry name in data/upstreams, for upstream facts shared by many pages. A missing entry warns and omits the attribution
upstream_modified , boolean , defaultfalse
Changes the credit verb to say the work was adapted. If the site has repository information, the same attribution sentence also includes a “view history” link. A non-boolean warns and the page is treated as unmodified

Missing any one of the four required keys (upstream_name, upstream_copyright, upstream_license, upstream_notice) warns and omits the attribution. A partial attribution can mislead readers more than an obvious omission. The theme ships an SPDX table at data/licenses.yaml, and a site adds to or overrides it with a file of the same name.

Image zoom

image_zoom , boolean , defaultsite value (false)
Whether images on this page open full size — see Images. A non-boolean warns and falls back

Blog posts

The guide is Blog posts.

See List layouts for the four presentation choices. gallery and blog_gallery are available in OINK v1.4.0; the supported layouts are list, cards, table, and gallery.

author , string , default—
Post byline. Inline Markdown is allowed. Ignored on a page that has authors
authors , string array , default—
Terms of the authors taxonomy, in byline order — see Authors and bylines. Needs author: authors under taxonomies:
series , string array , default—
Terms of the series taxonomy. The strip above the body uses the first one — see Series
series_weight , integer , default—
Place in the series. Weighted members come first in ascending order, the rest follow by ascending date
tags , string array , default—
Tags — see Taxonomies
categories , string array , default—
Categories, likewise
images , string array , default—
The first entry becomes the post’s featured image and share card. Put it in a section _index.md cascade for a section-wide default. images: [] opts the page out of an inherited cascade value. It does not suppress an image the page bundle already supplies under a featured, cover or thumbnail name
byline , string , default—
Credit shown with the resolved featured image when that image is rendered
featured_image , none / banner / wash / hero , defaultsite value (none)
How this article renders its own featured image. hero creates a full-width image shell. An invalid value warns and falls back
blog_index , list / cards / table / gallery , defaultsite value (list)
Blog section index layout. A standalone table with blog_index_toggle: false lists the whole section without pagination. An invalid value warns and falls back
blog_index_columns , positive integer , defaultsite value (3)
Card columns at wide breakpoints. Medium and narrow layouts retain their responsive limits
blog_index_size , positive integer , defaultsite value (12)
Posts per page for list, cards, gallery, and all four views when switching is enabled. A standalone table ignores it
blog_index_toggle , boolean , defaultsite value (false)
Publishes all four layouts and remembers the reader’s choice. When false, publishes only the configured layout and ignores saved layout choices
blog_gallery , map , defaultsite block
Replaces the whole site gallery block. Omitted fields use built-in defaults; invalid fields warn and fall back. Reader choices for mode and size take precedence
blog_gallery.mode , wall / cards / seamless , defaultwall within a page block
Justified rows with 6px gaps, masonry with visible captions, or rows without gaps. Independent of the outer blog_index layout
blog_gallery.size , integer 160–360 , default240 within a page block
Target gallery tile size in CSS pixels; independent of the posts-per-page setting
share , string array or false , defaultsite params.ui.share (empty)
The page-end share targets. Replaces any inherited list. false opts this page out. See Share. An unknown target warns and is dropped
summary , string , default—
Fallback excerpt for gallery entries and post rows on tag and category pages. description takes precedence

Book

The guide is Books. A whole book sets type: book through a section cascade.

book_number , string , default—
Chapter number, shown before the page title and the sidebar entry
book_status , draft , default—
Marks a draft chapter: flagged in the sidebar and contents, and left out of the indexes by default
sidebar_headings , false / true / integer 2–4 , defaultsite value (false)
Expands the h2–h4 branch under the current sidebar entry. Out of range warns and falls back
book_draft_banner , boolean , defaultsite value (false)
Adds a banner at the top of a draft chapter. A non-boolean warns and falls back

Expo albums

OINK 1.3.0 uses type: expo for album pages. See Expo albums for the authoring guide.

expo , map , defaultsite params.ui.expo
Replaces the site block; absent fields use the built-in defaults below
expo.images , string , defaultimages/*
Pattern for image resources in the page bundle
expo.mode , wall / cards / seamless , defaultwall
Initial mode when the reader has no stored choice
expo.size , integer , default240
Target size from 160 to 360 CSS pixels; a stored reader size takes precedence

Image titles, captions, credits, weights, explicit IDs, and separate originals use native Hugo resources metadata. See Image metadata for the fields and an example.

Landing

The guide is Home and landing pages. Any page with layout: landing uses the landing shell.

landing , string , default—
Data is taken from data/landing/<key>/<language>.yaml
sections , array , default—
Section definitions inlined in front matter, taking precedence over landing. A non-array value warns. No sections render

Release pages

The guide is Releases and downloads. A section with layout: releases ignores weight and sorts by release date and SemVer, newest first.

release_url , string , default—
One GitHub release URL, https://github.com/<owner>/<repo>/releases/tag/<tag>. The theme extracts the owner, project and tag and generates source archive links. The date comes from the page’s date. Download assets come from the release body’s checksums block (see Releases). Anything else warns and the release block is skipped

3.4 - Blog posts

Set up a blog section. Learn directory conventions, post front matter, featured images, the date-ordered list page, and RSS.

Write a blog post’s body like a documentation page. The shell differs. A post has a date, author, tags, and featured image. The list uses date order, newest first. The section has an RSS feed. This page covers creating the blog section, a post’s front matter, featured images, list layouts, pagination and feeds.

The blog directory

A blog is a section under content/, and type: blog gives it the blog shell. Subdirectories divide it by publisher and audience, with posts sitting flat inside. Year directories are unnecessary: the list orders posts by their date.

this site's content/blog/

  • content/
    • blog/
      • _index.mdtype: blog + cascade
      • _index.zh.md
      • oink/engineering notes and announcements
        • _index.mdcascade: images: [/images/oink.webp]
        • oink-announcement.md
        • oink-announcement.zh.md
      • release/versioned release notes
        • _index.mdcascade: images: [/images/releasenote.webp]
        • 0.4.0.md
        • 0.4.0.zh.md

The section root applies the type to the whole subtree. It sets the section’s shared behavior:

content/blog/_index.md
---
title: Blog
description: OINK engineering notes and release announcements
type: blog
icon: fa-solid fa-blog
sidebar_root_for: self      # the blog has its own sidebar tree
cascade:
  type: blog
  feedback: false           # posts do not ask "was this page helpful?"
  comments: true            # but they do take comments
---

params.ui.blog_section (default blog) names where the blog root is. Rename the directory and either change that parameter or use sidebar_root_for: self as above.

Blog sections are expanded by default in the sidebar and ordered by date, newest first. To place one post at the top, give it a weight.

A post’s front matter

content/blog/release/0.4.0.md
---
title: Oink 0.4.0 — scenario components for a complete release workflow
linkTitle: Oink v0.4.0        # short name in the sidebar and pager
date: 2026-08-14              # publication date; decides list order
lastmod: 2026-08-14
description: >-
  Oink 0.4.0 delivers sequential reading and release surfaces, reusable landing
  pages, book publishing with stable references, and a keyboard-first site shell.
author: The OINK maintainers
categories: [Release]
tags: [Oink, Release]
---

Where it differs from a documentation page:

  • date is required. It decides the post’s place in the list and its RSS timestamp. A future date excludes the post from the default build. Use hugo server -F to preview it.
  • description appears as an introduction above the body and as a search snippet. Write a sentence for the reader.
  • author accepts inline Markdown, so [Vonng](https://vonng.com) works. For more than one author, a portrait, or a profile page, use the authors taxonomy below. The two do not interfere. A post renders author if authors is absent.
  • The date display format comes from params.time_format_blog and can be set per language (this site uses Monday, January 02, 2006 in English and 2006年1月2日 in Chinese).

Bilingual posts are stored in pairs, keeping date, author, weight and aliases identical across the two. Titles, descriptions and tags are translated. Do not translate commit IDs, version numbers, commands, or URLs.

Each row on a list or tag page has a thumbnail on the left. The resolver uses the first match in this order:

  1. images in the post’s front matter, first entry;
  2. An image resource in the page bundle whose filename matches featured or feature, then cover or thumbnail (it is cropped to a thumbnail, and the resource’s own byline becomes its caption);
  3. An images value inherited from an ancestor section’s cascade, nearest first.

A section-wide default uses Hugo’s native cascade over the whole subtree. This site sets one for each of its two subsections:

content/blog/release/_index.md
cascade:
  images: [/images/releasenote.webp]

To clear an inherited image on one post, write images: [] in its front matter. To clear it for a whole subsection, put it in that level’s cascade. This does not suppress an image supplied by the page bundle. To hide the image on the article itself, use featured_image: none. List thumbnails and share cards use images separately. The site-level params.images feeds the share card only and is never rendered as a list thumbnail.

On the article itself

By default the resolved image appears on list rows and in the social card. Set params.ui.featured_image to use the same image on the article itself:

Mode What the article shows
none No article image. This is the theme default
banner The image above the title in a fixed 16:9 figure. Articles use a consistent layout
wash A faint image behind the article header, fading before the body
hero A full-width image header
hugo.yml
params:
  ui:
    featured_image: banner

The page key is featured_image. A subsection’s cascade enables it for that tree. An individual post can opt out. A post without an image shows none in any mode. A section can therefore enable it even if only some posts have images. These modes need no additional script.

content/blog/release/_index.md
cascade:
  featured_image: wash

List layouts and pagination

After the section _index.md body, the theme appends the post index, sorted by date, newest first, without year groups. Choose its layout with blog_index:

Value Layout What the reader sees
list Horizontal entries (default) Thumbnail, title, date, subsection, tags, and a summary
cards Regular card grid A cropped 16:9 cover above the title, metadata, and summary
table Compact table Date, title, and tags in one row per article
gallery Image gallery Proportional covers with titles and descriptions; click to open the article

OINK v1.4.0 includes list, cards, table, and gallery.

Set the site default with params.ui.blog_index in hugo.yml, or set blog_index in a blog section’s _index.md front matter to override it. Use a section’s cascade to apply it to descendant sections. These settings control native blog section indexes; term and taxonomy pages keep their own layouts, and custom homepages need their own template integration.

List, card, and gallery views show 12 posts per page by default. Set the theme’s blog_index_size in hugo.yml to change it:

hugo.yml
params:
  ui:
    blog_index_size: 20

The same key in a blog section’s front matter overrides the site value. The theme explicitly passes this size to Hugo’s paginator. pagination.pagerSize does not control these lists.

The card form

params.ui.blog_index: cards renders the same list as a grid of content cards instead of rows. Each card shows a 16:9 image crop above the title, a date and subsection line, and a three-line summary.

hugo.yml
params:
  ui:
    blog_index: cards
    blog_index_columns: 3

The column count applies above the xl breakpoint. Between md and xl, the grid has two columns. Below md, it has one. If a card image resource supports processing, Hugo applies .Fill, so readers download cropped previews.

Table and layout switching

A standalone blog_index: table with blog_index_toggle: false lists the entire section without pagination. Set blog_index_toggle: true to add the layout switch beside the title. In OINK 1.4.0, it cycles through list → cards → table → gallery → list:

content/blog/_index.md
blog_index: cards
blog_index_toggle: true

All layouts preserve date order and manual_link destinations. With switching enabled, all four views show the same current page of posts, using blog_index_size. Only the standalone table shows the entire section. The switch remembers the reader’s choice. Set blog_index_toggle: false to publish only the configured layout, regardless of a saved reader preference. Hidden views use lazy images, although their markup and generated thumbnails still contribute to build work. In v1.4.0, the switch cycles through all four layouts.

gallery makes each article a linked cover, using the existing images field or bundled featured image. The default wall joins images into rows while preserving their proportions. Hover or keyboard focus reveals the title and description; touch devices show them below the image. Clicking opens the article. Posts without a cover keep a title tile.

content/blog/_index.md
blog_index: gallery
blog_index_toggle: true
blog_gallery:
  mode: wall
  size: 240

The gallery toolbar offers three arrangements:

blog_gallery.mode Arrangement Captions
wall (default) Justified rows with 6px gaps On hover or keyboard focus; below images on touch devices
seamless The same rows without gaps On hover or keyboard focus; below images on touch devices
cards Masonry columns Always visible below images

blog_index: cards selects the regular card grid. blog_index: gallery with blog_gallery.mode: cards selects masonry inside the gallery. blog_gallery.size sets the target tile size, from 160 to 360 CSS pixels, with a default of 240. It is independent of blog_index_size, the number of posts per page. Readers can change the arrangement and size; their choices are remembered separately from Expo albums.

Site defaults use the same blog_gallery block under params.ui; a page or cascade block replaces it in full. Omitted fields use wall and 240; invalid values warn and use those defaults. These settings are available in v1.4.0. See the site parameters and page parameters for the reference tables.

RSS

outputs determines which pages produce a feed. Adding RSS to section gives every section its own feed:

hugo.yml
outputs:
  home: [HTML, markdown, LLMS]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]

Setting outputs replaces all Hugo defaults. Include RSS explicitly if you need feeds. If you omit it, that page kind has no feed. The build does not warn.

This site therefore has /blog/index.xml (the whole blog) and /blog/release/index.xml (release notes only). A section feed recursively includes every subsection’s posts. Subscribing to /blog/ covers the whole blog. An individual post has no .xml of its own.

Each language has its own feed at that language’s route plus index.xml. The item limit is Hugo’s services.rss.limit. On the blog root and its first-level subsection pages, the first action button beside the title row is the RSS link, so readers do not need to construct the address manually.

To drop feeds site-wide, turn the kind off with disableKinds, which is more thorough than removing RSS from each page kind:

hugo.yml
disableKinds: [RSS]

Components use their static form in a feed. Disclosures are expanded, and interactive controls are removed. The four-output rules are the same for blog posts as for documentation.

Categories and tags

tags and categories are Hugo’s taxonomies, and the theme renders them as chips in the post header, a tag cloud in the right column, and a filter menu in the navbar. Enabling them, bilingual term labels, and switching them per content type are covered in Taxonomies.

Release notes

A versioned release announcement is an ordinary post, conventionally under blog/release/, with the version in linkTitle (Oink v0.4.0). For a download page with release cards, asset tables and checksums, see Releases and downloads.

Components in a post

Callouts, tabs, code blocks, images and tables work exactly as on a documentation page. The syntax is in Components. Headings in a post body take explicit English {#id} anchors too.

The four blocks at the end of a post — feedback, last modified, pager, comments — behave as on a documentation page. See Writing pages. A blog usually turns feedback off and keeps comments.

Authors and bylines

Declare the taxonomy to enable this feature. The theme adds no parameter:

hugo.yml
taxonomies:
  category: categories
  tag: tags
  author: authors

A post then names its authors in order:

authors: [vonng, ada-example]

The article header renders portraits and linked names in that order. List rows show the names. The blog feed includes each author alongside the site-level managingEditor.

An author’s profile is the taxonomy term page. No data/authors file is needed:

content/authors/vonng/_index.md
---
title: Vonng
description: Maintainer of OINK and Pigsty.
images: [portrait.webp]
---

The long introduction, rendered on the profile page under the name.

The display name is the term page’s link title. It uses linkTitle if available, otherwise title. A profile can use a full name and the byline a short handle. description gives a one-line introduction. The body gives the long introduction. The featured-image resolver selects the avatar. images: and a bundled portrait follow the same rules as an article’s image. A bilingual profile is an _index.zh.md beside it. An author without a profile page still has a byline. It shows the link title, an initial, and an archive link.

The 0.4 author: string is untouched wherever authors is absent, and neither form warns about the other.

Series

A series is a reading path through articles that each stand alone. Numbering, cross-references, and aggregate output belong to Book. A series has less structure. Declare the taxonomy to enable it:

hugo.yml
taxonomies:
  series: series

An article names the series and may place itself in it:

series: [shell-internals]
series_weight: 20

The article then shows a strip above its body. It names the series, position, and next part. A <details> contains the full list. This requires no JavaScript or bundle member. The term page content/series/<name>/_index.md is the introduction, and an _index.zh.md beside it makes the pair bilingual.

Members with series_weight come first in ascending order. Other members follow from oldest to newest. The content path breaks ties. The series strip and term page use this same reading order. For implementation details, see Authors and series.

A member of several series shows one strip, for the first term it names. A series of one shows none.

Neither authors nor series appears in the generic taxonomy chip row on an article, because each has a surface of its own. Name one in params.taxonomy.page_header to put it back.

Share

params.ui.share puts a share bar at the top of the page end. It is empty by default, so nothing renders until a site names its targets, in the order it wants them:

hugo.yml
params:
  ui:
    share: [x, bluesky, mastodon, reddit, hackernews, email, copy]

Sixteen targets are available: x, bluesky, mastodon, facebook, linkedin, reddit, hackernews, telegram, whatsapp, line, pinterest, weibo, chatgpt, claude, email, and copy. An unknown name warns and is dropped. Discord is deliberately absent. It provides no share-intent URL. Use copy for Discord. The theme does not infer a private scheme.

The page key is share, so a cascade scopes the bar to one tree, a page’s own list replaces the inherited one, and share: false opts a single page out:

content/blog/_index.md
cascade:
  share: [x, bluesky, email, copy]

Only regular pages render the bar. Lists, term pages, and the home page have no single item to share. Print, Markdown, and RSS omit the bar.

Share targets are links carrying the page’s permalink and title, plus a local copy button. The bar loads no third-party scripts or stylesheets. It contacts a target only when the reader clicks its link. See the Share contract for the implementation rules.

chatgpt and claude hand that same build-time permalink to an assistant with a prompt asking it to read the page. They are not the “open in ChatGPT” / “open in Claude” entries of the page action menu, which the runtime rewrites at activation time to the live browser URL and which therefore stay behind page_context_menu.assistant_links.

The copy button is the built-in copy_link action, so the Command Palette includes it on every page of every site. This applies even if no bar is configured.

Verify

hugo --printPathWarnings

The build must complete with Total in … and no ERROR. Then confirm:

  1. The post appears in the right date order at /blog/, with the date in the expected format.
  2. public/blog/index.xml exists, contains the post, and its links are complete absolute addresses.
  3. The thumbnail shows in the list (a missing one means none of the three featured-image sources matched).
  4. Tag chips lead to the corresponding tag page.

3.5 - Books

Turn a directory tree into a book with type: book: chapter numbering, numbered figures and tables, cross-references, generated indexes and whole-book print.

A book is a content tree with type: book. The directory determines chapter order. Front matter determines chapter numbers. Each figure, table, equation, and example has a manually assigned number and stable anchor. Cross-references resolve in all four outputs. The book root can generate whole-book print HTML.

Two settings are required. Enable attribute lines and passthrough in the site’s markup.goldmark (see Components). Keep book in params.ui.shell_types. The theme default includes it.

For a new book, start with the directory below. For an existing manuscript, see Migrating an existing manuscript.

A book’s directory

The book root is an ordinary Hugo section. Chapters are its subdirectories. Sections are pages inside each chapter. There is no second chapter list. The sidebar, pager, and generated contents all read this tree.

content/handbook/, one book

  • content/handbook/
    • _index.mdbook home: type: book + cascade, holding book-toc and the indexes
    • ch01/
      • _index.mdchapter 1 front page: book_number: 1
      • install.mdsection 1.x
      • bootstrap.md
    • ch02/
      • _index.mdchapter 2: numbered with book_number, optionally marked draft
      • replication.md
      • failover.md
    • appendix.mdan unnumbered appendix, still in the sidebar and the reading order

Assign chapter numbers manually. book_number displays exactly what you write. The theme never numbers chapters by directory order. The num on a figure, table, equation, or example also uses an author-controlled string. 2-1, 5.3, and A-2 are valid. Rendering does not compute an index. Rearranging the tree never changes a number that has already been printed.

The book home and chapter pages

The book root declares the type, cascades it to descendants, and explicitly requests the print output. That aggregate is expensive to build, so the theme does not turn it on for a consuming site:

content/handbook/_index.md
---
title: The PostgreSQL operations handbook
type: book
book_number: B
cascade:
  type: book
outputs: [HTML, print, markdown]
---

A book section uses Hugo’s section output kind. home applies only when the book sits at the site root:

hugo.yml
outputs:
  section: [HTML, print, markdown]
params:
  ui:
    sidebar_headings: false # default; use 3 to add h2–h3 below the current entry
    book_draft_banner: true # draft chapters get a localized banner above the body

A chapter page needs only its number and its order:

content/handbook/ch02/_index.md
---
title: Replication and failover
book_number: 2
book_status: draft
weight: 20
---

book_number appears before the page title, in the sidebar and in the generated contents. book_status: draft is a visible editorial label and does not change Hugo’s publication state. A draft chapter builds and publishes as usual.

sidebar_headings defaults to false and accepts true (h2 only) or a maximum level from 2 to 4. A page or section cascade can override the site setting. The right outline remains available when this left branch is disabled. Give every heading that will be referenced an explicit ID, such as ## Synchronous replication {#sync-replication}. A generated slug works for navigation. Do not use it as a long-term reference target.

The full key definitions are in Configuration and Page parameters.

Numbering: the native form

Each of the four numbered kinds has a native form. Put an attribute line immediately after one Markdown block. Use num= for the number, #id for the anchor, and caption= for a plain-text caption.

Figures

An attribute line follows the image block. Omitting #id defaults it to fig-<num>.

Source
![The OINK release notes page](/images/releasenote.webp)
{#book-release-note num="2-1" caption="The release notes page is also the single source of release facts." width=600 height=300}
The OINK release notes page
Figure 2-1 The release notes page is also the single source of release facts.

The native figure form requires the site to set markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false. Otherwise, the attribute line attaches to the paragraph and is ignored. The alternative text comes from the Markdown image itself and is never replaced by the caption.

Tables

An attribute line follows a pipe table, and the default ID is tbl-<num>.

Source
| Isolation level | Dirty read | Non-repeatable read | Phantom read |
| --- | --- | --- | --- |
| Read Committed | Not possible | Possible | Possible |
| Repeatable Read | Not possible | Not possible | Possible |
| Serializable | Not possible | Not possible | Not possible |
{#tbl-2-1 num="2-1" caption="Anomalies permitted at each PostgreSQL isolation level."}
Isolation level Dirty read Non-repeatable read Phantom read
Read Committed Not possible Possible Possible
Repeatable Read Not possible Not possible Possible
Serializable Not possible Not possible Not possible
Table 2-1 Anomalies permitted at each PostgreSQL isolation level.

Equations

An attribute line follows a $$ block, and the default ID is eq-<num>. The number and caption sit to the right of the formula on wider screens. On narrow screens, they move below it and wrap. A wide formula can scroll horizontally. Short captions keep the equation easy to scan.

Source
$$
A = \frac{\mathrm{MTBF}}{\mathrm{MTBF} + \mathrm{MTTR}}
$$
{#eq-2-1 num="2-1" caption="Availability from MTBF and MTTR."}
A=MTBFMTBF+MTTR A = \frac{\mathrm{MTBF}}{\mathrm{MTBF} + \mathrm{MTTR}}
Equation 2-1 Availability from MTBF and MTTR.

The native form depends on the site enabling Goldmark passthrough. Without it, use the eq shortcode below, which goes through local server-side KaTeX.

Examples

A code fence with num= and caption= is a numbered example, and the default ID is eg-<num>. An #id on the fence names the enclosing <figure>, which is the reference target. The caption is required: a caption without a number is ignored, and a number without a caption is dropped with a warning. A numbered example renders in one frame where the caption forms the header and the body sits inside; a single code block shares the frame border without adding a second border.

Source
```sql {num="2-1" caption="Daily write volume on the primary." #eg-2-1}
SELECT date_trunc('day', ts) AS day, count(*)
FROM pg_stat_statements_history
GROUP BY 1 ORDER BY 1 DESC LIMIT 7;
```
Example 2-1 Daily write volume on the primary.
SELECT date_trunc('day', ts) AS day, count(*)
FROM pg_stat_statements_history
GROUP BY 1 ORDER BY 1 DESC LIMIT 7;

Numbering: the shortcode form

The four shortcodes fig, tbl, eq and eg render a <figure> identical to the native form, register into the same target table, and sort by source position. Use them when the native form is insufficient. Examples include figures with several images or other Markdown, several tables under one number, sites without passthrough, and examples with several fences and prose. A single numbered image can use link in its native attribute line. It does not need fig just for an outbound link.

fig takes src= (it also accepts inner Markdown content, and the two are mutually exclusive) and additionally supports link, alt, width, height, class, and the migration alias title:

Source
{{< fig num="2-2" src="/images/docsy.webp" alt="The default Docsy shell"
    caption="OINK's upstream: the Docsy content model is still underneath." width="600" height="300" />}}
The default Docsy shell
Figure 2-2 OINK's upstream: the Docsy content model is still underneath.

tbl wraps the label, the table, the caption and the anchor in one semantic figure:

Source
{{< tbl num="2-2" caption="How a numbered component appears in each of the four outputs." >}}
| Output | Label | Anchor |
| --- | --- | --- |
| HTML | Visible | Stable |
| Print | Visible | Stable |
{{< /tbl >}}
Output Label Anchor
HTML Visible Stable
Print Visible Stable
Table 2-2 How a numbered component appears in each of the four outputs.

eq hands its content to local server-side KaTeX, so it does not depend on passthrough:

Source
{{< eq num="2-2" caption="Connection pool saturation." >}}U = \frac{\lambda}{\mu \cdot c}{{< /eq >}}
U=λμ⋅cU = \frac{\lambda}{\mu \cdot c}
Equation 2-2 Connection pool saturation.

Use a bare {{< eq >}} without parameters for unnumbered display math. It registers no target. xref cannot reference it. It does not appear in the equation index.

eg is a wrapping shortcode whose body renders under the page’s Markdown policy, usually holding one or more fences:

Source
{{< eg num="2-2" caption="Bringing up a new replica with pg_basebackup." >}}
```bash
pg_basebackup -h primary -U replicator -D /pg/data -Fp -Xs -P -R
```
{{< /eg >}}
Example 2-2 Bringing up a new replica with pg_basebackup.
pg_basebackup -h primary -U replicator -D /pg/data -Fp -Xs -P -R

IDs must be unique within a page, and within one kind a number maps to a single ID. A duplicate logs a warning with the first registration’s line number and keeps the first registration.

Footnotes cannot appear in a shortcode body

Hugo renders each shortcode body as a separate Goldmark document, whereas footnotes belong to the page. A [^label] inside the body of tbl, eg, fig, card, tab, field, or include logs a warning naming the file, line, and label: if the definition sits on the page, the reference would print literally as [^label]; if the definition sits inside the shortcode body, it would generate a second footnote list whose fn:N IDs collide with the page’s own footnotes.

If a table or code block needs footnotes, use the native form. A table, image, or fence with {num=… caption=…} keeps its content in the page document. Its footnotes then use normal numbering, links, and backlinks. The rendered figure is the same either way, so this is usually a one-line change. Text that resembles a footnote in code remains unchanged. This includes a [^0-9] character class in a listing or a code span.

Cross-references

A target on the same page can be reached with a plain Markdown link: Table 2-1 points at the isolation table above. You must write the label and number manually. If you change a number, you must find its references yourself.

xref combines the label, number, and anchor. It works across pages and languages:

Source
See {{< xref fig="2-2" />}} and {{< xref eg="2-1" />}};
with an explicit anchor: {{< xref fig="2-1" anchor="book-release-note" />}}.

See Figure 2-2 and Example 2-1; with an explicit anchor: Figure 2-1.

The rules:

  • At most one kind key (fig, tbl, eq, eg). The kind supplies the localized label (Figure / Table / Equation / Example) and derives the default anchor <kind>-<num>.
  • anchor= overrides the derived anchor, for a target that wrote an explicit #id.
  • page= references another page through Hugo’s page lookup in the current language, so the source never hard-codes a /zh/ prefix.
  • Without a kind, both anchor= and inner link text are required: {{< xref page="../ch01/install" anchor="sync-replication" >}}synchronous replication{{< /xref >}}.
  • A reference may precede its target. Nothing reads the registry at render time. Forward references are valid.

A plain cross-page Markdown link is still a site URL inside the whole-book print. A reference that must also jump within the aggregate document is written as an xref.

Indexes: contents and lists of figures

Five index shortcodes walk the same book tree, triggering descendant content and aggregating what it registered. They usually sit on the book home (_index.md) or on a dedicated “list of figures” page.

content/handbook/_index.md
{{< book-toc depth=3 >}}

## List of figures {#lof}
{{< book-figures >}}

## List of tables {#lot}
{{< book-tables >}}

## List of equations {#loe}
{{< book-equations >}}

## List of examples {#lox}
{{< book-examples >}}

These five appear here only as source. They start at the current page’s navigation root and process its descendants. In an ordinary documentation tree, one would list the whole docs tree as a book. For the real effect, read Write Beautiful Docs and inspect its content/book/_index.md source.

  • book-toc takes a depth of 1 to 3: 1 lists chapters, 2 adds nested sections, 3 also projects each page’s heading tree. drafts=false filters book_status: draft rows out of this generated list only, and does not affect publication.
  • book-figures, book-tables, book-equations and book-examples take no parameters. Each lists one kind, with entries like “Figure 2-1 — caption” linked to the stable ID.
  • In whole-book print, all of these links become in-document fragments.

Sequential reading and drafts

The pager is on by default for docs, book, and blog. It uses a pre-order walk of the sidebar tree. The section index comes first, followed by its children in weight order. Turn a whole type off with params.ui.pager_types, and a single page off with pager: false.

hugo.yml
params:
  ui:
    pager_types: [docs, book]

Entries hidden with toc_hide, manual_link link-only placeholders and sidebar_divider rows never become pager destinations.

Besides the “draft” label in the sidebar, a draft chapter can carry a banner above its body:

hugo.yml
params:
  ui:
    book_draft_banner: true

The banner appears only on pages that are both type: book and book_status: draft, and its wording comes from the localization key book_draft_notice.

Printing the whole book

Once the book root has the print output, it generates a cover, a local table of contents, the root page’s body and every descendant chapter in visible reading order, all inside one HTML document. Pages with no_print: true, link-only nodes, divider rows and hidden placeholders never become chapters.

Inside the aggregate, the IDs of numbered components are preserved byte for byte. Markdown heading and footnote IDs within a page are prefixed with their source page to avoid collisions when several chapters share an anchor such as summary or start with fn:1. Generated links are rewritten to match. A page rendered alone as Print keeps the same page-local IDs as ordinary HTML. Only a multi-page section or whole-Book aggregate adds the namespace.

The output is print-oriented HTML. An opt-in BookManifest output records that same reading order as JSON, and the theme ships bin/book-epub.py and bin/book-pdf.py, which turn the manifest and the print HTML into EPUB and PDF.

The switches themselves, and per-chapter print, are covered in Print.

Migrating an existing manuscript

An existing manuscript usually expresses figure and table numbering with the site’s own figure shortcode, bold pseudo-captions, and bare links to #fig_*. The theme repository ships a migration script that rewrites those legacy forms into fig, tbl and xref while preserving the public anchors already published. Pin the site to a released OINK version that includes the Book components first, then migrate the content.

Dry run: diff and report only, no files changed
python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme \
  --root /path/to/your-book \
  --report /tmp/book-migrate.json > /tmp/book-migrate.diff

Four profiles cover the legacy conventions of three real manuscripts (DDIA contributes one each for v1 and v2), and each recognizes only the forms actually observed in them:

--profile Legacy form it recognizes
tpme A pseudo-h6 caption beside an image, a caption beside a table, and bare /en/...#fragment links
ddia-v2 The site’s own figure shortcode, classified by number into figure / table / code example
ddia-v1 A bare image with an adjacent bold numbered caption, with the ID derived from the image filename
pg-internal A bold or italic “Figure N” caption in Chinese or English next to an image, and a numbered table caption next to a table
--profile
Required; one of the four values above
--root
Required; the consuming repository’s root
--path
Restricts to a file or directory under --root; repeatable. The default scans the whole content tree
--write
Applies the rewrite. The default is a dry run that writes nothing
--no-diff
Suppresses the diff while keeping the summary and the report
--report
Writes the machine-readable JSON report

The diff goes to standard output, the summary to standard error, and the report carries files_scanned, files_changed, counts, skipped and idempotent. The script rewrites only targets it can identify uniquely. If the number is unclear, the caption is not unique, or the marker form is unrecognized, it preserves the text. It records the item in skipped for human review. Bold text, inline code and formulas inside a legacy caption degrade to plain text, because a Book caption is plain text by contract.

After reviewing the diff, apply it on a dedicated branch and run a second pass to confirm idempotency:

Apply, then verify idempotency
python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme --root /path/to/your-book --write \
  --report /tmp/book-migrate-written.json

python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme --root /path/to/your-book --no-diff \
  --report /tmp/book-migrate-second.json

The second report should show files_changed: 0, empty counts, and idempotent: true. The script signals idempotency with exit code 0.

The profiles recognize only the legacy forms actually observed in those three manuscripts. Where a manuscript’s conventions fall outside the four, the script does not apply and the rewrite is manual, following Numbering: the native form. The theme repository’s bin/check-book-migrations.py covers all four profiles with a dry-run and an idempotency check.

Verify

  1. Build with visible diagnostics: hugo --printPathWarnings. A malformed number, a duplicate ID and a missing caption all produce warnings that need review.
  2. The page should show a localized label such as “Figure 2-1”, clickable xref links, and anchors that land correctly.
  3. Compare the chapter order across all four places: sidebar, pager, book-toc and whole-book print.
  4. Check the Markdown output: curl -s http://localhost:1313/handbook/ch02/index.md. The shortcode form should degrade to **Figure 2-2.** caption plus the original body, and the native form should keep its source block and attribute line as they are.
  5. Run the anchor check from the theme repository against the build output:
python3 ~/pgsty/oink/bin/check-book.py --site-public public

It verifies that every reference’s target anchor exists, that kind and number agree, that page-local IDs are unique, and that a numbered image has alternative text appropriate for its caption.

Book shortcode parameters

num , string , default—
Required (except for the bare eq form). Matches [0-9A-Za-z.-]+ and must be quoted
id , string , defaultfig-<num> / tbl-<num> / eq-<num> / eg-<num>
Matches [A-Za-z][A-Za-z0-9_.:-]* and is preserved byte for byte
caption , plain text , defaultempty
Required for eg. Optional for fig, tbl, and eq. Not Markdown
class , class token , default—
Appended to the <figure>. Requires num
src , image path , default—
fig only. Mutually exclusive with inner content, and follows the shared image resolution order
link alt width height , — , default—
fig only. Width and height are positive integers
title , plain text , default—
fig only. A migration alias for caption, mutually exclusive with it

xref:

fig tbl eq eg , number string , default—
At most one. Supplies the localized label and derives the anchor
anchor , ID , defaultderived from kind and number
Required when no kind is given, together with inner link text
page , page reference , defaultcurrent page
Resolved through page lookup in the current language. A missing page warns and renders text without a link

book-toc:

depth , integer 1–3 , default2
1 chapters / 2 with nested sections / 3 with the heading tree
drafts , boolean , defaulttrue
false filters draft chapters out of the generated list

book-figures, book-tables, book-equations and book-examples take no parameters.

Limits

  • There is no automatic numbering. Assign chapter, figure, and table numbers manually. Only an explicit edit changes a number. A build does not change it.
  • The attribute line must touch its block, with no blank line between. If a tool such as Prettier moves an attribute line, it fails silently. The figure becomes a plain image.
  • book_kind and book_part are metadata keys the contract acknowledges but the current templates do not render. The ones with a visible effect are book_number and book_status.
  • The index shortcodes trigger descendant content rendering, which noticeably lengthens the build on a very large tree. The same reason is why whole-book print has to be requested explicitly.
  • A footnote reference cannot appear inside a shortcode body; the theme logs a warning and points to the native form instead (see Numbering: the shortcode form).
  • Packaging requires explicit selection and runs outside the build. BookManifest plus bin/book-epub.py / bin/book-pdf.py produce EPUB and PDF. Hugo alone emits neither file. Typeset pagination, font embedding, and index compilation remain outside the contract.
  • Organizing content — how the tree becomes the sidebar and the reading order
  • Images — captions, sizing, zoom and image processing
  • Tables — table attribute lines and full-width tables
  • Math — KaTeX and passthrough configuration
  • Print — per-chapter and whole-book print

3.6 - Image albums with Expo

Create an image album, add optional metadata, select a layout, and use the image viewer.

Expo displays image collections as albums. A sidebar groups the albums. The main area shows the images in the selected album. Each image opens in a viewer.

Available in OINK 1.3.0

Expo is included in OINK v1.3.0.

Open the example albums to try the controls. Use the Gallery component for an image grid inside a text page.

Create an album

Use one directory for each album. Put the images in that directory’s images/ folder. One index.md describes the album. Filenames supply the default image titles. Expo does not require a Markdown or YAML file for each image.

content/photos/
├── _index.md
├── nature/
│   ├── _index.md
│   └── field-notes/
│       ├── index.md
│       └── images/
│           ├── 01-fern.jpg
│           └── 02-river.gif
└── buildings/
    ├── index.md
    └── images/
        └── arch.jpg
  1. Create the section entry:

    content/photos/_index.md
    ---
    title: Photo albums
    type: expo
    cascade:
      type: expo
    ---
  2. Create a topic entry for each group of albums:

    content/photos/nature/_index.md
    ---
    title: Nature
    weight: 10
    ---
  3. Create the album page:

    content/photos/nature/field-notes/index.md
    ---
    title: Field notes
    description: Plants and water from a field visit.
    weight: 10
    ---
    
    Photographs from the north trail.
  4. Put the images in the album’s images/ folder.

The section lists its child topics and albums. The sidebar follows the same content tree. On narrow screens, open the navigation drawer to select an album. The album body is optional. If supplied, it appears above the images.

Split large collections into separate albums. Expo has no automatic image pagination. A section card uses the first sorted image as its cover. A topic without images uses the first available image from its descendant albums. The cover fills a 4:3 frame and can crop the preview.

Add image metadata

Add a resources entry to the album front matter when an image needs a title, description, credit, or fixed position. The src path is relative to the album directory.

Album front matter
resources:
  - src: images/01-fern.jpg
    title: Fern
    params:
      id: fern
      alt: A green fern frond seen from above
      caption: Branching leaves beside the north trail.
      byline: Alex Chen
      weight: 10
Field Use
title Image title. If omitted, Expo derives a title from the filename
params.alt Text that describes the image. If omitted or empty, it uses the title
params.caption Plain-text description
params.byline Plain-text credit
params.weight Numeric image order. Lower values come first. The default is 0
params.id Stable image ID within the album
params.original Optional path to another image in the same album bundle

Images with equal weights keep the resource name order. To set a complete order, give each image a weight. Images without a weight come before positive weights.

Use ASCII letters or digits for the first ID character. The remaining characters can also include _ and -. Keep IDs unique within an album. The example creates /photos/nature/field-notes/#img-fern. This link stays valid after a filename change if the album URL and explicit ID stay the same. Without an explicit ID, Expo uses a filename hash. Renaming that file changes the link.

To use a separate original, put it in originals/ and set params.original: originals/fern.jpg. Keep the preview and original at the same aspect ratio. Keep originals outside the expo.images pattern to avoid duplicate entries. Expo reads bundled images; it does not fetch remote files for an album.

Set the defaults

Set site defaults in hugo.yml:

hugo.yml
params:
  ui:
    expo:
      images: images/*
      mode: wall
      size: 240
Key Allowed value Built-in default
images A pattern for image resources in the album bundle images/*
mode wall, cards, or seamless wall
size An integer from 160 to 360, in CSS pixels 240

Use a top-level expo block in album front matter to replace the site block:

Album front matter
expo:
  images: images/*
  mode: cards
  size: 280

This override replaces the whole block. Omitted fields use built-in defaults, not values from the site block. A section can supply the block through cascade. The size is a layout target, not a fixed width and height for every image.

The browser saves the reader’s layout and size choices for the same site origin. These choices take precedence over author defaults. They remain in use after a reload, an album change, or an appearance change. If browser storage is unavailable, Expo uses the author defaults when the page loads.

Select a layout

Mode Display
wall Rows of image tiles with 6 CSS pixels between them
cards Columns of cards with visible titles, descriptions, and credits
seamless Rows of image tiles with no gaps between them

The toolbar has three layout icons, a play/pause button, and an image-size slider. On devices with a mouse, Wall and Seamless show captions on hover or keyboard focus. Images with extreme proportions can have space inside their tiles. The album tiles contain the whole image.

GIFs play by default. Select Pause to show still previews. If the system requests reduced motion, Expo starts with still previews. Select Play to start them. The play button is disabled when the album has no GIFs. Images load as they approach the visible area; large GIF files still require their full transfer size.

Use the image viewer

Select an image to open the viewer. A small image starts at its original size. A large image shrinks until both dimensions fit the available image area.

Control Action
Reset Restore the initial size and center the image
Expand to fit Enlarge or shrink the image until its limiting edge fits the image area
Zoom out / Zoom in Divide or multiply the current scale by 1.25
Open original Open the original image file in a new tab
Locate in album Close the viewer and move to this image in the album
Previous / Next Show the adjacent image in the same album
Close Close the viewer and restore focus to an image link

Expand to fit keeps the whole image visible. It does not switch the browser to fullscreen. Manual zoom can make the image larger than the viewing area. Hold the left mouse button and drag to move the enlarged image. Movement stops at the image bounds. On a touch screen, an enlarged image uses native scrolling. When the image fits, swipe horizontally with one finger to change images; pinching keeps native browser behavior. Double-click alternates between the initial fit and a closer view: a reduced image opens at 100% under the pointer, while an image already at its original size expands to fit.

Changing images resets the scale and position. Previous and Next stop at the ends of the album. The percentage uses CSS pixels: at 100%, one source pixel occupies one CSS pixel.

Use keyboard shortcuts

These shortcuts work while the viewer is open. They take precedence over the page’s single-key navigation. They do not handle text input, input method composition, or Ctrl/Command/Alt combinations.

Key Action
W / A / S / D Move the enlarged image up / left / down / right
Q or H Previous image
E or L Next image
J or − Zoom out
K or + Zoom in
R or 0 Reset the size and position
F Expand to fit
Escape Close the viewer

WASD moves the image by 80 CSS pixels per press, only along an axis that overflows. Left and Right arrow keys change images when horizontal scrolling is unnecessary. When the image is wider than the viewing area, the arrows retain native scrolling. The keyboard_nav setting controls page shortcuts; it does not disable the viewer controls.

Use the site appearance

Expo uses Paper, Slate, Ink, and Terminal with the site’s light or dark mode. Select these options in the shared navigation. The album body has its image toolbar and no article action menu.

To expose all four presets and keep the top navbar visible, use:

hugo.yml
params:
  ui:
    preset: paper
    preset_menu: [paper, slate, ink, terminal]
    navbar_enabled: true
    navbar_autohide: false

Expo uses the full available page width by default and has no right-hand outline. Set page_width in page front matter if the album needs a narrower body. If your site overrides params.ui.shell_types, include expo to retain its sidebar.

Translate and export an album

Add index.zh.md beside index.md for a Chinese album. Add _index.zh.md for each translated section. Both languages can share the image files. Translate the title, description, and resource metadata in each page. Keep image IDs the same when they refer to the same image.

HTML works without JavaScript: image links open the original files, and captions remain visible. When enabled, Print uses still previews. Markdown and LLMSFULL include image links and metadata. RSS includes static image descriptions. See Print and Agent outputs for output configuration.

Check an album

  1. Run hugo --printPathWarnings with a theme version that includes Expo.
  2. Open each album and confirm its image order, titles, and descriptions.
  3. Test a small image, a wide image, a long image, and a GIF if the album has one.
  4. Test Reset, Expand to fit, zoom, dragging, keyboard navigation, and Locate in album.
  5. Check both languages, narrow screens, the four presets, and light/dark mode.

If an album is empty, check its type, bundle structure, and expo.images pattern. If a default seems to have no effect, check the stored reader choice and the whole-block override rule. For required behavior and maintainer checks, see the Expo contract and design decision.

3.7 - Releases and downloads

Record versions, tags, archive links, checksums, and install commands locally. Release cards, asset tables, download blocks, and index pages use these records.

OINK stores release metadata in two local places: a page’s front matter release_url identifies its GitHub release, and data/download/<key>.yaml records installation instructions. Release cards, asset tables, download blocks, and index pages derive their links from those records without contacting GitHub at build time.

This page carries demonstration release facts

Its front matter holds a release_url (OINK v0.4.0), and the card, asset table and download block below are really rendered. The checksums and asset filenames are fabricated. The URLs are derived locally from the repository and tag. Their target files do not exist in any real release. Do not use these hashes to verify anything.

Components and where the facts come from

What you want What renders it Facts come from
A version summary card (tag, date, archives, repo) release-card The page’s release_url
A checksum asset table The checksums fence / release-assets sha*sum lines in the body
A multi-channel download block download data/download/<key>.yaml
A chronological release index layout: releases Each page’s release_url, or its title

The page owns the release facts

One front matter key contains the release record. It is the GitHub release URL for an exact tag:

content/blog/release/0.4.0.md
release_url: https://github.com/pgsty/oink/releases/tag/v0.4.0

The URL supplies the owner, project, and tag. The page’s date supplies the date. An invalid exact-tag GitHub release URL warns and skips the release block. The 0.5 release map (product / version / repo / tag / date / prev / checksums) and its string shorthand are removed. A page that still uses either gets a warning that names release_url.

Put a parameterless shortcode where you want the summary. The call accepts no release facts:

Source
{{< release-card >}}

The card shows four locally derived links: the release, both source archives, and the repository. Checksum files belong in the asset table below a note, and comparisons live on GitHub.

The release index page

A section can switch to the release index layout. It lists every regular page in the section, newest first by page date. Within one day, the tag’s version breaks ties. It uses SemVer precedence with a deterministic fallback for non-SemVer tags:

content/blog/release/_index.md
---
title: Releases
layout: releases
---

If release_url parses, the entry shows project tag, such as oink v0.4.0, above the page description. A page without that URL keeps its own title. A plain note between releases is therefore an ordinary entry and causes no warning. The 0.5 release_products filter and release_group_by_product grouping are gone; Using either warns.

This site’s Releases currently uses the ordinary blog list. Switch to layout: releases when a strict chronology is wanted.

Checksum assets

The checksums fence is the native form of a checksum table, holding the verbatim output of a sha*sum command:

Source
```checksums
1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4  oink-0.4.0-linux-amd64.tar.gz
7b3d9e0c145a8f26d0b7e93c48156aa2f0d9c7b31e846a5029df1b6c7a3e8250 *oink-0.4.0-darwin-arm64.tar.gz
```
Download asset
FileChecksum
oink-0.4.0-linux-amd64.tar.gz Linuxamd64SHA-256 1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4
oink-0.4.0-darwin-arm64.tar.gz macOSarm64SHA-256 7b3d9e0c145a8f26d0b7e93c48156aa2f0d9c7b31e846a5029df1b6c7a3e8250

Only two line shapes are accepted: <hex><two spaces><filename> and <hex><space>*<filename>. Blank lines and lines starting with # are ignored. The hash length determines the algorithm (MD5 / SHA-1 / SHA-256 / SHA-512), and each block holds a single algorithm. Malformed lines log a warning with the line number and are skipped. Each filename must be a single path segment; package type, OS, and architecture badges are inferred from the filename when recognized.

If the page has release_url front matter, the base for asset links is derived as https://github.com/<repo>/releases/download/<tag>/. A page without release facts must set base= explicitly. Having both is an error.

a page with no release front matter
```checksums {base="https://repo.pigsty.io/oink/v0.4.0/" algo="sha256"}
1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4  oink-0.4.0-linux-amd64.tar.gz
```

release-assets is the shortcode form of the same parser and renderer. It adds one parameter that the fence lacks, src=. Use it for a checksum file stored as a page resource or global asset. src and inner content are mutually exclusive. group="auto" groups by platform and architecture:

Source
{{< release-assets group="auto" >}}
5a0c7d1e93b4826f0ad35c9e17b6402d8f1c95ae63d70b28c4e19a5f38207db6  oink-0.4.0-1.el9.x86_64.rpm
c93f16a8d052b7e41ac68d3907b25fe0a41d8c7362b95e0187ac4d63f9520ea8  oink-0.4.0-1.el9.aarch64.rpm
{{< /release-assets >}}

.rpm

Download asset
FileChecksum
oink-0.4.0-1.el9.x86_64.rpm Linuxamd64SHA-256 5a0c7d1e93b4826f0ad35c9e17b6402d8f1c95ae63d70b28c4e19a5f38207db6
oink-0.4.0-1.el9.aarch64.rpm Linuxarm64SHA-256 c93f16a8d052b7e41ac68d3907b25fe0a41d8c7362b95e0187ac4d63f9520ea8

HTML shows a truncated hash. The accessible name and copied text retain the full value. The copy button uses a local runtime loaded on demand. With JavaScript disabled it is still a complete linked table. Print expands the full hash without controls, and Markdown and RSS emit a pipe table of full hashes.

Download channel data

Installation instructions belong to the product rather than one release. Store them in data/download/<key>.yaml. This site’s real record is data/download/prd5.yaml:

data/download/prd5.yaml
version: 0.4.0
repo: pgsty/oink
published: true
channels:
  - id: script
    kind: rolling
    title: Install script
    title_zh: 安装脚本
    icon: fa-solid fa-bolt
    note: The rolling channel deliberately contains no version interpolation.
    note_zh: 滚动渠道刻意不插入版本号。
    steps:
      - title: Install
        title_zh: 安装
        code: curl -fsSL https://repo.example.org/oink/install | bash
        lang: bash
  - id: source
    kind: pinned
    title: Source archive
    title_zh: 源码归档
    icon: fa-solid fa-code-branch
    url: https://github.com/pgsty/oink/archive/refs/tags/${tag}.tar.gz
    steps:
      - title: Clone the tag
        title_zh: 克隆标签
        code: git clone --branch ${tag} https://github.com/pgsty/oink.git
        lang: bash
  - id: assets
    kind: pinned
    title: Release assets
    title_zh: 发布资产
    icon: fa-solid fa-box-open
    checksums: |
      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa  oink-0.4.0.tar.gz

The record has five top-level fields: version, repo, tag, published, and channels (any unknown top-level key logs a warning and skips the record). version may be omitted here and supplied by the site’s params.version instead.

version , string , defaultsite params.version
Missing in both places warns and skips the block
repo , owner/name , default—
Required once a pinned channel has a link or assets
tag , string , defaultv{version}
URL-safe characters only
published , boolean , defaulttrue
false means the immutable release does not exist yet
channels , array , default—
Must be non-empty

Each channel:

id , ^[a-z][a-z0-9-]*$ , default—
Unique within the record. Used as the anchor
kind , rolling | pinned , default—
Decides whether release facts may be interpolated
title , localized string , default—
Must resolve to a non-empty value
note , localized string , default—
One line of explanation under the channel
icon , Font Awesome class pair , default—
For example fa-solid fa-bolt
url , http(s) or a site path , default—
Interpolatable on pinned only
steps[] , title / code / lang , defaultlang: text
Code steps go through OINK’s enhanced code renderer
checksums , sha*sum text , default—
pinned only. Mutually exclusive with checksums_src
checksums_src , asset path , default—
Reads the checksum file as a Hugo asset

Two rules:

  • Localization resolves by suffix: <field>_<exact language> → <field>_<base language> → <field>. A Chinese site resolves title_zh_cn, then title_zh, then title. camelCase aliases are not accepted.
  • Only a pinned channel’s url and steps[].code interpolate ${version} and ${tag}. A rolling channel refuses interpolation, so a stable install command is never bound to one version. Titles and notes never interpolate.

Rendering the download block

download takes exactly one positional parameter, the data key:

Source
{{< download "prd5" >}}

Install script

The rolling channel deliberately contains no version interpolation.

Install
curl -fsSL https://repo.example.org/oink/install | bash

Source archive

Source archive
Clone the tag
git clone --branch v0.4.0 https://github.com/pgsty/oink.git

Release assets

Download asset
FileChecksum
oink-0.4.0.tar.gz SHA-256 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

HTML renders a row of anchor chips and one section per channel. Code steps reuse the enhanced code block and its on-demand copy runtime. A checksum channel reuses the asset table above. Print statically expands the same content, Markdown emits the titles, source fences and full hashes, and RSS omits the component.

Before you create the tag and upload assets, mark the record unpublished:

data/download/<key>.yaml
published: false

Rolling channels keep working. Pinned channels become an unclickable “pending release” state, omit the pinned commands, and disable asset links and copy controls. After the tag and assets resolve, change the publication state. Do not write a guessed link in the prose.

The same record can supply a landing page’s download section. No second version model is required. See Home and landing pages.

How this relates to blog release notes

The two have different jobs:

  • A release note in the blog (this site keeps them in content/blog/release/) explains what changed, how to upgrade, and what breaks. Its front matter carries release_url, and a release-card can sit at the top. For writing instructions, see Blog posts.
  • Download data identifies the channel, command, and hash. It is separate from the version number. An upgrade changes one place.

For a release, follow this order: update version in data/download/<key>.yaml → write a new content/blog/release/<version>.md with its release_url → set published to true after the tag and assets are in place.

Verify

  1. Build with visible diagnostics: hugo --printPathWarnings. A malformed hash line, mixed algorithms, a missing base and a misspelled channel field all produce warnings that need review.
  2. On the page: the card’s tag and date match the repository, and every asset row opens a real download URL.
  3. Check hashes against the actual artifacts manually. The component only displays them. It verifies nothing.
  4. Confirm the hashes are complete in non-HTML output:
curl -s http://localhost:1313/docs/write/releases/index.md | grep -c '^| '
  1. Test with published: false first. Set it to true only after the tag and assets exist. Test each language and a subpath deployment.

3.8 - API reference pages

Add an OpenAPI specification to the site. Use bundled Swagger UI or Redoc to render a browsable API reference without a CDN.

An API reference page is one OpenAPI specification plus one shortcode. Choose Swagger UI when readers need to try requests, or Redoc for browsing endpoint descriptions and schemas. Both runtimes ship with the theme and load only on pages that use them in HTML output, without a CDN. Swagger UI’s online validator is disabled. Remote specifications and API requests still contact their configured hosts.

Put the specification file under static/. Create a page with the shortcode. If the page needs the dedicated shell, change its type to swagger.

Where the specification file goes

Put the specification under static/. Hugo publishes it unchanged at the site root. Both shortcodes then receive a URL that the browser can fetch:

where the specification lives

  • static/
    • openapi/
      • docs-demo.yamlpublished as /openapi/docs-demo.yaml
  • content/
    • docs/
      • write/
        • openapi.mdthis page

Do not put the specification beside the page. Both shortcodes treat a local value as a path under static/. Neither resolves page resources. A .yaml file beside a content page is a page resource. A shortcode reference to its name does not publish it. The browser therefore receives a 404.

Both shortcodes accept a remote specification starting https://…. This creates a network dependency and exposes the reader’s metadata to that host. Intranet deployments and sites with a CSP should use a same-origin specification. Only http and https are accepted. Any other scheme, a protocol-relative //host, or an empty value warns. The shortcode then renders nothing.

To try the examples, download docs-demo.yaml and save it as static/openapi/docs-demo.yaml in your site. It describes a demonstration cluster-management API with no reachable server behind it.

Swagger UI

swagger has one named parameter, src, whose value is a URL from the site root. It passes through the theme’s URL validation, so a subpath deployment resolves correctly:

Source
{{< swagger src="/openapi/docs-demo.yaml" >}}

In your local preview, this displays expandable API operations, request parameters and response schemas. “Try it out” sends requests to the specification’s servers address. The sample has no working backend.

This page shows Swagger UI source and a live Redoc example below. Both widgets have known accessibility limitations. See Limits.

Redoc

redoc takes exactly one positional parameter, the specification path. A second parameter warns and the shortcode renders nothing.

Source
{{< redoc "openapi/docs-demo.yaml" >}}

OpenAPI specification — https://oink.pgsty.com/openapi/docs-demo.yaml

An http or https URL remains remote. Any other accepted value is a path under static/. Leading and non-leading slash forms are equivalent. For example, openapi/docs-demo.yaml and /openapi/docs-demo.yaml both become https://example.com/preview/openapi/docs-demo.yaml when the site baseURL is https://example.com/preview/. Unlike swagger, Redoc receives this absolute URL based on baseURL.

The theme pins five attributes — hide-hostname, hide-logo, suppress-warnings, lazy-rendering, native-scrollbars — and hides the Redocly brand mark with CSS. Redoc’s remaining attributes are not exposed to authors. If a site needs them, override layouts/_shortcodes/redoc.html.

The dedicated page shell

API reference pages are often wide and long. Use the swagger page type for this layout:

content/api/_index.md
---
title: Cluster management API
type: swagger
page_width: wide
cascade:
  type: swagger
---

swagger is one of the theme’s default shell types (params.ui.shell_types defaults to [docs, book, blog, swagger], and a site that overrides the list needs to keep it). It differs from the docs shell in exactly two ways. It adds a td-swagger class to <body> for styling hooks. It omits the version banner. Sidebar, table of contents, breadcrumbs, pager and page end all behave normally.

Shells and page width are covered fully in Layouts and page types.

Output

Output What appears
HTML The full interactive Swagger UI / Redoc. Only HTML loads the runtime, on demand from local files without a CDN
Print A labeled static link showing the specification’s address. Neither runtime loads
Markdown A plain Markdown link, [OpenAPI specification](/openapi/example.yaml). It does not become an endpoint list
RSS The same plain link

Outside HTML, the API reference contains only a link to the specification. To include endpoint information in print or agent output, describe key endpoints in prose on the same page. Body text outside the shortcode remains intact in all four outputs.

Limits

  • Both components derive their container ID from the page address and the shortcode’s ordinal, so several on one page never collide.
  • The two can coexist on one page, but the page becomes long and its HTML output loads both runtimes. Pick one for a production site.
  • Both embedded third-party widgets have upstream accessibility limitations: Swagger UI renders unnamed server controls and scrollable regions without keyboard access, and Redoc’s operation descriptions fall below AA contrast ratios. Evaluate these trade-offs against your site’s accessibility requirements, or document key endpoints directly in Markdown prose when strict conformance is required.
  • redoc accepts no attribute parameter. A second positional argument warns and the shortcode renders nothing.
  • A local redoc path starts under static/. A leading / is optional. Page resources are not resolved.
  • The browser must be able to fetch the specification. Put it in static/. After a build, confirm that the file exists under public/.
  • There is no mock server. Swagger UI’s “Try it out” sends a real request to the address in servers. The sample specification’s address is unreachable.

Verify

  1. Build with visible diagnostics: hugo --printPathWarnings.
  2. The specification really was published: ls public/openapi/docs-demo.yaml, or open http://localhost:1313/openapi/docs-demo.yaml.
  3. Expand endpoints on the page and confirm that their schemas appear. The browser console must show no 404 or cross-origin error.
  4. Disconnect from the external network while keeping the local preview server reachable, then reload. Local runtimes and a same-origin specification should still appear.

4 - Components

Find each writing component, from simple examples to detailed examples, with its output matrix, parameters, and limits.

Use this section to write components in Markdown. Each page starts with the shortest example, then gives more detailed examples. The page also gives the output matrix, parameter table, and limits. Use the cheatsheet below for a quick syntax reference.

Two forms

The native form uses Markdown blockquotes, lists, tables, images, or fences, followed by one {…} attribute line. This form stays readable on GitHub and in Markdown editors. Markdown output preserves the source instead of rendered HTML.

Use a shortcode for features that the native form cannot express. Examples include tabs in prose and parameter tables with block descriptions. Cards with icons and badges, and terminal recordings, also need shortcodes. Follow these five rules:

  • Write every shortcode as {{</* name */>}}. Only {{%/* steps */%}} uses the % delimiter because its body is page-level Markdown.
  • Use nested names (tab, card, field) only inside their parent.
  • Invalid author parameters never fail silently. Ordinary previews warn, identify the source position, and use the documented fallback or omit unsafe content. Review the warning and rendered fallback before publication.
  • Public string parameters, such as captions, labels, and titles, are plain text. They are not parsed as Markdown. Only bodies are Markdown: tab, card and field bodies, files loaded by include, and Book fig / tbl / eg bodies.
  • Components that a page does not use load no runtime. HTML references only the stable capability chunks that the page needs. Print, Markdown, and RSS load no interactive runtime.

Site prerequisites

Components need three Goldmark settings. OINK Starter configures these settings. If you start from scratch, copy this snippet:

hugo.yml
markup:
  goldmark:
    renderer:
      unsafe: true # keep HTML that content emits
    parser:
      attribute:
        block: true # enable {…} attribute lines
      wrapStandAloneImageWithinParagraph: false # standalone images are not wrapped in <p>
  • renderer.unsafe: true keeps raw HTML in content. Goldmark drops it by default. Without this setting, HTML inside component bodies disappears.
  • parser.attribute.block: true enables attribute lines. Without it, {.steps} and {caption="…"} appear as text.
  • parser.wrapStandAloneImageWithinParagraph: false prevents <p> wrappers around standalone images. This permits captioned figures and attribute lines after images.

Some components need other settings. Mathematics needs Goldmark passthrough. Draw.io needs an explicitly configured editor service. Each component page explains its requirements. Configuration lists all configuration keys.

OINK 1.4.0 renders PlantUML locally when enabled without an endpoint, using two bundled scripts. An explicit endpoint keeps server mode; see the PlantUML guide for both modes.

Cheatsheet

The Form column uses three labels. Native means Markdown syntax plus an attribute line. Fence means a fenced block with a language tag. Shortcode means {{</* … */>}}. The Runtime column shows whether the component loads JavaScript on the page.

Component In one line Shortest form Form Runtime
Callouts Separate prerequisites, warnings and asides from the prose > [!NOTE] native none
Images Captions, sizing, zoom, numbering and build-time processing ![alt](oink.webp) native site switch
Code Blocks Highlighting, titles, copy, folding, linkable lines ```sh fence per page
Tabs One thing, several platforms or languages attribute {tab="Linux"} native + shortcode per page
Tables Plain tables plus full-width, matrix, caption and numbering {.full-width} native tabs or .expand only
Fields Parameter lists with type / required / default chips {.fields meta="type default"} native + shortcode none
Steps A procedure with an order {.steps} native + shortcode none
Cards A set of parallel destinations {.cards} native + shortcode none
FileTree Directory structure with an aligned comment column ```filetree fence per page
Math KaTeX inline and display formulas $$ … $$ native none
Mermaid Flowcharts, sequence diagrams, Gantt charts ```mermaid fence per page
PlantUML UML diagrams; local rendering in OINK 1.4.0 ```plantuml fence site switch
Markmap A Markdown outline becomes a mind map ```markmap fence site switch
Draw.io Diagrams that stay editable; needs a server ![alt](arch.drawio.svg) native site switch
ECharts Declarative statistical charts ```echarts fence per page
Infographic AntV infographics ```infographic fence per page
Gallery A set of images sharing one zoom dialog ```gallery fence site switch
Badge Inline status markers {{</* badge text="Beta" */>}} shortcode none
Kbd Key names and chords {{</* kbd "Ctrl" "K" */>}} shortcode none
Includes Pull in files, print site parameters, drop build-time notes {{</* include file="parts/x.md" */>}} shortcode none
Asciinema Terminal recordings {{</* asciinema file="images/x.cast" */>}} shortcode per page

Four notes on the Runtime column:

  • A code block loads code-block.js only if a block on the page has a copy or fold control. A file tree loads filetree.js only if it has a comment column. This runtime lets readers drag the splitter.
  • Images and galleries share one zoom dialog runtime. It needs site-level ui.image_zoom and at least one eligible image on the page.
  • KaTeX renders mathematics to HTML and MathML at build time. The page loads a KaTeX stylesheet and fonts, but no script.
  • Draw.io loads only on pages whose rendered content has PNG or SVG candidates. It inspects each distinct image URL once.

Every component defines its behavior in four outputs: HTML, print, Markdown, and RSS. See Output on each component page.

4.1 - Callouts

Write notes, warnings, and collapsible asides with colors, icons, and titles. Use > [!NOTE] blockquotes without shortcodes.

A callout is a GitHub / Obsidian style blockquote. Put > [!TYPE] on the first line and the body below it. Use callouts to separate prerequisites, warnings, and asides from prose. If a prose sentence is sufficient, a callout is unnecessary.

Shortest form

Source
> [!NOTE]
> Hugo Modules need Go on the machine; an offline archive does not.
Note

Hugo Modules need Go on the machine; an offline archive does not.

If you omit the title, the callout uses the localized type name: “Note” in English or 「注意」 in Chinese. GitHub displays the source as a GitHub callout. Other Markdown readers display a plain blockquote. All content remains available.

Ten types

The first five types match GitHub. OINK adds five semantic types. Each type has a default icon and accent color.

Source
> [!TIP]
> `hugo server -D` previews drafts.

> [!IMPORTANT]
> The floor is Hugo Extended 0.168.0; anything older fails the build outright.

> [!WARNING]
> `hugo --cleanDestinationDir` empties `public/`.

> [!CAUTION]
> The first build after deleting `resources/_gen` is much slower.

> [!SUCCESS]
> Build passed with reviewed warnings — ship it.

> [!DANGER]
> Never commit `go.work`.

> [!QUESTION]
> Should the site have comments? See [enabling comments](/docs/admin/comments/).

> [!EXAMPLE]
> `pgsty.com` is a documentation site built from callouts and tables alone.

> [!QUOTE]
> Documentation is a love letter that you write to your future self.
Tip

hugo server -D previews drafts.

Important

The floor is Hugo Extended 0.168.0; anything older fails the build outright.

Warning

hugo --cleanDestinationDir empties public/.

Caution

The first build after deleting resources/_gen is much slower.

Success

Build passed with reviewed warnings — ship it.

Danger

Never commit go.work.

Question

Should the site have comments? See enabling comments.

Example

pgsty.com is a documentation site built from callouts and tables alone.

Quote

Documentation is a love letter that you write to your future self.

Type names are case-insensitive.

Custom title

Text after the marker on the same line becomes the title. It accepts inline Markdown, including code, bold text, and links.

Source
> [!WARNING] Rewrites `public/`
> Check that `baseURL` points at the production domain before a production
> build, or every absolute link will be wrong.
Rewrites public/

Check that baseURL points at the production domain before a production build, or every absolute link will be wrong.

Body content

The body uses page-level Markdown. It can contain lists, fenced code, tables, images, and nested callouts. Start every line with >, including fence lines.

Source
> [!TIP] Three commands to a live preview
>
> 1. Clone: `git clone https://github.com/pgsty/oink-starter my-docs`
> 2. Enter the directory and preview:
>    ```bash
>    cd my-docs && hugo server
>    ```
> 3. Open <http://localhost:1313/>
>
> | Port | Purpose |
> | --- | --- |
> | 1313 | Hugo development server |
Three commands to a live preview
  1. Clone: git clone https://github.com/pgsty/oink-starter my-docs
  2. Enter the directory and preview:
    cd my-docs && hugo server
  3. Open http://localhost:1313/
Port Purpose
1313 Hugo development server

Collapsing

A - after the type makes the callout initially closed. A + makes it initially open. Both use native <details> and load no JavaScript. Use them for full command output, alternatives, or background information that can remain hidden initially.

Source
> [!NOTE]- Why is Go needed?
> Hugo downloads themes through Go's module system (`hugo mod get`). A submodule
> or an offline archive works without Go installed.

> [!TIP]+ Open by default, but the reader can close it
> The closed state is not remembered; a reload returns to the default.
Why is Go needed?

Hugo downloads themes through Go’s module system (hugo mod get). A submodule or an offline archive works without Go installed.

Open by default, but the reader can close it

The closed state is not remembered; a reload returns to the default.

The neutral disclosure, DETAILS

[!DETAILS] is a disclosure without a semantic color. It is closed by default; [!DETAILS]+ is open. Use it for long output, complete configuration files, or other content that must be collapsible.

Source
> [!DETAILS] Full `hugo version` output
> ```text
> hugo v0.168.0+extended+withdeploy darwin/arm64
> ```
Full hugo version output
hugo v0.168.0+extended+withdeploy darwin/arm64

Custom icon

To replace the default icon, put {icon="fa-solid fa-xxx"} after the blockquote. Use one Font Awesome class pair. The attribute line must immediately follow the blockquote, with no blank line between them.

Source
> [!TIP] PostgreSQL 18 is supported
> Pigsty v4 installs PostgreSQL 18 by default.
{icon="fa-solid fa-database"}
PostgreSQL 18 is supported

Pigsty v4 installs PostgreSQL 18 by default.

Nesting

To nest callouts, add one > per level. Callouts can also appear inside list items or steps. One nesting level is sufficient.

Source
> [!WARNING] Back up before upgrading
> A theme version bump can change how a page renders.
>
> > [!TIP]- How to back up
> > `git tag pre-upgrade` is enough — rolling back is `git checkout pre-upgrade`.
Back up before upgrading

A theme version bump can change how a page renders.

How to back up

git tag pre-upgrade is enough — rolling back is git checkout pre-upgrade.

Unknown types and common slips

Unknown type names do not fail the build or remove content. The block renders as an ordinary blockquote with its [!TYPE] marker visible.

Source
> [!NOTICE] Not a valid type
> The marker stays on the page to tell you so.

[!NOTICE] Not a valid type

The marker stays on the page to tell you so.

Other common problems:

  • Formatters such as Prettier can merge titles into the body. Keep an empty > line below the title to prevent this.
  • Formatters can move attribute lines. Wrap marker lines such as {icon=…} in <!-- prettier-ignore-start --> / <!-- prettier-ignore-end -->.
  • style, onclick, and unsupported attributes warn and are ignored. The attribute line accepts only icon and class. See the table below.

Output

Output Shape
HTML Static types are <div class="td-callout" role="note">; collapsible types are a native <details> + <summary>
Print All static and expanded; disclosures carry a data-td-callout-collapsible marker
Markdown The source blockquote is preserved, [!TYPE] marker and title included
RSS Same as print — static and expanded

Callouts load no script.

Parameter reference

The marker line > [!TYPE]± Title:

TYPE , enum , default—
NOTE TIP IMPORTANT WARNING CAUTION SUCCESS DANGER QUESTION EXAMPLE QUOTE DETAILS; case-insensitive; an unknown value renders as a plain blockquote
± , - / + / none , defaultnone
- collapses closed, + collapses open; bare DETAILS is closed
Title , inline Markdown , defaultthe localized type name
On the same line as the marker

The attribute line {…}, immediately after the blockquote:

icon , Font Awesome class pair , defaultthe type’s icon
For example fa-solid fa-database; DETAILS has no default icon
class , space-separated classes , default—
Passed through verbatim for site CSS

style, on*, and other keys warn and are ignored.

Limits

  • Colors cannot be customized. The type sets the color. If you need a new meaning, choose the nearest type and write a title.
  • The collapsed state is not saved.
  • Callouts work inside {.steps} list items and {{%/* steps */%}} steps. See Steps. Start every blockquote line with > and align it with the list item’s indentation.
  • Steps — callouts inside a procedure
  • Tabs — the same note split per platform
  • Writing pages — when to use a callout and when to use prose

4.2 - Images

Use Markdown image syntax and attributes for captions, sizing, zoom, links, numbering, and Hugo image processing.

Write images with Markdown’s ![alt text](source "title") syntax. For an image in its own paragraph, add a {…} attribute line to enable captions, zoom, numbering, or Hugo processing. The theme has no image shortcode.

Shortest form

Source
![The OINK documentation shell: sidebar, article and table of contents](oink-shell.webp)
The OINK documentation shell: sidebar, article and table of contents

This image is in the page’s directory, which forms a page bundle. The theme reads its intrinsic dimensions and writes width/height to prevent layout changes during loading. All images use lazy loading. You should always write alternative text for screen readers and search engines. Empty alt text marks a decorative image, which is excluded from zoom.

Where images come from

Sources resolve in this order. Use the corresponding path form for each location:

Placement How it is written Suited to
Beside the page (a bundle: index.md plus the image) ![…](oink-shell.webp) A screenshot only this page uses; it travels with the page and is shared by translations
Current section bundle (_index.md and its resources) ![…](post/image.png) Images belonging to a section; use the resource path relative to that section
Global resource assets/images/… ![…](images/logo/oink.webp) Images several pages share, especially ones needing processing (resize / crop)
Static directory static/images/… ![…](/images/hero-light.webp) Large images and downloads that need no processing; supply width/height where the theme cannot measure them
Remote URL ![…](https://example.com/a.png) Rare: nothing is downloaded at build time and nothing can be processed

The theme resolves relative paths as page resources, current-section resources, then global resources. If no resource matches, it emits a static path. It does not check whether static paths or remote URLs exist. If image processing cannot find a processable resource, ordinary previews warn and preserve the unprocessed image. Review the warning and rendered fallback before publication.

Markdown alternative text takes precedence over resource metadata, including explicitly empty alt text for decorative images. Resource params.alt must be a string. Invalid metadata warns and is ignored, preserving the authored alt text. Review the warning and rendered fallback before publication.

Inline versus block

An image within a text line is inline. It renders as one <img> and accepts no attributes. An image in its own paragraph is a block image and accepts an attribute line.

Source
This little one ![shell thumbnail](oink-mini.webp) sits inside a sentence — an inline image.

![shell thumbnail](oink-mini.webp)
{width="100" height="64"}

This image shell thumbnail is inside a sentence, so it is inline.

shell thumbnail

Inline images display at their intrinsic dimensions: 50×32 in this example. An inline SVG without intrinsic dimensions fills the container width. Use a block image with explicit width/height for SVG.

Note

Block images need markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false. This site has that setting. See Configuration. Without it, Goldmark wraps standalone images in <p> and treats attribute lines as prose.

Captions

caption="…" creates a <figure> with a <figcaption>. Captions are plain text and are not parsed as Markdown.

Source
![Release card: version, publication date and asset buttons](release-note.webp)
{caption="The release card uses the page's release_url and date"}
Release card: version, publication date and asset buttons
The release card uses the page's release_url and date

A Markdown "title" is a hover tooltip. It never becomes a caption.

Size

width/height take positive integers and override the resource dimensions. For static or remote images, they reserve space to prevent layout changes. They can also display a large image at a smaller size. The browser scales the image. The file stays unchanged.

Source
![The OINK home page illustration (light)](/images/hero-light.webp)
{width="450" height="300" caption="A 900×600 illustration from static/images/ shown at half size"}
The OINK home page illustration (light)
A 900×600 illustration from static/images/ shown at half size

Processed images

Hugo can process page and global resources at build time. Provide both command and options. Commands are Fit, Resize, Fill, or Crop. Options use Hugo’s image-processing syntax. The rendered src points to the derivative. With zoom enabled, the dialog opens the original.

Source
![shell thumbnail](oink-shell.webp)
{command="Fit" options="300x150" caption="Fit 300x150: scaled to fit inside a 300×150 box"}

![the left half of the shell](oink-shell.webp)
{command="Fill" options="300x150 Left" caption="Fill 300x150 Left: fills the box, cropped from the left"}
shell thumbnail
Fit 300x150: scaled to fit inside a 300×150 box
the left half of the shell
Fill 300x150 Left: fills the box, cropped from the left

Static paths, remote URLs, and SVG do not support processing. Using command for these sources warns and preserves the unprocessed image. Hugo image processing documents anchors, quality, and format conversion, such as 300x150 webp q80.

Two forms, for different purposes:

  • No caption, and the image itself is the link: wrap it in a Markdown link, [![alt](src)](href).
  • A captioned figure that is clickable as a whole: add link="…" to the attribute line (which requires caption or num).
Source
[![Go to the highlights page](oink-shell.webp)](/docs/about/features/)

![Release card](release-note.webp)
{caption="Click the image for the releases and downloads guide" link="/docs/write/releases/"}

Go to the highlights page

Release card
Click the image for the releases and downloads guide

Linked images never zoom. Using link= without a caption warns and removes the link, recommending [![…](…)](…).

Numbered figures

For books or long manuals, add num to the attribute line, with an optional #id. The author writes the number as a string, such as 2-1 or 3.4. The theme never counts automatically. The caption receives a localized “Figure 2-1” prefix. #id defaults to fig-<num>. Reference it with [Figure 2-1](#fig-2-1) or xref. See Books for the whole-book figure list.

Source
![Release card](release-note.webp)
{#fig-release num="2-1" caption="The release card: version, date and assets"}

See [Figure 2-1](#fig-release).
Release card
Figure 2-1 The release card: version, date and assets

See Figure 2-1.

A numbered figure can be a processed image at the same time (num plus command), and can carry a link.

Zoom

Image zoom is off by default. When enabled, block images, figures, and gallery images with alt text become clickable buttons. They open the full image in a native <dialog>. Esc closes it and restores the previous focus. This page enables zoom in front matter, so every image above is clickable.

Since OINK 1.1, the button’s accessible name includes the preview action and image description. Copied articles receive no helper text. Copied HTML keeps images and authored captions for rich-text editors. In v1.0.0, a hidden preview label can appear after pasting. Until you upgrade, use image_zoom: false and rebuild to avoid it.

hugo.yml
params:
  ui:
    image_zoom: true
One page's front matter: off for this page only
image_zoom: false

Inline images, decorative images with empty alt text, linked images, and images with data-no-zoom never zoom. The runtime loads only if the page has an eligible image. Print, Markdown, and RSS have no dialog.

Source: a decorative image does not zoom
![](oink-shell.webp)
{width="150" height="75"}

Light and dark images

The theme has no parameter to switch images by color scheme. If you need two images, give each a class. Use [data-bs-theme="dark"] in site CSS to display the image for each scheme:

Source
![Sidebar (light)](sidebar-light.webp)
{class="only-light"}

![Sidebar (dark)](sidebar-dark.webp)
{class="only-dark"}
assets/scss/_styles_project.scss
html[data-bs-theme="dark"] .only-light,
html:not([data-bs-theme="dark"]) .only-dark { display: none; }

Replace sidebar-light.webp and sidebar-dark.webp with your images. The theme sets data-bs-theme on html. These selectors display only the matching image. class passes through for site CSS.

Output

Output What appears
HTML Inline <img>; block <img class="td-image">; with a caption or number, <figure class="td-figure"> plus <figcaption>; a zoom candidate carries data-td-image-zoom
Print As HTML, with the zoom controls removed
Markdown ![alt](src) and the attribute line as they stand
RSS The image src becomes absolute; no zoom

Parameter reference

The attribute line {…} (the line immediately after a block image):

caption , plain text , default—
Its presence makes a figure; not parsed as Markdown
#id , identifier , defaultfig-<num> when num is set
[A-Za-z][A-Za-z0-9_.:-]*; the anchor and the Book target ID
num , string , default—
[0-9A-Za-z.-]+; registers a Book figure target and prefixes the caption with “Figure N.”
width / height , positive integer , defaultthe resource’s intrinsic size
Overrides the size; static and remote images use it to avoid layout shift
command , enum , default—
Fit, Resize, Fill, Crop; must accompany options; page and global resources only
options , string , default—
Hugo image processing options such as 600x300, 300x150 Left, 800x webp q80
link , URL , default—
Wraps the figure in a link; requires caption or num; a linked image does not zoom
class , class list , default—
Passed through for the site’s CSS
data-* / aria-* , string , default—
Passed through

style, on*, alt, title, src, and unsupported attribute-line keys warn and are ignored. Write alt, title, and src in the Markdown image itself.

Limits

  • Captions do not support Markdown. Public string parameters are plain text. Put detailed explanations in a paragraph below the image.
  • title is not a caption. The c in ![a](b "c") is a hover tooltip.
  • Only resources support processing. Move images that need processing from static/ to the page bundle or assets/.
  • Remote images are never downloaded at build time.
  • Zoom has no drag, pan, or previous / next controls. Use a gallery for related images.
  • Gallery — a set of images sharing one zoom dialog
  • Books — the list of figures and xref cross-references
  • Brand and appearance — where the site logo and favicon go
  • Cards — images on cards

4.3 - Code Blocks

Use a Markdown fence and attributes for titles, exact copying, line numbers, highlighting, wrapping, folding, and line links.

A code block uses an ordinary Markdown fence. Hugo’s embedded Chroma highlighter runs at build time. The browser has no highlighter. Use code blocks for commands, configuration snippets, and source code. The {…} attributes on the fence’s info line control the title bar, copy behavior, line numbers, and line anchors. Diagram fences such as mermaid, echarts, and filetree use their own render hooks.

Optional presentation

A one-line fence with {class="td-code-command"} becomes a compact command strip with a separate copy column. OINK 1.4.0 supports this variant; see examples and fallback rules. Multiline, numbered, or collapsed blocks warn and retain regular presentation.

Shortest form

Source
```sql
SELECT datname, numbackends FROM pg_stat_database ORDER BY numbackends DESC;
```
SELECT datname, numbackends FROM pg_stat_database ORDER BY numbackends DESC;

A fence without attributes still has the complete shell and a copy button. Without a title, there is no empty title bar. The copy button appears at the top right on hover or when focus enters the block. It is always visible on touch devices. The shell does not display the language. It stores the lexer name in data-language for stylesheets and tests.

The language tag is Chroma’s lexer name. A diff fence displays a patch with Chroma’s styles for added and removed lines. It needs no additional component:

Source
```diff {title="a change to hugo.yml"}
 params:
   ui:
-    sidebar_menu_compact: true
+    sidebar_menu_compact: false
     sidebar_menu_foldable: true
```
a change to hugo.yml
 params:
   ui:
-    sidebar_menu_compact: true
+    sidebar_menu_compact: false
     sidebar_menu_foldable: true

Filename titles

title adds a visible title bar, usually containing a filename or path. It also sets the block’s accessible name.

Source
```yaml {title="hugo.yml"}
markup:
  goldmark:
    parser:
      attribute:
        block: true
    renderer:
      unsafe: true
```
hugo.yml
markup:
  goldmark:
    parser:
      attribute:
        block: true
    renderer:
      unsafe: true

filename is a historical alias for title. If you provide both, the theme warns and uses filename.

Line numbers, start line and highlighting

lineNos accepts inline or table. Inline numbers share the code column. Table numbers have a separate column, can be selected separately, and are never copied. lineNoStart sets the first displayed number. hl_lines marks source lines for emphasis. It counts from 1 inside the fence, independently of lineNoStart.

Source
```ini {title="postgresql.conf" lineNos="inline" lineNoStart=120 hl_lines="2 4-5"}
shared_buffers = 8GB
max_connections = 200
work_mem = 64MB
wal_level = replica
max_wal_senders = 10
```
postgresql.conf
120shared_buffers = 8GB
121max_connections = 200
122work_mem = 64MB
123wal_level = replica
124max_wal_senders = 10

lineNos="table" puts numbers in a separate column. The copy button removes line numbers in both modes:

Source
```bash {title="deployment in three commands" lineNos="table"}
./configure -c rich
./install.yml
pig ext install pg_duckdb
```
deployment in three commands
1
2
3
./configure -c rich
./install.yml
pig ext install pg_duckdb

tabWidth sets the number of spaces per tab. The theme passes it and style directly to Chroma. This site uses class-based Chroma palettes for light and dark modes. Here, style takes effect only if Hugo is changed to inline style mode.

Wrapping long lines

wrap=true changes only the display. It preserves the source and copied text. Without it, long lines scroll horizontally.

Source
```text {title="config/artifacts.env" wrap=true}
ARTIFACT_URL=https://repo.pigsty.io/pkg/infra/v3.6.0/infra-pkg-v3.6.0.el9.x86_64.tgz
CHECKSUM=sha256:6d3dce4f7acb18f586469adcb80ab35f3e859f9837786e151cfbc2b3c0f587b2
```
config/artifacts.env
ARTIFACT_URL=https://repo.pigsty.io/pkg/infra/v3.6.0/infra-pkg-v3.6.0.el9.x86_64.tgz
CHECKSUM=sha256:6d3dce4f7acb18f586469adcb80ab35f3e859f9837786e151cfbc2b3c0f587b2

wrap=true cannot be combined with table line numbers. Wrapping misaligns the separate number and code cells. If you provide both, the theme warns and disables wrapping. It suggests lineNos="inline" or removing wrapping.

Folding long code

collapse=N initially shows N lines, with a “show all N lines” button below them. The server emits complete code. After measuring line N, the browser clips the visual display. The complete code remains available without JavaScript, in screen readers, and in print.

Source
```yaml {title="hugo.yml" collapse=8}
baseURL: https://oink.pgsty.com/
title: OINK
defaultContentLanguage: en
languages:
  en:
    languageName: English
    weight: 1
  zh:
    languageName: 简体中文
    weight: 2
params:
  offline_search: true
  ui:
    sidebar_menu_foldable: true
```
hugo.yml
baseURL: https://oink.pgsty.com/
title: OINK
defaultContentLanguage: en
languages:
  en:
    languageName: English
    weight: 1
  zh:
    languageName: 简体中文
    weight: 2
params:
  offline_search: true
  ui:
    sidebar_menu_foldable: true

If the block is no longer than collapse, no button appears. Wrapping and folding can be combined. Folding measures the bottom of the Nth source-line node, so it never cuts a wrapped line in half.

What gets copied

By default, the copy button copies the complete source. For console and shell-session lexers, it copies only commands. It keeps prompted lines but removes prompts and output lines. The example below copies two commands, without $ or output.

Source
```console
$ pig ext list duckdb
name       version  category
pg_duckdb  1.0.0    OLAP
$ pig ext install pg_duckdb
INFO installing pg_duckdb
```
$ pig ext list duckdb
name       version  category
pg_duckdb  1.0.0    OLAP
$ pig ext install pg_duckdb
INFO installing pg_duckdb

To include prompts and output, use copy="all". On ordinary lexers such as bash or sh, copy="command" warns and uses copy="all", because those lexers cannot distinguish prompts, commands, and output. For multiline commands, put a continuation prompt, usually >, on each continuation line. Otherwise, those lines are treated as output and excluded.

If a session-lexer block has no prompt, copying fails. The icon shows an error, and an error is logged to the console. The clipboard stays unchanged. The button never falls back to copying everything.

copy=false removes a block’s copy button. Use it for counter-examples that readers should not paste:

Source
```yaml {title="counter-example: an invalid boolean" copy=false}
params:
  ui:
    image_zoom: sometimes   # wrong: use true or false
```
counter-example: an invalid boolean
params:
  ui:
    image_zoom: sometimes   # wrong: use true or false

To disable copying by default, use params.ui.code_copy: false. An explicit copy fence attribute overrides the default. See Configuration. The copy button uses only an icon. Success or failure changes the icon and announces a localized status. Copied text keeps indentation, blank lines, and Unicode. It removes line numbers and ends with exactly one newline.

To link “see line 3”, give the fence an explicit id, then enable anchorLineNos=true. Line numbers become links of the form #<id>-<line>.

Source
```sql {id="ex-explain" title="explain.sql" lineNos="table" anchorLineNos=true}
EXPLAIN (ANALYZE, BUFFERS)
SELECT relname, n_live_tup
FROM pg_stat_user_tables
WHERE n_live_tup > 1000
ORDER BY n_live_tup DESC;
```

Jump to [line 4](#ex-explain-4).
explain.sql
1
2
3
4
5
EXPLAIN (ANALYZE, BUFFERS)
SELECT relname, n_live_tup
FROM pg_stat_user_tables
WHERE n_live_tup > 1000
ORDER BY n_live_tup DESC;

Jump to line 4.

Without an id, the theme generates an ID that is unique on the page. It depends on the fence’s position. Adding another fence above it changes the ID. Only an author-provided id is a permanent link. IDs must contain no whitespace or control characters. They must not duplicate viewport, tab, panel, title, or line-anchor IDs on the page. Invalid or duplicate IDs warn and use a generated fallback.

Numbered examples

For books or long manuals, use num and caption to create a numbered Book “example” target. xref can reference the target, which also appears in the book-wide list of examples. The author provides the number. The theme never counts examples. id defaults to eg-<num>.

Source
```sql {num="4-1" caption="Bloat ratio per table" #eg-bloat}
SELECT schemaname, relname, n_dead_tup, n_live_tup
FROM pg_stat_user_tables
WHERE n_dead_tup > n_live_tup * 0.2;
```

See {{< xref eg="4-1" anchor="eg-bloat" >}}.
Example 4-1 Bloat ratio per table
SELECT schemaname, relname, n_dead_tup, n_live_tup
FROM pg_stat_user_tables
WHERE n_dead_tup > n_live_tup * 0.2;

See Example 4-1.

num and caption must appear together. A caption without a number is ignored. A number without a caption is removed with a warning. num cannot be combined with the tab attribute tab. For figure, table, and equation numbering and indexing, see publishing books.

A set of fences as tabs

The browser combines consecutive fences with tab into a tab set. Add group to the first fence to enable shared links, synchronization, and saved selections.

Source
```bash {tab="Homebrew" group="oink-install" value="brew"}
brew install hugo
```
```bash {tab="APT" value="apt"}
sudo apt install hugo
```
Homebrew
brew install hugo
APT
sudo apt install hugo

See Tabs for group syntax, URL hashes, synchronization across groups, and tabs in prose.

Common errors

  • Showing a shortcode in the docs: a fence does not stop Hugo from parsing, so a {{< tabs >}} written inside a code block still executes. To display it verbatim, add a comment marker inside each delimiter — {{</* tabs */>}}, and {{%/* steps */%}} for the percent form. Every shortcode shown on this page is written that way.
  • Fences inside fences: four backticks outside, three inside — every “Source” block on this page does it. Add another backtick when the inner block has fences of its own.
  • Attributes go on the info line: a fence’s attributes follow the language on the opening line. Only tables and images take their attributes on the line below. Put them on the next line and you get a visible line of braces.
  • Unknown, unsafe, and theme-reserved attributes warn and are ignored; the message lists the allowed names.
  • Fences in list items: indent them to line up with the item’s content (three spaces after 1.), or the fence leaves the list.

Output

Output Shape
HTML A <div class="td-code"> shell around Chroma’s .highlight/.chroma; copy and fold buttons ship hidden and appear once the script confirms it can run
Print Complete code; copy, fold and the fade are removed; long blocks may break across pages; the title bar stays
Markdown The source fence, {…} attributes and all, emitted as written
RSS A static code block with no buttons

A page with no copy or fold control never loads code-block.js; print, Markdown and RSS never load it.

Parameter reference

Inside the {…} after the language on the opening line, OINK’s own attributes:

title , non-empty string , defaultnone
The visible title bar (usually a filename) and the accessible name
filename , non-empty string , defaultnone
Historical alias of title; both together warn and use filename
copy , all command true false , defaultcommand for session lexers, all otherwise
true is all; command is allowed only on console/shell-session
wrap , boolean , defaultfalse
Visual wrapping, source unchanged; mutually exclusive with table line numbers
collapse , positive integer , defaultnone
Lines shown initially; ignored when the block is shorter
label , non-empty string , defaultderived from the title
Accessible name, not displayed; mutually exclusive with aria-label
id , non-empty token , defaultgenerated
Stable block ID and line-anchor prefix; no whitespace
tab , non-empty string , defaultnone
Tab label, see Tabs; mutually exclusive with num
group , ^[a-z][a-z0-9_-]*$ , defaultnone
On the first fence of a set; enables hash / sync / persistence; requires tab
value , ^[a-z0-9][a-z0-9_-]*$ , defaultnone
Required on every fence of a group, forbidden without one; requires tab
num , [0-9A-Za-z.-]+ , defaultnone
Numbered example (Book eg); must appear with caption
caption , plain text , defaultnone
The numbered example’s caption; must appear with num
class , class list , defaultnone
Appended to the .td-code root element
data-* / aria-* / role , string , defaultnone
Passed through to the root element

title, filename, and label give the block an accessible name and role="group". If you also provide aria-label, aria-labelledby, or role, the theme warns and ignores the conflicting attribute. These three attributes pass through only if the block has neither a title nor label.

The same line accepts Chroma options. The theme passes them to Hugo unchanged:

lineNos , false inline table , defaultfalse
Line-number style; table is mutually exclusive with wrap=true
lineNoStart , positive integer , default1
First displayed number; does not affect how hl_lines counts
hl_lines , lines and ranges , defaultnone
For example "2 4-5", counted over the source lines in the fence
anchorLineNos , boolean , defaultfalse
Line numbers become anchor links prefixed with the block’s id
tabWidth , positive integer , defaultHugo’s default
Spaces a tab expands to

Limits

  • The highlighter cannot be replaced. There is no Shiki, Twoslash, browser highlighting, or runnable playground. For patches, use a diff fence. Chroma’s .gi/.gd style added and removed lines.
  • copy="command" supports only session lexers. Other languages warn and copy everything.
  • Generated IDs are not permanent links. Write id for a link that you plan to share.
  • mermaid, math, chem, markmap, plantuml, echarts, infographic, checksums, filetree, and gallery are not code blocks. Each has its own render hook, without a code shell or copy button.
  • Tabs — the full rules for assembling adjacent fences
  • Include — pull a real file from the repository in as a code block
  • Publishing books — numbered examples, cross references, the list of examples
  • Print — what long code looks like on paper

4.4 - Tabs

Add {tab=} to adjacent fences or tables to create tabs. Add a group to enable links, synchronization, and saved selections.

Use tabs for equivalent alternatives, such as package managers, distributions, YAML / TOML / JSON, or environment variables and configuration keys. Do not use tabs for ordered steps or unrelated content. Readers see only one panel at a time.

The native form uses a tab attribute on adjacent blocks. Use tabs/tab shortcodes for panels with prose, multiple paragraphs, lists, or callouts. Both forms share one runtime, one DOM, and the same keyboard behavior.

Shortest form

Write two adjacent fences with tab, separated only by a blank line.

Source
```bash {tab="Homebrew"}
brew install hugo
```
```bash {tab="Debian / Ubuntu"}
sudo apt install hugo
```
Homebrew
brew install hugo
Debian / Ubuntu
sudo apt install hugo

The server emits two complete titled code blocks. After page loading, the runtime combines adjacent blocks of the same kind into tabs. GitHub, print, and browsers without JavaScript display both complete blocks in sequence.

Groups: links, sync and memory

Write group only on the first block. This enables the public URL hash #<group>-<value>, in-page synchronization, and browser persistence. Every block in the group must have value.

Source
```bash {tab="npm" group="pkgmgr" value="npm"}
npm create hugo-site@latest
```
```bash {tab="pnpm" value="pnpm"}
pnpm create hugo-site
```
```bash {tab="Yarn" value="yarn"}
yarn create hugo-site
```
npm
npm create hugo-site@latest
pnpm
pnpm create hugo-site
Yarn
yarn create hugo-site

value is the machine identifier, matching ^[a-z0-9][a-z0-9_-]*$. tab is the independent human label. The pnpm panel uses #pkgmgr-pnpm. Opening this page with that hash selects the panel.

Groups move together

The set below also uses group="pkgmgr". Changing either set’s package manager changes the other. The selection is saved in localStorage under td-tabs:v1:pkgmgr. It also applies to the same group on other pages.

Source
```bash {tab="npm" group="pkgmgr" value="npm"}
npm run build
```
```bash {tab="pnpm" value="pnpm"}
pnpm build
```
npm
npm run build
pnpm
pnpm build

This set has no yarn panel. If a value is missing, that set keeps its current selection. A set always has a selection. Initial selection follows this order: URL hash, stored value, shortcode default or first block, then first tab. Opening with a hash selects the panel without replacing the reader’s stored preference.

Tables can be tabs too

Use the same attributes on table attribute lines to combine adjacent tables into tabs.

Source
| Parameter | Default |
| --- | --- |
| `shared_buffers` | 25% RAM |
| `max_connections` | 100 |
{tab="PostgreSQL 18" group="pgver" value="pg18"}

| Parameter | Default |
| --- | --- |
| `shared_buffers` | 128MB |
| `max_connections` | 100 |
{tab="PostgreSQL 13" value="pg13"}
PostgreSQL 18
Parameter Default
shared_buffers 25% RAM
max_connections 100
PostgreSQL 13
Parameter Default
shared_buffers 128MB
max_connections 100

Fences and tables cannot form one set, even when adjacent. A native tab set contains only fences or only tables. To combine them, use the shortcode form below.

A label and a filename together

A fence can have both tab and title. The tab bar displays the label. The filename title bar remains inside the panel.

Source
```yaml {tab="YAML" title="hugo.yml" group="conffmt" value="yaml"}
params:
  ui:
    sidebar_menu_foldable: true
```
```toml {tab="TOML" title="hugo.toml" value="toml"}
[params.ui]
sidebar_menu_foldable = true
```
YAML
hugo.yml
params:
  ui:
    sidebar_menu_foldable: true
TOML
hugo.toml
[params.ui]
sidebar_menu_foldable = true

A lone block is just a titled block

A block needs an adjacent block of the same kind to form a tab set. A single block keeps its title instead of creating a one-tab bar.

Source
```ini {tab="on its own"}
listen_addresses = '*'
```
on its own
listen_addresses = '*'

Only blank lines may separate blocks. Prose, headings, or lists between blocks break the set. HTML comments, commonly <!-- prettier-ignore-end -->, also break it. A later block with its own group starts a new set. Only the first block in a set may have this attribute.

Tabs around running text

For panels with paragraphs, lists, callouts, or multiple blocks, use tabs/tab shortcodes. Their bodies use full Markdown.

Source
{{< tabs group="deploy" default="pages" label="Deployment target" >}}
{{< tab label="GitHub Pages" value="pages" >}}
The repository ships `.github/workflows/`; a push to `main` builds and publishes.

> [!NOTE]
> `baseURL` has to be the repository's Pages address.
{{< /tab >}}
{{< tab label="Cloudflare Pages" value="cloudflare" >}}
Connect the repository in the Cloudflare dashboard; the build command is:

```bash
hugo --gc --minify
```
{{< /tab >}}
{{< /tabs >}}

The repository ships .github/workflows/; a push to main builds and publishes.

Note

baseURL has to be the repository’s Pages address.

Connect the repository in the Cloudflare dashboard; the build command is:

hugo --gc --minify

default sets the initial panel. It must match a child’s value and requires group. Without group, value is forbidden. The theme generates tab1, tab2, and later identifiers. These ungrouped tabs switch locally without changing the URL or storage. The shortcode form reports mistakes at build time; the attribute form reports them in the browser.

Output

Output Shape
HTML <div class="td-tabs"> with role="tablist" buttons and panels; every panel is visible until the runtime takes over
Print Consecutive titled static sections, no tab bar
Markdown The fence form keeps the source fence, {tab=} included; the shortcode form emits **Label** plus the body
RSS Same as print — stacked titled sections

Only a page that uses tabs loads tabs.js; print, Markdown and RSS never do.

Parameter reference

Attributes on a fence info line or a table attribute line:

tab , non-empty string , defaultnone
The visible label; on a lone block it is simply that block’s title
group , ^[a-z][a-z0-9_-]*$ , defaultnone
On the first block of a set; enables hash, in-page sync and persistence; requires tab
value , ^[a-z0-9][a-z0-9_-]*$ , defaultnone
Required on every block of a group, forbidden without one; requires tab

The tabs shortcode:

group , ^[a-z][a-z0-9_-]*$ , defaultnone
As above: hash, sync and persistence
default , a child’s value , defaultthe first child
The initially selected panel; requires group
label , plain text , defaultlocalized “Tabs”
Accessible name for the tab bar; not displayed

The tab shortcode:

label , plain text , required
The visible label
value , ^[a-z0-9][a-z0-9_-]*$ , required
Forbidden without a group, where tab1, tab2 … are generated

In a group, panel IDs use <group>-<value>. If the same group appears again on one page, later sets receive -2 and -3 suffixes. Deep links still target the first set. Ungrouped sets have generated IDs. The storage key is td-tabs:v1:<group>. Clicks or keypresses update the hash through replaceState and write storage. Arriving with a hash only changes the selection. Left and right arrows follow RTL direction. These keys and Home/End move and activate tabs, while focus stays on the tab.

Limits

  • Invalid grouping or composition warns during Hugo builds and uses a safe fallback. The theme removes unusable group/value/default settings, ignores stray content, keeps later duplicates, or omits empty sets. Warnings identify the source position.
  • In the attribute form, a sequence without usable value loses synchronization and remains local tabs. Grouping never silently creates an identity.
  • Fences and tables cannot form one set. Use shortcodes to combine prose and code.
  • To collapse long output, use > [!DETAILS] instead of tabs. See Callouts.
  • group names are shared site-wide. Selecting pnpm on page A also selects pnpm in the same group on page B. Choose meaningful group names instead of tabs1.
  • Code blocks — the rest of the fence attributes (title, copy, line numbers, folding)
  • Tables — the rest of the table attribute line
  • Callouts — for folding rather than juxtaposing
  • Steps — tabs inside a procedure

4.5 - Tables

Add attributes to a GFM table for captions, compatibility matrices, field lists, numbering, or tabs. Wide tables scroll within their own regions.

Tables use ordinary GFM pipe syntax. The table render hook wraps each table in a horizontally scrollable region. The {…} attribute line below selects captions, compatibility matrices, field lists, numbering, or tabs. Merged cells, sorting, and filtering are unsupported. If you need these features, change the presentation of the data.

Shortest form

Without an attribute line, the result is an ordinary table. The delimiter row controls alignment. Header cells use th scope="col".

Source
| Component | Port | Purpose |
| --- | :---: | --- |
| PostgreSQL | 5432 | Database |
| Pgbouncer | 6432 | Connection pool |
| Patroni | 8008 | High-availability orchestration |
Component Port Purpose
PostgreSQL 5432 Database
Pgbouncer 6432 Connection pool
Patroni 8008 High-availability orchestration

Wide tables scroll themselves

Wide tables scroll within their own region instead of widening the page. Press Tab to focus the region, then use arrow keys to scroll. Its accessible name is the localized “Scrollable table”.

Source
| Cluster | Role | Version | State | Lag | Connections | Size | Backup |
| --- | --- | --- | --- | --- | --- | --- | --- |
| pg-meta | primary | 18.1 | running | — | 42 | 12 GB | 2026-08-17 |
| pg-test | replica | 18.1 | streaming | 12 ms | 8 | 12 GB | 2026-08-17 |
Cluster Role Version State Lag Connections Size Backup
pg-meta primary 18.1 running — 42 12 GB 2026-08-17
pg-test replica 18.1 streaming 12 ms 8 12 GB 2026-08-17

Captions

{caption="…"} adds a visible <caption>. The caption is plain text and does not number the table. Its title has balanced padding inside the table frame and aligns with the column labels below it.

Source
| Item | Value |
| --- | --- |
| Theme version | v0.8.1 |
| Hugo floor | 0.168.0 Extended |
| Licence | Apache-2.0 |
{caption="Theme facts this site currently builds against"}
Theme facts this site currently builds against
Item Value
Theme version v0.8.1
Hugo floor 0.168.0 Extended
Licence Apache-2.0

Compatibility matrices

Use {.matrix} for support matrices. The first column becomes a row header with th scope="row". The header row and first column stay fixed while scrolling. Other cells are centered unless the delimiter row specifies another alignment. Authors write ✅ and ❌ characters. The theme does not interpret them.

Source
| OS / PG | PG18 | PG17 | PG16 | PG15 | PG14 |
| --- | :---: | :---: | :---: | :---: | :---: |
| EL 9 | ✅ | ✅ | ✅ | ✅ | ✅ |
| EL 8 | ✅ | ✅ | ✅ | ✅ | ✅ |
| Debian 13 | ✅ | ✅ | ✅ | ❌ | ❌ |
| Ubuntu 24.04 | ✅ | ✅ | ✅ | ✅ | ❌ |
{.matrix}
OS / PG PG18 PG17 PG16 PG15 PG14
EL 9 ✅ ✅ ✅ ✅ ✅
EL 8 ✅ ✅ ✅ ✅ ✅
Debian 13 ✅ ✅ ✅ ❌ ❌
Ubuntu 24.04 ✅ ✅ ✅ ✅ ❌

Using the whole canvas

{.full-width} removes the reading-column limit and uses the article’s full width. Use it for many short columns.

Source
| Language | Code | Sidebar | Search | TOC | Print | Status |
| --- | --- | --- | --- | --- | --- | --- |
| 简体中文 | `zh` | ✅ | ✅ | ✅ | ✅ | Reviewed |
| English | `en` | ✅ | ✅ | ✅ | ✅ | Reviewed |
{.full-width}
Language Code Sidebar Search TOC Print Status
简体中文 zh ✅ ✅ ✅ ✅ Reviewed
English en ✅ ✅ ✅ ✅ Reviewed

Technical wide tables

.wide in OINK 1.4.0

The short marker is included in the release. Existing technical-table markup remains supported.

Technical tables containing long configuration keys, environment variables, or identifiers can produce awkward token breaks and very tall rows when forced into the reading column. Adding {.wide .expand} to the attribute line opts the inner table into natural intrinsic sizing (width: max-content), scrolling horizontally within its wrapper when it exceeds the available space.

Source
| Variable | Required | Default | Configuration key | Description |
| --- | --- | --- | --- | --- |
| `HG_PD_GRPC_HOST` | Yes | None | `grpc.host` | Advertised gRPC hostname/IP; use container hostnames within a container network. |
| `HG_PD_INITIAL_STORE_COUNT` | No | `1` | `pd.initial-store-count` | Minimum Store count needed for cluster availability. |
| `HG_PD_ACTUATOR_EXPOSURE` | No | `health,metrics,prometheus` | `management.endpoints.web.exposure.include` | Public Actuator endpoint allowlist; `*` is forbidden. |
{.wide .expand}
Variable Required Default Configuration key Description
HG_PD_GRPC_HOST Yes None grpc.host Advertised gRPC hostname/IP; use container hostnames within a container network.
HG_PD_INITIAL_STORE_COUNT No 1 pd.initial-store-count Minimum Store count needed for cluster availability.
HG_PD_ACTUATOR_EXPOSURE No health,metrics,prometheus management.endpoints.web.exposure.include Public Actuator endpoint allowlist; * is forbidden.

Difference from .full-width

  • .full-width controls the outer canvas: it expands the table container to break out of the reading column into the full width of the article layout. It suits tables with many short columns.
  • .wide controls the inner table sizing: it sets the table to its natural intrinsic width (width: max-content), using horizontal scrolling to accommodate wide content. It suits tables with long technical identifiers.
  • Both can be combined: {.full-width .wide} expands the container to the full canvas while allowing the inner table to keep its natural wide measure.

Applicability boundaries

  • Explicit opt-in: Ordinary tables keep their existing layout. Only tables marked .wide use overflow-wrap: break-word, word-break: normal, and a soft max-width: 40ch body-cell bound. Very long tokens may still wrap; the bound is not a fixed column width.
  • Static outputs: This policy applies only on screen. Print and RSS use static wrappers and retain their existing layout; Markdown preserves the authored GFM table and attribute line. The marker has no effect on .fields definition lists.

Expand selected tables

.expand in OINK 1.4.0

The opt-in reader is included in the release.

Add .expand only to tables that benefit from a larger reading area. A small expand icon sits at the top-right corner: desktop hover or keyboard focus reveals it; touch devices keep it visible. Click once to open the table, then use Close or Escape to return to the same reading position. Text size, links, and selection stay intact; wide content still scrolls horizontally on small screens.

Source
| Package | Version | Role |
| --- | --- | --- |
| `pgvector` | `0.8.1` | Vector similarity search |
| `pg_stat_statements` | `1.11` | Statement statistics |
{.wide .expand}
Example extension packages
Package Version Role
pgvector 0.8.1 Vector similarity search
pg_stat_statements 1.11 Statement statistics

Goldmark accepts multiple classes: {.wide .expand} and {class="wide expand"} are equivalent. Enable markup.goldmark.parser.attribute.block: true and put the attribute line immediately after the last row, without a blank line.

.wide controls layout; .expand enables the reading action. Either works alone. You can also combine .expand with .full-width, .matrix, captions, numbering, or tabs. There is no automatic size threshold or site-wide switch. The existing .td-table--wide spelling remains compatible with .wide.

Only pages with opted-in tables load the small reader script. Without JavaScript, a loaded script, or native dialog support, the complete inline table remains readable without a dead button. Print and RSS keep static tables; Markdown keeps the author syntax. Browser printing restores an open table to the article first. Tables whose headers, aria-labelledby, or aria-describedby reference nodes outside the table stay inline; custom styles depending on article ancestors should be reviewed in the reader.

Field lists

{.fields} converts a table to a definition list. The first column is the name, the last is the description, and middle columns are metadata. Use it for configuration keys, command flags, and API fields. See Fields for the complete syntax.

Source
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `offline_search` | boolean | `false` | Build the local search index |
| `page_width` | string | `normal` | Width of the reading column |
{.fields meta="type default"}
offline_search , boolean , defaultfalse
Build the local search index
page_width , string , defaultnormal
Width of the reading column

Numbered tables

For books or long manuals, add num, with optional #id and caption. The theme wraps the table in a <figure> with a localized “Table N.” label and registers it as a Book target. xref can reference it, and it appears in the book-wide table list. Authors provide numbers. The theme never counts. id defaults to tbl-<num>.

Source
| Isolation level | Dirty read | Non-repeatable read | Phantom read |
| --- | --- | --- | --- |
| Read committed | no | yes | yes |
| Repeatable read | no | no | yes |
| Serializable | no | no | no |
{#tbl-iso num="9-1" caption="Anomalies each PostgreSQL isolation level permits"}

See {{< xref tbl="9-1" anchor="tbl-iso" >}}.
Isolation level Dirty read Non-repeatable read Phantom read
Read committed no yes yes
Repeatable read no no yes
Serializable no no no
Table 9-1 Anomalies each PostgreSQL isolation level permits

See Table 9-1.

Tables as tabs

Adjacent tables with {tab="…"} form a tab set, using the same rules as adjacent fences. group on the first table enables URL hashes, synchronization, and saved selections. Each later table needs value. See Tabs for the complete rules.

Source
| Directory | Contents |
| --- | --- |
| `content/` | Pages |
| `data/` | Landing and release data |
{tab="Content" group="repo-layout" value="content"}

| Directory | Contents |
| --- | --- |
| `assets/` | SCSS and image resources |
| `static/` | Files copied verbatim |
{tab="Assets" value="assets"}
Content
Directory Contents
content/ Pages
data/ Landing and release data
Assets
Directory Contents
assets/ SCSS and image resources
static/ Files copied verbatim

Output

Output Shape
HTML A focusable <div class="td-table-scroll"> around the <table>; matrix and full-width are modifier classes on that wrapper
Print Complete static content with a keyboard-scrollable region in the browser preview; print-media CSS expands the region for paper output
Markdown The source table and its attribute line, emitted as written
RSS The complete static table

Ordinary tables load no script; .expand loads its reader only in HTML, and tabbed tables load the tabs runtime.

Parameter reference

The attribute line on the row below the table:

.full-width , marker , defaultnone
Exceed the reading column and use the article canvas
.wide , marker , defaultnone
Sizes the inner table to max-content; suits technical tables with long identifiers
.expand , marker , defaultnone
Add an optional corner action for reading the table in a larger dialog
.matrix , marker , defaultnone
First column as row header, header and first column pinned, other cells centred
.fields , marker , defaultnone
Render as a definition list, see Fields
caption , plain text , defaultnone
Visible table caption; on .fields it labels the list
meta , role list , defaultnone
Names the meaning of the middle .fields columns: type required default -; requires .fields
#id , identifier , defaulttbl-<num> when num is set
[A-Za-z][A-Za-z0-9_.:-]*; lands on the <table>, or on the <figure> for a numbered table
num , string , defaultnone
[0-9A-Za-z.-]+; registers a Book table target and prefixes the caption with “Table N.”
tab / group / value , see Tabs , defaultnone
Adjacent tables become a tab set
class , class list , defaultnone
Left on the <table> for site CSS
data-* / aria-* , string , defaultnone
Passed through

style, on*, and other keys warn and are ignored.

Limits

  • .fields cannot be combined with .matrix, .full-width, .expand, or num. num and tab are mutually exclusive. group/value require tab, and meta requires .fields.
  • The attribute line must immediately follow the table. A blank line makes the braces appear as text. To prevent formatters moving the line, wrap it in <!-- prettier-ignore-start --> / <!-- prettier-ignore-end -->.
  • GFM tables do not support merged cells, sorting, or filtering. Split complex tables with merged headers into two tables or use a matrix.
  • Cells cannot contain block content. Use fields/field shortcodes for multiple paragraphs, lists, or fences.
  • .matrix centering uses CSS. Explicit delimiter-row alignment takes precedence.
  • Fields — everything {.fields} can do
  • Tabs — adjacent tables as a tab set
  • Publishing books — numbered tables, cross references, the list of tables
  • Code blocks — where attributes go on the info line instead of the next line

4.6 - Fields

Add {.fields} to a table to document configuration keys, command flags, and API fields. Entries adapt to narrow screens and have individual anchors.

Fields display named values, metadata, and descriptions as a responsive definition list. Each name has its own line. Small chips beside it show type, required status, and default value. The description starts on the next line. Each entry has an anchor. Use fields for configuration keys, command flags, and API fields. Use a plain table to compare many rows across the same columns. Use steps for sequences of actions.

There are two forms. Use a plain table with {.fields} by default. Use fields/field shortcodes for descriptions with multiple paragraphs, lists, or code blocks. Both forms display the same entries.

Shortest form

Write a pipe table with at least two columns, then put {.fields} on the next line. The first column is the name. The last is the description. Middle columns are metadata labeled with their header text.

Source
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `offline_search` | boolean | `false` | Build the local search index and enable the command palette |
| `offline_search_max_results` | integer | `10` | Maximum number of search results |
| `page_width` | string | `normal` | Reading column width: `normal` `wide` `full` |
{.fields}
offline_search , Typeboolean , Defaultfalse
Build the local search index and enable the command palette
offline_search_max_results , Typeinteger , Default10
Maximum number of search results
page_width , Typestring , Defaultnormal
Reading column width: normal wide full

Metadata appears as “Header: value”. The theme does not infer meaning from headers. Type is only a label. The next section explains standard chips. Cells accept inline Markdown, including code, emphasis, and links. Empty middle cells are omitted.

Semantic columns with meta=

meta assigns a role to each middle column, in order: type, required, default, or - for a plain header label. With this attribute, tables display the same chips as shortcodes.

Source
| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `baseURL` | string | yes | | Site address, subpath included |
| `title` | string | yes | | Site name, shown in the navbar and the tab |
| `defaultContentLanguage` | string | | `en` | Default language; decides which language unprefixed paths belong to |
{.fields meta="type required default"}
baseURL , string , required
Site address, subpath included
title , string , required
Site name, shown in the navbar and the tab
defaultContentLanguage , string , defaulten
Default language; decides which language unprefixed paths belong to

The rules:

  • meta should specify one role per middle column: the total column count minus two. Too many or too few roles warns and ignores meta. Review the warning and rendered fallback before publication.
  • Every nonempty required cell means true. “yes”, “是”, and “✔” all display the untranslated required chip. Empty cells display nothing. Leave optional fields empty, because no and 否 also mean true.
  • type and default cells without their own inline markup receive code formatting, as in the shortcode form.
  • Semantic chips always appear in this order: type, required, default, regardless of column order. - columns follow in their original order.

Combine - with semantic roles to keep a custom label:

Source
| Environment variable | Type | Scope | Description |
| --- | --- | --- | --- |
| `HUGO_MODULE_WORKSPACE` | string | build | Points at `go.work` so the theme resolves from a local checkout |
| `HUGO_ENV` | string | build | Selects production mode; OINK fingerprints CSS/JS assets. Use `--minify` to minify HTML |
{.fields meta="type -"}
HUGO_MODULE_WORKSPACE , string , Scopebuild
Points at go.work so the theme resolves from a local checkout
HUGO_ENV , string , Scopebuild
Selects production mode; OINK fingerprints CSS/JS assets. Use --minify to minify HTML

Labels and container IDs

caption sets the list’s visible label and accessible name. id identifies the outer container for links or styles.

Source
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `image_zoom` | boolean | `false` | Turn image zoom on |
| `featured_image` | string | `none` | Article image mode: `none`, `banner`, `wash` or `hero` |
{.fields caption="params.ui" id="zoom-params" meta="type default"}

params.ui

image_zoom , boolean , defaultfalse
Turn image zoom on
featured_image , string , defaultnone
Article image mode: none, banner, wash or hero

Every entry is linkable

Each entry has a field-<name> anchor. A self-link icon appears beside the name on hover. For example, page_width in the first table has #field-page_width. Share this link to refer to that entry.

Duplicate names on a page receive -2 and -3 suffixes, as Goldmark does for duplicate headings. Only HTML generates these anchors. Print and RSS combine pages into one document, where these anchors would collide.

The shortcode form

If a description needs multiple paragraphs, a list, or a code block, use fields/field. A table cell cannot contain these blocks.

Source
{{< fields label="Common pig flags" >}}
{{< field name="--config" type="path" required=true >}}
Path to the configuration file. Relative paths resolve against the working
directory.

When `PIG_CONFIG` is also set, the command-line flag wins.
{{< /field >}}
{{< field name="--log-level" type="string" default="info" >}}
Log level, from low to high:

- `debug`: print every remote call
- `info`: the default
- `error`: output only on failure
{{< /field >}}
{{< field name="--dry-run" type="boolean" default=false >}}
Print what would happen and change nothing:

```bash
pig ext install pg_duckdb --dry-run
```
{{< /field >}}
{{< /fields >}}

Common pig flags

--config , path , required

Path to the configuration file. Relative paths resolve against the working directory.

When PIG_CONFIG is also set, the command-line flag takes precedence.

--log-level , string , defaultinfo

Log level, from low to high:

  • debug: print every remote call
  • info: the default
  • error: output only on failure
--dry-run , boolean , defaultfalse

Print what would happen and change nothing:

pig ext install pg_duckdb --dry-run

required=true and default=false are booleans. Do not quote them. default accepts any scalar. Both default=0 and default="" display their values; the empty string appears as "". If you omit default, the chip is omitted. Every field needs a nonempty body and must be a direct child of fields.

Which form to use

Situation Use
One-sentence descriptions that fit in a table cell table + {.fields}
Descriptions with paragraphs, lists or code blocks the fields/field shortcode
Readers comparing many rows across the same columns a plain table, not a field list
Content that is a sequence of actions Steps

The table form stays readable on GitHub and remains a table in OINK’s Markdown output. It is therefore the default.

Output

Output Shape
HTML <div class="td-fields"> around a semantic <dl>; entries carry #field-<name> anchors and self-links
Print The complete definition list, without entry anchors
Markdown The table form keeps the source table; the shortcode form emits a bulleted list of “name — type; required; default: value” plus the indented description
RSS The complete static <dl>, without entry anchors

No script is loaded.

Parameter reference

The table attribute line, on the row below the table:

.fields , marker , defaultnone
Required; renders the table as a field list
meta , role list , defaultnone
Space-separated type required default -; one per middle column; semantic roles cannot repeat
caption , plain text , defaultnone
Visible label and the list’s accessible name
id , identifier , defaultnone
ID of the outer container
class , class list , defaultnone
Passed through for site CSS
data-* / aria-* , string , defaultnone
Passed through

The fields shortcode:

label , non-empty string
Visible label; the same thing the table’s caption does
id , identifier
Container ID; no whitespace, quotes, <, > or &
class / data-* / aria-* , string
The same policy as the table attribute line

The field shortcode:

name , non-empty string , required
The field name
type , non-empty string
Type label such as boolean, string[], duration
required , boolean
true shows the untranslated required chip; defaults to false
default , scalar
String / boolean / integer / float; false, 0 and "" all display

Limits

  • The first column must be nonempty and unique within each table. Empty or duplicate names warn and skip the row.
  • .fields cannot be combined with .matrix, .full-width, or num. meta requires a table with .fields.
  • Table cells cannot contain block content. Use shortcodes for paragraphs, lists, and fences.
  • required and default remain untranslated English API terms in every language. They are contract terms, not interface text.
  • Fields do not support kind, since, deprecated, location, per-field links, or nested structures. They do not parse TypeScript or OpenAPI schemas at build time.
  • Tables — the rest of the attribute line and the exclusion rules
  • Configuration — the full site parameter table, itself a field list
  • Front matter — the full front matter table
  • Steps — ordered actions do not belong in a field list

4.7 - Steps

Add {.steps} to an ordered list for a numbered procedure. Use the steps shortcode when each step needs a table-of-contents heading.

Steps display an ordered list with numbered dots and a connecting line. Add a {.steps} marker line to an ordered list. CSS draws the dots and line. No script loads. Use steps for ordered procedures. Use plain lists or cards for unordered items.

There are two forms. Use an ordered list with {.steps} by default. Use {{% steps %}} when each step needs a heading in the table of contents.

Optional presentation

Use {.steps .td-steps-compact} for a short procedure, or steps variant="compact" for the heading form. OINK 1.4.0 supports these opt-in variants; compare the examples. Numbering, nesting, and heading anchors retain their existing meaning.

Shortest form

Write 1. for each item so Markdown counts automatically. Then you can add, remove, or reorder steps without renumbering. Content always uses three spaces of indentation.

Source
1. Install Hugo Extended
1. Clone OINK Starter
1. Start the local preview
{.steps}
  1. Install Hugo Extended
  2. Clone OINK Starter
  3. Start the local preview

{.steps} must immediately follow the last list line. A blank line makes it display as text with braces.

What goes in a step

List items accept block content: paragraphs, fenced code, callouts, tables, nested lists, and images. Align content with the item’s content column, using three spaces.

Source
1. Clone OINK Starter; it is the small project template for the theme.

   ```bash
   git clone https://github.com/pgsty/oink-starter my-docs
   cd my-docs
   ```

1. Start the local server.

   ```bash
   hugo server
   ```

   > [!NOTE]
   > The first build fetches the theme through the Go module proxy, which needs
   > Go on the machine.

1. Replace three things and it is your site.

   | Where | Replace with |
   | --- | --- |
   | `title` in `hugo.yml` | your site name |
   | `baseURL` in `hugo.yml` | your domain |
   | `content/` | your content |
{.steps}
  1. Clone OINK Starter; it is the small project template for the theme.

    git clone https://github.com/pgsty/oink-starter my-docs
    cd my-docs
  2. Start the local server.

    hugo server
    Note

    The first build fetches the theme through the Go module proxy, which needs Go on the machine.

  3. Replace three things and it is your site.

    Where Replace with
    title in hugo.yml your site name
    baseURL in hugo.yml your domain
    content/ your content

Shortcodes in {{< … >}} form, such as tabs, cards, and badges, also work inside list items. The {{% … %}} form does not. See Limits.

Splitting one step per platform

If a step differs by platform, put adjacent {tab=} fences inside that list item. They still form a tab set.

Source
1. Install Hugo Extended.

1. Install the dependencies:

   ```bash {tab="EL / RHEL" group="stepdemo" value="rpm"}
   sudo dnf install golang git
   ```
   ```bash {tab="Debian / Ubuntu" value="deb"}
   sudo apt install golang-go git
   ```

1. Run `hugo server` to preview.
{.steps}
  1. Install Hugo Extended.

  2. Install the dependencies:

    EL / RHEL
    sudo dnf install golang git
    Debian / Ubuntu
    sudo apt install golang-go git
  3. Run hugo server to preview.

Continuing the numbering

If prose interrupts a procedure, start the next group with its actual step number. Markdown emits start and continues numbering from that value, up to 40.

Source
4. Configure `baseURL` and the deployment workflow.
1. Push to `main` and wait for GitHub Actions to finish.
{.steps}
  1. Configure baseURL and the deployment workflow.
  2. Push to main and wait for GitHub Actions to finish.

Steps with headings

For long procedures with linked headings in the table of contents, use {{% steps %}}. The body uses page-level Markdown without indentation. Each direct child heading is one step. The three headings below appear in this page’s table of contents.

Source
{{% steps %}}

### Install the toolchain {#install-toolchain}

You need Hugo Extended ≥ 0.168.0 and Go.

### Run the server {#run-server}

{{< tabs group="oink-os" default="macos" >}}
{{< tab label="macOS" value="macos" >}}
`brew install hugo go`
{{< /tab >}}
{{< tab label="Debian" value="debian" >}}
`sudo apt install hugo golang-go`
{{< /tab >}}
{{< /tabs >}}

### Publish {#publish}

Push to `main`; the workflow the repository ships builds and publishes.

{{% /steps %}}

Install the toolchain

You need Hugo Extended ≥ 0.168.0 and Go.

Run the server

brew install hugo go

sudo apt install hugo golang-go

Publish

Push to main; the workflow the repository ships builds and publishes.

This is the theme’s only {{% … %}} shortcode. The percent form gives its body to Goldmark as page-level Markdown. This allows headings in the table of contents and container shortcodes such as tabs, cards, and fields inside steps. It cannot nest inside list items or other percent containers.

Use the same heading level for every step in a procedure. Never nest steps inside another steps container.

Which form to use

Situation Use
A step is a sentence or two plus a command ordered list + {.steps}
Each step needs a heading, a link and a place in the TOC {{% steps %}}
A step must contain a tabs, cards or fields container {{% steps %}}
The procedure itself has to nest inside another list item ordered list + {.steps}

Output

Output Shape
HTML The native form is <ol class="steps"> with numbers and rule drawn in CSS; the shortcode form is <div class="td-steps"> plus headings
Print Numbers and content unchanged, the rule stays
Markdown The source as written: an ordered list plus {.steps}, or headings plus bodies
RSS A static list or titled sections

Steps load no script. Disabling JavaScript does not change the result.

Parameter reference

OINK 1.4.0 supports the optional variant="compact" on the full form. Follow these conventions:

{.steps} , Whereline below the ordered list
Required; has no effect on an unordered list
1. , Whereevery item
Let Markdown count; the content indent is always three spaces
4. (first item) , Wherefirst item
Emits <ol start="4"> and continues from 4; supported for 2–40
{{% steps %}} , Wherearound a set of headings
Direct child headings (##–######) are the steps; the body is not indented

Limits

  • Do not put {{% … %}} inside list items. A percent shortcode’s multiline output truncates the list. To put a container in a step, use the shortcode form for the whole procedure.
  • {{% steps %}} cannot be inside list items or other percent containers.
  • Put the marker immediately after the list, with no blank line before {.steps}. With formatters such as Prettier, wrap it in <!-- prettier-ignore-start --> / <!-- prettier-ignore-end -->.
  • {.steps} applies only to ordered lists. A - list has no numbers.
  • Steps do not collapse, expand, or track progress. There is no “done” state.
  • Tabs — commands split per platform
  • Callouts — prerequisites and warnings inside a step
  • Code blocks — the commands in a step
  • Cards — “what next” once the procedure is done

4.8 - Cards

Add {.cards} to a link list to create navigation cards. Use the shortcode for icons, badges, or images.

Cards provide related links. Each card has a linked title and a sentence. The grid adapts to the container width. Use cards for section landing pages, reading recommendations, or a few related entry points. Use paragraphs for prose and a gallery for image collections.

Optional presentation

Use {.cards .td-cards-list} for a single-column resource list with shared dividers, or cards variant="list" for the full form. OINK 1.4.0 supports these opt-in variants; compare the examples. The default remains a responsive grid.

Shortest form

Add {.cards} to a link list to make a card grid. The link becomes the title. Text after — becomes the description.

Source
- [Get started](/docs/start/) — Start with OINK Starter, preview locally, then replace the site details with your own.
- [Authoring](/docs/write/) — How pages are organized and which front matter keys exist.
- [Customization](/docs/customize/) — Navigation, search, branding, languages.
{.cards}
  • Get started — Start with OINK Starter, preview locally, then replace the site details with your own.
  • Authoring — How pages are organized and which front matter keys exist.
  • Customization — Navigation, search, branding, languages.

The whole card is clickable. There is no columns parameter. The number of columns follows the container width and decreases to one on narrow screens.

Title-only cards

The description is optional. Put one link on each line and {.cards} after the list.

Source
- [Callouts](/docs/components/callout/)
- [Tabs](/docs/components/tabs/)
- [Steps](/docs/components/steps/)
- [Fields](/docs/components/fields/)
{.cards}

Loose lists and longer descriptions

If a sentence is insufficient, use a loose list. Put the link and description in separate paragraphs, with blank lines between items. The title appears above the description. {.cards} must immediately follow the last paragraph, with no blank line between them.

Source
- [Front matter](/docs/write/frontmatter/)

  Every page parameter is defined here exactly once: type, default, accepted
  values, and the page that explains it.

- [Configuration](/docs/customize/config/)

  Site parameters grouped by feature, each row linking back to the guide that
  explains it.
{.cards}
  • Front matter

    Every page parameter is defined here exactly once: type, default, accepted values, and the page that explains it.

  • Configuration

    Site parameters grouped by feature, each row linking back to the guide that explains it.

Icons and badges

Link lists do not support icons, badges, images, or descriptions with multiple paragraphs. Use the cards / card shortcode for these features. icon takes exactly one Font Awesome class pair. badge is plain text.

Source
{{< cards >}}
{{< card title="Get started" link="/docs/start/" icon="fa-solid fa-rocket" badge="start here" >}}
Use OINK Starter and establish a local preview before customizing.
{{< /card >}}
{{< card title="Release and download pages" link="/docs/write/releases/" icon="fa-solid fa-box-open" badge="v0.5" >}}
A release card from `release_url` and `date`, plus a `checksums` block for download assets.
{{< /card >}}
{{< card title="Keyboard navigation" link="/docs/customize/keyboard/" icon="fa-solid fa-keyboard" >}}
Site-wide shortcuts and focus order.
{{< /card >}}
{{< /cards >}}
Get startedstart here

Use OINK Starter and establish a local preview before customizing.

An invalid Font Awesome class pair warns and is removed in ordinary previews. Review the warning and rendered fallback before publication.

Markdown bodies

A card body uses page-level Markdown, including inline code, emphasis, links, and lists. Parameters such as title and badge are plain text. They are not parsed as Markdown.

Source
{{< cards >}}
{{< card title="Hugo Module" icon="fa-brands fa-golang" >}}
`hugo mod get github.com/pgsty/oink`. The recommended way; upgrading is one
version line.
{{< /card >}}
{{< card title="Git submodule" icon="fa-solid fa-code-branch" >}}
No Go installation needed:

- `git submodule add`
- the theme lands in `themes/oink`
{{< /card >}}
{{< /cards >}}
Hugo Module

hugo mod get github.com/pgsty/oink. The recommended way; upgrading is one version line.

Git submodule

No Go installation needed:

  • git submodule add
  • the theme lands in themes/oink

A card without link renders as a bold title and produces no link.

Cards with images

image uses the same resolution order as ![alt](src): page resource → current section resource → global resource in assets/ → static path /images/… → remote URL. Local resources include their intrinsic size, which prevents layout changes during loading.

With image, provide exactly one alternative-text source. Use image_alt="…" for informative images or decorative=true for decorative images. If you provide both, the theme warns and keeps the alt text. If you provide neither, it warns and makes the image decorative. Review the warning and rendered fallback before publication.

Source
{{< cards >}}
{{< card title="The OINK shell" link="/docs/about/features/" image="/images/oink.webp" image_alt="An OINK documentation page: sidebar, article and table of contents" >}}
Sidebar, article, table of contents — each can be turned off on its own.
{{< /card >}}
{{< card title="Release notes" link="/docs/write/releases/" image="/images/releasenote.webp" decorative=true >}}
A decorative cover: `decorative=true` emits an empty alt and screen readers skip it.
{{< /card >}}
{{< /cards >}}
An OINK documentation page: sidebar, article and table of contents

Sidebar, article, table of contents — each can be turned off on its own.

A decorative cover: decorative=true emits an empty alt and screen readers skip it.

Card images do not take part in image zoom — the whole card is already a link.

Automatic cards on section pages

A section landing page (_index.md) needs no manual card list. The theme generates cards from each child page’s title, description, and icon. This site enables section cards globally in hugo.yml:

hugo.yml
params:
  ui:
    section_index: cards # list | cards

A section can override this setting in its front matter. Use cascade to apply the setting to the whole subtree:

content/docs/customize/_index.md
section_index: list

Automatic and manual cards share the td-content-card styles. Only their data source differs. Do not manually list child pages on a section page. The list can become inconsistent with the sidebar. Use manual cards for other sets, such as external links or recommendations from other sections. Configuration defines these keys.

Which form to use

What you want Which form
A grid of links with one-sentence descriptions {.cards} link list
Icons, badges, images cards / card shortcode
Lists, code or several paragraphs in the description cards / card shortcode
A card with no link cards / card shortcode
This section’s child pages nothing at all — section_index: cards

The native form stays readable as a link list on GitHub. Shortcodes do not. Use the native form when it supports your content.

Output

Output Shape
HTML Native form: <ul class="cards">. Shortcode form: <div class="td-content-cards"> with one <article class="td-content-card"> each. Both are pure CSS grids and load no script
Print The native form stacks; the shortcode form collapses to two columns; in both, a card avoids breaking across pages
Markdown The native form keeps the link list; the shortcode form emits - [Title](link) (badge) — description
RSS The same markup as HTML — a readable list of links without site CSS

Parameter reference

The native form:

{.cards} , list attribute line , default—
On the line after an unordered list; unordered lists only
First link in an item , Markdown link , default—
The card title and the whole card’s click target
Everything else , Markdown , default—
The description: after — in a tight list, its own paragraph in a loose one

card parameters (cards also accepts an optional variant="list"):

title , plain text , default—
Required, non-empty. The card title
link , URL , default—
Site path, relative path, http(s):, mailto:; external links get rel="noopener"
icon , Font Awesome class pair , default—
For example fa-solid fa-rocket; a malformed value warns and is dropped
badge , plain text , default—
A small label beside the title
image , image source , default—
Page resource / global resource / static path / remote URL
image_alt , plain text , default—
With image, exactly one of this and decorative
decorative , boolean , defaultfalse
true marks a decorative image and emits an empty alt
Body , Markdown , default—
The card description

There is no cols, columns, accent, desc, or color parameter. Unknown parameters warn and are ignored.

Limits

  • {.cards} supports only unordered lists. It has no effect on ordered lists.
  • {.cards} must immediately follow the list. A blank line or indentation inside a list item silently removes the marker. The build succeeds, but the result stays a list. Check this line first if no card grid appears.
  • A card must be inside cards. Alone or inside another shortcode, it warns and is skipped.
  • The grid adapts to its container. Its column count cannot be configured. Only automatic section cards accept a count, through params.ui.section_index_columns.
  • If a description exceeds two lines, use a paragraph or a callout.

4.9 - FileTree

Write directory listings in a filetree fence. Use indentation for depth, trailing slashes for directories, and aligned comments for explanations.

A filetree fence contains a directory listing. Indentation sets depth, a trailing / marks a directory, and text after # is a comment. Use it to explain the relevant directory structure, with one annotation per entry. If readers need to copy the exact listing, use an ordinary code block.

Shortest form

Source
```filetree
- content/
  - _index.md
  - docs/
  - blog/
- hugo.yml
- go.mod
```
  • content/
    • _index.md
    • docs/
    • blog/
  • hugo.yml
  • go.mod

Bullets (-, *, +) are optional and do not change the result. Entries with children are directories. For entries without children, a trailing / marks a directory.

Adding comments

Text after the first whitespace-preceded # is a comment. Comments appear in an aligned column on the right. They are plain text, so Markdown appears literally. Use \# for a literal hash.

Source
```filetree
- content/          # every page, both languages in one directory
  - docs/          # the documentation tree you are reading
  - blog/           # release notes and articles
- assets/scss/      # the site's own SCSS, overriding theme variables
- layouts/          # site-level template overrides, the fewer the better
- static/images/    # images that need no build-time processing
- hugo.yml         # site configuration: languages, menus, params.ui
```
  • content/every page, both languages in one directory
    • docs/the documentation tree you are reading
    • blog/release notes and articles
  • assets/scss/the site's own SCSS, overriding theme variables
  • layouts/site-level template overrides, the fewer the better
  • static/images/images that need no build-time processing
  • hugo.ymlsite configuration: languages, menus, params.ui

At build time, the widest row determines the comment column’s starting position. Each # aligns even if the source does not. The comment column occupies at least three tenths and at most half of the panel. Drag the dashed splitter to change the width. Alternatively, focus it with Tab and move it with arrow keys. Home / End move it to the limits.

By default (wrap=false), long names and comments are truncated with an ellipsis within their columns. Hover to see the full text in title. Only trees with comments load JavaScript. This script controls the splitter.

Source
```filetree {title="truncation in both columns"}
- runbooks/
  - a-deliberately-long-runbook-filename-for-a-failover-drill.md  # an equally overlong comment, kept on one line so it has to be clipped inside the comment column
  - restart.md                                                    # short
```

truncation in both columns

  • runbooks/
    • a-deliberately-long-runbook-filename-for-a-failover-drill.mdan equally overlong comment, kept on one line so it has to be clipped inside the comment column
    • restart.mdshort

Wrap long comments

Use wrap=true on the filetree fence to show complete comments in their column. Rows grow as needed, including for unbroken identifiers. The default wrap=false keeps compact rows with ellipsis. Names, folding, splitter controls, mobile layout, and expanded Print output retain their existing behavior.

Source
```filetree {title="Complete storage comments" wrap=true}
- storage/
  - metadata/ # Schema definitions: property keys, vertex and edge labels, indexes, and counters.
  - graph/    # Graph data: vertices, edges, index tables, and OLAP analysis data.
```

Complete storage comments

  • storage/
    • metadata/Schema definitions: property keys, vertex and edge labels, indexes, and counters.
    • graph/Graph data: vertices, edges, index tables, and OLAP analysis data.

Invalid wrap values warn and fall back to false. Review the warning and fallback before publication.

Title bars

The fence attribute {title="…"} adds a title bar above the tree. Without it, no title bar appears.

Source
```filetree {title="the oink.pgsty.com repository root"}
- content/          # pages
- assets/           # resources that take part in the build
- data/             # data for the home page, landings and downloads
- layouts/          # template overrides
- static/           # files copied verbatim
- tests/            # Playwright and node --test
- hugo.yml
- go.mod            # the theme, imported as a Hugo Module
- Makefile          # make d / make b / make c
```

the oink.pgsty.com repository root

  • content/pages
  • assets/resources that take part in the build
  • data/data for the home page, landings and downloads
  • layouts/template overrides
  • static/files copied verbatim
  • tests/Playwright and node --test
  • hugo.yml
  • go.modthe theme, imported as a Hugo Module
  • Makefilemake d / make b / make c

Indentation and depth

Indentation sets depth. Two spaces, four spaces, and tabs, counted as four columns, are supported. Indentation widths can vary within one tree, but every level that you return to must have been opened previously. You can paste complete tree command output, including the root and summary lines. The summary is removed.

Source
```filetree
content/docs
├── about
│   ├── _index.md
│   └── features.md
├── components
│   ├── filetree.md
│   └── image
│       └── index.md
└── _index.md

3 directories, 5 files
```
  • content/docs
    • about
      • _index.md
      • features.md
    • components
      • filetree.md
      • image
        • index.md
    • _index.md

Returning to an unopened indentation level warns and skips that line. The warning identifies the line inside the fence.

Folding and explicit types

Directories with children are open by default. {open=false} makes a directory initially closed. Directories use native <details> and work with the keyboard without JavaScript. Only directories accept open. An entry without children or a trailing / is a file. {type=dir} forces a directory, and {type=file} forces a file.

Source
```filetree {title="the content directory"}
- content/
  - docs/                # the documentation tree
    - components/         # 22 component pages    {open=false}
      - callout.md
      - filetree.md
      - image/            # page bundle: body + images  {type=dir}
    - customize/          # site-level configuration    {open=false}
      - config.md
  - blog/
    - release.md
```

the content directory

  • content/
    • docs/the documentation tree
      • components/22 component pages
        • callout.md
        • filetree.md
        • image/page bundle: body + images
      • customize/site-level configuration
        • config.md
    • blog/
      • release.md

Icons and tones

The theme infers icons from names. Directory icons follow the open state. Files are matched first by full filename (LICENSE, Makefile, go.mod, package.json, .gitignore …), then by extension (md yml toml json sh py go js sql css png svg pdf zip …). Other files use a generic file icon.

{icon=…} overrides the icon with exactly one Font Awesome class pair. {tone=…} colors the icon. It uses the badges vocabulary: neutral info success warning danger.

Source
```filetree {title="deployment layout: permissions and what matters"}
- /etc/pigsty/                 # 0755 root:root · configuration root        {icon="fa-solid fa-server" tone=info}
  - pigsty.yml                 # 0644 root:root · cluster inventory
  - ca/                        # 0700 root:root · self-signed CA, never commit  {icon="fa-solid fa-lock" tone=danger open=false}
    - ca.key                   # 0600 root:root
- /var/lib/pgsql/18/data/      # 0700 postgres:postgres · data directory    {tone=warning}
  - postgresql.conf            # 0600 postgres:postgres
- /usr/bin/pig                 # 0755 root:root · command-line tool         {icon="fa-solid fa-terminal" tone=success}
```

deployment layout: permissions and what matters

  • /etc/pigsty/0755 root:root · configuration root
    • pigsty.yml0644 root:root · cluster inventory
    • ca/0700 root:root · self-signed CA, never commit
      • ca.key0600 root:root
  • /var/lib/pgsql/18/data/0700 postgres:postgres · data directory
    • postgresql.conf0600 postgres:postgres
  • /usr/bin/pig0755 root:root · command-line tool

tone colors only the icon. It never colors the text. State the meaning in the name or comment; color supplements it.

Write [name](link) to make an entry name a link. Site paths, relative paths, and http(s): are supported. Links use the same URL validation as other components.

Source
```filetree {title="this site's component pages"}
- content/docs/
  - [callout.md](/docs/components/callout/)     # callouts
  - [filetree.md](/docs/components/filetree/)   # this page
  - [gallery.md](/docs/components/gallery/)     # galleries
  - image/                                      # page bundle
    - [index.md](/docs/components/image/)       # images
- [hugo.yml](https://github.com/pgsty/oink/blob/main/tests/site/hugo.yaml)   # fixture configuration on GitHub
```

this site's component pages

One tree per platform

A fence carrying tab= (and group= / value=) becomes one panel of a tab set and can sit alongside code fences.

Source
```filetree {tab="Linux" group="platform" value="linux"}
- /etc/pigsty/          # configuration
- /var/lib/pgsql/       # data
- /usr/bin/pig          # executable
```
```filetree {tab="macOS" value="macos"}
- ~/Library/Application Support/pigsty/   # configuration
- /opt/homebrew/bin/pig                   # executable
```
Linux
  • /etc/pigsty/configuration
  • /var/lib/pgsql/data
  • /usr/bin/pigexecutable
macOS
  • ~/Library/Application Support/pigsty/configuration
  • /opt/homebrew/bin/pigexecutable

Output

Output Shape
HTML <div class="td-filetree">, an optional title bar, directories as native <details>; a tree with comments also gets the draggable splitter, its only runtime
Print The same tree, fully expanded, no splitter, comments wrapped instead of truncated
Markdown The filetree fence, emitted as written
RSS The fence source inside a <pre>

Below the sm breakpoint, the layout becomes one column. Comments move below names, display without truncation, and the splitter is hidden. Trees without comments always use one column and load no script.

Parameter reference

Fence attributes, after ```filetree:

title , plain text , default—
Title bar above the tree; omitted when absent; must not be empty
wrap , boolean , defaultfalse
Wrap long comments and grow rows; filenames keep ellipsis
tab , plain text , default—
Makes this tree one panel of a tab set
group / value , string , default—
Tab group and sync value; must appear with tab
class , class list , default—
Passed through for site CSS

Entry attributes, in the {…} at the end of a line:

icon , Font Awesome class pair , defaultmatched by name / extension
For example fa-solid fa-lock; a malformed value warns and uses the default icon
tone , enum , defaultneutral
neutral info success warning danger; colours the icon only
open , boolean , defaulttrue
Directories only; false starts it closed
type , enum , defaultinferred
dir or file, overriding the inference

The line syntax itself:

Indentation
Two spaces / four spaces / tabs / the │ ├── └── drawing from tree
- name
The bullet is optional; -, * and + are equivalent
name/
A trailing slash marks a directory; the name renders as written, slash kept
[name](url)
A linked entry
# comment
Everything after the first whitespace-preceded #; \# is a literal hash
N directories, M files
The tree summary line, dropped automatically

Unknown attributes or values, open on files, malformed {…}, and dedents to unopened levels warn. The theme uses a safe fallback or skips the invalid line. The message identifies the line.

Limits

  • filetree fences are the only form. There is no {.filetree} list marker or shortcode.
  • Names and comments are plain text. **bold** appears literally, so the source remains readable in other tools.
  • The tree reads nothing from disk. You write or paste static content, which does not track the repository.
  • There is no search, multiple selection, or control to copy the whole tree. Use a code block when readers must copy the exact listing.
  • Split positions are not saved. Reloading restores the width calculated at build time.
  • Code blocks — listings meant to be copied verbatim
  • Tabs — one tree per platform, side by side
  • Badges — tone uses the same vocabulary
  • Organizing content — how a real content directory is laid out

4.10 - Math

Use KaTeX for inline and display mathematics at build time. Readers download no script.

KaTeX renders mathematics to HTML + MathML at build time. Pages with formulas load one local KaTeX stylesheet. They load no JavaScript and contact no remote mathematics service. Use \(…\) for inline formulas, $$…$$ or \[…\] for display formulas, or math and chem fences. For TikZ or macro packages unsupported by KaTeX, use a pre-rendered image.

Validate generated formulas with Hugo Extended 0.168.0 and the bundled stylesheet. TeX and MathML remain unchanged. No browser mathematics runtime or extra site switch is needed.

Site prerequisites

math and chem fences need no configuration. The $$, \[…\], and \(…\) delimiters need Goldmark’s passthrough extension. Hugo does not merge theme markup configuration. Put this block in your site’s configuration file. This site uses:

hugo.yml
markup:
  goldmark:
    parser:
      attribute:
        block: true # numbered equations need the attribute line
    extensions:
      passthrough:
        enable: true
        delimiters:
          block: [['\[', '\]'], ['$$', '$$']]
          inline: [['\(', '\)']]

Configuration defines each key. Delimiters must not conflict with prose. A single $ is deliberately not configured, so prices such as “$5” are never treated as mathematics.

Shortest form

Put inline formulas inside sentences. Keep surrounding spaces and punctuation outside the delimiters.

Source
The shared buffer hit ratio is \(\mathrm{hit} = \frac{H}{H + R}\), where \(H\) is `blks_hit` and \(R\) is `blks_read`.

The shared buffer hit ratio is hit=HH+R\mathrm{hit} = \frac{H}{H + R}, where HH is blks_hit and RR is blks_read.

Display formulas

Put a formula in its own paragraph between $$ delimiters. It displays larger and centered. \[…\] has the same effect.

Source
A B-tree with fan-out \(f\) over \(N\) keys has height:

$$
h = \left\lceil \log_{f} N \right\rceil
$$

A B-tree with fan-out ff over NN keys has height:

h=⌈log⁡fN⌉ h = \left\lceil \log_{f} N \right\rceil

Long formulas scroll horizontally within the reading column instead of widening the layout. In print, they remain static.

The math fence

The math fence is another form for display formulas. It needs no site passthrough configuration. GitHub displays its source as an ordinary code block.

Source
```math
N_{\text{conn}} = \lambda \cdot \bar{t}_{\text{resp}}
```
Nconn=λ⋅tˉrespN_{\text{conn}} = \lambda \cdot \bar{t}_{\text{resp}}

This is Little’s law for a connection pool. In steady state, required concurrency equals arrival rate multiplied by mean response time. The pool is usually much smaller than the client count.

Chemistry and units

The chem fence uses KaTeX’s mhchem extension with a \ce{…} body. This extension also typesets physical units.

Source
```chem
\ce{CO2 + H2O <=> H2CO3 <=> H+ + HCO3^-}
```
COX2+HX2O⇌HX2COX3⇌HX++HCOX3X−\ce{CO2 + H2O <=> H2CO3 <=> H+ + HCO3^-}

For the syntax see the mhchem manual.

Numbered equations

An attribute line below a display formula creates a numbered equation. num is an author-provided string, such as 3-1 or 5.3. The theme never counts. #id defaults to eq-<num>. The number appears to the right of the formula with a localized “Equation” prefix.

Source
$$
\text{WAL}_{\text{day}} \approx \text{TPS} \times \bar{s}_{\text{record}} \times 86400
$$
{#eq-wal num="3-1" caption="Estimating daily WAL volume"}

See [Equation 3-1](#eq-wal): multiply by the retention period for the floor on archive disk size.
WALday≈TPS×sˉrecord×86400 \text{WAL}_{\text{day}} \approx \text{TPS} \times \bar{s}_{\text{record}} \times 86400
Equation 3-1 Estimating daily WAL volume

See Equation 3-1: multiply by the retention period for the floor on archive disk size.

caption is optional plain text. #id and caption require num. Incomplete or duplicate targets warn and remove unusable parts or keep the first target. On narrow screens, long captions wrap within the reading column without widening the page.

Cross references

Reference numbered equations with ordinary links, as above. For cross-page references or automatic “Equation N” labels, use xref:

Source
Capacity planning starts from {{< xref eq="3-1" anchor="eq-wal" />}}.

Capacity planning starts from Equation 3-1.

xref may appear before its target. Forward references are allowed. For the book-wide equation list and book-equations index, see publishing books.

The eq shortcode

Use eq when your site cannot enable passthrough. It uses the same KaTeX renderer. Without parameters, it displays an unnumbered formula. With num, it is equivalent to the attribute-line form.

Source
{{< eq >}}\sigma_{\text{idx}} = \frac{\text{rows}_{\text{matched}}}{\text{rows}_{\text{total}}}{{< /eq >}}

{{< eq num="3-2" caption="Where a sequential scan and an index scan cost the same" >}}
c_{\text{seq}} \cdot P = c_{\text{rand}} \cdot \sigma \cdot T
{{< /eq >}}
σidx=rowsmatchedrowstotal\sigma_{\text{idx}} = \frac{\text{rows}_{\text{matched}}}{\text{rows}_{\text{total}}}
cseq⋅P=crand⋅σ⋅Tc_{\text{seq}} \cdot P = c_{\text{rand}} \cdot \sigma \cdot T
Equation 3-2 Where a sequential scan and an index scan cost the same

This site enables passthrough, so ordinary authoring uses $$. Use eq for migrated manuscripts or sites whose hugo.yml cannot be changed.

Output

Output Shape
HTML KaTeX HTML + MathML rendered at build time; this page also loads a local katex.min.css, which pages without formulas never load
Print Same as HTML, static, long formulas do not scroll
Markdown The source as written: $$ blocks with their attribute line, math / chem fences, \(…\); the eq shortcode emits **Equation 3-2.** caption plus a $$ block
RSS The same static text as Markdown

No form loads JavaScript.

Parameter reference

Four spellings:

\(…\) , Placementinline
Governed by the site’s passthrough configuration; takes no attributes
$$…$$ / \[…\] , Placementdisplay
As above; may be followed by an attribute line to become numbered
```math , Placementdisplay fence
Independent of passthrough; takes no attributes
```chem , Placementdisplay fence
As above, with \ce{…} in the body

The attribute line {…} under a display formula:

num , string , default—
[0-9A-Za-z.-]+; registers a numbered equation and shows “Equation N” at the right
#id , identifier , defaulteq-<num>
[A-Za-z][A-Za-z0-9_.:-]*; the anchor and cross-reference target
caption , plain text , default—
Caption after the number; requires num

The eq shortcode:

num , string , default—
As above; without it the formula is an unnumbered display formula
id , identifier , defaulteq-<num>
Requires num
caption , plain text , default—
Requires num
class , class list , default—
Requires num; passed through for site CSS
Body , TeX , default—
Required, non-empty

Invalid TeX warns and preserves the expression in ordinary previews. The message includes KaTeX details and the source position. Review the warning and rendered fallback before publication.

Limits

  • The site’s markup.goldmark passthrough extension controls whether $$, \[…\], and \(…\) render. The theme does not read a math: true front matter key. Without configuration, $$ appears literally. The math fence and eq work without this configuration.
  • Only $$ blocks and eq support numbering. The math fence accepts no attribute line. Use another form when you need numbers.
  • Authors provide numbers. The theme neither counts nor renumbers. If you reorder chapters, edit num.
  • Inline formulas accept no attributes. Attribute lines apply only to display formulas.
  • caption is plain text. Markdown in captions is not parsed.

4.11 - Mermaid

Use a mermaid fence for flowcharts, sequence diagrams, Gantt charts, class diagrams, and state diagrams. Bundled Mermaid follows the color scheme; source diffs show changes.

A mermaid fence displays text as a flowchart, sequence diagram, Gantt chart, class diagram, ER diagram, or state diagram. Git stores the diagram source, diffs show changes, and search can find it. The reader’s browser renders it with bundled Mermaid. It contacts no external service. If a diagram needs precise pixel control, use SVG as an image.

Shortest form

Source
```mermaid
flowchart LR
  content["content/"] --> Hugo
  config["hugo.yml"] --> Hugo
  theme["OINK theme"] --> Hugo
  Hugo --> site["public/"]
```
flowchart LR
  content["content/"] --> Hugo
  config["hugo.yml"] --> Hugo
  theme["OINK theme"] --> Hugo
  Hugo --> site["public/"]

Use mermaid as the fence language. No other switch is needed. Only pages with this fence load the Mermaid runtime. Even ten diagrams on one page load it only once.

Sequence diagrams

sequenceDiagram describes messages between participants over time. Use it for request paths and loading order.

Source
```mermaid
sequenceDiagram
  autonumber
  participant Reader as Reader's browser
  participant CDN as Static hosting
  participant JS as Page script bundle
  Reader->>CDN: GET /docs/components/mermaid/
  CDN-->>Reader: HTML (a figure plus the fence source)
  Reader->>CDN: GET this page's bundle
  CDN-->>Reader: mermaid.min.js
  JS->>JS: render the fence source into SVG
  Note over JS: runtimes the page never used are not downloaded
```
sequenceDiagram
  autonumber
  participant Reader as Reader's browser
  participant CDN as Static hosting
  participant JS as Page script bundle
  Reader->>CDN: GET /docs/components/mermaid/
  CDN-->>Reader: HTML (a figure plus the fence source)
  Reader->>CDN: GET this page's bundle
  CDN-->>Reader: mermaid.min.js
  JS->>JS: render the fence source into SVG
  Note over JS: runtimes the page never used are not downloaded

Gantt charts

gantt displays intervals. This example shows each PostgreSQL major version’s five-year community support period, measured from release. 1825d means five years.

Source
```mermaid
gantt
  title Five-year community support per PostgreSQL major version
  dateFormat YYYY-MM-DD
  axisFormat %Y
  section PG 15
  released 2022-10-13 :2022-10-13, 1825d
  section PG 16
  released 2023-09-14 :2023-09-14, 1825d
  section PG 17
  released 2024-09-26 :2024-09-26, 1825d
  section PG 18
  released 2025-09-25 :active, 2025-09-25, 1825d
```
gantt
  title Five-year community support per PostgreSQL major version
  dateFormat YYYY-MM-DD
  axisFormat %Y
  section PG 15
  released 2022-10-13 :2022-10-13, 1825d
  section PG 16
  released 2023-09-14 :2023-09-14, 1825d
  section PG 17
  released 2024-09-26 :2024-09-26, 1825d
  section PG 18
  released 2025-09-25 :active, 2025-09-25, 1825d

Class and ER diagrams

classDiagram displays types and relationships. erDiagram displays entities and cardinality. Use either to explain a data model.

Source
```mermaid
classDiagram
  class Page {
    +string Title
    +string Description
    +int Weight
    +Content()
    +OutputFormats()
  }
  class Resource {
    +string Name
    +string RelPermalink
    +Resize(spec)
  }
  class OutputFormat {
    +string Name
    +string MediaType
  }
  Page "1" --> "0..*" Resource : page bundle resources
  Page "1" --> "1..*" OutputFormat : html / print / markdown / rss
```
classDiagram
  class Page {
    +string Title
    +string Description
    +int Weight
    +Content()
    +OutputFormats()
  }
  class Resource {
    +string Name
    +string RelPermalink
    +Resize(spec)
  }
  class OutputFormat {
    +string Name
    +string MediaType
  }
  Page "1" --> "0..*" Resource : page bundle resources
  Page "1" --> "1..*" OutputFormat : html / print / markdown / rss
Source
```mermaid
erDiagram
  pg_database ||--o{ pg_namespace : "contains schemas"
  pg_namespace ||--o{ pg_class : "contains relations"
  pg_class ||--o{ pg_attribute : "has columns"
  pg_class ||--o{ pg_index : "is indexed by"
  pg_class {
    oid oid PK
    name relname
    char relkind
  }
  pg_attribute {
    oid attrelid FK
    name attname
    smallint attnum
  }
```
erDiagram
  pg_database ||--o{ pg_namespace : "contains schemas"
  pg_namespace ||--o{ pg_class : "contains relations"
  pg_class ||--o{ pg_attribute : "has columns"
  pg_class ||--o{ pg_index : "is indexed by"
  pg_class {
    oid oid PK
    name relname
    char relkind
  }
  pg_attribute {
    oid attrelid FK
    name attname
    smallint attnum
  }

State diagrams

stateDiagram-v2 displays states and transition conditions. This example shows the five stages of a theme release.

Source
```mermaid
stateDiagram-v2
  [*] --> SourceComplete
  SourceComplete --> Validated : theme checks + site suite green
  Validated --> Published : an immutable signed vX.Y.Z tag is pushed
  Published --> Documented : the site's go.mod pins that tag
  Documented --> Deployed : the production build goes live
  Deployed --> [*]
  Published --> SourceComplete : a problem means a new patch version; tags never move
```
stateDiagram-v2
  [*] --> SourceComplete
  SourceComplete --> Validated : theme checks + site suite green
  Validated --> Published : an immutable signed vX.Y.Z tag is pushed
  Published --> Documented : the site's go.mod pins that tag
  Documented --> Deployed : the production build goes live
  Deployed --> [*]
  Published --> SourceComplete : a problem means a new patch version; tags never move

Per-diagram title and configuration

A fence body can start with Mermaid’s YAML header. This is not Hugo front matter. title sets the diagram title. config overrides Mermaid configuration for this diagram only. If a diagram fixes config.theme, it no longer follows the site’s color scheme.

Source
```mermaid
---
title: Only the runtimes a page used are bundled
config:
  flowchart:
    curve: linear
---
flowchart TD
  Page --> Which{which components?}
  Which -->|Mermaid fence| M[mermaid.min.js]
  Which -->|ECharts fence| E[echarts.min.js]
  Which -->|none| B[base bundle only]
```
---
title: Only the runtimes a page used are bundled
config:
  flowchart:
    curve: linear
---
flowchart TD
  Page --> Which{which components?}
  Which -->|Mermaid fence| M[mermaid.min.js]
  Which -->|ECharts fence| E[echarts.min.js]
  Which -->|none| B[base bundle only]

Light and dark

At initialization, the theme reads the current color scheme. Dark mode uses Mermaid’s dark theme. Light mode uses the site’s configured theme. Changing the color scheme redraws diagrams in place without reloading the page. Each diagram keeps its height during redrawing, so page content does not move.

Put site-wide defaults in hugo.yml with lowercase keys. The theme maps them to Mermaid’s key casing:

hugo.yml
params:
  mermaid:
    theme: neutral
    flowchart:
      diagrampadding: 6

Configuration lists all keys. See the Mermaid configuration reference for accepted values.

Inside tabs and steps

A mermaid fence has no tab attribute. Adjacent-fence tabs apply only to ordinary code fences. To compare two diagrams, use the tabs shortcode.

Source
{{< tabs >}}
{{< tab label="By data flow" >}}
```mermaid
flowchart LR
  Markdown --> Goldmark --> RenderHooks --> HTML
```
{{< /tab >}}
{{< tab label="By output format" >}}
```mermaid
flowchart LR
  Page --> HTML
  Page --> Print
  Page --> Markdown
  Page --> RSS
```
{{< /tab >}}
{{< /tabs >}}
flowchart LR
  Markdown --> Goldmark --> RenderHooks --> HTML
flowchart LR
  Page --> HTML
  Page --> Print
  Page --> Markdown
  Page --> RSS

Each step in {{% steps %}} uses page-level Markdown and can contain a mermaid fence. See Steps.

Output

Output Shape
HTML A figure retaining readable source; the runtime draws SVG as it approaches the viewport
Print The source inside <pre class="td-mermaid-source">, static — no runtime runs there
Markdown The mermaid fence and its source, kept as written
RSS The source inside <pre class="td-mermaid-source"> — subscribers see text

Parameter reference

A mermaid fence reads no attribute line. {height=…} and {class=…} have no effect and produce no error. The diagram and container width determine the size. The diagram is centered.

Site parameters (hugo.yml):

params.mermaid , map , defaultunset
The whole map is passed to Mermaid’s initialize(); write keys in lowercase and the theme matches them back to Mermaid’s casing
params.mermaid.theme , string , defaultMermaid’s default
The light-mode theme; dark mode forces dark

Put per-diagram configuration (title, config) in the YAML header at the start of the fence body. These are Mermaid keys, not theme parameters.

Enlarging a diagram

OINK 1.4.0 reading experience

This shared viewer and demand loading are part of OINK 1.4.0.

Mermaid and local PlantUML share the corner-button viewer. It reuses the rendered result without running the engine again. Drag or use arrow keys to pan; use the wheel, pinch, + or - to zoom. Choose Fit to window or press 0 to see the whole diagram. Esc closes the viewer and restores focus to its opener. Very wide pictures initially open at natural size for readable labels. Controls retain touch size on phones. Theme changes retain your position; reduced motion adds no animation.

Choose Source to inspect the original text and Copy code to copy it. Arrows, quotes, ampersands and Chinese stay intact. Download SVG exports a styled, standalone image requiring no remote resources. Copy and export failures show feedback. An SVG with external image or style dependencies cannot be exported. The source disclosure below each figure also supports copying.

Demand loading and printing

A visible diagram within 320px of the viewport loads its engine and renders. Multiple diagrams share engines and both types render serially. Hidden tabs, closed disclosures and zero-width containers wait until visible. Browsers without IntersectionObserver use scroll and size checks. Rapid theme changes discard obsolete results. Diagram-free pages load neither the viewer nor these engines. No installation dependency or build step is added.

Disabled JavaScript, blocked engines and syntax errors retain readable source. In OINK 1.4.0, ordinary HTML printing scales completed light diagrams to the page. Dark diagrams and unrendered diagrams print black-on-white source with wrapping, without waiting for an asynchronous engine or requiring background printing. To print pictures throughout, switch to light mode, then visit and expand the diagrams you need before printing. Printing restores source disclosures afterward and does not change your appearance preference. Dedicated Print, Markdown and RSS continue to show source.

Limits

  • Mermaid emits inline SVG instead of <img>, so {#id num=} numbering does not apply. If you need numbering, export an image and use image numbering.
  • Fence attributes have no effect. Control width through diagram syntax, such as flowchart direction or class-diagram layout, or CSS. There is no alignment attribute. Diagrams are always centered.
  • Syntax errors appear only in the browser because Hugo does not parse Mermaid. Invalid diagrams display an alert and keep readable source, but the build still passes. Check the browser before publishing.
  • RSS, Markdown, and Print include source instead of pictures. State conclusions in prose as well as in diagrams.
  • PlantUML — UML diagrams; see its rendering modes and version requirements
  • Markmap — outline-shaped hierarchies
  • ECharts — charts with numbers in them
  • Images — hand-drawn SVG and numbering

4.12 - Component presentation

Cards, disclosures, tabs, and compact procedures across OINK’s four visual presets.

Use the Appearance menu to compare Paper, Slate, Ink, and Terminal in light or dark mode. These examples use the same content and preserve the normal Markdown authoring model.

Available in OINK 1.4.0

The optional variants on this page ship in OINK 1.4.0.

Related pages

For a compact resource directory, use {.cards .td-cards-list}. A divided list has one enclosing frame and grows naturally with its descriptions.

The full form accepts cards variant="list" for icons, badges, or block descriptions:

InstallationStart here

Build a local preview before changing the site configuration.

Notes and disclosures

Keep a local preview

Review the page at a narrow width as well as on your desktop.

Check the destination

Confirm the destination before publishing a new version.

How does this work without JavaScript?

The disclosure is a native HTML details element. It remains usable with scripts disabled.

The title, chevron, and focus outline share one stable summary row.

Longer titles can wrap without displacing the chevron or reducing the interactive area

Each disclosure opens independently. Other content stays in its current state.

Tabs and commands

macOS / Homebrew
brew install hugo
Debian / Ubuntu
sudo apt install hugo

A marked one-line fence uses {class="td-code-command"}. Its copy button occupies its own column so the command can scroll or wrap without being obscured.

hugo server --bind 127.0.0.1 --port 1313
hugo --environment production --baseURL https://example.com/documentation/ --destination public

wrap, copy, and accessible labels keep their existing meaning. Multiline, numbered, or collapsed input warns and falls back to an ordinary code block. Single-line fences without this modifier keep their usual presentation.

Short procedures

Add {.steps .td-steps-compact} to a short ordered list. The regular .steps layout remains available for longer tutorials.

  1. Review the configuration.
  2. Run the local preview.
  3. Check the generated output.

For a procedure with headings, use steps variant="compact" with percent delimiters:

Preview

Run the development server and open the changed page.

Review

Check the page in the language and appearance your readers use.

Tables and metadata

View Purpose Input
Cards Related resources Link list
Steps Ordered procedure Ordered list
Tabs Equivalent alternatives Adjacent blocks
title , Typestring
A concise title that may wrap naturally.
description , Typestring
Supporting information, separate from the main label.

Preview Validated Review needed

Open search with Ctrl with K and move between results with the arrow keys.

Compatibility

These variants load no additional runtime. Existing links, keyboard behavior, copy text, print content, and Markdown output retain their meaning. Unsupported cards or steps variants warn and use the regular presentation. The visual refinement does not change the default preset or font policy.

4.13 - PlantUML

Render sequence, class, component, activity and use-case diagrams in the browser with bundled assets and no rendering service.

Write PlantUML source in a plantuml fence. OINK renders it in the reader’s browser with two bundled scripts. No Java, Graphviz executable, npm install, CDN or rendering service is needed. Diagram source stays in the browser when using local rendering.

Local rendering in OINK 1.4.0

Local rendering is available when PlantUML is enabled without an endpoint. An explicit rendering endpoint continues to select server mode.

Enable it in your site configuration:

hugo.yml
params:
  plantuml: true

plantuml: { enable: true } also works. The default remains disabled. Enabled pages without PlantUML fences do not load either engine script.

Sequence diagram

Wrap the source in a plantuml fence. This live example renders entirely locally:

```plantuml
@startuml
actor Reader
participant Browser
Reader -> Browser : open the page
Browser -> Browser : render locally
Browser --> Reader : SVG diagram
@enduml
```
@startuml
actor Reader
participant Browser
Reader -> Browser : open the page
Browser -> Browser : render locally
Browser --> Reader : SVG diagram
@enduml

Class diagram

Declare classes, fields and relationship cardinalities.

@startuml
class Publication {
  + pubname : name
  + puballtables : bool
  + pubinsert / pubupdate / pubdelete : bool
}
class Subscription {
  + subname : name
  + subconninfo : text
  + subslotname : name
}
class ReplicationSlot {
  + slot_name : name
  + plugin : name
  + confirmed_flush_lsn : pg_lsn
}
Publication "1" -- "0..*" Subscription : subscribed by
Subscription "1" -- "1" ReplicationSlot : bound to
@enduml

Component diagram

Packages group components; arrows show their dependencies.

@startuml
package "Monitoring node" {
  [Grafana] as grafana
  [Prometheus] as prom
  [Alertmanager] as alert
}
package "Database node" {
  [node_exporter] as node
  [pg_exporter] as pgexp
  [PostgreSQL] as pg
}
pg --> pgexp : query the statistics views
node --> prom : /metrics
pgexp --> prom : /metrics
prom --> alert : rule fired
grafana --> prom : PromQL
@enduml

Activity diagram

Use decisions and branches to explain a workflow.

@startuml
start
:write content/docs/**/*.md;
:add the translated peer, copying the rendered heading IDs;
if (hugo passes?) then (yes)
  :npm test;
else (no)
  :fix using the file and line in the error;
  stop
endif
if (tests green?) then (yes)
  :open the PR;
  stop
else (no)
  :back to editing;
  stop
endif
@enduml

Use-case diagram

Actors and use cases describe what each reader can do.

@startuml
left to right direction
actor Reader as reader
actor Author as author
actor Maintainer as maintainer
rectangle "Documentation site" {
  reader --> (full-text search)
  reader --> (switch language)
  reader --> (export the print view)
  author --> (add a page)
  author --> (preview locally)
  maintainer --> (upgrade the theme)
  maintainer --> (publish)
}
@enduml

Dark mode

Local diagrams follow light/dark mode; authored skinparam settings still apply. Obsolete render results are discarded when the mode changes. An open viewer retains its zoom and pan position.

Enlarge, source and download

Local PlantUML uses the same viewer as Mermaid. Use the corner button to open it, drag or arrow keys to pan, wheel/pinch/+/- to zoom, Fit to window or 0 to fit, and Esc to close and restore focus. The viewer reuses the Blob image without inserting PlantUML SVG markup or links into the page DOM, and without rendering again.

Source and Copy code preserve arrows, quotes, ampersands and Chinese. Download SVG exports a styled standalone file; external resource dependencies prevent export. Failures show feedback in the toolbar. The source disclosure below each diagram also supports viewing and copying. Explicit server mode keeps its legacy behavior.

Demand loading and printing

The two local engines load once when a diagram comes within 320px of the viewport. Hidden tabs, disclosures and zero-width containers wait until shown. Browsers without IntersectionObserver use scroll and size checks. Mermaid and PlantUML render serially on the main thread. No installation or build step is added.

Ordinary browser printing retains rendered images and shows source for unrendered diagrams. It cannot wait for asynchronous engines; visit and expand the diagrams first when you need pictures throughout. Dedicated Print, Markdown and RSS keep source fallback.

Optional rendering service

Existing sites can keep an explicitly configured endpoint:

hugo.yml
params:
  plantuml:
    enable: true
    svg_image_url: https://plantuml.internal.example/plantuml/svg/
    svg: false

A nonempty svg_image_url selects server rendering. Removing it selects local rendering. The server URL must end with the appropriate path separator and be an HTTP(S) URL with a host or a same-site path. Invalid endpoints warn and disable rendering; they do not silently switch modes. OINK supplies no public endpoint.

Server mode sends encoded diagram source to that endpoint and retains its existing svg option. Local mode ignores svg: generated SVG is displayed as an image, so authored links and markup do not enter the page DOM.

Outputs

Output Result
HTML Local SVG image when enabled; readable source until rendering succeeds
Browser print of HTML Rendered image scaled to fit, or readable source if not rendered
Dedicated Print output Readable source, with no PlantUML runtime
Markdown and RSS Original diagram source; no rendering request

With JavaScript disabled, a failed engine download, unsupported input or a rendering error, the source remains readable. A failed diagram does not prevent other diagrams on the page from rendering.

Parameters

params.plantuml , boolean or map , defaultdisabled
true enables local rendering
params.plantuml.enable , boolean , defaultfalse
Enables the map form
params.plantuml.svg_image_url , string , defaultabsent
Optional endpoint selecting legacy server rendering
params.plantuml.svg , boolean , defaultfalse
Server mode only: inline SVG instead of an image

Fences do not accept code-block title, copy, line-number, numbering or zoom attributes. Source without @startuml / @enduml is wrapped for local rendering; explicit diagram delimiters are preserved.

Limits

  • The browser bundle includes the core engine and Graphviz layout only. !include, !includeurl, external themes, C4, AWS and other optional sprite libraries are not downloaded. Unsupported resources leave the source visible. Use self-contained diagrams, pre-rendered images, or an explicit server for these.
  • The pinned engine is @plantuml/core 1.2026.9 with Viz.js 3.24.0. The two scripts total about 4.8 MB before compression, or 1.7 MB with gzip. They load only as enabled diagrams approach the viewport; normal HTTP caching applies.
  • Rendering uses the browser’s JavaScript and WebAssembly support. Sites with a restrictive CSP need to allow their own scripts, WebAssembly compilation (wasm-unsafe-eval) and blob: images. Blocked resources retain source.
  • Large diagrams still cost browser CPU and memory. Rendering proceeds one diagram at a time; this is not a background-worker renderer. After an uncancellable engine timeout, remaining PlantUML figures keep source until the page is reloaded.
  • Diagrams are images: SVG hyperlinks are inactive. Numbering is not part of this integration.
  • Mermaid — another bundled browser diagram engine
  • Images — pre-rendered diagrams, numbering and zoom
  • Configuration — site settings

4.14 - Markmap

Use a markmap fence for an expandable mind map with zoom controls. Its source remains a readable Markdown outline.

A markmap fence contains a Markdown outline. Headings and lists define the hierarchy. The browser displays an expandable, collapsible tree. Use it to give an overview of a section. For flows with direction or conditions, use Mermaid.

Shortest form

Enable Markmap in site configuration first. It is off by default. Without this setting, the fence stays a readable code block.

hugo.yml
params:
  markmap: true

Then put the outline in a markmap fence:

Source
```markmap
# OINK
## Local-first
- every runtime ships with the theme
- no CDN involved
## Markdown-native
- components are fences and attribute lines
- usable without writing a shortcode
## Four output states
- HTML
- print
- Markdown
- RSS
```
# OINK
## Local-first
- every runtime ships with the theme
- no CDN involved
## Markdown-native
- components are fences and attribute lines
- usable without writing a shortcode
## Four output states
- HTML
- print
- Markdown
- RSS

The first-level heading is the root. Indentation places other headings and list items below it. Click a node’s dot to expand or collapse its branch. Scroll to zoom, and drag to pan. In OINK 1.4.0, Fit to window below the map restores the whole outline and resumes automatic fitting when the column changes width. Dragging or zooming suspends automatic fitting to preserve your reading position. The button also works with Tab and Enter.

Depth

Deeper levels use smaller text, and the canvas arranges itself. This example shows the theme documentation site’s six sections and their page counts.

Source
```markmap
# OINK documentation
## Introduction (4 pages)
### What it is
### Feature tour
### Showcase
### Licences
## Get started (4 pages)
### Choose a path
### OINK Starter
### Repository tour
### From scratch
## Authoring (8 pages)
### Organizing content
### Writing pages
### Front matter
### Blog
### Books
### Releases and downloads
### OpenAPI
## Components (22 pages)
### Callouts / tabs / steps / cards
### Images / galleries / tables / fields
### Diagrams: Mermaid / PlantUML / Markmap / ECharts
## Customization (15 pages)
### Branding / navigation / search / languages
### Landing / versions / taxonomies / print
## Operations (7 pages)
### Preview / deploy / upgrade
### Comments / analytics / troubleshooting
```
# OINK documentation
## Introduction (4 pages)
### What it is
### Feature tour
### Showcase
### Licences
## Get started (4 pages)
### Choose a path
### OINK Starter
### Repository tour
### From scratch
## Authoring (8 pages)
### Organizing content
### Writing pages
### Front matter
### Blog
### Books
### Releases and downloads
### OpenAPI
## Components (22 pages)
### Callouts / tabs / steps / cards
### Images / galleries / tables / fields
### Diagrams: Mermaid / PlantUML / Markmap / ECharts
## Customization (15 pages)
### Branding / navigation / search / languages
### Landing / versions / taxonomies / print
## Operations (7 pages)
### Preview / deploy / upgrade
### Comments / analytics / troubleshooting

Links, code and emphasis

Nodes accept inline Markdown. Links are clickable, inline code is monospaced, and bold and italic text display normally.

Source
```markmap
# Everyday commands
## Preview
- `hugo server` — open [localhost:1313](http://localhost:1313/)
- `hugo server -D` — **including drafts**
## Build
- `hugo --printPathWarnings`
- `hugo --gc --minify` — for publishing
## Theme
- `hugo mod get -u github.com/pgsty/oink`
- [theme repository](https://github.com/pgsty/oink)
- [site source](https://github.com/pgsty/oink.pgsty.com)
```
# Everyday commands
## Preview
- `hugo server` — open [localhost:1313](http://localhost:1313/)
- `hugo server -D` — **including drafts**
## Build
- `hugo --printPathWarnings`
- `hugo --gc --minify` — for publishing
## Theme
- `hugo mod get -u github.com/pgsty/oink`
- [theme repository](https://github.com/pgsty/oink)
- [site source](https://github.com/pgsty/oink.pgsty.com)

Mathematics in nodes

The Markmap runtime includes local KaTeX. $…$ in nodes renders as a formula.

Source
```markmap
# PostgreSQL metrics worth watching
## Cache hit ratio
- $\frac{blks\_hit}{blks\_hit + blks\_read}$
- below 0.99, look at shared_buffers
## Replication lag
- $lsn_{primary} - lsn_{replica}$
## Transaction throughput
- $TPS = \frac{\Delta xact\_commit}{\Delta t}$
```
# PostgreSQL metrics worth watching
## Cache hit ratio
- $\frac{blks\_hit}{blks\_hit + blks\_read}$
- below 0.99, look at shared_buffers
## Replication lag
- $lsn_{primary} - lsn_{replica}$
## Transaction throughput
- $TPS = \frac{\Delta xact\_commit}{\Delta t}$

Controlling the initial depth

The fence body can start with Markmap’s YAML header. This is not Hugo front matter. initialExpandLevel initially expands only the first levels. Readers can open the rest. colorFreezeLevel sets the level from which a branch keeps one color.

Source
```markmap
---
markmap:
  initialExpandLevel: 2
  colorFreezeLevel: 2
---

# Check scripts in the theme repository
## Source-level contracts
### check-i18n.py
### check-taxonomy.py
### check-font-tokens.py
## Output-level checks
### check-output.py
### check-goldens.py
### check-code-blocks.py
### check-content-primitives.py
### check-media-primitives.py
## Browser runtimes
### node --test tests/js/**/*.test.js
```
---
markmap:
  initialExpandLevel: 2
  colorFreezeLevel: 2
---

# Check scripts in the theme repository
## Source-level contracts
### check-i18n.py
### check-taxonomy.py
### check-font-tokens.py
## Output-level checks
### check-output.py
### check-goldens.py
### check-code-blocks.py
### check-content-primitives.py
### check-media-primitives.py
## Browser runtimes
### node --test tests/js/**/*.test.js

Folded into a disclosure

Every map is 300 pixels tall. Three consecutive maps occupy considerable page space. Put a large overview in > [!DETAILS] so readers can open it. Start every disclosure line with >, including fence lines.

Source
> [!DETAILS] What the theme repository looks like
> ```markmap
> # pgsty/oink
> ## layouts/
> - baseof.html and the per-type shells
> - _partials/shell/
> - _markup/ render hooks
> - _shortcodes/
> ## assets/
> - scss/ tokens and component styles
> - js/ browser runtimes
> - third_party/ libraries shipped with the theme
> ## i18n/
> - 32 locale files with identical keys
> ## docs/
> - maintainer contracts
> ```
What the theme repository looks like
# pgsty/oink
## layouts/
- baseof.html and the per-type shells
- _partials/shell/
- _markup/ render hooks
- _shortcodes/
## assets/
- scss/ tokens and component styles
- js/ browser runtimes
- third_party/ libraries shipped with the theme
## i18n/
- 32 locale files with identical keys
## docs/
- maintainer contracts

Output

Output Shape
HTML <pre><code class="language-markmap"> first; the runtime replaces it with <div class="markmap"> and draws the SVG
Print Same as HTML: the print view loads the runtime too
Markdown The markmap fence and its outline, kept as written
RSS The outline source only — a readable outline for subscribers

The outline supplies the content. Without JavaScript, the complete hierarchy remains readable.

Parameter reference

A markmap fence reads no attribute line. The theme fixes its height at 300px through .markmap > svg. Its width fills the reading column.

Site parameters (hugo.yml):

params.markmap , bool , defaultfalse
With it off, the fence stays a code block and no runtime loads

Configuration defines the site key. Put per-map behavior in the markmap: YAML header at the start of the body, such as initialExpandLevel, colorFreezeLevel, or maxWidth. This is Markmap syntax. See the Markmap documentation for accepted keys.

Limits

  • Inline SVG output is fixed at 300px tall by one .markmap > svg rule. Fences cannot change this height. For many levels, use initialExpandLevel or split the map. {#id num=} numbering and image zoom do not apply to inline SVG.
  • Node text follows the site’s text color; connecting lines retain Markmap’s own palette. Check custom colors in both modes.
  • Without params.markmap, the fence remains a code block. Sites that do not use Markmap load no runtime.
  • OINK 1.4.0 provides a fit control below each map; it is hidden in print.
  • Avoid <, >, &, and " in outlines. The current theme double-escapes them, displaying literal &gt; or &#34;. Use [text](URL) links instead of angle-bracket autolinks.

4.15 - Draw.io

Display a .drawio.svg with an embedded editable copy as an ordinary image. Its edit button opens the Draw.io editor.

Draw.io uses plain Markdown images. It has no fence or shortcode. When exporting from Draw.io, select “Include a copy of my diagram”. The SVG or PNG then contains an mxfile copy. The theme detects this copy and adds an edit button. Use this integration for diagrams that readers can download and change. Use an ordinary image for diagrams that only need to be viewed.

Shortest form

To show the edit button, first set params.drawio.enable: true and configure params.drawio.drawio_server as shown under The editor address. Without this setup, the diagram remains an ordinary image without editing.

Use ordinary image syntax. The filename does not affect detection. .drawio.svg is only a naming convention.

Source
![The Hugo build pipeline: content goes through Hugo and out as public](pipeline.drawio.svg)
{width="620" height="140"}
The Hugo build pipeline: content goes through Hugo and out as public

An export with an mxfile copy is wrapped in a .drawio container. A pencil button appears at the bottom right on hover or keyboard focus. It stays visible on touch devices and in forced-colors mode. The button opens a full-screen iframe with the site’s configured editor. When image zoom is enabled, Edit and Zoom are separate sibling buttons. Edit does not open the zoom dialog.

How the copy is detected

The runtime checks whether the file contains mxfile. It does not check the filename. A hand-drawn SVG with the same block-image syntax and attribute line has no embedded copy, so it gets no edit button.

Source
![The three columns of the documentation shell: sidebar, article, table of contents](plain-shell.svg)
{width="620" height="140"}
The three columns of the documentation shell: sidebar, article, table of contents

With a caption

Draw.io images use the ordinary image render hook. All image attributes apply. Add caption to create a captioned figure. The edit button still appears on the image.

Source
![The Hugo build pipeline](pipeline.drawio.svg)
{caption="Content, configuration and theme templates flow into Hugo and out as public/" width="620" height="140"}
The Hugo build pipeline
Content, configuration and theme templates flow into Hugo and out as public/

As a numbered figure

Add {#id num=…} to create a numbered figure. xref can reference it, and it appears in the list of figures.

Source
![The Hugo build pipeline](pipeline.drawio.svg)
{#fig_pipeline num="1-1" caption="From content to a static site" width="620" height="140"}
The Hugo build pipeline
Figure 1-1 From content to a static site

The complete numbering and cross-reference rules are in publishing books.

SVG or PNG

Both SVG and PNG are recognized. A Draw.io PNG can store the editable copy in a text chunk. The runtime uses the same detection test.

Source
![The Hugo build pipeline (PNG export)](pipeline.drawio.png)
{width="620" height="140"}
The Hugo build pipeline (PNG export)

Prefer SVG for documentation. It scales without quality loss, contains searchable text accessible to screen readers, and has readable diffs. Use PNG for very complex diagrams or platforms without SVG support. Only PNG supports Hugo image processing; processing an SVG warns and preserves the source.

What the button does

The button performs three actions in this order.

Lay an overlay over the page

In OINK 1.3.0, the runtime opens a full-screen <dialog class="drawioframe"> containing an iframe. The editor URL keeps the configured drawio_server query parameters and fragment, and sets embed=1, ui=atlas, proto=json, saveAndEdit=1, and noSaveBtn=1. The modal includes a localized Close button and contains keyboard focus. Close it with that button or a background click. Escape closes it while focus is in the host dialog; the embedded editor handles its own keys. Closing restores focus to Edit unless another surface has already taken focus.

Hand the diagram to the editor

When the editor is ready, the runtime sends the image and its mxfile copy to the iframe as a data URL. This transfer does not use your server.

Save and write back

When you save, the editor exports the original format, SVG or PNG. The browser downloads it with the same filename. The runtime never writes to the repository. Replace the file in content/ with the download and commit it yourself.

The edit button lets readers download and change a diagram. It does not edit the site online.

The editor address

hugo.yml
params:
  drawio:
    enable: true
    drawio_server: https://drawio.internal.example/
  • enable: true without drawio_server warns and disables editing. The theme does not select a public service.
  • The address must be an HTTP(S) URL or local path. Local paths honor the baseURL deployment subpath. Whitespace, control characters, raw backslashes, protocol-relative URLs (//host/), and other schemes warn and disable editing.
  • If editing must stay within your organization, deploy a self-hosted editor and configure its address.
  • The public endpoint https://embed.diagrams.net/ works. With this endpoint, the reader’s diagram is sent to a third-party page.

Both keys are defined in Configuration.

Output

Output Shape
HTML A plain <img> or <figure>; once enabled, the runtime wraps an image that carries a copy in <div class="drawio"> and adds the button
Print The image prints as usual; this output loads no Draw.io runtime and creates no edit button
Markdown Plain Markdown image syntax
RSS A plain <img> with an absolute URL and no button

All four outputs include the image. Only HTML adds the optional edit button.

Parameter reference

Draw.io has no fence or shortcode parameters. It uses the Images attribute line: caption, width, height, link, #id, num, command, and options.

Site parameters (hugo.yml):

params.drawio.enable , bool , defaultfalse
With it off no script loads and an image is just an image
params.drawio.drawio_server , string , defaultnone
The editor address; required when enable: true

Limits

  • The runtime loads only if rendered page content has .svg or .png candidates. It groups matching images by URL and reads each URL once to find mxfile.
  • Without “Include a copy of my diagram” on export, the image has no edit button.
  • Editing needs the editor and never writes to the repository. Offline, images display normally, but the button has no effect. Saving downloads a file. You must replace and commit the file manually.
  • Exported SVG colors are fixed and do not follow the site’s color scheme. Use none fills and neutral gray lines and text so the image is readable in both modes.
  • Images — captions, numbering, sizing and zoom in full
  • PlantUML — UML diagrams; see its rendering modes and version requirements
  • Mermaid — diagrams from text with no server at all
  • Configuration — the full definition of params.drawio.*

4.16 - ECharts

Write ECharts options as YAML or JSON in an echarts fence. Hugo validates them, and bundled ECharts draws charts that follow the color scheme.

An echarts fence contains an ECharts option object in YAML or JSON. It contains data, not code. Use it for quantitative charts with axes, series, and a legend. Use Mermaid for relationships and flows. Use Infographic for order and hierarchy. Hugo parses options at build time. Invalid input warns and preserves readable source. The browser draws charts with the theme’s bundled ECharts. Only pages that use it load the runtime.

Shortest form

A bar chart needs xAxis, yAxis, and series. This chart shows page counts for the six documentation sections.

Source
```echarts {height="320px"}
tooltip:
  trigger: axis
xAxis:
  type: category
  data: [Introduction, Get started, Authoring, Components, Customization, Operations]
yAxis:
  type: value
  name: pages
series:
  - name: pages
    type: bar
    data: [4, 4, 8, 22, 15, 7]
```
tooltip:
  trigger: axis
xAxis:
  type: category
  data: [Introduction, Get started, Authoring, Components, Customization, Operations]
yAxis:
  type: value
  name: pages
series:
  - name: pages
    type: bar
    data: [4, 4, 8, 22, 15, 7]

Both formats are supported. YAML is shorter because it needs no quotes or commas. Incorrect indentation or an array instead of a map warns, identifies the source line, and shows the source instead of a blank chart.

Multiple line series

series is an array. Add an entry for each line. legend lets readers hide a series. This chart shows PostgreSQL major release years and the end-of-support years from the community’s five-year policy.

Source
```echarts {height="360px"}
tooltip:
  trigger: axis
legend:
  data: [Released, End of support]
grid:
  left: 56
  right: 24
  top: 48
  bottom: 40
xAxis:
  type: category
  name: major version
  data: ["9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18"]
yAxis:
  type: value
  min: 2015
  max: 2031
  name: year
series:
  - name: Released
    type: line
    smooth: false
    data: [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
  - name: End of support
    type: line
    lineStyle:
      type: dashed
    data: [2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030]
```
tooltip:
  trigger: axis
legend:
  data: [Released, End of support]
grid:
  left: 56
  right: 24
  top: 48
  bottom: 40
xAxis:
  type: category
  name: major version
  data: ["9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18"]
yAxis:
  type: value
  min: 2015
  max: 2031
  name: year
series:
  - name: Released
    type: line
    smooth: false
    data: [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
  - name: End of support
    type: line
    lineStyle:
      type: dashed
    data: [2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030]

Quote version numbers. YAML reads unquoted 10 and 9.6 as numbers. Category-axis labels must be strings.

Pie and doughnut charts

Give radius two values to create a doughnut chart. This example groups OINK’s 29 shortcodes by purpose.

Source
```echarts {height="340px"}
tooltip:
  trigger: item
  formatter: "{b}: {c} ({d}%)"
legend:
  bottom: 0
series:
  - type: pie
    radius: [42%, 70%]
    itemStyle:
      borderRadius: 6
      borderWidth: 2
    label:
      formatter: "{b} {c}"
    data:
      - { value: 14, name: Core components }
      - { value: 10, name: Book numbering and indexes }
      - { value: 3, name: Releases and downloads }
      - { value: 2, name: OpenAPI }
```
tooltip:
  trigger: item
  formatter: "{b}: {c} ({d}%)"
legend:
  bottom: 0
series:
  - type: pie
    radius: [42%, 70%]
    itemStyle:
      borderRadius: 6
      borderWidth: 2
    label:
      formatter: "{b} {c}"
    data:
      - { value: 14, name: Core components }
      - { value: 10, name: Book numbering and indexes }
      - { value: 3, name: Releases and downloads }
      - { value: 2, name: OpenAPI }

{b}, {c}, and {d} are ECharts placeholders for name, value, and percentage. Use them in a string. No function is needed.

Height and full width

height defaults to 400px and accepts px rem em vh vw %. full=true removes the reading-column limit and fills the content area. Use it for charts with many points or long labels.

Source
```echarts {height="260px" full=true}
tooltip:
  trigger: axis
grid:
  left: 40
  right: 16
  top: 24
  bottom: 32
xAxis:
  type: category
  data: [i18n, taxonomy, font tokens, content contracts, navigation, runtime, sidebar icons, search, actions, palette, params, reading, release assets, download, landing, book, migrations, keyboard, shell, output, goldens]
yAxis:
  type: value
  name: scripts
series:
  - type: bar
    data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
```
tooltip:
  trigger: axis
grid:
  left: 40
  right: 16
  top: 24
  bottom: 32
xAxis:
  type: category
  data: [i18n, taxonomy, font tokens, content contracts, navigation, runtime, sidebar icons, search, actions, palette, params, reading, release assets, download, landing, book, migrations, keyboard, shell, output, goldens]
yAxis:
  type: value
  name: scripts
series:
  - type: bar
    data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

Invalid heights such as 360 and 36pt warn and use the default 400px.

Light and dark

Without theme, charts use the reader’s current color scheme. They redraw in place when the scheme changes, without reloading the page. Charts resize when their containers resize. Switch this page to dark mode to change each chart’s background and text.

A fixed theme keeps the same colors in both modes:

Source
```echarts {height="240px" theme="dark"}
xAxis:
  type: category
  data: [HTML, Print, Markdown, RSS]
yAxis:
  type: value
series:
  - type: bar
    data: [1, 1, 1, 1]
```
xAxis:
  type: category
  data: [HTML, Print, Markdown, RSS]
yAxis:
  type: value
series:
  - type: bar
    data: [1, 1, 1, 1]

dark is the only built-in runtime theme. Register other ECharts themes with echarts.registerTheme() before naming them here. Unless branding requires a fixed theme, omit theme so charts follow the site.

Callbacks with $fn:

A fence contains data and cannot contain JavaScript. If an option needs a function, such as a tooltip formatter or data-dependent color, use "$fn:name" in the options. Register the name on window.OinkEchartsFunctions:

Source
<script>
  window.OinkEchartsFunctions = window.OinkEchartsFunctions || {};
  window.OinkEchartsFunctions.pageShare = function (params) {
    var p = params[0];
    return p.name + ': ' + p.value + ' pages, ' + Math.round((p.value / 60) * 100) + '% of the site';
  };
</script>

```echarts {height="300px"}
tooltip:
  trigger: axis
  formatter: "$fn:pageShare"
xAxis:
  type: category
  data: [Introduction, Get started, Authoring, Components, Customization, Operations]
yAxis:
  type: value
series:
  - type: bar
    data: [4, 4, 8, 22, 15, 7]
```
tooltip:
  trigger: axis
  formatter: "$fn:pageShare"
xAxis:
  type: category
  data: [Introduction, Get started, Authoring, Components, Customization, Operations]
yAxis:
  type: value
series:
  - type: bar
    data: [4, 4, 8, 22, 15, 7]

Hover over a bar to see the tooltip sentence from the function. Unregistered names resolve to undefined. The chart draws as if the option were absent, without build or runtime warnings. Keep the script beside the fence so they change together.

The script is site code and needs code review. If string templates ({b}, {c}, {d}) can express the formatting, no function is needed.

Where the data lives

Fence bodies are literal text. Hugo does not expand shortcodes, front matter variables, or data/ files inside them. Write the numbers in the fence. Data cannot be shared this way, but Git stores the chart with its data and shows changed numbers.

Do not chart data that changes frequently, such as version matrices or asset lists. Use a table or a data/-driven component on a release page.

Output

Output Shape
HTML A canvas container inside <div class="td-echarts"> plus an application/json options block; the local ECharts draws it
Print No chart; the fence source inside <pre class="td-echarts-source">
Markdown The echarts fence and its option source, kept as written
RSS Same as print — source only

Explain the chart’s information in prose. Print and RSS do not include the chart.

Parameter reference

The fence attribute line (```echarts {…}):

height , CSS length , default400px
A non-negative number plus px rem em vh vw %; anything else warns and uses the default
theme , string , defaultunset
Pin an ECharts theme and stop following the site’s colour scheme; only dark is built in
full , bool , defaultfalse
true drops the reading-column limit and fills the content area
class , space-separated classes , default—
Passed through to the container for site CSS

style, on*, and unknown attributes warn and are ignored. If a fence body does not parse to a YAML/JSON map, it warns and displays the source. ECharts defines the option keys in its official option manual.

ECharts has no site-level parameter. It needs no switch in hugo.yml and loads only on pages that use it.

Limits

  • Fences cannot contain JavaScript. Use $fn: for functions. Unregistered names resolve to undefined without an error.
  • Fences cannot read external data. They cannot access data/, front matter, or shortcodes. Write the numbers in the fence.
  • Print and RSS include only the source. State the conclusion in prose.
  • Quote category-axis values such as 10, 9.6, on, and yes. YAML can convert them to numbers or booleans.
  • Use line styles or marker shapes as well as color to distinguish series. Check legend contrast in both color schemes.
  • Infographic — structure and order, not statistics
  • Tables — for few values that must be read exactly
  • Mermaid — relationship and flow diagrams
  • Code blocks — the general rules for fence attribute lines

4.17 - Infographic

Use an infographic fence and AntV template to display a title and items as a flow, timeline, funnel, grid, or hierarchy.

An infographic fence selects an AntV template to display a title and item list. Use it for order, hierarchy, or comparison. Use ECharts for axes and precise numbers. Use Mermaid for flows with conditional branches. The fence contains data and stays readable as text on GitHub.

Shortest form

Start with infographic <template>, then add a data block. title sets the title. Every entry under items needs at least a label.

Source
```infographic
infographic list-row-simple-horizontal-arrow
data
  title Three steps in one documentation change
  items
    - label Write
      desc Start with the source language
    - label Check
      desc Zero build warnings, every example really rendered
    - label Ship
      desc Add the translated peer, open the PR
```
infographic list-row-simple-horizontal-arrow
data
  title Three steps in one documentation change
  items
    - label Write
      desc Start with the source language
    - label Check
      desc Zero build warnings, every example really rendered
    - label Ship
      desc Add the translated peer, open the PR

Use two spaces per indentation level to define structure. Keep labels short and put explanations in desc.

Timelines

The sequence-timeline-* family arranges items on a time axis. label gives the point in time, and desc gives the event.

Source
```infographic {height="420px"}
infographic sequence-timeline-simple
data
  title The last five PostgreSQL major versions
  items
    - label 2021
      desc 14: another round of parallel query and logical replication work
    - label 2022
      desc 15: the MERGE statement
    - label 2023
      desc 16: logical replication from a standby
    - label 2024
      desc 17: incremental backup and JSON_TABLE
    - label 2025
      desc 18: the asynchronous IO subsystem
```
infographic sequence-timeline-simple
data
  title The last five PostgreSQL major versions
  items
    - label 2021
      desc 14: another round of parallel query and logical replication work
    - label 2022
      desc 15: the MERGE statement
    - label 2023
      desc 16: logical replication from a standby
    - label 2024
      desc 17: incremental backup and JSON_TABLE
    - label 2025
      desc 18: the asynchronous IO subsystem

Funnels

sequence-funnel-simple displays narrowing stages. This example shows the five stages of a theme release.

Source
```infographic {height="420px"}
infographic sequence-funnel-simple
data
  title The five states a theme release passes through
  items
    - label Source complete
      desc The code is written, and that is all
    - label Validated
      desc Theme checks and the site suite are green
    - label Published
      desc An immutable signed tag, resolvable through the Go proxy
    - label Documented
      desc The documentation site pins that tag
    - label Deployed
      desc Production runs this version
```
infographic sequence-funnel-simple
data
  title The five states a theme release passes through
  items
    - label Source complete
      desc The code is written, and that is all
    - label Validated
      desc Theme checks and the site suite are green
    - label Published
      desc An immutable signed tag, resolvable through the Go proxy
    - label Documented
      desc The documentation site pins that tag
    - label Deployed
      desc Production runs this version

Grid cards

For unordered items, list-grid-* uses a grid instead of a sequence.

Source
```infographic {height="380px"}
infographic list-grid-compact-card
data
  title One page, four outputs
  desc Every content component has to produce something usable in all four
  items
    - label HTML
      desc Interactive, runtimes loaded on demand
    - label Print
      desc Disclosures expanded, zoom and copy removed
    - label Markdown
      desc Plain text, compared byte for byte against goldens
    - label RSS
      desc Static, from the same source as print
```
infographic list-grid-compact-card
data
  title One page, four outputs
  desc Every content component has to produce something usable in all four
  items
    - label HTML
      desc Interactive, runtimes loaded on demand
    - label Print
      desc Disclosures expanded, zoom and copy removed
    - label Markdown
      desc Plain text, compared byte for byte against goldens
    - label RSS
      desc Static, from the same source as print

Items with values

Add value to an item for templates that show proportions, such as pies, doughnuts, or progress displays.

Source
```infographic {height="400px"}
infographic chart-pie-donut-plain-text
data
  title How the 29 shortcodes break down
  items
    - label Core components
      value 14
    - label Book numbering and indexes
      value 10
    - label Releases and downloads
      value 3
    - label OpenAPI
      value 2
```
infographic chart-pie-donut-plain-text
data
  title How the 29 shortcodes break down
  items
    - label Core components
      value 14
    - label Book numbering and indexes
      value 10
    - label Releases and downloads
      value 3
    - label OpenAPI
      value 2

Hierarchy and hand-drawn style

Nest items with children. hierarchy-mindmap-* displays two levels of structure. A top-level theme block sets the appearance. type accepts light, dark, or hand-drawn.

Source
```infographic {height="320px"}
infographic hierarchy-mindmap-level-gradient-compact-card
theme
  type hand-drawn
data
  root
    label Theme repository
    children
      - label layouts
        desc templates
        children
          - label _markup
            desc render hooks
          - label _partials
            desc shell and helpers
      - label assets
        desc resources
        children
          - label scss
            desc tokens and component styles
          - label js
            desc browser runtimes
          - label third_party
            desc libraries shipped with the theme
```
infographic hierarchy-mindmap-level-gradient-compact-card
theme
  type hand-drawn
data
  root
    label Theme repository
    children
      - label layouts
        desc templates
        children
          - label _markup
            desc render hooks
          - label _partials
            desc shell and helpers
      - label assets
        desc resources
        children
          - label scss
            desc tokens and component styles
          - label js
            desc browser runtimes
          - label third_party
            desc libraries shipped with the theme

theme belongs to the DSL. In OINK 1.4.0, each diagram has its own canvas: transparent default and hand-drawn diagrams use white, and an authored background takes priority. A type dark diagram keeps its dark background even on a light page. Site mode changes preserve the palette, text colors, and drawing style without rerendering. Explicit transparency or custom text colors still require checking contrast in both modes.

Picking a template

Template names use structure-variant. Each structure has several visual variants. Common families are listed below:

Structure prefix What it expresses Example
list-row-* list-column-* Items in a row or a column list-row-simple-horizontal-arrow
list-grid-* A grid, no order between items list-grid-compact-card list-grid-badge-card
list-pyramid-* sequence-funnel-* Narrowing stages sequence-funnel-simple
sequence-timeline-* sequence-roadmap-vertical-* Timelines and roadmaps sequence-timeline-simple
sequence-steps-* sequence-snake-steps-* Ordered steps sequence-steps-simple
compare-binary-horizontal-* compare-quadrant-* Binary comparison and quadrants compare-binary-horizontal-simple-vs
hierarchy-mindmap-* hierarchy-structure-* Hierarchy, with children hierarchy-mindmap-level-gradient-compact-card
chart-pie-* chart-bar-* chart-column-* Illustrative charts, with value chart-pie-donut-plain-text
relation-network-* relation-dagre-flow Networks and flows, with relations relation-dagre-flow

Choose the simplest form that explains the relationship. See the full AntV Infographic gallery. Template names match the version bundled with the theme.

Output

Output Shape
HTML A canvas container inside <div class="td-infographic"> plus the DSL; the local AntV runtime draws the SVG
Print No diagram; the DSL source inside <pre class="td-infographic-source">
Markdown The infographic fence and its DSL, kept as written
RSS Same as print — source only

Explain the diagram’s information in prose. Print and RSS include only the DSL source.

Parameter reference

The fence attribute line (```infographic {…}):

height , auto or a CSS length , defaultauto
A non-negative number plus px rem em vh vw %; anything else warns and uses auto
full , bool , defaultfalse
true drops the reading-column limit
class , space-separated classes , default—
Passed through to the container

style, on*, and unknown attributes warn and are ignored. Empty DSL bodies warn and render nothing.

AntV defines these top-level DSL keys:

infographic / template
The template name, on the first line
data
title, desc, items (or sequences, compares, nodes, values, relations, root, depending on the structure), order
theme
type (light / dark / hand-drawn), palette, colorPrimary, stylize …
width / height
Canvas size at the DSL level; usually left to the fence’s height
design
Per-part tuning; rarely needed

Each entry under items accepts label, desc, value, icon, children, group and id. The DSL is defined by the AntV Infographic documentation; the version shipped with the theme and its checksum are recorded in the theme’s VENDOR.json.

Limits

  • Incorrect template names do not fail the build. Hugo checks only fence attributes; the browser runtime parses the DSL. Missing templates display error text in the container. Check the page after changing template names.
  • theme is part of the DSL and does not follow the site’s color scheme. Check contrast in both modes.
  • Print and RSS include only the DSL. State conclusions in prose.
  • SVG does not provide semantic structure. Screen reader order can differ from visual order. Prefer headings, lists, or tables when they can express the information.
  • Keep labels short. Narrow screens can truncate or compress long text. Check at phone width after editing.
  • ECharts — when you need axes and exact numbers
  • Steps — when the reader has to follow the procedure
  • Cards — a grid of clickable entry points
  • Mermaid — flows with branches and conditions

4.18 - Gallery

Use a gallery fence for a responsive image grid with optional descriptions and links. Images share the page zoom dialog.

A gallery displays related images in a responsive grid. Put one image per line inside the fence. Use it for multiple views of the same subject, such as screenshots, states, or color schemes. Use an image for a single picture. Gallery images should have an order or support a comparison.

Shortest form

Write one image per line with Markdown’s ![alt](src) syntax.

Source
```gallery
![OINK's default documentation shell](/images/oink.webp)
![The classic Docsy layout upstream](/images/docsy.webp)
```

Alternative text is mandatory. It sets the item’s title, supplies its only screen reader text, and determines zoom eligibility. There is no column parameter. The grid follows the container width and reduces columns on narrow screens.

Descriptions

Add # after the image to start a description below it. Descriptions are plain text. Markdown appears literally. Use \# for a literal hash.

Source
```gallery
![The three-column layout of an OINK page](/images/oink.webp) # The default shell: sidebar, article, table of contents
![The classic Docsy documentation layout](/images/docsy.webp) # Docsy upstream — the content model is the same lineage
![A release notes page](/images/releasenote.webp) # Release cards use release_url and date; checksums blocks list download assets
```

Descriptions can have different lengths. The grid aligns to the tallest item, and wrapped descriptions do not affect neighboring items. The image is parsed first. A # in alternative text or a path needs no escaping.

Put {link=…} at the end of a line to make that item a link. Site paths, relative paths, and http(s): are supported.

Source
```gallery
![OINK's default documentation shell](/images/oink.webp) # Opens the Images component page {link=/docs/components/image/}
![A release notes page](/images/releasenote.webp) # Opens "Releases and downloads" {link=/docs/write/releases/}
```

Linked items do not zoom because clicking opens their link. A gallery can contain both linked and unlinked items. Linked items open pages. Unlinked items open full images.

Where images come from

Sources use the plain-image resolution order: page resource in the page bundle → global resource in assets/ → static path /images/… → remote URL. Local resources include intrinsic dimensions to prevent layout changes while loading. Remote images are not downloaded or measured at build time.

Source
```gallery
![OINK documentation overview (global resource)](images/content-primitives/oink.webp) # Under assets/images/…, eligible for build-time processing
![The light home page (static path)](/images/hero-light.webp) # Under static/images/…, published as is
```

Unresolved page or global resources are kept as static paths. The theme does not check whether static or remote images exist.

Decorative images and zoom

Empty alternative text marks decorative images. They have no title, are skipped by screen readers, and never qualify for zoom.

Image zoom is a site-level switch and is off by default. This page enables it in front matter. Images above with alternative text and no link open at full size. Esc closes the dialog and restores the previous focus.

this page's front matter
image_zoom: true
Source: one decorative image, one ordinary one
```gallery
![](/images/docsy.webp) # Decorative, never zooms
![The Pigsty release notes page](/images/releasenote.webp) # Has alt text, so it opens
```

Galleries share the page’s image zoom dialog and have no separate zoom runtime. If no image on the page qualifies for zoom, this runtime does not load. See Images · Zoom.

Classes and tabs

Put class on the fence after the language, or on an item at the end of its line. The theme passes it through for site CSS without interpreting it. A fence with tab= and group= / value= becomes a tab set panel.

Source
```gallery {tab="OINK" group="shell" value="oink"}
![OINK's default documentation shell](/images/oink.webp) # Sidebar, article, table of contents
```
```gallery {tab="Docsy" value="docsy"}
![The classic Docsy layout upstream](/images/docsy.webp) # The same content-model lineage
```
OINK
Docsy

Output

Output Shape
HTML <ul class="td-gallery"> with one <li> per item; eligible images carry data-td-image-zoom; everything is lazy-loaded
Print The same images stacked, without zoom markers
Markdown The gallery fence, emitted as written
RSS The same static stack as print

Galleries load no JavaScript of their own.

Parameter reference

The line syntax ![alt](src) [# description] [{key=value …}]:

![alt](src) , Requiredyes
Must start the line. alt is the item’s title; empty means decorative
src , Requiredyes
Page resource / global resource / static path / remote URL
# description , Requiredno
Plain text under the image; \# is a literal hash; must not be empty
{link=…} , Requiredno
Makes the item a link, and therefore not zoomable
{class=…} , Requiredno
Adds a site CSS class to that item

Fence attributes:

tab , plain text , default—
Makes this gallery one panel of a tab set
group / value , string , default—
Tab group and sync value; must appear with tab
class , class list , default—
Passed through for site CSS

There is no columns, caption, or title attribute. Malformed lines or attributes warn and remove only the invalid part or line. The warning identifies the line inside the fence.

Limits

  • Fences are the only form. There is no {.gallery} list marker or shortcode. GitHub does not display the source as images. The theme guarantees behavior across four outputs and applies zoom eligibility rules.
  • Column counts cannot be set. The grid follows the viewport, and images keep their proportions without aspect-ratio cropping.
  • There is no slideshow, carousel, or previous / next control. The zoom dialog displays one image at a time.
  • Remote images are not downloaded at build time. Their dimensions remain unknown until the browser loads them, so layout can change.
  • Descriptions do not support Markdown. Put rich text in a paragraph below the gallery.

For album navigation, image sequencing, and drag-to-pan viewing, use the Expo layout introduced in OINK 1.3.0. The gallery fence is for image groups inside text pages.

  • Images — single images, captions, numbering, the zoom switch
  • Cards — a grid of links with images
  • Tabs — one gallery per platform or theme
  • File trees — the same line syntax family

4.19 - Badge

Add a semantic status label beside a feature name, version, or table cell. Choose from five tones; custom colors are not supported.

A badge is an inline status label after a name, such as Beta, deprecated, v0.5, or needs a server. Use it for a status of one or two words. Choose a semantic tone. The theme selects colors with contrast in light and dark modes. If the status needs an explanation, procedure, or deadline, use prose or a callout.

Shortest form

Source
{{< badge text="Beta" tone="warning" >}}
Beta

text is the only required parameter and must be a non-empty string.

Five tones

Use one of these five values. Custom colors are not supported.

Source
{{< badge text="Default" >}}
{{< badge text="Info" tone="info" >}}
{{< badge text="Supported" tone="success" >}}
{{< badge text="Experimental" tone="warning" >}}
{{< badge text="Deprecated" tone="danger" >}}

Default Info Supported Experimental Deprecated

If you omit tone, the badge uses neutral. Other values warn and use neutral in ordinary previews. The warning identifies the source location and should be reviewed with the rendered fallback before publication.

Inside a sentence

A badge is an inline element after a name. It never occupies a separate line.

Source
With `params.ui.image_zoom` {{< badge text="off by default" tone="neutral" >}} enabled,
block images that have alt text open full size. PlantUML {{< badge text="opt-in" tone="info" >}}
uses its own site switch. Draw.io {{< badge text="needs a server" tone="warning" >}}
warns and remains off when no endpoint is configured.

With params.ui.image_zoom off by default enabled, block images that have alt text open full size. PlantUML opt-in uses its own site switch. Draw.io needs a server warns and remains off when no endpoint is configured.

Next to a heading

Never put a shortcode in a heading. Hugo builds the table of contents before it expands shortcodes. The badge displays in the heading, but an internal Hugo placeholder remains in the table of contents. Put the status in the first paragraph below the heading:

Source
### OpenAPI pages {#openapi-example}

{{< badge text="new in 0.5" tone="success" >}} This section covers…

OpenAPI pages

new in 0.5 The badge appears below the heading. The table of contents stays readable. The shared anchor link does not include the badge text.

In table cells

Badges make a comparison table easier to scan than a column of “yes” and “no”.

Source
| Component | Form | Status |
| --- | --- | --- |
| Callouts | `> [!NOTE]` | {{< badge text="stable" tone="success" >}} |
| Galleries | ` ```gallery ` fence | {{< badge text="stable" tone="success" >}} |
| PlantUML | ` ```plantuml ` fence | {{< badge text="opt-in" tone="info" >}} |
| The `image` shortcode | — | {{< badge text="removed" tone="danger" >}} |
Component Form Status
Callouts > [!NOTE] stable
Galleries ```gallery fence stable
PlantUML ```plantuml fence opt-in
The image shortcode — removed

In lists and steps

Source
1. Install Hugo Extended {{< badge text="≥ 0.168.0" tone="info" >}}
1. Create a site from OINK Starter and change `baseURL` in `hugo.yaml`
1. `hugo server` to preview {{< badge text="port 1313" tone="neutral" >}}
{.steps}
  1. Install Hugo Extended ≥ 0.168.0
  2. Create a site from OINK Starter and change baseURL in hugo.yaml
  3. hugo server to preview port 1313

On cards

A card has its own badge parameter. This plain text appears to the right of the title. The card body can also contain badge shortcodes.

Source
{{< cards >}}
{{< card title="Hugo Module" icon="fa-brands fa-golang" badge="recommended" >}}
One `hugo mod get` and you are done {{< badge text="needs Go" tone="info" >}}
{{< /card >}}
{{< card title="Offline archive" icon="fa-solid fa-box-archive" >}}
Builds on a machine with no network {{< badge text="manual upgrades" tone="warning" >}}
{{< /card >}}
{{< /cards >}}
Hugo Modulerecommended

One hugo mod get and you are done needs Go

Offline archive

Builds on a machine with no network manual upgrades

With link, the badge becomes an <a> element. Site paths, relative paths, http(s):, and mailto: are supported.

Source
Current version {{< badge text="v0.5" tone="info" link="/blog/" >}};
for the upgrade steps see {{< badge text="Upgrading" tone="neutral" link="/docs/admin/upgrade/" >}}.

Current version v0.5; for the upgrade steps see Upgrading.

An invalid link warns and is removed, rendering a plain badge.

Output

Output Shape
HTML <span class="td-badge td-badge--<tone>">, or <a class="td-badge …"> when linked
Print Same as HTML, a static inline element
Markdown **Beta**, or [**Beta**](/…) when linked
RSS Same as print

Badges load no JavaScript. A badge is not a live region, so adding one does not produce a screen reader announcement.

Parameter reference

text , plain text , default—
Required, non-empty. What the reader sees
tone , enum , defaultneutral
neutral info success warning danger
link , URL , default—
Turns the badge into a link

Use named parameters only. There is no icon, class, color, outline, or size parameter. Invalid input warns and uses a safe fallback: unknown parameters are ignored, empty text renders nothing, invalid tone uses neutral, and unsafe links are removed.

Limits

  • Tone supplements the text. The text must state the status. {{< badge text="🔴" >}} gives a screen reader no meaning.
  • There is no icon parameter. If you need an icon, use cards or a callout.
  • Keep text short. Badges follow names without wrapping. Put text longer than a few words in the prose.
  • Use no more than three badges in one place. More badges make the associated name difficult to find.
  • Badges use only shortcodes. There is no native Markdown form. Plain Markdown readers show them as bold text.
  • Cards — card has a badge parameter of its own
  • File trees — tone uses the same vocabulary
  • Keys — the other inline shortcode
  • Callouts — when the status needs explaining

4.20 - Kbd

Use kbd with one key name per parameter to display a semantic key sequence. Instructions remain readable in print and Markdown.

Use keys to show shortcuts and chords separately from prose. Supply one positional parameter per key. The theme draws keycaps, adds separators, and gives screen readers a readable sequence. Use inline code for command names, flags, and text to type. They are not physical keys.

Shortest form

Source
Press {{< kbd "Ctrl" "K" >}} to open the command palette.

Press Ctrl with K to open the command palette.

Quote each positional parameter and use one parameter per key. Missing, empty, or named parameters warn. Ordinary previews do not render invalid keys. Review the warning and rendered fallback before publication.

A single key

Each parameter represents one key. Write symbol keys as displayed on the keyboard.

Source
{{< kbd "Escape" >}} closes a dialog;
{{< kbd "/" >}} jumps to search;
{{< kbd "t" >}} toggles light and dark;
{{< kbd "l" >}} cycles through languages.

Escape closes a dialog; / jumps to search; t toggles light and dark; l cycles through languages.

Chords

Multiple parameters display in order, separated by +. Assistive technology receives a localized connector instead of the visible plus sign.

Source
{{< kbd "⌘" "Shift" "P" >}} and {{< kbd "Ctrl" "Shift" "P" >}} are the same action.
For a literal plus, treat it as a key of its own: {{< kbd "Ctrl" "+" >}} zooms the page in.

⌘ with Shift with P and Ctrl with Shift with P are the same action. For a literal plus, treat it as a key of its own: Ctrl with + zooms the page in.

Platform differences

Use the reader’s keyboard labels: ⌘ on macOS and Ctrl on Windows and Linux. Never combine platform labels in one sequence. Labels such as Ctrl/⌘ cannot be read aloud correctly. Name the platform in the sentence or use tabs.

Source
On macOS press {{< kbd "⌘" "K" >}}; on Windows and Linux, {{< kbd "Ctrl" "K" >}}.

On macOS press ⌘ with K; on Windows and Linux, Ctrl with K.

Shortcut tables

Keys are common in cheatsheets. This table shows some global keys that the site supports:

Source
| Key | Action |
| --- | --- |
| {{< kbd "Ctrl" "K" >}} | Open the command palette ({{< kbd "⌘" "K" >}} on macOS) |
| {{< kbd "/" >}} | The palette's full search state |
| {{< kbd "t" >}} | Toggle light and dark |
| {{< kbd "q" >}} / {{< kbd "e" >}} | Previous / next page |
| {{< kbd "w" >}} {{< kbd "s" >}} {{< kbd "a" >}} {{< kbd "d" >}} | Move, collapse and expand in the sidebar tree |
| {{< kbd "Escape" >}} | Leave the sidebar tree for the article |
Key Action
Ctrl with K Open the command palette (⌘ with K on macOS)
/ The palette’s full search state
t Toggle light and dark
q / e Previous / next page
w s a d Move, collapse and expand in the sidebar tree
Escape Leave the sidebar tree for the article

The complete list of site-wide shortcuts is in keyboard navigation.

In steps

Source
1. Press {{< kbd "Ctrl" "K" >}} to open the command palette
1. Type `>` for the command-only state, or type a keyword to search
1. Select with {{< kbd "↑" >}} {{< kbd "↓" >}} and press {{< kbd "Enter" >}} to go
1. {{< kbd "Escape" >}} closes it and focus returns where it was
{.steps}
  1. Press Ctrl with K to open the command palette
  2. Type > for the command-only state, or type a keyword to search
  3. Select with ↑ ↓ and press Enter to go
  4. Escape closes it and focus returns where it was

Raw <kbd> tags

Raw <kbd> tags get the same styles, and GitHub also displays them. With raw tags, you maintain separators and accessible sequences yourself. Either form works for single keys. Use the shortcode for chords.

Source
Press <kbd>F5</kbd> to reload; in an editor, <kbd>Ctrl</kbd>+<kbd>S</kbd> saves.

Press F5 to reload. In an editor, Ctrl+S saves.

Output

Output Shape
HTML <span class="td-kbd-sequence"> around one <kbd> per key; the visible + is hidden from screen readers, which get a localized connector
Print Same as HTML, static
Markdown Plain text: Ctrl + K, ⌘ + Shift + P
RSS Same as print

The instruction remains readable without CSS or JavaScript.

Parameter reference

Positional 1..n , string , default—
At least one; each must be non-empty and quoted; order is display order

Only positional parameters are supported. There is no separator, label, platform, class, or size parameter. Hugo cannot combine positional and named parameters in one shortcode call.

Limits

  • Each sequence represents keys pressed together. For consecutive keys, use two kbd calls and describe the order: press Escape, then Enter.
  • There is no platform detection. The page never replaces Ctrl with ⌘ based on the visitor’s operating system.
  • There is no key mapping or recording. Menu paths, gestures, and gamepads are outside the scope.
  • Missing quotes fail the build. Ctrl in {{< kbd Ctrl K >}} is not a string parameter.
  • Do not use keys for commands. hugo server is inline code; Ctrl is a key.

4.21 - Includes

Use include to insert a file, param to print a site parameter, and comment to remove a note from every output.

include inserts another file’s content into the page. param prints a page or site parameter. comment removes a passage. Use them for fragments or constants shared by multiple pages. For example, use include for installation steps on three pages and param for a version number on dozens of pages. You then edit one source. Keep content used by only one page on that page.

Shortest form

include takes one required parameter, file:

Source
{{< include file="parts/install-oink.md" >}}

The included file contains ordinary Markdown under assets/:

assets/parts/install-oink.md
Installing OINK into an existing Hugo site takes three commands:

```sh
hugo mod init github.com/you/your-site
hugo mod get github.com/pgsty/oink
hugo server
```

> [!NOTE]
> `hugo mod get` needs Go on the machine; an offline archive or a submodule does not.

The current release is {{< param version >}}.

The result is the same as writing the content here. The code block has a copy button, and the callout displays normally.

Installing OINK into an existing Hugo site takes three commands:

hugo mod init github.com/you/your-site
hugo mod get github.com/pgsty/oink
hugo server
Note

hugo mod get needs Go on the machine; an offline archive or a submodule does not.

The current release is v1.5.1.

An included file is not a separate page. It has no sidebar entry, translation pairing, or URL.

Where the file comes from

file resolves in this order. The first match takes precedence:

Order Looked up as Written as
1 A page resource — a file in this page’s bundle file="config.yaml"
2 A global resource under assets/ file="snippets/dsn.txt"
3 A file under content/: a leading / is the content root, otherwise relative to the page’s directory file="notes/caveat.md", file="/shared/notice.md"

If no source matches, or the path contains .., the include warns and emits nothing. Review the warning and rendered fallback before publication. Include reads only from content/ and assets/.

Markdown fragments are read as source. Use the file’s actual disk name. Page resources in step 1 have one exception. Hugo attaches language-suffixed resources such as notice.zh.md under a name without the language suffix. Requesting notice.md from a bundle therefore gives include rendered HTML instead of source. The Markdown output can then contain <div class="td-code">. Under assets/ and content/, the written name identifies the exact file. Non-Markdown files (.yaml, .sh, .txt) have no such distinction.

Each language of this page includes its own fragment. English uses assets/parts/install-oink.md; Chinese uses assets/parts/install-oink.zh.md. Storing fragments under assets/ instead of the page bundle lets both languages retrieve source with the written filename.

Including code files

code=true displays the file as a code block. lang= sets its highlighting language. Use a real repository file to keep the documentation consistent with that file.

Source
{{< include file="parts/module.yml" code=true lang="yaml" >}}
module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: 0.168.0

Included code and fences share the same pipeline for highlighting, line numbers, and copying. You cannot pass fence attributes such as title=, collapse, or hl_lines= through include. If you need them, write an ordinary code block.

What a fragment can contain

Fragments use page-level Markdown in the current page’s context. They support callouts, tables, lists, images, steps, and shortcodes. The example fragment’s last line, “The current release is v0.8.1”, comes from {{< param version >}} expanded on this page.

If two pages include the same fragment, each renders it separately. Each generates its own heading anchors and code-block IDs without collisions.

What makes a good fragment

Use fragments for installation commands, connection strings, support matrices, and legal notices that must change on all pages together. Keep content used by only one page on that page.

Printing a site parameter

param prints one parameter. It checks page front matter first, then site configuration, following Hugo’s .Param rule.

Source
This site publishes {{< param version >}}, copyright from {{< param copyright.from_year >}},
and this page's front matter says `pigsty_pg_major: 18`, which reads back as {{< param pigsty_pg_major >}}.

This site publishes v1.5.1, copyright from 2026, and this page’s front matter says pigsty_pg_major: 18, which reads back as 18.

Join nested keys with .. For example, copyright.from_year reads params.copyright.from_year. Missing parameters and values that are maps or lists warn and print nothing. Review the warning and rendered fallback before publication.

Parameters inside commands, tables and links

param emits escaped plain text. It can appear in code fences, table cells, or link targets. For example, use it for a version number in an installation command:

Source
```sh
hugo mod get github.com/pgsty/oink@{{< param tdVersion.latest >}}
```

| Item | Value |
| --- | --- |
| Current version | {{< param version >}} |
| Hugo floor | {{< param hugoMinVersion >}} |

[Release notes](https://github.com/pgsty/oink/releases/tag/{{< param tdVersion.latest >}})
hugo mod get github.com/pgsty/[email protected]
Item Value
Current version v1.5.1
Hugo floor 0.168.0

Release notes

Configuration defines site parameters. Front matter defines page parameters.

Notes deleted at build time

A comment body appears in none of the four outputs: HTML, print, Markdown, or RSS. HTML comments remain in page source and reach llms.txt.

Source
Since PostgreSQL 18, `pg_stat_io` breaks out WAL statistics.

{{< comment >}}
TODO: after v0.5 ships, bump the version above to 19 and add a pg_stat_io screenshot.
This text reaches no output at all, llms.txt included.
{{< /comment >}}

Verify the dashboards on a test database before upgrading.

Since PostgreSQL 18, pg_stat_io breaks out WAL statistics.

Verify the dashboards on a test database before upgrading.

A comment exists between those paragraphs, but it does not appear in the page source.

Output

Output include (Markdown) include code=true param comment
HTML The fragment renders as normal content Highlighted code block + copy button Escaped plain text nothing
Print As HTML As HTML, without the copy button As HTML nothing
Markdown The fragment’s source, as written A source fence The value itself nothing
RSS As HTML As HTML As HTML nothing

Markdown output preserves fragment source instead of rendered HTML. Its shortcodes remain as {{< param version >}}. This is the intended source-preserving behavior. None of these three shortcodes loads a script.

Parameter reference

include (named parameters only):

file , path (required) , default—
Resolution order in Where the file comes from; a .., missing file, or empty value warns and emits nothing
code , boolean , defaultfalse
true renders as a code block; quoted code="true" warns and includes ordinary content
lang , string , default—
Code language; without code=true it warns and is ignored

Other parameter names warn and are ignored. The message identifies the file and line.

param (one positional parameter):

parameter name , string (required) , default—
Nested keys join with .; page front matter first, then site params; missing or non-scalar values warn and print nothing

comment takes no parameters. Use opening and closing shortcodes. All content between {{< comment >}} and {{< /comment >}} is discarded.

Limits

  • include is not a template. You cannot pass variables, include conditionally, or supply code-block fence attributes such as title= or collapse. For platform variants, create two fragments and use tabs.
  • You maintain fragment languages. include has no language fallback and uses the exact written path. You can share a fragment across languages, as this page’s Chinese translation includes the same English file. Alternatively, write one fragment per language and use its path on the corresponding page.
  • param prints only scalars. Put structured data, such as version matrices or download lists, in data/ and use the matching component.
  • comment discards content on every build. To temporarily unpublish a whole page, use draft: true.
  • Do not use include to create an index page. If a page needs ten fragments, give readers ten links instead.
  • Code blocks — every fence attribute, and the pipeline include code=true reuses
  • Tabs — per-platform or per-language fragments
  • Configuration — the site parameters param can reach
  • Front matter — page parameters, which win over site configuration

4.22 - Asciinema

Display a .cast terminal recording with selectable text. The theme includes the player and does not use a CDN.

asciinema displays a .cast recording in a terminal player. Use it for command-line procedures. Readers can select and copy the terminal text. The six-minute installation recording on this page is about 196 KB. This component plays only terminal recordings. Use screenshots or video for graphical interfaces. The theme includes the player and styles. It downloads nothing at build time and contacts no CDN at runtime. The runtime loads only in HTML output on pages that use the component.

Shortest form

file is the only required parameter:

Source
{{< asciinema file="images/install.cast" >}}

images/install.cast — /images/install.cast

The recording shows a single-node Pigsty installation on Debian in a 120×36 terminal. It lasts about 6 minutes and 42 seconds. Download the sample recording to static/images/install.cast in your site. Write its path from the site root. For a file under assets/, use a relative path. The theme checks resources first, then treats the value as a site-root path. If you omit title, the window title shows the value of file.

Window title and theme

title sets the window title. theme sets the colors:

Source
{{< asciinema file="images/install.cast" title="Pigsty single-node install" theme="dracula" >}}

Pigsty single-node install — /images/install.cast

theme defaults to auto and follows the site’s color scheme. It uses td-light in light mode and td-dark in dark mode. When the reader changes modes, the player remounts in place. To fix the terminal palette, use a player theme: asciinema, dracula, gruvbox-dark, monokai, nord, seti, solarized-dark, solarized-light, or tango. You can also use the theme’s td-light / td-dark. A fixed theme does not follow the site’s color scheme. solarized-light does not provide usable contrast on a dark site. The player uses the same code font as the site’s code blocks.

Speed, start point and poster

Three parameters control the initial playback state. speed sets the rate, startAt skips the opening, and poster sets the frame before playback.

Source
{{< asciinema file="images/install.cast" title="From 60 seconds in, at double speed"
  speed="2" startAt="60" poster="npt:1:30" >}}

From 60 seconds in, at double speed — /images/install.cast

speed and startAt take numbers (seconds). poster uses the player’s npt: time notation. For example, npt:1:30 means one minute and thirty seconds. The player above shows the frame at 90 seconds and starts playback at 60 seconds.

idleTimeLimit limits silent intervals to N seconds. This recording already limits idle time through idle_time_limit: 0.5 in its .cast header. It needs no additional limit. Only recordings made without an idle limit need this setting.

Size and fit

By default, the player scales to the container width (fit="width"). It reads the terminal dimensions from the .cast header. Use cols / rows to override those dimensions:

Source
{{< asciinema file="images/install.cast" title="Only 16 rows tall" rows="16" >}}

Only 16 rows tall — /images/install.cast

Dimensions smaller than the recording clip its content. The example above shows 16 of the 36 rows. Use cols / rows to correct incorrect header dimensions. Do not use them to control layout. To make the player shorter, record again in a smaller terminal.

fit accepts four values: width, the default, scales to width; height scales to height; both fits both axes. none disables scaling, so a wide terminal overflows.

Looping and preloading

loop restarts playback at the end. preload fetches the .cast when the page loads, so playback starts without waiting:

Source
{{< asciinema file="images/install.cast" title="Looping: the first minute after login"
  startAt="0" speed="3" loop="true" preload="true" >}}

Looping: the first minute after login — /images/install.cast

autoplay="true" starts playback when the page opens. This is not recommended. The “reduce motion” preference disables only player control transitions. It does not stop autoplay. If autoplay is necessary, use loop, keep the clip very short, and use only one clip per page.

Inside steps

Put the recording beside its step. The text gives the action. The recording shows the result.

Source
1. Install the dependencies and fetch the installer:

   ```sh
   curl -fsSL https://repo.pigsty.io/get | bash
   ```

2. Run the install; the recording plays the process at four times speed:

   {{< asciinema file="images/install.cast" title="pig install" speed="4" >}}

3. Open `http://<node address>:3000` and sign in to Grafana with `admin / pigsty`.
{.steps}
  1. Install the dependencies and fetch the installer:

    curl -fsSL https://repo.pigsty.io/get | bash
  2. Run the install; the recording plays the process at four times speed:

    pig install — /images/install.cast

  3. Open http://<node address>:3000 and sign in to Grafana with admin / pigsty.

A page can hold several players, and the script and styles load once.

Recording a cast file

The theme plays recordings but does not create them. Record with asciinema. Use asciinema rec --idle-time-limit=2 --cols=100 --rows=28 install.cast, then check locally with asciinema play install.cast.

  • Keep the terminal under 100 columns so it stays readable on narrow screens. Run clear before you start.
  • Remove secrets before recording. A .cast is plain text, so every recorded character can be searched. Check the file before committing.
  • Put the file at static/images/install.cast and use file="images/install.cast". assets/images/install.cast works with the same value. Page-bundle resources are not resolved. Commit the recording instead of using a .cast URL from another site.

Output

Output Shape
HTML A <div class="td-asciinema"> window frame plus the player; the player CSS/JS and the runtime load on demand, once per page, and only in this output
Print A labelled static link showing the recording’s address; no player, no runtime
Markdown A plain Markdown link, [title](/images/install.cast) — no component markup, no configuration block
RSS The same plain link

A recording must never be the only source of information. Write the key commands and output beside it as text or code. Offline readers, consumers of llms.txt, and readers of printed pages receive the link and prose. They do not receive the terminal session.

Parameter reference

file , path (required) , default—
Named, or the first positional parameter; looked up as a global resource first, then as a site-root path; a full URL with a scheme is passed through unchanged
title , plain text , defaultthe value of file
The window title
theme , enum , defaultauto
auto follows the site’s colour scheme; or td-light td-dark asciinema dracula gruvbox-dark monokai nord seti solarized-dark solarized-light tango
fit , enum , defaultwidth
width height both none; anything else warns and uses width
cols / rows , integer , defaultfrom the .cast header
Override the terminal size; smaller than the recording clips it
speed , number , default1
Playback rate
startAt , number (seconds) , default0
Where playback starts
idleTimeLimit , number (seconds) , defaultfrom the .cast header
Longest a silent stretch plays for
poster , string , default—
The frame shown before playback, npt:mm:ss
autoplay , "true" / omitted , defaultoff
Play as the page opens; not recommended
loop , "true" / omitted , defaultoff
Replay at the end
preload , "true" / omitted , defaultoff
Fetch the .cast when the page loads
pauseOnMarkers , "true" / omitted , defaultoff
Pause at chapter markers
markers , time:label,time:label , default—
Chapter markers; see the limits — the labels do not reach the player today

Boolean-like parameters compare their value with the text true. Both loop="true" and loop=true enable the option. Other values disable it. Other invalid values warn and use a fallback: invalid fit uses width, nonnumeric speed uses 1, nonnumeric startAt uses 0, and nonnumeric cols, rows, idleTimeLimit, and marker times are ignored.

Limits

  • markers labels are lost. The theme converts the time:label list to a one-dimensional array, but the player accepts only pairs. The timeline therefore has unlabeled markers. A nonnumeric marker time warns and is skipped. If you need chapters, write a list beside the recording.
  • The player needs JavaScript. With browser scripts disabled, only the window frame remains. Print, Markdown, and RSS provide a link instead. See Output.
  • Recordings are not searchable. The site index covers page text, so it cannot find commands that appear only in recordings.
  • Do not use a remote .cast. Although http and https addresses are accepted, they make the page depend on another site. Other schemes, protocol-relative //host addresses, and empty values warn and render nothing.
  • Keep clips short. Few readers finish recordings longer than five or six minutes. Split long procedures into short recordings, each with its own text.
  • Code blocks — the key commands and output, copyable
  • Steps — the recording beside the step it belongs to
  • Images — static screenshots: recordings for terminals, screenshots for graphical interfaces
  • Include — when the same commands appear on several pages

5 - Customization

Configure the site brand, navigation, layout, search, languages, versions, print output, and agent output.

Use this section to configure site parameters in hugo.yml, data files under data/, and style entry points under assets/. To write individual pages and their front matter, see Authoring.

Find it by what you want to change

What you want to change Page
Site name, logo, favicon Brand and appearance
Colors, light and dark mode, fonts Brand and appearance
The navbar menu and its dropdowns Navigation and menus
Sidebar width, icon density, outline depth Layouts and page types
The home page and landing pages Home and landing pages
Full-text search and its index scope Search
What appears in the command palette Command palette
Keyboard shortcuts Keyboard navigation
Adding a language Languages
Multi-version sites and the archive banner Versions
Tags and categories Taxonomies
Edit this page, last modified, contributors Repository links and page info
Print and whole-chapter export Print
llms.txt and the per-page .md output AI-agent support
A parameter’s type and default Configuration

Comments, analytics, and deployment require an external service. See Operations.

5.1 - Configuration

Find every site parameter that the theme reads, with its type, default, description, and related guide.

This page is the complete reference for site-level parameters in hugo.yml. Every key that the theme reads appears in a table below with its type, default, short description, and a link to its dedicated guide for copy-ready examples. For page-level front matter parameters, see Page parameters.

The tables are grouped by function, with one ## heading per group (so you can link directly to anchors such as /docs/customize/config/#sidebar). An empty default column means the theme has no default—if you omit the key, that feature stays off.

The layers of hugo.yml

An OINK site’s configuration has four kinds of key. Choose the layer that controls the feature you want to change:

Layer Examples Who defines it
Hugo’s own top-level keys baseURL title languages markup outputs taxonomies module Hugo itself. See gohugo.io for behavior
Top-level params logo offline_search github_repo version page_width comments Site-level options the theme reads
params.ui.* navbar_enabled sidebar_width_min typography pager_types The shell, navigation and reading interface
params.<runtime> mermaid plantuml drawio markmap Each content runtime’s own switch and endpoint

A minimal working configuration needs only the first two layers (including the three Goldmark settings that power OINK’s native Markdown components):

hugo.yml
title: Product Docs
baseURL: https://docs.example.com/
defaultContentLanguage: en
enableGitInfo: true

markup:
  goldmark:
    renderer:
      unsafe: true
    parser:
      attribute:
        block: true
      wrapStandAloneImageWithinParagraph: false
  highlight:
    noClasses: false

module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: 0.168.0

params:
  offline_search: true
  github_repo: https://github.com/example/product-docs

Configuration principles

  • The theme’s defaults are conservative; write only the keys you change. Local search, image zoom, comments, feedback, and the light/dark menu are off by default. The site chooses its policy for these interactive features. Add keys as needed. Reducing a “complete configuration” can leave unnecessary keys.

  • There is no theme master switch. There is no oink.enabled or params.oink.* namespace. No option switches between a “Docsy shell” and an “OINK shell”. A switch that is absent from this page does not exist.

  • An invalid value warns and falls back to the documented default. The setting params.ui.typography: solarized reports invalid params.ui.typography "solarized" (allowed: technical | system) -- using "technical". The site still builds. The settings footer_style: thin, page_width: huge, and section_index: grid behave the same way. One typo therefore changes one setting instead of producing HTTP 500 on every URL under hugo server.

  • One warning keeps the value instead of dropping it. If theme_color is below AA body-text contrast (4.5:1) against the theme canvas, the color still appears in output. The author may choose it for a custom canvas or brand requirement. The warning includes the ignoreLogs id that can silence it. Use a darker color or silence the warning with one configuration line. Only an unparseable hex value is discarded and falls back to the default palette.

  • The theme itself never stops the build. Its templates contain no errorf. Every invalid value uses the warning and fallback behavior above. Draw.io and Algolia warn and stay off when their required service configuration is missing. OINK 1.4.0’s PlantUML renderer needs no endpoint for local mode; an invalid explicit endpoint warns and disables rendering. The theme never chooses a public service for you. Incomplete upstream attribution produces a warning and omits the entire notice. A partial notice could appear complete. Hugo can still stop a build if a content reference resolves to nothing or its version is below module.hugoVersion.min.

Page-level override precedence

Hugo’s .Param lookup lets you override most parameters per page. It applies the following precedence, from highest to lowest:

  1. The page’s own front matter;
  2. cascade in an ancestor section’s _index.md (nearer wins);
  3. Site params.

Remove the ui. prefix in front matter. The site key params.ui.reading_time becomes reading_time on a page. The theme ignores a front matter ui: block and does not report it. If a setting has no effect, first check Page parameters.

content/docs/wide-reference.md
---
title: Wide reference
page_width: wide
navbar_enabled: false
footer_style: slim
reading_time: false
---

A cascade sets a whole subtree at once:

content/docs/_index.md
---
title: Docs
cascade:
  type: docs
  footer_style: slim
  feedback: true
---

Use overrides for actual content differences. Separate visual systems on individual pages can become inconsistent after a theme upgrade.

The three Goldmark prerequisites

Hugo does not merge a theme module’s markup configuration into the site. Put these three prerequisites in the site’s own hugo.yml. Without them, attribute lines, component HTML, and mathematics do not work:

hugo.yml
markup:
  goldmark:
    parser:
      # block images may carry an attribute line ({caption=…}, numbered figures)
      wrapStandAloneImageWithinParagraph: false
      attribute:
        block: true
    renderer:
      # HTML emitted by `{{% … %}}` shortcodes has to survive
      unsafe: true
    extensions:
      passthrough:
        enable: true
        delimiters:
          block: [['\[', '\]'], ['$$', '$$']]
          inline: [['\(', '\)']]
  highlight:
    # class-based highlighting, so light and dark can each have a palette
    noClasses: false
  tableOfContents:
    endLevel: 4

Without attribute.block, {.fields}, {.steps}, and {caption=…} render as literal text. Without passthrough, \(x\) does not become a formula. Without unsafe, Hugo escapes the structure of steps and cards.

The setting renderer.unsafe: true also permits raw HTML in Markdown. It is for trusted authors and is not a submission filter. Review untrusted content in the contribution process.

Site identity and brand

Hugo’s own top-level keys:

title , string
Site name, shown in the navbar, <title> and the footer
baseURL , string
The production domain. Include the path segment for a subpath deployment
copyright , string
Fallback for the copyright line, rendered as HTML when params.copyright is unset
enableGitInfo , boolean , defaultfalse
Required before “last modified” and commit information exist
enableRobotsTXT , boolean , defaultfalse
Generates robots.txt
enableEmoji , boolean , defaultfalse
Allows :smile: shortcodes

Theme parameters:

params.logo , string , defaulticons/logo.svg
Brand mark. May use an assets/ resource or a static/ path. See Brand and appearance
params.wordmark , string
Horizontal wordmark. If set, replaces “icon + site name” in the navbar
params.description , string
Site description, the meta fallback when a page has no description
params.copyright , string or map
A string renders as Markdown. A map accepts authors, from_year, and to_year. The value present means this year
params.footer_center_info , string , defaultPowered by Oink
Inline Markdown in the footer center. An empty string hides it
params.author , string or map
The RSS author. A map accepts name and email
params.ui.theme_color , string
#rgb/#rrggbb hex color for shell accent backgrounds. Does not affect prose links or inline code. See Brand and appearance
params.ui.theme_color_dark , string , defaultderived
The dark accent color. If omitted, the theme derives it from theme_color until it meets AA contrast on the dark canvas

There is no favicon parameter. The theme scans static/ for conventional names (favicon.ico, favicon.svg, favicon-NxN.png, apple-touch-icon.png, apple-touch-icon-NxN.png). See Brand and appearance.

Shell types and section roots

The shell follows the page type, not the path. Documentation can live in any directory, with a cascade giving it type: docs.

params.ui.shell_types , list , default[docs, book, blog, swagger]
Which types use the reading shell with a sidebar — see Layouts and page types
params.ui.docs_section , string , defaultdocs
The documentation section’s root directory name, used for navigation resolution only
params.ui.blog_section , string , defaultblog
The blog section’s root directory name
params.ui.docs_sidebar_root , enum , defaultsection
With section, a docs sidebar starts at the documentation section. With home, it starts at the site home. An invalid value warns and uses a fallback
params.ui.quick_links , list , default[docs_section, blog_section]
Top-level menu identifiers listed by the command palette on an empty query — see Command palette
params.ui.sidebar_root_enabled , boolean , defaulttrue
Allows a subsection to become its own sidebar tree with sidebar_root_for: self
params.ui.sidebar_root_menu , boolean , defaulttrue
Shows the section switcher above the sidebar. With only one entry, it becomes a plain link
params.ui.section_index , enum , defaultlist
Child list style on a section index: list or cards, overridable per section
params.ui.section_index_columns , integer , default2
Column count when section_index: cards

Expo albums

OINK 1.3.0 adds expo to the default shell_types list and supports these settings:

params.ui.expo.images , string , defaultimages/*
Pattern for image resources in the bundle
params.ui.expo.mode , enum , defaultwall
wall, cards, or seamless
params.ui.expo.size , integer , default240
Target size from 160 to 360 CSS pixels

A page’s expo block replaces the site block. Absent fields use built-in defaults. Stored reader choices override the author defaults for mode and size. See Expo defaults for the complete rules.

Blog

These settings shape a blog section. They apply to the section named by params.ui.blog_section, and each can be overridden per section through front matter or a cascade on the blog root.

The layout guide compares rows, cards, tables, and galleries. OINK v1.4.0 supports all four layouts and the blog_gallery settings below.

params.ui.featured_image , enum , defaultnone
Controls the article’s featured image on single pages and section indexes. The mode none renders nothing. The mode banner shows a 16:9 figure above the title. The mode wash shows the image behind the article header at a tenth of its opacity. The mode hero uses a full-bleed shell background and moves the opening down. It uses the same image as the page card and og:image, so they cannot differ. Without an image, every mode renders nothing
params.ui.blog_index , enum , defaultlist
list shows horizontal entries, cards shows regular image cards, table shows compact rows, and gallery shows linked covers. All sort by date, newest first. Only a standalone table with blog_index_toggle: false shows the complete section without pagination
params.ui.blog_index_columns , integer , default3
Column count for blog_index: cards. Regardless of this value, uses two columns between md and xl breakpoints and one below md
params.ui.blog_index_size , integer , default12
Posts per page for list, cards, gallery, and all four views when switching is enabled. A standalone table ignores it
params.ui.blog_index_toggle , boolean , defaultfalse
Publishes all four layouts and lets readers cycle through list → cards → table → gallery, remembering their choice. When false, publishes only the configured layout
params.ui.blog_gallery.mode , enum , defaultwall
Gallery arrangement: wall has 6px gaps, seamless has no gaps, and cards uses masonry with visible captions. This is independent of the outer blog_index layout
params.ui.blog_gallery.size , integer , default240
Target gallery tile size, 160–360 CSS pixels. Does not set the number of posts per page
params.ui.toc_style , enum , defaultfixed
Right-rail presentation. The mode fixed pins a panel to the viewport. The mode flow uses a wider panel in content flow. It starts with the article and pins only on scroll
params.ui.toc_taxonomies , boolean , defaulttrue
Taxonomy term clouds in the right rail. If the rail has neither a table of contents nor clouds, it renders nothing

A page or cascade blog_gallery block replaces the whole site block. Missing fields use wall and 240; invalid values warn and fall back to those defaults. Saved gallery arrangement and size override the author defaults and are independent of Expo preferences. See Gallery.

Article authorship and series are taxonomies rather than parameters — see Taxonomies and Writing a blog.

params.ui.navbar_enabled , boolean , defaulttrue
Whether the site navbar renders; overridable with a top-level navbar_enabled on a page — see Navigation and menus
params.ui.navbar_autohide , boolean , defaultfalse
The navbar retracts above the viewport. It returns when the pointer enters the wake zone. Inactive below 768px and on coarse pointers
params.ui.footer_style , enum , defaultfat
fat is a multi-column grid plus the copyright line, slim is the copyright line only, none renders nothing. An invalid value warns and falls back
params.ui.dark_mode , boolean or map , defaultfalse
true enables the dark palette and theme control. For only the control, use dark_mode: { show_menu: true }
params.ui.breadcrumb , boolean , defaulttrue
Breadcrumbs; false turns them off. A top-level section already omits a one-level breadcrumb
params.ui.page_context_menu.enable , boolean , defaulttrue
The page action split button beside the title
params.ui.page_context_menu.assistant_links , boolean , defaultfalse
Shows “Open in ChatGPT / Claude”; clicking sends the full URL off-site
params.ui.page_context_menu.links , list , default[]
Custom external actions; url supports the {url}, {title} and {markdown_url} placeholders
params.ui.github_stars , string or number
Star count on the navbar GitHub mark. Uses a local constant and makes no request
params.ui.alt_site , map
Sibling-site link in a single-language footer. Requires both label and an absolute http(s) url

The fat footer’s column data comes from data/footer/<language>.yaml rather than from a parameter — see Navigation and menus.

Sidebar

params.ui.sidebar_menu_compact , boolean , defaulttrue
Expands only the current branch and its neighbours
params.ui.sidebar_menu_foldable , boolean , defaulttrue
Lets the reader expand and collapse sections
params.ui.sidebar_menu_truncate , integer , default2000
Maximum rendered entries in one section. Truncates the remaining entries
params.ui.sidebar_cache_limit , integer , default500
At this page count, reuses visible neutral navigation markup for matching language/root/effective settings. The browser adds active state
params.ui.sidebar_width_min , integer , default220
Lower bound in pixels for drag-resizing on the desktop
params.ui.sidebar_width_max , integer , default480
Upper bound in pixels for drag-resizing
params.ui.sidebar_item_overflow , enum , defaultellipsis
ellipsis truncates a long title, wrap wraps it
params.ui.sidebar_icon_policy , enum , defaultall
Icon density: all everywhere, groups only on the root and nodes with children, none nowhere. An invalid value warns and falls back to all
params.ui.sidebar_expand_levels , integer , default2
Tree levels expanded by default
params.ui.sidebar_headings , boolean or integer , defaultfalse
For type: book only. Expands a heading branch below the current sidebar row. Accepts an integer from 2 to 4. The value true means 2
params.ui.sidebar_enabled , boolean , defaulttrue
The left sidebar. The value false disables it, usually per page instead of per site
params.ui.taxonomy_icons , map
Right-column group icons by taxonomy plural, for example tags: fa-solid fa-tags

For sidebar use, see Layouts and page types. The tree uses the structure of content/. See Organizing content.

Table of contents

Hugo’s configuration sets the outline levels. The theme controls only tracking behavior:

markup.tableOfContents.startLevel , integer , default2
Hugo’s own: the highest heading level collected
markup.tableOfContents.endLevel , integer , default3
Hugo’s own: the lowest heading level collected
params.ui.scroll_spy , boolean , defaultfalse
Quiet 1.x compatibility no-op; the normal shell runtime always tracks the active outline heading and this key emits no asset

Hide the outline on one page with the front matter notoc: true — see Page parameters.

Pager and page end

Page-end components have a fixed order: share → feedback → page information → pager → comments. Each has its own switch. Backlinks appear in the right rail beside the table of contents.

params.ui.share , list , default[]
Page-end share targets, in the order given, from x bluesky mastodon facebook linkedin reddit hackernews telegram whatsapp line pinterest weibo chatgpt claude email copy. An empty list disables the bar. Each target is a plain intent link. There is no SDK, iframe, third-party script, or share count. See Writing a blog. An unknown target warns and is discarded
params.ui.pager_types , list , default[docs, book, blog]
Types that show previous / next links. To disable them on a page, use front matter pager: false. An unknown type warns and is discarded
params.ui.annotation , boolean , defaulttrue
The “last modified” and provenance block at the end of the body; the upstream attribution line is driven by the page’s upstream_link family — see Page parameters
params.ui.backlinks , boolean , defaultfalse
Lists the pages that link to this one as a “Backlinks” group in the right rail beside the table of contents, derived at build time from ordinary links — see Navigation and menus
params.ui.compare , boolean , defaultfalse
Adds a two-pane Duo View to Docs, Book, and blog pages that have a same-origin HTML translation. A language can set it to false to opt out — see Duo view
params.ui.translation_notice , language code or false , defaultfalse
The language code of the authoritative version, so a translated page shows a line pointing back at it; a page opts out with translation_notice: false
params.ui.reading_time , boolean , defaultfalse
Shows a reading time under the page title
params.ui.book_draft_banner , boolean , defaultfalse
Adds a banner at the top of a draft Book page

Local search is off by default, and the command palette appears only once it is on (the navbar magnifier, Cmd/Ctrl with K, /, \).

params.offline_search , boolean , defaultfalse
Generates one local index per language and enables the command palette — see Search
params.offline_search_on_serve , boolean , defaulttrue
Also builds the index under hugo server, so preview matches production. For a very large site, set false to reduce local rebuild time
params.offline_search_index , enum , defaultcontent
Index scope, cumulative: title, heading, summary, content. An invalid value warns and uses content
params.offline_search_summary_length , integer , default70
Word cut-off for the summary scope’s excerpt
params.offline_search_max_results , integer , default10
Result cap, bounding both Lunr and the CJK substring fallback
params.ui.landing_search , boolean , defaulttrue
Whether a layout: landing page keeps a search entry point
params.ui.command_palette.commands , list , default[]
Custom commands, each with either url or a built-in action — see Command palette
params.gcs_engine_id , string
A Google Programmable Search engine ID; enabling it brings in an external service
params.search.algolia , map
Algolia DocSearch. You must supply appId, apiKey, and indexName explicitly. Otherwise, the theme warns and DocSearch remains off

A custom command accepts only seven keys: id, title, description, icon, keywords, url, and action. The id must match ^[a-z][a-z0-9_-]*$ and must not duplicate a built-in action ID. Put per-language titles under languages.<lang>.params.ui.command_palette.commands.

Keyboard

params.ui.keyboard_nav , boolean , defaulttrue
Single-key navigation for the tree (WASD / arrows), headings (j/k), paging (q/e), palette, and shell switches. With false, the runtime is excluded from the bundle. See Keyboard navigation

Image zoom

params.ui.image_zoom , boolean , defaultfalse
Lets body images open at full size. Override per page with front matter image_zoom. A non-boolean warns and uses a fallback

Which images become zoom candidates is in Images.

Typography

params.ui.preset , enum , defaultpaper
Site-wide visual preset: paper, slate, or the explicit experiments ink, terminal. Available in 1.2.0; choose slate to retain the previous appearance
params.ui.preset_menu , boolean or list , defaultfalse
true offers Paper, Slate and the site default; a list explicitly opts into experiments and must include the site default. Independent of dark_mode
params.ui.typography , enum , defaulttechnical
technical uses the selected preset’s local fonts (Paper: Plex Sans; Slate: Inter); system uses the platform stack only and requests no brand font. An invalid value warns and falls back
params.ui.fonts , map
Font-family names for the ui, body, heading, code, display, meta, brand, and print roles. The theme validates names but never loads font files. End each list with a generic family
params.page_width , enum , defaultnormal
Overall shell width: normal, wide, full; overridable per page
params.reading_width , enum , defaultnormal
Book body reading width: slim, normal, wide. Does not affect the shell

Use params.ui.fonts when the faces already exist on the reader’s system or the site has declared them with @font-face. To bundle font files or change lower-level typography, use the SCSS/CSS entry points. See Brand and appearance.

Comments and feedback

params.comments.enable , boolean , defaultfalse
The site-level comment switch; a page overrides it with the front matter comments — see Comments
params.comments.type , string , defaultgiscus
Only giscus actually renders today
params.comments.giscus.repo , string
The GitHub repository hosting the discussions; required
params.comments.giscus.repoId , string
The repository ID; required
params.comments.giscus.category , string
The discussion category name; required
params.comments.giscus.categoryId , string
The discussion category ID; required
params.comments.giscus.mapping , string , defaultpathname
How pages map to discussions
params.comments.giscus.term , string
The discussion title or number when mapping is specific or number; the attribute is omitted when unset
params.comments.giscus.strict , string , default0
Strict title matching
params.comments.giscus.reactionsEnabled , string , default1
Shows reactions on the main post
params.comments.giscus.emitMetadata , string , default0
Sends discussion metadata to the parent page
params.comments.giscus.inputPosition , string , defaulttop
Whether the input box sits above or below the list
params.comments.giscus.theme , string , defaultauto
The giscus theme; auto follows the site’s light/dark state
params.comments.giscus.lightTheme , string , defaultlight
The giscus theme or custom CSS URL used in light mode
params.comments.giscus.darkTheme , string , defaultdark
The giscus theme or custom CSS URL used in dark mode
params.comments.giscus.loading , string , defaultlazy
The iframe loading strategy
params.comments.giscus.lang , string , defaultderived from the site language
The giscus interface language. If unset, Chinese sites resolve zh-CN / zh-TW / zh-HK. Other languages use the base language code. Unsupported languages fall back to en
params.comments.giscus.ariaLabel , string , defaultComments
The comment container’s aria-label. The default is English. For a multilingual site, write one per language
params.comments.giscus.errorMessage , string , defaultComments could not be loaded.
Text shown after loading fails. The default is English. For a multilingual site, write one per language
params.ui.feedback.enable , boolean , defaultfalse
The two “was this page helpful?” buttons at the page end. There is no backend. Records a structured event if gtag is present
params.ui.feedback.reasons , boolean , defaulttrue
Expands four optional reasons after “no”

If a required giscus value is missing, the theme warns and skips the comment section. See Comments for the required fields.

Repository links and page information

params.github_repo , string
The content repository URL, resolving “edit this page”, “view history”, “create child page” and “open a documentation issue” — see Repository links and page info
params.github_project_repo , string , defaultgithub_repo
The product repository URL, for “open a project issue” and the navbar GitHub entry
params.github_branch , string , defaultmain
The branch edit links point at
params.github_subdir , string
The content site’s subdirectory inside a monorepo
params.path_base_for_github_subdir , string or map
Rewrites normalized / source paths. The map accepts from and to. External mounts require an explicit mapping to a repository-relative result. See repository links.
params.github_url , — , default—
Removed. Use params.github_repo. The old migration registry no longer names the replacement. The theme now ignores an old key
params.ui.lastmod_commit , enum , defaultsubject
What follows “last modified”: subject the commit subject, hash the short hash, none nothing. An invalid value warns and falls back
params.images , string array , default—
Site-level social card. Supplies og:image if a page has no image. Used only for metadata. Never rendered as a list thumbnail
params.upstream_source , string , default—
Default data/upstreams record name for pages that declare upstream_link; page front matter can override it
params.upstream_modified , boolean , defaultfalse
Site default for whether attributed material is adapted. A page can override it. No attribution renders without upstream_link
params.default_featured , — , default—
Removed. Use params.images or a section cascade with images. As above, the theme now ignores an old key

Content runtimes

The theme detects Mermaid, KaTeX, ECharts, Infographic, Asciinema, Swagger UI, and Redoc from content. They load only on pages that use them, and only in HTML output. They have no site switch. Only the features below need a switch or external endpoint:

PlantUML in OINK 1.4.0

Local rendering is used when enabled without an endpoint. An explicit svg_image_url selects server mode. Both modes and their limits are described in PlantUML. Local-development Mermaid and local PlantUML load engines as diagrams approach the viewport. Their shared viewer and source export need no new configuration. Ordinary browser printing retains source for unrendered diagrams.

params.markmap , boolean , defaultfalse
Enables the mind map fence site-wide — see Markmap
params.mermaid , map
Configuration passed to mermaid.initialize(). Keys are lowercase. Dark mode automatically overrides theme
params.plantuml , boolean or map , defaultdisabled
true enables local rendering; false disables all PlantUML rendering
params.plantuml.enable , boolean , defaultfalse
Enables local PlantUML rendering — see PlantUML
params.plantuml.svg_image_url , string
Optional PlantUML service endpoint; absent means local browser rendering
params.plantuml.svg , boolean , defaultfalse
Server mode only: inline SVG instead of an <img>
params.drawio.enable , boolean , defaultfalse
Enables the edit button on .drawio.svg images — see Draw.io
params.drawio.drawio_server , string
Draw.io editor address. Required when enabled. If absent, the theme warns and leaves Diagrams.net off
params.highlight_classes , boolean , defaulttrue
Emits Chroma classes for highlighting; false returns to Hugo’s inline styles
params.ui.code_copy , boolean , defaulttrue
Copy button on code blocks. The value false removes it globally. A fence’s own copy= still takes precedence

Mathematics needs no parameter, only the passthrough prerequisite.

Output formats

The theme declares custom output formats but does not enable them for a site. Request the formats you need under outputs. You must explicitly enable expensive aggregate and machine-readable outputs.

hugo.yml
outputs:
  home: [HTML, markdown, LLMS, NAVJSON]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]
Format Output Description
HTML index.html The interactive form; required
markdown index.md Plain Markdown version of each page. Required for “copy Markdown” and “view source”. See AI-agent support
LLMS llms.txt A plain-text format the theme declares, usually attached to home only
LLMSFULL llms-full.txt A top-level section opt-in: the same per-page Markdown concatenated in sidebar reading order, one bundle per language
NAVJSON navigation.json A home opt-in: the sidebar/pager navigation authority serialized once per language, validated by schema/nav.v1.schema.json
print _print/index.html The whole-section print page the theme declares — see Print
BookManifest book.json Optional Book-root JSON input for EPUB/PDF packaging tools. It is not an ebook
RSS index.xml Hugo’s own format. Attach it to section to give each section a feed

LLMSFULL and BookManifest are enabled in the relevant top-level section’s front matter rather than globally. NAVJSON belongs on outputs.home. The complete examples and constraints are in AI-agent support and Books.

Two parameters for print output:

params.print.toc , boolean , defaulttrue
Generates a table of contents at the top of the print page. The value false omits it
params.print.section_break_wordcount , integer , default50
How many words a section needs before it starts a new print page

Languages and versions

Define languages with Hugo’s languages block. The theme reads the translation relationships that Hugo establishes:

defaultContentLanguage , string , defaulten
The primary language, served without a path prefix
languages.<lang>.label , string
The language’s endonym, shown in the language menu
languages.<lang>.locale , string
The full locale, used for <html lang> and SEO
languages.<lang>.weight , integer
Language order, and the cycle order when clicking the language icon
languages.<lang>.title , string
The site name in that language
languages.<lang>.direction , string , defaultltr
Set rtl for a right-to-left language

Paired files, anchor alignment and fallback for untranslated pages are in Languages.

Version parameters:

params.version , string
The identifier of this site variant, which need not be a Git ref — see Versions
params.version_menu , string , defaultVersion
The version menu’s title
params.version_menu_pagelinks , boolean
On switching version, try the same path on the target site first
params.versions , list
Version entries: version, url, kind; name: '---' is a divider
params.archived_version , boolean
Shows the “this is an archived version” banner at the top
params.url_latest_version , string
The link to the current version inside that banner
params.time_format_blog , string , default2006-01-02
Blog date format, overridable per language
params.time_format_default , string , default2006-01-02
All other date formats, overridable per language

Miscellaneous

taxonomies , map
Hugo’s own: enables tag: tags / category: categories — see Taxonomies
params.taxonomy.page_header , list
Shows only listed taxonomies in a post header. If unset, shows all
services.googleAnalytics.id , string
Hugo’s own: the analytics script is injected in production builds only — see Analytics and SEO
module.hugoVersion.min , string , default0.168.0
The theme’s declared Hugo floor. An older version fails the build
module.hugoVersion.extended , boolean , defaulttrue
Hugo Extended is required (SCSS has to be compiled)

Editor completion via generated schemas

The theme ships two generated JSON Schemas under schema/: site-params.schema.json for site hugo.yaml and front-matter.schema.json for page front matter. They derive from the theme’s hugo.yaml defaults and parameter-scan registry. Configuration comments supply hover text. Theme CI regenerates the schemas and fails if they differ. Use the schema from the same release tag as your theme pin.

With the VS Code YAML extension, map the site schema in your settings. This example matches OINK v1.5.1; replace that tag with the one in your go.mod. Both common YAML configuration filenames are covered:

.vscode/settings.json
{
  "yaml.schemas": {
    "https://raw.githubusercontent.com/pgsty/oink/v1.5.1/schema/site-params.schema.json": ["hugo.yml", "hugo.yaml"]
  }
}

To check the association, temporarily give a known boolean, such as params.offline_search, a string value. The editor should report the type mismatch. Then restore the valid value. Front matter completion depends on your Markdown tooling. Associate it with front-matter.schema.json in the same way. The front matter schema deliberately omits type constraints. Keys such as share and theme_color accept a bare-boolean opt-out as well as their normal type.

Verifying a configuration change

Run a production build after changing configuration:

hugo --printPathWarnings

It passes when the output reads Total in … with no ERROR. Common warnings and what they mean:

Warning fragment Cause
invalid params.ui.typography The presets are technical and system
invalid footer_style … (allowed: fat | slim | none) A bad footer style; the warning names the page
invalid page_width … (allowed: normal | wide | full) A bad page width
invalid params.ui.section_index … (allowed: list | cards) A bad section index style
invalid params.offline_search_index The scopes are title, heading, summary, content
params.drawio.enable requires an explicit params.drawio.drawio_server Draw.io enabled with no server address
params.search.algolia requires explicit appId, apiKey, and indexName All three Algolia values are required
params.ui.image_zoom must be a boolean Written as the string "true"
theme_color … is not a #rgb or #rrggbb hex color The value is not a hex color; the default palette is kept
theme_color … reads at about N:1 against the theme's … canvas Advisory: the color ships, and the message prints the id that silences it
theme_color_dark … has no theme_color to pair with The dark half was set without a valid theme_color; it is ignored and the default palette is kept in both modes
command … must define exactly one of url or action A custom command gave both url and action, or neither
invalid params.ui.sidebar_icon_policy …; using all Only a warning, but the value is misspelled

After a configuration change, perform at least three checks. Open one page in each language. Open a page without a translation to check fallback. Check links under the production baseURL, including any deployment subpath.

The theme requires Hugo Extended 0.168.0 or newer. Validate configuration changes with the pinned baseline:

# require v0.168.0+extended in this output
hugo version
hugo --printPathWarnings

The theme declares the minimum in hugo.yaml and theme.toml. The site’s module.hugoVersion.min should match it.

5.2 - Brand and appearance

Change the site name, logo, favicon, accent color, light and dark palettes, and fonts. Use configuration and two SCSS entry points.

This page assumes the site already builds (Quick start). Configure the name, logo, page width, and footer in hugo.yml. To select an existing system or site font, use params.ui.fonts. Section accent colors also have configuration keys.

Add icons under static/. Use assets/scss/_variables_project.scss for SCSS variables and assets/scss/_styles_project.scss for custom styles or new @font-face declarations only when needed. Do not edit files inside the theme directory. An upgrade replaces them.

Visual presets

OINK 1.2.0 defaults to Paper: warm paper backgrounds, ink text, blue links, Plex Sans and framed tables. Slate keeps OINK’s existing cool blue-gray identity. Enable reader choice with:

params:
  ui:
    preset: paper
    preset_menu: true
    dark_mode: true

The theme default for preset_menu is false. Choose preset: slate to keep the previous appearance. Preset and mode are saved separately; selecting the site default (identified in its tooltip) restores the site policy. All styles use one stylesheet and local fonts.

Ink and Terminal are available as explicit opt-ins in OINK 1.2.0:

params:
  ui:
    preset: paper
    preset_menu: [paper, slate, ink, terminal]
    dark_mode: true

Ink uses black/white surfaces, red markers, square panels and underlined prose links. Terminal uses mono controls/headings, teal links, amber accents and compact desktop navigation; its long-form prose stays sans-serif. Both support light/dark mode and reuse existing fonts. The menu shows four compact style buttons in a two-column grid, each with a colored icon and no experiment badge. preset_menu: true offers Paper, Slate and the site default; it does not opt into every experiment. Set preset: ink or preset: terminal to make an experiment the site default, even without a reader menu. See the experiment record for tested scope and remaining design work.

Custom dark brand rules using only [data-bs-theme='dark'] have lower specificity than Paper’s dark palette. Retain Slate, or scope those rules to [data-td-preset='paper'][data-bs-theme='dark']. Font configuration and section theme_color overrides keep their precedence in every preset. Print remains light on white paper.

Site name

The site name appears in the navbar, the browser title and the footer. A multilingual site writes one per language:

hugo.yml
title: Product Docs

languages:
  en:
    title: Product Docs
    label: English
    locale: en-US
    weight: 1
  zh:
    title: 产品文档
    label: 简体中文
    locale: zh-CN
    weight: 2

The top-level title is the fallback. The value of languages.<lang>.title takes precedence.

The theme ships assets/icons/logo.svg and uses it by default. To replace it, put the icon file in the site’s assets/ or static/ and point the configuration at it.

hugo.yml
params:
  logo: images/product-mark.svg
  wordmark: logo.svg
  • params.logo is the square mark used by the navbar, sidebar, and footer. Under assets/, it uses Hugo’s resource pipeline and can be fingerprinted. Under static/, Hugo publishes it unchanged. The path is relative to the assets/ or static/ root.
  • params.wordmark is the horizontal wordmark. If you set it, the navbar replaces “icon + site name” with it. On a narrow screen, the navbar falls back to params.logo. If you do not set it, the navbar retains “icon + site name”.

Crop the source SVG closely around the artwork so that sizes align. The SVG must have a viewBox. Its colors should inherit currentColor or provide sufficient contrast in both light and dark modes.

This site sets neither parameter. The navbar combines the theme’s assets/icons/logo.svg with the site title in the display font.

favicon

The favicon has no parameter. The theme scans the site’s static/ directory for conventional filenames. For each file it finds, it emits the corresponding <link> on every page:

File Link generated
static/favicon.ico rel="icon"
static/favicon.svg rel="icon" type="image/svg+xml"
static/favicon-32x32.png rel="icon" with sizes, emitted in ascending size order
static/apple-touch-icon.png rel="apple-touch-icon"
static/apple-touch-icon-180x180.png rel="apple-touch-icon" with sizes

The minimum sufficient set is favicon.ico, favicon.svg, and apple-touch-icon.png. A file with a size suffix must be square (NxN). Otherwise, the theme does not recognize it.

Generate these with any graphics tool. The theme needs no Node.js, and Hugo simply publishes what is already in static/.

The scan does not cover extra head metadata, such as a Web App Manifest. Emit it through layouts/_partials/hooks/head-end.html. To change discovery rules, such as the directory or accepted filenames, override layouts/_partials/favicons.html in the site’s layouts/.

Accent colour and palette

Color has two layers. Bootstrap’s semantic colors use Sass variables at compile time. OINK’s brand layer uses CSS custom properties at run time.

First change the semantic colors. They determine the colors of buttons, links, and callouts:

assets/scss/_variables_project.scss
$primary: #315f8f;
$secondary: #b4762e;
$success: #2c7a4b;
$warning: #9a6700;
$danger: #b42318;

The theme loads this file before Bootstrap and the OINK defaults. Override Sass variables here. If you need a variable or map already defined by Bootstrap, use _variables_project_after_bs.scss instead.

The brand layer uses CSS custom properties. You must override light and dark in pairs. Otherwise, one mode retains the original color:

assets/scss/_styles_project.scss
:root {
  --td-brand-copper: #a66722;
  --td-brand-mark-from: #1d588c;
  --td-brand-mark-to: #a66722;
}

[data-bs-theme='dark'] {
  --td-brand-copper: #e0a35c;
  --td-brand-mark-from: #7fb8e8;
  --td-brand-mark-to: #e0a35c;
}

Available brand properties include --td-brand-elev for overlay backgrounds and --td-brand-silk for secondary text. The accent and muted accent use --td-brand-copper and --td-brand-copper-dim. Rules use --td-brand-line-strong, and the navbar background uses --td-brand-header-bg. Shadows use --td-brand-shadow-sm / --td-brand-shadow-md. The brand gradient uses --td-brand-mark-from / --td-brand-mark-to / --td-brand-mark-gradient.

Section theme colour

The brand palette above sets the site’s overall color. The theme_color key uses one hex color to tint the shell’s accent backgrounds. This color helps readers identify their current part of the site.

hugo.yml
params:
  ui:
    theme_color: '#6d28d9'
    theme_color_dark: '#a78bfa' # optional

This key is more useful per section than for the entire site. Set it in the section root’s cascade to give the section its own color. For example, Docs can use navy, Blog violet, and Book orange. The site default retains the brand color:

content/blog/_index.md
cascade:
  theme_color: '#6d28d9'
  theme_color_dark: '#a78bfa'

Hugo resolves these cascade values on the section page as well as its descendants, so the pair is declared once. The same resolved pair drives the page’s accent and that section’s mark in the root switcher.

What it touches: the selected sidebar row, adjacent rows’ gray hover backgrounds, and hover fills. It colors the outline pill and its moving rail and dot, Book chapter headings on hover, and tag and chip hover states. It also colors content card borders and share button fills on hover, text selection, focus rings, and sidebar switcher root marks.

What it deliberately does not touch: prose links, external URLs, and inline code. These use consistent reading conventions across sections. Readers should recognize code, prose, and links even on pages with many identifiers. Therefore, the accent uses its own custom property. It does not change Bootstrap’s link color.

The dark color is optional. If you omit it, the theme lightens the light color toward white until it meets AA body-text contrast on the dark canvas. This prevents an unreadable dark palette when you supply only one color. Set the dark color explicitly if the derived hue does not match your brand. The light color is required. If theme_color_dark is alone or accompanies an invalid theme_color, neither mode changes color. The theme warns and retains the default palette. It does not tint only dark mode.

To remove the section color from one page, set theme_color: false in its front matter. This removes both the light color and inherited dark color. The page uses the default palette without a warning. Any other non-hex value, including a number, true, or a named color, produces a warning.

Contrast is checked, not enforced

The theme checks your color against its own canvases. It warns if contrast is below AA body text (4.5:1). The color still appears in the output. You may choose it for a custom canvas or brand requirement. The warning includes an ignoreLogs id that can silence it. Review the contrast on the actual canvas and adjust the color when needed.

The check compares the color with the page canvas. Some interactive surfaces use it for both text and a translucent background. For example, a linked solid badge uses accent text over a 12% accent background. This combination provides less contrast than the canvas check. If a color barely passes, inspect these surfaces. Use a darker color when necessary.

Hugo merges params by key. If a section cascade sets theme_color_dark, a page that overrides only theme_color still inherits that dark value. Override both values or neither.

Light and dark mode

The theme does not show a light/dark control by default. To enable it:

hugo.yml
params:
  ui:
    dark_mode: true

The sun/moon icon shows the current state: sun for light, moon for dark. Click Appearance in the navbar or footer to open the Light / Dark / System radio group. It supports touch and keyboard; on phones it opens a bottom sheet. The preference is saved locally and synchronized across tabs. No saved choice means follow prefers-color-scheme. The inline head script applies the resolved mode before the stylesheet loads.

To enable the dark palette without the control, set dark_mode: { show_menu: false, enable: true }. The default, dark_mode: false, enables neither.

Custom components need readable hover, focus, disabled and selected states in both modes, with at least 4.5:1 contrast for body text and 3:1 for large text.

Fonts

The typography policy is selected at build time; visual preset switching chooses its bundled faces:

hugo.yml
params:
  ui:
    typography: technical # technical | system
  • technical (the default): Paper uses IBM Plex Sans for interface, body and display text; Slate uses Inter for interface/body and Chakra Petch for display. Both use Chakra Petch for the wordmark and IBM Plex Mono for code. Chinese and emoji use platform fallbacks. Plex Sans and Inter include local Latin, Cyrillic, Greek and Vietnamese subsets. No Google Fonts request is made.
  • Experimental fonts: Ink uses Inter for UI, prose and headings; Terminal uses IBM Plex Mono for controls/headings and Plex Sans for prose. Both reuse existing code fonts. Explicit fonts.ui still supplies the main face; use fonts.body for a separate prose face.
  • system: interface, display, metadata, print, and monospace roles use the platform stack. The browser requests no brand font. Font files remain bundled with the theme, but the output does not reference them.

If the value is invalid, the theme warns and falls back to technical. Normal hugo server preview remains usable. The selected value appears in <html data-td-typography="…">. Check it in the browser.

Custom fonts

Eight CSS custom properties define the font roles. Override these properties instead of searching for component selectors:

Property Config key Where it is used
--td-ui-font-family ui Navigation, controls and interface text
--td-body-font-family body Body text and blog posts
--td-heading-font-family heading Headings in the body
--td-code-font-family code Code and terminals
--td-display-font-family display Display headings
--td-meta-font-family meta Technical labels and metadata
--td-brand-font-family brand Wordmark
--td-print-font-family print Print body text

The ui role is the main font. The body role resolves through it, and heading resolves through body. One line can therefore change the interface, prose, and headings together.

From configuration

To change only font families, use params.ui.fonts. You do not need SCSS:

hugo.yml
params:
  ui:
    fonts:
      # The main face: interface, body and headings follow it
      ui: "'Source Han Sans SC', 'PingFang SC', sans-serif"
      # Monospace needs a CJK fallback, or mixed code blocks stop aligning
      code: "'Sarasa Mono SC', 'Noto Sans Mono CJK SC', monospace"

These values are family names, not font files. This key never causes the theme to download or load a font. Each family must already exist on the reader’s system or have a site @font-face declaration. End every list with a generic family (sans-serif, monospace, serif). Readers without your font use this fallback.

Values accept only plain font family syntax: quoted names, bare identifiers, a leading hyphen (-apple-system), and names in any script (苹方 is a family name). They reject semicolons, braces, parentheses, url(), and angle brackets. An unknown role or unsafe value produces a warning. The theme drops only that entry and retains the rest of the map. If the site sets no values, the theme adds no style element to <head>.

The block renders after the stylesheet. At equal specificity, an authored font therefore takes precedence over the typography preset.

From a stylesheet

To bundle your own font file or change the font for one content kind, use a stylesheet. Put the .woff2 file in the site’s static/webfonts/. Declare the font in the project stylesheet. Then override the roles:

assets/scss/_styles_project.scss
@font-face {
  font-family: 'My Sans';
  font-display: swap;
  font-style: normal;
  font-weight: 400 800;
  src: url('../webfonts/my-sans-variable.woff2') format('woff2');
}

:root {
  --td-ui-font-family: 'My Sans', 'Noto Sans SC', sans-serif;
  --td-body-font-family: var(--td-ui-font-family);
  --td-heading-font-family: var(--td-ui-font-family);
  --td-display-font-family: var(--td-heading-font-family);
}

Roles inherit through normal CSS rules. To change the font for one content kind, you do not need component selectors:

assets/scss/_styles_project.scss
body.td-blog {
  --td-body-font-family: 'My Serif', 'Noto Serif SC', serif;
  --td-heading-font-family: var(--td-body-font-family);
}

A monospace stack needs a CJK fallback, or mixed code blocks fail to align:

assets/scss/_styles_project.scss
:root {
  --td-code-font-family: 'My Mono', 'Sarasa Mono SC', 'Noto Sans Mono CJK SC', monospace;
}

A site migrating from Docsy can retain its font configuration. The old Sass variables still supply the corresponding roles from _variables_project.scss. They take precedence over preset defaults:

Legacy Sass variable Font role it feeds Note
$td-fonts-serif --td-ui-font-family / --td-body-font-family Docsy’s interface stack, assigned to $font-family-sans-serif
$font-family-sans-serif --td-ui-font-family / --td-body-font-family If a project supplies its own stack, the technical preset no longer prepends the preset’s bundled sans face
$font-family-base --td-ui-font-family / --td-body-font-family Bootstrap’s body variable, reaching the role through --bs-body-font-family
$headings-font-family --td-heading-font-family Unset, headings inherit the body role
$font-family-code --td-code-font-family Code, terminals and pre / code / kbd
$td-font-family-monospace --bs-font-monospace Assigned to $font-family-monospace
$font-family-monospace --bs-font-monospace Under the system preset, an explicit project value takes precedence over the platform monospace stack

The theme no longer reads Docsy’s three Google Fonts variables: $td-enable-google-fonts, $td-google-font-name, and $td-web-font-path. Leaving them in _variables_project.scss causes no failure and has no effect. The theme bundles IBM Plex Sans, Inter, Chakra Petch, and IBM Plex Mono. No preset requests Google Fonts. The print role --td-print-font-family follows the body role. The theme bundles no separate print font.

YAML accepts only font family names. It accepts neither remote font URLs nor arbitrary CSS. Font files and styles must be auditable local inputs. A normal build makes no network request for a font.

Page width

hugo.yml
params:
  page_width: normal # normal | wide | full

The page_width key controls the overall shell width. Override it per page or per section through a cascade. Book pages also support reading_width (slim / normal / wide), which changes the body’s reading width instead of the shell. If either key has an invalid value, the theme warns and uses a fallback.

hugo.yml
params:
  ui:
    footer_style: fat # fat | slim | none
  copyright:
    authors: '[The product team](https://example.com/)'
    from_year: 2026
    to_year: present
  footer_center_info: 'Powered by [Oink](https://oink.pgsty.com)'
  • fat (the default): a multi-column link grid plus the copyright line;
  • slim: the copyright line only;
  • none: no footer at all.

Page front matter, including a section cascade, can override the footer style. This site’s documentation section uses footer_style: slim. An unrecognized value warns and falls back to fat.

Store grid data in data/footer/<language>.yaml. See Navigation and menus. If fat is configured but no data exists, the footer automatically falls back to slim. You can therefore enable it before you write the content.

The params.copyright key accepts a Markdown string or a map with authors / from_year / to_year. The value present means this year. The footer_center_info key accepts inline Markdown for the footer’s center. Set it explicitly to an empty string to hide that region.

SCSS entry points, and what not to do

The site’s SCSS overrides join the theme’s single style bundle. A production build still emits one fingerprinted stylesheet with an integrity attribute. Put the three entry files under the site’s assets/scss/:

File When to use it
_variables_project.scss Sass variables set before Bootstrap and the OINK defaults ($primary, the font variables)
_variables_project_after_bs.scss Variables or maps that depend on Bootstrap’s own definitions
_styles_project.scss Selectors and CSS custom properties written after the theme’s component styles

The compilation order is: Bootstrap functions → project variables → OINK defaults and Bootstrap → post-Bootstrap project variables → OINK components and the brand layer → project styles.

The CSS interface has a defined boundary. The eight font roles in Fonts and the --td-brand-* properties are public. The theme preserves their names and meanings across minor versions. Component aliases such as --td-asciinema-font-family work only within their component. Undocumented variables, such as --td-shell-*, are implementation details. They may be renamed or removed at any time.

What not to do:

  • Do not edit any file inside the theme directory (hugo mod overwrites it);
  • Do not @import the theme’s internal partials individually. They are not a public Sass interface, and their import order may change;
  • Do not override baseof.html to change one color. If a design variable exists, use it. Otherwise, write the narrowest selector that works;
  • Do not reference a remote stylesheet or a font CDN.

For additional third-party CSS, publish a local resource through layouts/_partials/hooks/head-end.html. Do not write a <link> in Markdown.

Verify

hugo --printPathWarnings
  • The build prints Total in … with no ERROR;
  • The page source has data-td-typography="technical" (or your chosen preset) on <html>;
  • In the browser the navbar shows your logo and site name, and the tab shows your favicon;
  • Switch to dark mode. Check body text, tables, callouts, code blocks, and focus rings again. Verify every color change in both modes;
  • Switch language and confirm the site name changes with it.

To verify the font change, inspect a paragraph’s font-family in the browser’s developer tools. It should include your declared family. Check the rendered font as well to confirm that the local file or system fallback is being used.

5.3 - Home and landing pages

Build a home page from one local YAML file. Add hero, cards, capability panels, timeline, pricing, case studies, and downloads. Reuse these sections on landing pages.

Configure home page content in a data file. The sections list in data/home/<language>.yaml sets the sections and their order from top to bottom. The theme finds each section’s content by name in the same file. An ordinary page with layout: landing uses the same sections.

Hugo renders every section on the server. Prices, star counts, screenshots, avatars, and download states must exist in the repository before Hugo starts. No section fetches data in the browser.

If you migrate a Docsy blocks/* home page, you must rewrite it. OINK has no blocks/cover, blocks/section, or blocks/feature shortcodes. If you retain them, the build fails with template for shortcode "blocks/cover" not found. Use the data/home/<language>.yaml file described here, or set layout: landing on an ordinary page.

Where the home page’s data lives

The home page’s content file keeps only a title and a description:

content/_index.md
---
title: OINK
description: A local-first, Hugo-only theme for technical documentation
---

Section data is a file per language:

home page data

  • data/
    • home/
      • en.yamlEnglish home page
      • zh.yamlChinese home page

The lookup order is data/home/<current language>.yaml → data/home/en.yaml → data/home.yaml for a single-language site.

The file has two levels: a sections list and the named keys that the list references.

the skeleton of data/home/en.yaml
sections:
  - hero          # uses the hero: key
  - capabilities
  - type: cards   # uses the cards section, reading the release: key
    key: release
  - cta

hero: { … }
capabilities: { … }
release: { … }
cta: { … }

This site uses that structure for its home page. The complete file is data/home/en.yaml in the repository.

A minimal working home page

Create the data file below. Replace the text and links with your own content. Put the two hero images in static/images/hero-light.webp and static/images/hero-dark.webp. For a text-only opening, omit the entire hero.image block. Write internal links as site paths without a leading slash. The theme adds the current language prefix (docs/start/ → /docs/start/).

data/home/en.yaml
sections:
  - hero
  - cards
  - cta

hero:
  eyebrow: Local-first · Hugo only
  title_lines:
    - words:
        - { text: PGSTY OINK }
  lead: Components are written in Markdown, assets ship with the theme, and one source produces four outputs.
  image:
    light: images/hero-light.webp
    dark: images/hero-dark.webp
    alt: OINK engineering documentation illustration
  actions:
    - { label: Quick start, url: docs/start/, icon: fa-solid fa-rocket, style: primary }
    - { label: See the components, url: docs/components/, style: ghost }

cards:
  eyebrow: What it does
  title: Everything engineering documentation needs
  columns: 3
  items:
    - title: Markdown-native components
      desc: Callouts, tabs, field lists and file trees are all part of Markdown syntax.
      icon: fa-solid fa-cubes
      url: docs/components/
    - title: Four outputs
      desc: HTML, print, Markdown and RSS share one source; interactive components use static or source fallbacks.
      icon: fa-solid fa-file-export
      url: docs/customize/agents/
    - title: Local-first
      desc: Fonts, icons, search and diagram runtimes all ship with the theme; no CDN.
      icon: fa-solid fa-plug-circle-xmark
      url: docs/about/features/

cta:
  title: Start from a bilingual site that already works.
  text: Start from OINK Starter, replace the project identity and content, then publish.
  label: Get started
  url: docs/start/
  style: primary

Hero

The hero is the first screen. It is the only section with a large title and an illustration.

data/home/en.yaml
hero:
  eyebrow: PROJECT 1.0 · Local-first       # small text above the title, with a status dot
  title_lines:                             # the large title, controlled line by line
    - words:
        - { text: PGSTY OINK }
  lead: One sentence saying what this is.  # inline Markdown and <br> allowed
  note: No Node.js required                # a supplementary line with an icon
  note_icon: fa-solid fa-circle-check
  title_size: 4.25rem                      # rem / em / px only
  image:
    light: images/hero-light.webp
    dark: images/hero-dark.webp            # with only one, both modes share it
    alt: First-screen illustration
  media:
    ratio: '1fr 240px'                     # column widths for text and image
    max_width: 240px
    hide_below: md                         # hide the image below sm | md | lg | xl
  actions:
    - { label: Get started, url: docs/start/, icon: fa-solid fa-rocket, style: primary }
    - { label: GitHub, url: 'https://github.com/pgsty/oink', external: true, style: ghost }
  detail: { label: See what it looks like, url: docs/about/showcase/ }

If title_lines is absent, the hero uses title. If both are absent, it uses the site title. The image is a CSS background. If alt is present, its container has role="img". Otherwise, the image is hidden from assistive technology.

The setting align: center creates a text-only centered first screen. It widens and centers the text block, balances title line breaks, and moves note below the buttons. If an image is also present, the theme warns and falls back to start so the image is preserved.

The section registry

There are 22 section types. Their names use hyphens. The theme normalizes underscores in older data. Except for the hero, each type has three heading fields, eyebrow / title / desc (or text), and a class.

Type What it holds
hero The first screen: large title, buttons, a theme-following image
metrics Numeric facts, with optional count animation and source links
capabilities Alternating left-right capability descriptions with a dedicated visual panel
principles Numbered product principles
cards A general card set: features, scenarios, entry points
logo-wall Tools and partners, as a grid or a pure-CSS marquee
gallery A wall of screenshots
testimonials Quotations with attribution
contributors People, roles, avatars and links
faq Questions and answers, collapsible or flat
markdown Free Markdown content
cta The closing call to action
pricing Pricing tier cards
pricing-compare A tier-by-feature comparison matrix
command-box One copyable command
steps An ordered procedure, optionally with commands
timeline Dated milestones
code-plate Code inside a presentation panel
preview Markdown source beside its rendered result
case-study A case: metrics plus a quotation plus a source
download One or more data/download/ records
bar-chart Numeric comparison without chart JavaScript

If a type is misspelled, the build emits an unknown section type warning and skips the section.

The commonest sections, minimally

Cards and capability panels are the most common sections. The cards section sets its column count with columns:

data/home/en.yaml
cards:
  title: Use cases
  columns: 4
  link_label: Learn more
  items:
    - title: Book publishing
      meta: Long form
      icon: fa-solid fa-book-open
      desc: Numbered figures and examples, cross-references, indexes and whole-book print.
      url: docs/write/book/

The capabilities section shows one capability per screen with a structured visual panel on the right. The visual.type must be shell, components, code, image, or card:

data/home/en.yaml
capabilities:
  eyebrow: Value
  title: What engineering documentation needs, out of the box
  items:
    - ref: 01 / Engineering docs
      title: Built for engineers and their documentation sites
      url: docs/start/
      motto: No extra friction from the first build to long-term maintenance
      bullets:
        - 'A [deployment](docs/admin/deploy/) experience that works out of the box'
        - 'Built-in [search](docs/customize/search/) and [languages](docs/customize/i18n/)'
      value: Content teams spend their time on documentation rather than rebuilding a site.
      visual:
        type: code
        title: build.sh
        lines:
          - { class: c, prefix: '# ', text: One command, one deterministic output }
          - { class: p, prefix: '$ ', text: hugo --gc --minify }
          - { class: ok, prefix: '✓ ', text: public/ is ready to deploy }
Minimal YAML for the other ten scenario sections

These fragments come from the theme repository’s executable regression fixture tests/site/data/landing/demo/en.yaml. You can copy the field names.

metrics:
  title: Facts
  animate: true
  items:
    - { value: 2189, compact: true, label: Stars, source: { label: Local CI data, url: 'https://example.org/' } }
    - { value: 32, suffix: '+', label: Languages }

command-box:
  title: Install
  code: hugo mod get github.com/pgsty/oink
  lang: bash
  note: The copy button comes from the on-demand landing runtime.

steps:
  title: Three steps to publish
  items:
    - { title: Clone, desc: Copy the documentation site repository. }
    - { title: Configure, desc: Change three settings., cmd: { code: hugo server } }
    - { title: Publish, desc: Push to GitHub Pages. }

timeline:
  title: Project history
  items:
    - { date: '2024', title: Prototype, desc: The first data-driven sections. }
    - { date: '2026', title: Scenario components, desc: Landing becomes a reusable shell. }

code-plate:
  title: Page configuration
  aria_label: Example configuration
  lang: yaml
  code: |
    layout: landing
    landing: pricing

preview:
  title: What you write is what you get
  file: guide.md            # the filename in the source panel header, default page.md
  source: |                 # the right side renders this Markdown with the site's own hooks
    > [!TIP] Markdown only
    > Callouts, steps and tabs are all ordinary syntax.

    1. Write Markdown
    2. Run `hugo`
    {.steps}

case-study:
  title: Migration outcome
  stats:
    - { value: 12, label: Reusable sections }
    - { value: 0, label: Remote requests }
  quote: "One YAML file replaced a bespoke page template."
  source: A site maintainer

pricing:
  title: Pricing
  tiers:
    - name: Community
      price: Free
      period: forever
      desc: The full open-source capability.
      features: [Every component, Community support]
      cta: { label: Download, url: docs/start/ }
    - name: Professional
      featured: true
      price: $3.4K
      period: /year
      features: [Priority response, Release packages]
      cta: { label: Contact us, url: 'mailto:[email protected]' }

pricing-compare:
  title: Tier comparison
  tiers: [Community, Professional]
  groups:
    - name: Support
      rows:
        - { name: Priority response, cells: [N, Y] }
        - { name: Annual fee, price_row: true, cells: [Free, $3.4K] }

download:
  title: Download
  keys: [prd5]

bar-chart:
  title: Build time
  unit: seconds
  items:
    - { label: Cold build, value: 12.3, group: cold }
    - { label: Warm cache, value: 1.6, group: warm, note: A repeat build on the same machine. }

The download section uses the same data/download/<key>.yaml record described in Releases and downloads. It introduces no second version model.

Turning any page into a landing page

Two front matter lines convert an ordinary content page to a landing page. This full-width canvas retains the navbar, command palette, and footer. It removes the sidebar and outline.

content/pricing.md
---
title: Pricing
layout: landing
landing: pricing
---

The data lives in a directory parallel to the home page’s, likewise split by language:

landing page data

  • data/
    • landing/
      • pricing/
        • en.yaml
        • zh.yaml

A non-home landing page searches for data in this order. If it finds no data, the theme warns and renders the landing shell without sections:

  1. sections in the page’s front matter;
  2. data/landing/<key>/<exact language>.yaml;
  3. The exact-language entry inside a single data/landing/<key>.yaml;
  4. The English or language-less record.

You can put small amounts of data in front matter. However, landing: and sections: are mutually exclusive:

content/pricing.md
---
title: Pricing
layout: landing
sections:
  - type: hero
    data:
      title: Publish a product page with Hugo alone
      actions:
        - { label: Read the docs, url: docs/, style: primary }
  - type: cta
    data:
      title: Ready to start?
      label: Read the docs
      url: docs/
---

Writing a section entry

Each item in sections is either a type-name string or a map:

Key What it does
type The section type. If omitted, uses key as the type
key The key that supplies data. Defaults to the name in type. Use it to distinguish two instances of one section
data Inline data. No top-level key lookup is needed
id The section’s anchor ID, generated from key / type by default
enabled: false Disables the section while keeping its data
partial Uses the site’s own partial. This is a local template convention, not portable landing data

Languages and local facts

Put descriptive text in per-language files (zh.yaml / en.yaml). A shared fact record can also use per-field fallback: <field>_<exact language> → <field>_<base language> → <field>. The theme normalizes - in a language tag to _. A Chinese site resolves title_zh_cn, then title_zh, then title. The theme does not accept camelCase suffixes.

Section display text belongs to site data. It does not use the theme’s i18n strings. Only theme controls, such as marquee pause and pricing states, use translation keys. To configure the whole multilingual site, see Languages.

A few optional facts on the landing shell are local too, written in hugo.yml and never fetched at runtime:

hugo.yml
params:
  offline_search: true
  ui:
    landing_search: true          # boolean; the palette appears only if the site enabled offline_search
    github_stars: 2189            # a committed number, never a GitHub API request
    alt_site: { label: 中文站, url: 'https://example.cn/' }

The footer uses separate data in data/footer/<language>.yaml, or data/footer.yaml for a single-language site. This site has one file per language. If data/home/<language>.yaml still contains a footer key, the theme warns, ignores it, and points to the new location. See Navigation and menus for authoring instructions.

Output

Output What appears
HTML The full static section content, plus landing.js loaded on demand for reveal, counting, copying and theme image switching
Print Retains content. Converts dynamic surfaces, such as the marquee, to a static grid. Removes controls
Markdown Titles, prose, lists, tables and code, with no component classes
RSS Landing sections are not emitted

If JavaScript is disabled or landing.js is unavailable, all server-rendered sections remain visible. Metrics include their configured number formatting, prefix, and suffix. Count animations finish on the same display text. The marquee’s duplicate track stays outside the accessibility tree. Its pause checkbox works without JavaScript. If the reader enables reduced motion, movement and reveal are disabled.

Verify

  1. Run hugo --printPathWarnings. This detects misspelled types, missing data keys, and simultaneous landing and sections.
  2. Open the home page and any landing page. Compare sections with their data files. Check every language.
  3. Reload with JavaScript disabled. Confirm that content remains visible without motion.
  4. Check light and dark modes. Confirm that image.light and image.dark show the correct images.
  5. For a subpath deployment, confirm that all internal links and images include the prefix.

5.4 - Navigation and menus

Configure the navbar menu, dropdowns, section switcher, breadcrumbs, page actions, pager, and footer links.

This page covers the ways a reader moves between pages: the navbar menu, the section switcher, breadcrumbs, page actions, previous / next, and the footer. The sidebar tree and the outline belong to Layouts and page types.

The navbar comes from Hugo’s menus.main. Docs and Book navigation defaults to the content/ tree; sites importing an existing reading order may use data/docs_nav.json. The sidebar, pager and section index share the selected order.

The navbar menu

Top-level entries go in each language’s menus.main:

hugo.yml
languages:
  en:
    menus:
      main:
        - identifier: docs
          name: Docs
          pageRef: /docs
          weight: 20
        - identifier: blog
          name: Blog
          pageRef: /blog
          weight: 50
        - identifier: download
          name: Download
          pageRef: /download
          weight: 60
          params:
            icon: fa-solid fa-download

Lower weight values appear first. Use pageRef for site pages and url for external pages. External links automatically receive target="_blank", rel="noopener noreferrer", and an external-link mark. The identifier is a stable configuration reference. The keys quick_links and sidebar_root_menu match it. Translate name per language. Do not translate the identifier.

You can also define a menu entry in page front matter. Use this when “this page is itself a top-level entry”:

content/download/_index.md
---
title: Download
menu:
  main:
    weight: 30
---

The navbar’s right-hand GitHub entry is not a menu item. It uses params.github_project_repo, with a fallback to params.github_repo. The menu area skips entries identified as github. To change the GitHub target, change the repository parameters. See Repository links and page info.

Dropdowns

Use Hugo’s parent to establish a parent-child relationship. Only one level of children is supported:

hugo.yml
menus:
  main:
    - identifier: docs
      name: Docs
      pageRef: /docs
      weight: 20
    - identifier: docs-start
      parent: docs
      name: Get started
      pageRef: /docs/start
      weight: 10
      params:
        icon: fa-solid fa-rocket
        description: Start from OINK Starter, customize in layers, deploy
    - identifier: docs-components
      parent: docs
      name: Components
      pageRef: /docs/components
      weight: 20
      params:
        icon: fa-solid fa-cubes
  • Every entry is one icon and one title on its own row, in one moderate-width column. A child’s params.description is configuration data only; the panel never renders it.
  • The parent is itself an ordinary link. Hover or focus it to expand the panel. Click it or press Enter to open the parent page. There is no separate expansion arrow. On touch screens, readers open the parent page, whose body lists the same links.
  • Keyboard: the down arrow expands the panel and focuses its first item. Esc closes it and returns focus to the link. Clicking outside also closes it.
  • The 0.5 params.columns parameter is retired: setting it emits a build warning and the panel keeps its single column.
  • A third level produces a build warning and becomes a static group heading. It does not create a third-level flyout. Put deeper levels in the sidebar.

Menu icons

Below lg a menu entry is reduced to its icon, so every top-level entry should have one. Icons resolve in this order:

  1. icon in the target page’s front matter;
  2. The menu entry’s own params.icon;
  3. A built-in default matched by identifier or section name (docs, blog, examples, community, about, download, github and others);
  4. fa-solid fa-link when none matched.

An icon is one Font Awesome class pair, with the free faces supplied locally by the theme:

hugo.yml
menus:
  main:
    - identifier: handbook
      name: Operations handbook
      pageRef: /handbook
      weight: 40
      params:
        icon: fa-solid fa-screwdriver-wrench

Taxonomy menus

A top-level taxonomy entry (/tags/, /categories/) needs no manually written submenu. The panel shows a grid of “term + count” chips, sorted by descending count.

hugo.yml
menus:
  main:
    - identifier: tags
      name: Tags
      pageRef: /tags
      weight: 60

Enabling taxonomies is in Taxonomies.

Navbar controls

The navbar is 50px tall. It contains the brand logo or wordmark, a centered menu, and utility controls at the end. When enabled, it appears across layouts. On narrow screens, Home and Landing use a site-menu drawer. Shell pages with a sidebar use the sidebar’s drawer instead.

The navbar has a full desktop tier and a compact icon tier:

Viewport State
lg and above Brand, centered menu text, and search, version, language, theme, and GitHub controls. No drawer button
md to below lg Centered menu icons and utility controls at the end. No drawer button
Below md Centered menu icons remain. Search and the appropriate drawer button remain at the end. Version, language, theme, and keyboard help remain in the footer’s bottom bar

Enable individual controls through their settings. The search icon requires params.offline_search; see Search. The version menu requires params.versions; see Versions. With two or more configured languages, the language menu appears automatically; see Languages. The theme control requires params.ui.dark_mode; see Brand and appearance.

Auto-hide

hugo.yml
params:
  ui:
    navbar_autohide: true

With auto-hide enabled, the hidden navbar retains its 50px band. Move the pointer into the upper 60% of that band, or focus the navbar with the keyboard. The navbar fades in at the same position. The layout does not move, and the hidden navbar does not cover content. The wake zone excludes 64px on each side. Collapsed sidebar and outline restore controls therefore remain usable.

It is disabled automatically below 768px, on a coarse pointer, and on a touch-only device, where the navbar stays visible. Home ignores the site-wide setting unless its own front matter enables it. Hero pages retain their overlay navbar, which scrolls with the hero. A top-level navbar_autohide in page front matter or a section cascade overrides the site setting.

Turning the navbar off

hugo.yml
params:
  ui:
    navbar_enabled: false

It can also be turned off for one page or one section:

content/docs/_index.md
---
title: Docs
cascade:
  navbar_enabled: false
---

If the navbar is off, the theme restores mobile subnavigation, a brand and search row at the sidebar top, and utility buttons on the outline rail. Use this switch for pages that need the whole viewport. It is not a general layout preference. This site’s documentation section keeps the navbar enabled and disables its auto-hide behavior with navbar_autohide: false, so section navigation stays visible.

The section switcher

The section switcher is the sidebar’s top row. It selects the displayed tree. The theme builds and deduplicates entries in this order: eligible top-level sections → eligible sections with sidebar_root_for: self → the current resolved root. Candidates must have a permalink. They cannot be divider groups.

hugo.yml
params:
  ui:
    sidebar_root_enabled: true
    sidebar_root_menu: true

To let a large subtree become a root of its own (a versioned API reference, a self-contained handbook), in its _index.md:

content/docs/api-v2/_index.md
---
title: API reference v2
sidebar_root_for: self
sidebar_root_link_self: true
---

The value self makes the section index and all descendants use the new tree. The value children keeps the index in the parent tree. Only descendants use the new tree. To exclude a top-level section or a nested self-root from the global choices, set sidebar_root_menu: false in its front matter. The current resolved root is still appended if absent. Inside that section, it remains a location marker. The 1.1 implementation excludes both nested self-roots and top-level sections. The 1.0 implementation could still include a nested self-root despite false. A root with build.render: never, or a divider root, never becomes a switcher link.

With no entries, the switcher renders nothing. One entry becomes a borderless link. Two or more entries create a dropdown. The tree below still lists the section index first. The switcher selects a tree. The root link selects a document.

Breadcrumbs and page actions

An ordinary content page has breadcrumbs above its title and page actions at the row’s right end. A top-level section omits single-level breadcrumbs that only repeat its title. Action buttons retain their positions.

hugo.yml
params:
  ui:
    breadcrumb: false

Breadcrumb labels use the localized linkTitle, and the hierarchy matches the sidebar.

The page action menu

The split button at the title row’s end contains page actions. Click its left half to copy the page’s Markdown. A successful copy shows a green check mark. Click the right arrow to expand the full menu. The menu groups actions for exporting content and for changing or producing content:

Action When it appears
Copy as Markdown The site enabled the markdown output format
Open in ChatGPT page_context_menu.assistant_links: true
Open in Claude The same
View Markdown source The markdown output format
View history params.github_repo can resolve the source path
Edit this page params.github_repo
Create child page params.github_repo
Open a documentation issue params.github_repo
Open a project issue params.github_project_repo
Print the whole section The section enabled the print output format
hugo.yml
params:
  ui:
    page_context_menu:
      enable: true
      assistant_links: false
      links: []

Assistant entries are off by default. When clicked, they send the full current URL, including its query and fragment, to a third party with a localized prompt. They do not upload the body. Before enabling them, confirm that URLs contain no sensitive information. Document this boundary in the privacy statement. Page front matter can restrict the site policy. It cannot enable assistant links that the site has not enabled:

Page front matter
page_context_menu:
  assistant_links: false

Confirm that ChatGPT and Claude are absent from both the title menu and the command palette on that page. See AI-agent support for the handoff behavior.

Custom external actions come last in the menu, and url supports three URL-encoded placeholders:

hugo.yml
params:
  ui:
    page_context_menu:
      links:
        - name: Ask the internal assistant
          icon: fa-solid fa-wand-magic-sparkles
          url: https://assistant.example.com/new?source={markdown_url}&title={title}

The placeholders are {url} (the page’s full address), {title} (the page title) and {markdown_url} (the Markdown version’s address).

On the blog root and its first-level subsections, the left half becomes an RSS subscription link. “copy as Markdown” remains in the menu. Without Markdown output, a page has no left half. The arrow becomes a labeled “Actions” button.

These actions are also entries in the command palette.

The pager

Previous / next are two text links at the body end. They follow the visible sidebar tree: root page → first page → through to the last. The root has no previous link. The last page has no next link. If the site provides data/docs_nav.json, that tree also orders the pager and declared docs or book section indexes. The sidebar, pager, and index therefore show children in the same order. Undeclared sections and sites without the file use the content tree. See Layouts and page types.

hugo.yml
params:
  ui:
    pager_types: [docs, book, blog]

The pager_types list accepts only docs, book, and blog. Any other value warns and is discarded. To disable paging on a page, use front matter:

content/docs/appendix.md
---
title: Appendix
pager: false
---

The page’s <head> uses the same order. When previous or next pages exist, it emits <link rel="prev"> and <link rel="next">. Browsers and crawlers can then identify the reading sequence.

page source
<link rel="prev" href="/docs/customize/home/">
<link rel="next" href="/docs/customize/layout/">

Paging applies to HTML output only. Print, Markdown and RSS have neither the links nor the two rel relationships.

The pager is the third of these four page-end components: feedback → annotation → pager → comments. Their order is fixed. Each has an independent switch. An optional Share bar appears before these components.

The right rail can list pages that link to the current page. Its “Linked from” group has a link icon and appears below the table of contents, above taxonomy clouds. It starts expanded. Below xl, it moves into the sidebar drawer with the table of contents. Readers arriving from search can identify related pages and the current page’s place in the site. Backlinks remain off until the site enables them:

hugo.yml
params:
  ui:
    backlinks: true

A page overrides it in front matter, and a section cascades it to everything below:

content/docs/_index.md
---
title: Docs
cascade:
  backlinks: true
---

Hugo derives the index at build time from ordinary Markdown links and ref / relref shortcodes in page source. No new syntax, migration, or JavaScript is required. The list is in HTML. Before scanning, the theme removes fenced and inline code. Multiple links to one target become one entry. It ignores self links, external links, mailto:, and same-page anchors.

The theme removes fragments when identifying target pages. Each language has its own graph. A Chinese page never appears below an English page. Entries sort by stable page path, so identical content always produces identical order. If no pages link in, the entire block is absent, including its heading and container.

The first eight entries are visible. Remaining entries use a native “Show N more” disclosure. This limits right-rail space for heavily referenced pages. No JavaScript is required. Each entry shows its source page’s description on hover.

Source scanning has a known limit. URLs in custom shortcode parameters and raw <a href> elements do not become edges. Unresolved destinations are discarded without a warning. Backlinks enhance navigation. They do not check links. Continue using a link checker to find broken links.

A non-boolean value warns and falls back to off.

The page’s Markdown output carries the same list, introduced by “Backlinks:”. RSS omits it, and the print output format omits it with the rest of the rail.

This site enables backlinks site-wide. See this page’s right rail for an example. The most-referenced page, Configuration, lists more than forty inbound links. Most use the disclosure.

The params.ui.footer_style setting selects fat / slim / none. See Brand and appearance. The fat link grid reads data/footer/<language>.yaml. It is not a menu. The theme has no menus.footer:

data/footer/en.yaml
brand:
  name: Product Docs
  tagline: A short description that **supports Markdown**.
  slogan: Close to the product, with clear answers.
columns:
  - title: Docs
    links:
      - { label: Get started, url: /docs/start/ }
      - { label: Components, url: /docs/components/ }
  - title: Project
    links:
      - { label: GitHub, url: https://github.com/pgsty/oink, external: true }
      - { label: Releases, url: /blog/release/ }
  • If brand.name and brand.logo are absent, the footer uses the site’s brand name, logo, and wordmark. The fields tagline and slogan render Markdown.
  • An internal url resolves against the current language root. The setting external: true opens a new tab with rel="noopener noreferrer".
  • The grid has as many columns as the data does.
  • A single-language site can use data/footer.yaml.
  • If fat is configured without data, the footer automatically falls back to slim. You can enable it before content exists.

The fat footer’s copyright row has a collapse arrow at its right end. It hides or restores the link grid above. The grid starts expanded. localStorage stores the reader’s choice under td-footer-collapsed across pages. The styles slim and none have no collapse button. This state is independent of focused reading mode.

Every rendered bottom bar ends with an icon dock in this order: version, language, theme, then keyboard help. Configured menus open upward. The version trigger shows only an icon. Its choices retain full labels. The fat footer’s collapse arrow follows the four controls. The sidebar has no duplicate dock. The setting footer_style: none removes both the footer and bar.

The copyright row and the centre note are parameters — see Configuration.

Verify

hugo --printPathWarnings

After changing navigation, check each of these:

  • Confirm that the build has no Navbar menu … supports one interactive child level warning. This warning indicates a three-level menu;
  • On desktop, click a parent to open its page. Hover to expand the panel. Press Esc to close it;
  • Narrow the window below lg. Confirm that each top-level entry has an icon. An entry without an icon is blank at this width;
  • Below md, confirm that Home and Landing navbars retain search and the drawer button on the right. Version, language, theme, and keyboard help must remain in the footer bottom bar;
  • The switcher at the top of the sidebar lists every top-level section, with the current one marked;
  • On a documentation page, use E / Q to check sidebar paging order. Confirm matching rel="prev" / rel="next" in page source;
  • With backlinks enabled, confirm that grep td-backlinks public/<a page that is linked to>/index.html finds the block. A page with no inbound links must have no backlink markup;
  • Open the page action menu. Confirm that expected actions appear and unavailable actions are absent. For example, “open a project issue” should be absent without github_project_repo.

5.5 - Layouts and page types

Select the page shell with type. Set sidebar width and icons, outline depth, section index style, and page width.

This page describes the page shell. Configure sidebar visibility and width, outline depth, and list or card styles for section indexes. For content locations, see Organizing content.

The rule is that the shell follows type, not the path. Documentation can live anywhere under content/ as long as it has type: docs.

Shell types

params.ui.shell_types lists the types that use the reading shell with a sidebar:

hugo.yml
params:
  ui:
    shell_types: [docs, book, blog, swagger]
type Shell
docs The documentation shell: left sidebar (section switcher + tree) + body + right-hand outline
book The documentation shell, plus numbered targets, the reading_width measure and the draft banner
blog The documentation shell, with the sidebar expanded by default and RSS as the left half of the title row
swagger The documentation shell, with Swagger UI or Redoc rendering the body. See API reference pages
Any other type An ordinary page: navbar + single-column body + footer, with no sidebar

OINK 1.3.0 adds expo: a shared sidebar and an image album, with full page width and no right-hand outline. Its default shell_types list includes expo. If you set the explicit list above, add expo to it. See Expo albums.

Taxonomy and term pages are not in this table but use the same shell.

Use a cascade to assign a type to a subtree. This lets you put documentation at any content path:

content/handbook/_index.md
---
title: Operations handbook
type: docs
cascade:
  type: docs
---

Section roots are only navigation starting points

hugo.yml
params:
  ui:
    docs_section: docs
    blog_section: blog

These two keys do not select the shell. They identify the documentation and blog tree roots. The theme uses them to resolve sidebar roots, quick links, and default icons. The content/handbook/ example still uses the documentation shell. Keeping docs_section set to docs does not affect it.

To make a docs page’s sidebar root the site home rather than the documentation section:

hugo.yml
params:
  ui:
    docs_sidebar_root: home # home | section

Only these two values are valid. Any other value warns and uses section.

Documentation at the site root

A documentation-first site can publish the docs section at the URL root. Its source can remain in content/docs/. Apply all three configuration changes below together.

The first uses Hugo’s own permalinks to drop the docs/ segment from URLs:

hugo.yml
permalinks:
  page:
    docs: /:sections[1:]/:slug/
  section:
    docs: /:sections[1:]

The second keeps the physical site root index usable as a link target. It prevents that index from competing for the same output path. Apply it to each language’s site root index (content/_index.md, content/_index.zh.md):

content/_index.md
---
title: Product Docs
build: { render: link }
---

The third sets the sidebar root to the site home. The sidebar and pager then share one tree:

hugo.yml
params:
  ui:
    sidebar_root_enabled: true
    docs_sidebar_root: home

With docs_sidebar_root: home, the tree includes every top-level section of the site home. Exclude overview sections outside the reading sequence, such as blog, community, and download. Set toc_root: true in each section’s _index.md to exclude it from both the tree and paging order:

content/blog/_index.md
---
title: Blog
toc_root: true
---

Documentation then shares the URL root with blog, community and the rest. Build with --printPathWarnings and resolve every duplicate target before publishing.

Landing pages

A page with layout: landing uses a navbar, a body assembled from sections, and a footer. It has no sidebar. To write the data, see Home and landing pages.

hugo.yml
params:
  ui:
    landing_search: true

landing_search: false removes the search entry point from the landing shell and affects no other page.

Sidebar

The sidebar tree uses the structure of content/. It sorts by weight and uses linkTitle labels when present. You can adjust its density and size:

hugo.yml
params:
  ui:
    sidebar_menu_compact: true
    sidebar_menu_foldable: true
    sidebar_menu_truncate: 2000
    sidebar_width_min: 220
    sidebar_width_max: 480
    sidebar_item_overflow: ellipsis # ellipsis | wrap
    sidebar_expand_levels: 2
  • sidebar_menu_compact expands only the current branch and adjacent branches. The value false expands the complete tree.
  • sidebar_menu_foldable lets readers expand and collapse sections manually. Blog sections start expanded. To start one collapsed, set sidebar_expanded: false in its _index.md.
  • sidebar_expand_levels is how many levels are expanded by default.
  • sidebar_menu_truncate limits rendered entries per section. This keeps the HTML usable for a thousand-page tree.
  • sidebar_width_min / sidebar_width_max set desktop drag-resizing limits in pixels. The browser stores the adjusted width locally. Double-click the divider to restore the default.
  • sidebar_item_overflow defaults to ellipsis, which truncates long titles. For many long titles, use wrap.

At sidebar_cache_limit, pages with the same effective settings may share a neutral rendered tree. It remains visible and navigable without JavaScript. The shell runtime only adds the current path and opens its ancestors. Page or cascade overrides select the matching cached variant. A Book page with sidebar_headings enabled keeps its page-specific tree instead.

Fold state, width and scroll position are stored locally per language. Below md the sidebar becomes a drawer with a backdrop.

To drop the sidebar on one page, use front matter:

content/docs/fullscreen-report.md
---
title: Full-screen report
sidebar_enabled: false
---

An explicit navigation tree, data/docs_nav.json

By default, the theme derives the sidebar tree from content/. A site may instead supply an explicit navigation manifest. The theme uses it only when all three conditions hold:

  • The site has a data/docs_nav.json containing a sections key;
  • The page’s type is docs or book;
  • The resolved sidebar root is not the site home.

The file contains a nested node tree. Each node’s page identifies a content path, and children defines its navigation children. The theme resolves links and the active branch from page identity and this tree. url and the active_path_by_url ancestor map are optional legacy hints:

data/docs_nav.json
{
  "sections": [
    {
      "page": "/docs/start",
      "url": "/docs/start/",
      "children": [{ "page": "/docs/start/install", "url": "/docs/start/install/" }]
    }
  ],
  "active_path_by_url": {
    "/docs/start/install/": ["/docs/start/"]
  }
}

Write these paths without a language prefix or the deployment subpath from baseURL. Since OINK 1.1, both prefixes are stripped before comparison, so the same /docs/start/ key works at /zh/docs/start/ and /handbook/zh/docs/start/. Rendered links retain the real language and deployment prefixes.

The manifest can group root-level manuals without moving their source files under content/docs/. Nodes apply to a sidebar root if they are that root, content descendants, or Docs/Book pages resolving to the same root. For example, with docs_section: docs and docs_sidebar_root: section, root-level pages whose type is docs can use this tree:

data/docs_nav.json
{
  "sections": [
    { "page": "/intro" },
    { "page": "/config", "children": [{ "page": "/api" }] }
  ]
}

Here content/config.md and content/api.md remain sibling files, while API appears under Config in navigation. Selecting API expands Config without URL hints. A custom page URL changes its link, not its sidebar ownership. A separate Book root or nested sidebar_root_for root selects its own branch; a root absent from the manifest uses its content tree.

This tree also sets paging order and navigation JSON. The sidebar and previous / next links therefore agree. An empty sections array warns and falls back to the content tree. A nonexistent page warns and skips that entry. Placeholder nodes with manual_link and sidebar_divider rows stay in the sidebar without becoming paging targets. In 1.1, a divider section retains its children in this explicit tree too; use the same group-only front matter as for the content-derived tree.

Use an explicit tree when an external tool generates navigation order. For example, a manual migrated from a Sphinx toctree may need to preserve chapter order. If weight in content/ maintains the order, you do not need this file.

Sidebar icon density

An icon in a page’s front matter appears in the sidebar. Icons on every leaf page reduce readability, so a density policy controls them:

hugo.yml
params:
  ui:
    sidebar_icon_policy: groups # all | groups | none
Value Effect
all Every entry with an icon shows it (the compatibility default when unset)
groups Only the root and nodes with children show icons
none No entry icons in the sidebar

An invalid value warns and falls back to all. It does not fail the build. This site uses groups.

Expanding headings in the sidebar

Book pages keep headings out of the left page tree by default. To add an h2–h4 branch under the current sidebar row, opt in explicitly:

hugo.yml
params:
  ui:
    sidebar_headings: 3 # false | true | 2 | 3 | 4

An integer sets the deepest expanded level (2–4). The value true means 2, or h2 only. The value false disables the branch. An out-of-range value warns and disables it. This feature applies only to type: book pages. It expands only below the current sidebar row. Page front matter or a section cascade can set the same key; sidebar_headings: false turns off an inherited opt-in. The right outline remains available.

Table of contents

Hugo generates the right-hand outline from Markdown headings. Configure the collected levels in Hugo’s own settings:

hugo.yml
markup:
  tableOfContents:
    startLevel: 2
    endLevel: 4
    ordered: false

The normal shell runtime always tracks the active heading. It draws a continuous rail, highlights the current section, and marks the position. No separate switch is required. Readers can collapse the right column. The browser stores that state locally. Below xl, the right column hides, and its content moves to the sidebar drawer.

Since OINK 1.2.0, heading tracking decodes valid URL fragments. A malformed percent sequence falls back to the literal heading ID, including when following a link to a heading near the page end.

The old params.ui.scroll_spy site key and scroll_spy page key remain accepted as quiet compatibility no-ops throughout 1.x. Either boolean value produces the same outline and loads no extra runtime. The theme will remove these keys only in a future breaking release.

To hide the outline on one page, use the front matter notoc: true.

The outline includes only headings in Hugo’s table of contents. Headings from Markdown-form shortcodes ({{%/* … */%}}) appear there. Headings from ordinary shortcodes ({{</* … */>}}) usually do not. Write structural headings in Markdown.

Section index style

A section with an _index.md lists its child pages automatically, in one of two styles:

hugo.yml
params:
  ui:
    section_index: cards # list | cards
    section_index_columns: 2
  • list (the default): one title plus description paragraph per child page;
  • cards: a grid of cards reading each child’s title (or linkTitle), description and icon.

You can override this setting per section. An invalid value warns and uses a fallback:

content/docs/components/_index.md
---
title: Components
section_index: cards
section_index_columns: 3
---

Related page switches control child listings. The setting no_list: true lists no children. The setting simple_list: true emits a bulleted list without descriptions. A child with hide_summary: true excludes itself from the list. Do not write a child list by hand. It can become inconsistent with the sidebar.

Page width

hugo.yml
params:
  page_width: normal # normal | wide | full

The value normal uses the usual reading width. The value wide widens the content column. The value full fills the viewport. Override the setting per page or per section. Wide tables, large images, and API reference pages often use wide:

content/docs/api/reference.md
---
title: API reference
page_width: wide
---

Book pages also support reading_width (slim / normal / wide). This changes body reading width without changing the shell. An invalid value in either key warns and uses a fallback.

Navbar and footer switches

Configure the navbar and footer per page at the top level of front matter. Do not put these keys under ui. To configure a section, set them once in a cascade:

content/docs/_index.md
---
title: Docs
cascade:
  navbar_enabled: false
  footer_style: slim
---

The behaviour is in Navigation and menus and Brand and appearance, and the key definitions in Page parameters.

Verify

hugo --printPathWarnings
  • The build prints Total in … with no ERROR;
  • Confirm that a new type: docs page has a left sidebar. If it does not, check its cascade and shell_types;
  • Drag the sidebar divider and reload. Confirm that the width persists. Double-click to restore the default;
  • Below md the sidebar becomes a closable drawer, and below xl the outline moves into the drawer;
  • A section index has as many cards as the sidebar has child pages;
  • Confirm that a page with page_width: wide is wider than adjacent pages;
  • With documentation at the site root, hugo --printPathWarnings reports no duplicate output paths.

5.6 - Search

Enable local search. Control index size and result ranking, and configure useful CJK query matches.

OINK uses local search. Hugo generates one JSON index per language at build time. The reader’s browser downloads the index and runs the search locally. Search requires no crawler, account, CDN, or external search service. It is off by default. Enable it with one configuration line.

Search opens through the command palette. See Command palette for opening methods and its other contents.

Turning on local search

hugo.yml
params:
  offline_search: true

This key controls the page’s index reference, Lunr runtime, and search dialog. All three conditions must hold:

  • params.offline_search is true;
  • The page is the home page, or uses a shell layout (docs / book / blog / swagger — see Layouts and page types), or is a landing page with params.ui.landing_search on;
  • The current output is not print.

If any condition fails, the build includes no dialog, index reference, or Lunr runtime on that page. The resources are not hidden. They are not generated.

By default, hugo server also generates the index, so preview matches production. On a very large site, rebuilding the complete index can noticeably slow each preview change. To disable preview index generation, use:

hugo.yml
params:
  offline_search: true
  # skip index building during preview; only needed on very large sites
  offline_search_on_serve: false

Controlling index size

The offline_search_index key sets how much page content the index contains. It determines whether readers can find body text and the download size for their first search.

hugo.yml
params:
  offline_search: true
  offline_search_index: summary
  offline_search_summary_length: 70
  offline_search_max_results: 10
Value What is indexed When to use it
title Title, tags, categories, search_keywords A very large site where titles alone locate a page
heading The above plus every heading in the page When headings are specific enough
summary The above plus description and summary Sites with thousands of pages. This site uses it
content The above plus the full plain text The default, suitable up to a few hundred pages

Any other value warns with invalid params.offline_search_index and uses content.

The offline_search_summary_length key limits result excerpts (default 70). The offline_search_max_results key limits result count (default 10). The full definitions are in Configuration.

One index per language, budgeted at 2 MiB raw and 512 KiB gzipped.

The browser downloads the complete index before the first search. If it exceeds this budget, reduce offline_search_index from content to summary.

Adjusting ranking

A page influences its own ranking from front matter:

content/docs/reference/pgsql.md
---
title: PostgreSQL parameters
search_keywords: [postgres, postgresql, pg, database parameters, GUC]
search_boost: 1.5
---

The search_keywords key adds matching terms as a string or array. It is more useful than the boost setting. Readers searching pg or GUC can find a page titled only “PostgreSQL parameters”. Keywords have less ranking weight than the title and more than the body.

The search_boost key is a positive multiplier applied to the text match score. Its default is 1.0. A value of 1.5 does not guarantee first place. In OINK 1.4.0, it raises rank within the same term-coverage and phrase tier. Zero, negative numbers, and non-numbers warn and use 1.0.

Set a section-wide default once with a cascade:

content/docs/_index.md
---
title: Docs
cascade:
  search_boost: 1.25
---

A page’s own value overrides its inherited value. Pages under this site’s docs/ use search_keywords for Chinese phrasing, English terms, and configuration key names.

Keeping a page out of the index

content/internal/draft-plan.md
---
title: Internal plan
search_exclude: true
---

Use only search_exclude. The theme ignores the removed exclude_search and excludeSearch keys, so they do not exclude a page. The migration checker reports them. The index also excludes pages with empty bodies.

The index is a static JSON file anyone can download; it is not access control.

Do not put content that should stay private on the site, and do not use search_exclude to protect it.

Chinese, mixed queries, and identifiers

OINK 1.4.0 local search

The behavior below is included in the current release.

Search splits whitespace and Latin/CJK boundaries: PostgreSQL 备份 and PostgreSQL备份 both search for postgresql and 备份. A contiguous CJK run remains a literal substring, without a dictionary: 主从复制 does not imply 复制主从. Use spaces to separate concepts, for example 连接池 超时. search_keywords remains useful for synonyms and abbreviations.

The search preserves underscores, hyphens, dots, paths, and command options. Try shared_buffers, pg_basebackup, --baseURL, or /pg/data. These are literal identifiers, not query operators or spelling corrections. English words retain Lunr stemming. If no exact/stemmed English term exists, spelling fallback allows one edit for words of 5–7 characters and two for longer words. Shorter words and identifiers use literal substring matching. Case and Unicode width are normalized; at most 24 distinct terms participate in a query.

Terms may match different indexed fields. A title containing PostgreSQL and a description containing 备份 can satisfy both. Ranking follows these rules:

  1. If any page covers every term, show only those pages. Otherwise, show partial matches, labeled with matched/total terms, in descending coverage order.
  2. Within equal coverage, a contiguous complete phrase ranks first.
  3. Apply search_boost to field scores within those tiers. Each term uses its strongest field: title, keywords, tags/categories, headings, description, then body. A term repeated in that field ranks the page above one that mentions it once in the same field, but never above a page with a stronger field. Remaining ties use title and URL for stable ordering.

Literal substrings also match while typing. A boost cannot promote a partial match over a complete one. Results stay grouped in the navbar’s content-root order; ranking applies within those groups after the global result limit. Keyword-only matches show the description or excerpt, never the synonym list.

Each page occupies one result row, even when several sections match. The row shows the best matching section title and nearby text, and opens its actual rendered heading ID. Explicit IDs, duplicate auto-generated headings, Chinese headings, and deployment subpaths are preserved. A metadata-only hit opens the page when no section matches. heading and summary scopes can target heading matches; body-only queries require content. No index setting silently grows to full text. Matched snippets use up to 160 characters around the hit; the configured summary length still controls fallback excerpts.

The palette’s Search in control shows two adjacent choices: all sections and the current top-level section (such as Docs, Blog, or Book). It defaults to all sections on each page load and retains the choice while reopening that page’s palette. Home has no current section. Commands and page actions stay available; command mode hides the scope control. Use Tab to reach the selected scope and the arrow keys to switch. Press Tab again to return to the query; Shift+Tab from the query returns to the selected scope.

During input-method composition, result updates and Enter activation pause. They resume after committed text. Empty, loading, unavailable, result-count, and partial-match states remain readable and are announced to screen readers.

Optional: hosted search

The theme also provides two hosted search integrations. Both are off by default. Enable only one at a time. If more than one is configured, the build warns You have more than one site-search option configured.

If you enable hosted search, you accept its crawling behavior, availability, and privacy boundary. Document these in the site’s privacy statement.

Algolia DocSearch

hugo.yml
params:
  search:
    algolia:
      appId: YOUR_APP_ID
      apiKey: YOUR_SEARCH_ONLY_KEY
      indexName: YOUR_INDEX

You must supply all three values explicitly. A missing value warns and disables Algolia. OINK never falls back to another project’s public index. The theme bundles DocSearch JS and CSS locally. They do not load from a CDN. However, every query sends a request to Algolia. The integration requires real credentials and an index, so this page renders no demonstration.

Google Programmable Search

hugo.yml
params:
  gcs_engine_id: YOUR_ENGINE_ID

A landing page for the results is needed too:

content/search.md
---
title: Search results
layout: search
---

The search box submits queries to <baseURL>/search/?q=…. Google’s script renders results on that page, which requires access to cse.google.com. This integration uses an external service. This page renders no demonstration.

Verify

  1. Build, and confirm one index per language was generated:

    hugo --printPathWarnings
    ls public/offline-search-index.*

    A development build uses offline-search-index.zh.json. A production build fingerprints the filename, such as offline-search-index.zh.7ab….json. Confirm one file per language. If a file is missing, that language’s pages never reached an index.

  2. Inspect the index first when diagnosing “Chinese finds nothing”:

    python3 -c "import glob,json; f=sorted(glob.glob('public/offline-search-index.zh*.json'))[0]; \
      d=json.load(open(f)); print(f, len(d)); print(d[0])"

    The entry count should be close to the number of Chinese pages, and the keywords and boost fields should show what the front matter set.

  3. Open the site and press /. Search an English word, then a Chinese word. Confirm that results group by content root. Each group uses the first breadcrumb segment as its name.

  4. For a subpath deployment, such as https://example.com/docs/, open the browser’s network panel. Confirm that the index request includes the subpath. An index request to the domain root can return 404 while the page works. This is the most common cause of “search returns nothing”.

  • Command palette — search’s entry point, and the commands and page actions beside it
  • Keyboard navigation — the four single keys that open search and commands
  • Languages — per-language indexes and untranslated fallback
  • Configuration — full definitions of the offline_search* keys
  • Page parameters — search_keywords / search_boost / search_exclude

5.7 - Command palette

Use one dialog for page search, page actions, and site commands. Open the palette, use its groups, and add custom commands.

The command palette provides one modal dialog for page search and site actions. Use it to copy page Markdown, switch language or version, and open site links. The theme assembles it with local search. If params.offline_search is off, the page has no palette, index, or Lunr runtime. See Search.

Opening the palette

How to open it What opens
Click the search box in the navbar or sidebar Full search mode
⌘ / Ctrl + K Full search mode. Press again to close
/ Full search mode
The backslash key Command-only mode (equivalent to a prefilled >)
f / c The same two, provided by keyboard navigation
Typing a query beginning with > in the box Command-only mode

/, backslash, f, and c are bare single keys. They do not open the palette during typing. This applies to focus in an input, textarea, select, or contenteditable, and to input-method composition. The keys type ordinary characters instead. The modified ⌘/Ctrl + K can open the palette from a text box. It also remains inactive during input-method composition. Another open native dialog or visible ARIA dialog blocks every opening shortcut, including fixed-position dialogs. Hidden ARIA dialogs do not block them.

Inside the palette, use ↑ ↓ to select an item. Press Enter to run it. Press Esc to close the palette and return focus to the opening control.

What the palette holds

With nothing typed, the palette lists four groups in a fixed order:

Group Contents Decided by
Quick links A few entry points chosen from the navbar’s top-level menu params.ui.quick_links
Page actions Copy Markdown, view Markdown source, edit this page, view history, create a child page, open an issue, print the section, open the duo view (compare_translations) Repository configuration, whether this page has a Markdown output, and for the duo view params.ui.compare plus a same-origin translation
Preferences Switch version → switch language → switch theme Whether the site configures versions, languages and the light/dark menu
Commands Open the GitHub repository, then the site’s own commands params.github_project_repo (falling back to github_repo) and ui.command_palette.commands

The three preferences follow the navbar order: version, language, theme. Selecting a preference such as “switch language” does not navigate immediately. The palette expands its options in place. Select an option in a second action.

When you type text, page results appear first, grouped by content root. Each group uses the first breadcrumb segment as its name. Groups follow the navbar’s top-level menu order. Commands and actions appear together in the final group.

A query starting with > lists commands and actions only and searches no pages. Use it when you are unsure which menu holds a feature.

If the theme can explain why an item is unavailable, the item remains listed. Without repository configuration, “edit this page” remains with an “unavailable” note.

OINK 1.4.0 adds two adjacent Search in options for all sections or the current top-level section. It filters page results before the result limit, while commands and page actions remain available. The choice persists only while this page stays loaded. Matching sections appear beside page titles and open real heading anchors; multiple hits on one page share one row. Partial matches show their term coverage. See query semantics. Tab focuses the selected scope; arrow keys switch it. The control disappears in command and preference-choice modes.

Select quick links from Hugo’s main menu by identifier. You do not need to define them again:

hugo.yml
params:
  ui:
    quick_links: [docs, blog]

Values use the identifier of entries in menus.main. If unset, the list defaults to the documentation and blog sections (params.ui.docs_section and blog_section). To configure the menu, see Navigation and menus.

Custom commands

A site’s own commands go under params.ui.command_palette.commands, after the built-in ones, in the order written:

hugo.yml
params:
  ui:
    command_palette:
      commands:
        - id: theme_issues
          title: OINK issues
          description: Report or browse theme and documentation issues
          url: https://github.com/pgsty/oink/issues
          icon: fa-brands fa-github
          keywords: [bug, support, roadmap]

This site uses the command above. Commands have seven fields. An unsupported key or invalid record warns and discards that command:

  • id is required. It must start with a lowercase letter and contain only lowercase letters, digits, underscores, and hyphens. It must not duplicate a built-in action ID.
  • title supplies the displayed title. The description supplies the smaller line below it. The icon is one Font Awesome class pair.
  • keywords is an array of possible reader search terms. It participates in matching but is not displayed.
  • url and action are mutually exclusive and one is required. The url accepts a full http/https address, site path, or in-page anchor beginning with #. An address with a host opens in a new tab. The action references a built-in action ID.
Do not alias a built-in action with action:

Built-in actions are already in the palette, and wrapping one makes the same feature appear twice under two names.

For a multilingual site, put commands under languages.<lang>.params.ui.command_palette.commands. This lets you localize titles and keywords. The default language’s list sets the order. An entry with the same id in another language overrides only fields. A new id is appended. Command order therefore remains the same across languages.

Configuration can supply only a link or built-in action reference. It cannot inject a JavaScript callback. The palette reads a plain data manifest.

Page actions

The palette’s “page actions” and the documentation title’s split button share one implementation. They use the same action descriptors, URL generation, and executor. The button’s left half copies the page’s Markdown. Its right arrow expands all actions.

To hide the button beside the title, set page_context_menu: false. The corresponding actions stay in the palette:

hugo.yml
params:
  ui:
    page_context_menu: false

To hide the button on just one page, use page_context_menu: false in that page’s front matter instead.

The assistant_links setting is off by default. Clicking an assistant link sends the current page’s full URL, including query string and anchor, to a third party. It never uploads the body. Enable links site-wide through params.ui.page_context_menu.assistant_links. A page can only restrict that policy with this front matter:

Page front matter
page_context_menu:
  assistant_links: false

Check the title menu and palette. Neither should offer assistant links on that page. See AI-agent support for handoff behavior.

links adds external actions that appear only in the menu beside the title, not in the palette:

hugo.yml
params:
  ui:
    page_context_menu:
      links:
        - name: Ask in Discussions
          url: https://github.com/pgsty/oink/discussions/new?title={title}
          icon: fa-solid fa-comments

The three placeholders {url}, {title} and {markdown_url} are replaced with the current page’s values.

Whether “edit this page”, “view history” and “open an issue” are available depends on repository configuration. See Repository links and page info. “Copy Markdown” and “view Markdown source” require the page’s markdown output. See AI-agent support.

The dialog uses two independent data sources:

  • Page results come from the local search index. If the index was not generated or fails to download, the palette still opens and runs commands. The page section reads “the page index is unavailable; actions still work”.
  • Commands and actions come from a JSON manifest embedded in the page and need no network.

The theme does not assemble the palette for print output. If offline_search is off, there is also no palette. The keys f and c then do nothing and do not affect normal typing.

Query-aware site actions

The runtime hook for trusted site JavaScript is available since OINK 1.1. Feature-detect it: v1.0.0 and pages without local search do not provide it. Load the integration after the theme scripts, for example through layouts/_partials/hooks/body-end.html. This example assumes the site implements openSiteAssistant and owns its provider settings:

if (window.OinkCommandPalette?.registerSearchTail) {
  const unregister = window.OinkCommandPalette.registerSearchTail({
    id: 'ask-site',
    rows(context) {
      return [{ id: 'ask', title: 'Ask the site', description: context.query }];
    },
    activate(row, context) {
      context.handoff();
      return openSiteAssistant(context.query, {
        locale: context.locale,
        signal: context.signal,
      });
    },
  });
  // Call unregister() when removing this integration.
}

Rows follow native results and actions, including empty/error searches. They are absent in empty, command, choice, and loading states. Keep rows() pure and synchronous. All strings render as text. Activation receives the query that created the row, not newer input. Before opening another coordinated surface, use handoff(). The site then owns its focus and failure UI. For non-UI actions, return the operation without calling handoff.

The Shell contract defines fields, cancellation, validation, and lifecycle. YAML cannot contain callbacks. OINK adds no remote service or telemetry by default.

Verify

Run from your site’s root after a production build. In the commands below, replace public/docs/getting-started/index.html with an actual generated documentation page in your site (including a language prefix if needed).

  1. After a build, confirm the command manifest reached the page:

    hugo --printPathWarnings
    PAGE=public/docs/getting-started/index.html
    test -f "$PAGE" && grep -o 'td-action-manifest' "$PAGE"

    This confirms that HTML contains the action data. The remaining steps check the palette interface with local search enabled.

  2. Open the site. Press ⌘/Ctrl + K without typing. Confirm this order: quick links, page actions, preferences, commands.

  3. Type >. Confirm that only commands and actions remain. A new command should appear after “open the GitHub repository”.

  4. Repeat step 3 in another language. Confirm that command titles change and order remains the same.

  5. Open print preview (⌘/Ctrl + P). Confirm that it contains no palette.

5.8 - Keyboard navigation

Use single-key shortcuts and understand when typing disables them. Disable shortcuts for the site or individual pages.

OINK’s interactive pages have single-key shortcuts. WASD moves through the sidebar tree. J K move between headings. Q E open previous and next pages. Other shortcuts switch the theme, language, and command palette. Shortcuts are on by default. Typing disables every binding. You can disable shortcuts per site or per page.

Keyboard navigation keeps no separate state. Expansion and collapse use the sidebar’s arrow buttons. Section jumps use the right-hand outline. Language and theme switching use command palette actions. Keyboard and mouse navigation therefore use the same order.

Sidebar

Key Behaviour
W S ↑ ↓ Move focus to the previous / next visible entry
A D ← → Collapse / expand a group. On a leaf, A opens the parent and D does nothing
Enter Space G Activate the focused entry: open a page link or toggle a group button
Esc Leave the tree. Return focus to the body

You can use the four letter keys before entering the tree. With focus in the body, S starts from the current page’s sidebar entry. It moves down one entry and takes focus. The focused row is darker than the “current page” row. Readers can distinguish the two states.

If the sidebar is in a narrow-screen drawer or collapsed on desktop, the first key press opens it. If a page has no sidebar tree, these keys do nothing.

Arrow keys control the tree only after focus enters the sidebar. In the body, they retain native browser scrolling. For a right-to-left language, ← → swap with the reading direction. A D always mean “collapse / expand”.

Since OINK 1.1, groups without their own pages use disclosure buttons for keyboard navigation. From a child, A first returns to the group. Press it again to collapse the group. D opens a collapsed group. If the group is open, it moves to the first visible child. Page navigation with Q / E skips group buttons.

Reading

Key Behaviour
J K Jump to the next / previous section along the page outline
N Home page only: jump to the next top-level section (a mnemonic alias for J there)
Q E Previous / next page
H Focused reading: hide / restore the navigation shell
B Open or close the duo view when the site enables params.ui.compare and the page has a translation

J K use the same target sequence as the right-hand outline. They reach the same location as an outline click. Each jump uses a 100 ms ease, regardless of distance. You can press again before the animation finishes. If you have scrolled into a section, K first returns to its start. A second press opens the previous section. Without headings, the keys perform a short scroll.

Q E follow the sidebar tree’s visible order. They do not sort by date. Section indexes are tree entries. At a blog section boundary, the sequence is “last post of the previous section → next section’s index → first post of the next section”. Collapsed branches are excluded. Paging and focus use the same order. Without a sidebar tree, paging uses the page-end pager. Without that pager, it uses rel=prev/next in <head>.

On the home page, H hides only the navbar and footer. On a documentation page, it also hides the left and right columns and floating buttons. The tab session stores this state and restores it before the first frame. Paging with Q E preserves it without flicker. While the shell is hidden, WASD cannot focus the invisible sidebar.

Appearance, language and routing

Key Behaviour
L Y Cycle the language (the two keys are equivalent)
T Toggle light and dark
R Cycle among same-origin top-level navbar entries

These three shortcuts work on every interactive page, including pages outside the documentation shell. L does nothing on a single-language site. T does nothing if the light/dark menu is off. R does nothing if there is only one top-level entry. R cycles only same-origin top-level menu items. It excludes external links and navbar utility controls.

Key Behaviour
F or / Open the command palette in full search mode
C or the backslash key Open the command palette in command-only mode
⌘ with K or Ctrl with K Open the palette; press again to close

/ and backslash belong to search. They work when keyboard navigation is off. Keyboard navigation adds F C as aliases for the same palette. Backslash is difficult to use on some non-US layouts. You can also type a > prefix in the palette to select command-only mode. See Command palette for its contents.

Expo image viewer

The Expo viewer has its own shortcuts. Page-level single-key navigation pauses while an image is open. It resumes when the viewer closes.

Key Viewer action
W / A / S / D Move the enlarged image up / left / down / right
Q or H, E or L Previous image, next image
J or −, K or + Zoom out, zoom in
R or 0 Reset size and position
F Expand to fit the whole image in the viewing area
Escape Close the viewer

These actions apply inside the viewer. keyboard_nav: false disables page shortcuts; it does not disable the viewer controls. Expo is available in OINK 1.3.0.

Keys deliberately left free

? is reserved and unbound. The cheatsheet uses the question mark button in the footer’s bottom bar. It opens on hover, keyboard focus, or touch. It lists only keys available on the current page. A single-language site therefore has no language row.

G G, Shift with G and the digits are likewise reserved, as possible future jump sequences.

When shortcuts stand down

Each binding is a bare single key. The following conditions disable all bindings to prevent conflicts with typing or overlays:

  • Focus is in an input, textarea, select or contenteditable region;
  • An input method is composing (a hard requirement on a Chinese site);
  • A modifier is held. ⌘ with C still copies, and Shift with ↓ remains a browser action;
  • The command palette, another open native dialog, or a visible ARIA dialog owns the keyboard. This includes fixed-position ARIA dialogs; hidden ARIA dialogs do not block shortcuts.

The comment section uses an iframe. Its key events do not propagate to the page. No additional isolation is needed.

Focus order and accessibility

Since OINK 1.1, hidden sidebar and drawer content is removed from keyboard focus. Clicking the article, a table viewport or a code block no longer produces a large outline after a later keypress. Keyboard focus remains visible: the skip link highlights the article title, and scrollable tables and code keep their own focus indication and keyboard scrolling.

  • Skip link: the first Tab on a page reveals “skip to main content”. It skips the navbar and sidebar in one action.
  • Real focus: tree navigation moves actual DOM focus instead of a virtual cursor. A screen reader announces the link name and “current page” marker. Enter retains the link’s native behavior. The Tab order remains unchanged.
  • High contrast: under forced-colors, the focused row’s background is removed. A system highlight outline replaces it.
  • Reduced motion: with prefers-reduced-motion enabled, section jumps and paging scroll position instantly without an ease.
  • The key caps in the cheatsheet share their styling with the Kbd component used in the body.

Turning it off

Site-wide:

hugo.yml
params:
  ui:
    keyboard_nav: false

For one page (interaction-heavy demonstration pages often need this), or for a whole section by cascade:

content/docs/playground.md
---
title: Interactive playground
keyboard_nav: false
---

The key accepts only a boolean. Passing "false" or any other non-boolean value warns (params.ui.keyboard_nav must be a boolean) and uses the site default. See the complete definition in Configuration.

If you disable keyboard navigation, its runtime is excluded from the JavaScript bundle. /, backslash, and ⌘ with K belong to search and still work. The footer’s collapsible link-grid arrows are unaffected.

Verify

Run from your site’s root after a production build. In the commands below, replace public/docs/getting-started/index.html with an actual generated documentation page in your site (including a language prefix if needed).

  1. After a build, confirm the cheatsheet button is in the page:

    hugo --printPathWarnings
    PAGE=public/docs/getting-started/index.html
    test -f "$PAGE" && grep -c 'td-shell-keyboard__trigger' "$PAGE"

    With keyboard navigation off and local search not enabled, the button is not generated at all.

  2. Open a documentation page. Leave focus in the body and press S repeatedly. Confirm that the sidebar moves down from the current page’s entry. The body should not move.

  3. Press E several times. Confirm that paging follows the sidebar from top to bottom. Collapse a group and page again. Confirm that paging skips collapsed pages.

  4. Click in the search box and press J. The page should not scroll. The character should appear normally. Repeat with a Chinese input method.

  5. Enable “reduce motion” in the system and press J. Confirm instant positioning without animation.

  6. For the 1.1 focus fixes, follow the skip link and check the title’s focus indication. Collapse the sidebar and use Tab: its hidden links should be skipped, while the restore control remains reachable.

  • Command palette — the dialog F C open
  • Search — where the palette’s page results come from
  • Layouts and page types — which pages have a sidebar and outline, and so which keys apply
  • Kbd — writing key caps in your own documentation
  • Configuration — the full definition of ui.keyboard_nav

5.9 - Languages

Add a language and keep translations beside their originals. Configure menus and interface strings per language. Use matching heading anchors across languages.

OINK uses Hugo’s multilingual model without adding directory conventions. Configure a languages block. Put each translation beside its original in the same directory, with a language suffix in its filename. This page explains how to convert a single-language site to a bilingual site. It also covers two common problems: resource ownership and heading anchors.

Enabling a second language

hugo.yml
defaultContentLanguage: en

languages:
  en:
    label: English
    locale: en-US
    weight: 1
    title: OINK
    params:
      description: A Hugo theme for engineering docs
  zh:
    label: 简体中文
    locale: zh-CN
    weight: 2
    title: OINK
    params:
      description: 为工程而设计的 Hugo 文档主题
      time_format_default: 2006年1月2日
      time_format_blog: 2006年1月2日

This site uses the configuration above. The four fields have these functions:

  • label is the name shown in the language picker, written in that language’s own script: 简体中文, not Chinese.
  • locale is the standard language tag. It supplies <html lang>, hreflang alternate links, and Open Graph metadata.
  • weight decides both language order and the picker’s cycle order, lowest first.
  • params contains per-language overrides. If a key is absent here, it inherits the global value with the same name. Date formats usually need per-language values.

The default language has no path prefix, such as English at /docs/…. Each other language has a prefix, such as Chinese at /zh/docs/…. To also prefix the default language, add defaultContentLanguageInSubdir: true. This changes every site URL. For a live site, configure redirects at the same time.

File naming and resources

Put each translation beside its original, with a language suffix. Hugo uses the shared base filename to identify them as language versions of one page:

  • content/docs/
    • install.mdEnglish
    • install.zh.mdChinese
    • _index.md
    • _index.zh.md

Page bundles work the same way: index.md and index.zh.md in one directory.

In a page bundle, every language shares resources without a language suffix. A resource with a language suffix belongs only to that language.

  • content/docs/install/
    • index.mdEnglish page
    • index.zh.mdChinese page
    • topology.webpavailable to both languages
    • screenshot.zh.webpavailable to the Chinese page only

When you reference a suffixed resource in the body, write its name without the suffix: ![Screenshot](screenshot.webp). Hugo resolves it for the current language.

If a bundle contains only index.zh.md, with no English counterpart, its unsuffixed resources do not belong to the Chinese page. They belong to the default language, which has no page in that bundle. Each resource therefore needs the .zh. suffix. This site’s Chinese page bundles under docs/ use that structure.

What needs translating:

  • Translate: title, description, summaries, menu labels, tag names, image alt text, callout bodies, and reader-facing shortcode parameters.
  • Keep identical: dates, weight, aliases, and any metadata affecting routing. A mismatch makes the sidebar order differ between languages.
  • Do not translate: commands, configuration keys, filenames, URLs, version numbers, product names, shortcode names.

Per-language configuration

Three things live outside content/ and need one copy per language.

Menus are written under their own language:

hugo.yml
languages:
  zh:
    menus:
      main:
        - identifier: docs
          name: 文档
          pageRef: /docs
          weight: 20

The identifier must match across languages. Command palette quick links and search result grouping both use it. For complete menu configuration, see Navigation and menus.

Home page data uses the language files data/home/en.yaml and data/home/zh.yaml. If the current language has no file, it falls back to en.yaml. A single-language site needs only data/home.yaml. See Home and landing pages.

Interface strings: the theme ships 32 complete interface catalogs. These use the 31 locale filenames supported by Docsy, plus generic zh. Every catalog contains all 235 OINK messages in its native language. No catalog depends on generated English fallback blocks. zh and zh-cn use Simplified Chinese, while zh-tw uses Traditional Chinese. The exact locale and placeholder contract is recorded in Architecture. To change a string, create a file of the same name under the site’s own i18n/ and write only the keys you override:

i18n/en.yaml
ui_search: Search the docs

The current Hugo baseline accepts locale: zh alongside regional Chinese catalogs. The concrete locale: zh-CN shown above also works; no older-Hugo locale workaround is required.

Untranslated fallback and the language picker

The language picker icon is a link. Click it to select the next language by weight. After the last language, it returns to the first. Hover or focus it to expand a menu of all languages. On a touch screen, a tap switches language directly without expanding the menu. A bilingual site switches between its two languages with one click.

The menu always lists every configured language, whether or not the current page is translated:

  • If the target language has a translation, open that page;
  • If the target language has no translation, open its home page.

The home page fallback avoids a 404. However, readers may not notice that they moved to the home page. A bilingual site should enforce “every page has a counterpart” as a constraint. Do not depend on the fallback.

This fallback belongs to the language picker. Since OINK 1.2.0, this fallback is separate from SEO: the hreflang links list only the current page and actual translations. Each blog pagination page has its own canonical URL, and later pages omit language alternates. See canonical URLs and language alternates.

A missing translation is never filled in with the original

If no Chinese page exists, the Chinese site has no version of that page. Its sidebar, search index, and paging order omit the page.

Search indexes also apply per language. A search from a Chinese page matches only Chinese content. Queries support separated CJK terms, mixed languages and technical identifiers. See Search.

Heading anchors must align

Hugo derives IDs from heading text. A Chinese heading therefore produces a Chinese ID. The links /docs/install/#prerequisites and /zh/docs/install/#前置条件 point to corresponding sections with different anchors. Cross-language deep links, contents links, and in-page jumps then break.

Set the original heading’s ID explicitly on the translated heading:

install.zh.md
## 前置条件 {#prerequisites}

Follow these two rules:

  1. Read the ID from the HTML the English page renders. Do not derive it from heading text. Inline code, badges, or shortcodes can produce different IDs.
  2. Corresponding pages must have the same heading count, order, and IDs. If a translation needs an extra section, give it an independent stable ID. It must not conflict with an English ID.

This documentation site’s translation checker compares source structure and rendered heading IDs. The following command runs in the oink.pgsty.com checkout; the script is not part of a normal consumer site:

node scripts/check-doc-translations.mjs --public public

Before adapting it to your CI, change its fixed content scopes and EN/ZH file conventions. It locates source files relative to the script itself; --public only chooses the rendered output directory. For a manual check, compare the heading IDs in a representative translated pair’s generated HTML.

Add explicit English {#id} anchors when you create a page. This requires less work than adding them later.

Duo view

Set params.ui.compare: true to let readers open a page alongside one of its translations:

hugo.yaml
params:
  ui:
    compare: true

On Docs, Book, and blog pages that have a published HTML translation on the same origin, OINK adds an Open duo view button beside the page actions. Readers can also launch Duo View from the command palette or press B when keyboard navigation is on (try it on this page); pressing B again returns to the page. The current page stays on the reading-start side while the other pane loads the selected translation; if a page has more than two languages, readers can switch the target language from the toolbar menu. Below 900px, the view switches to a single-pane layout with a language toggle bar.

The Sync Scroll switch keeps both panes aligned at the same section by matching the heading IDs described above, so consistent {#id} anchors give the best result. If some headings have no counterpart, or the two pages share none, the reader estimates the position between matching headings or from overall reading progress. An orange ! beside the switch then says which; hover over it, focus it, or tap it to read the note. If heading IDs are duplicated or appear in a different order across languages, Sync Scroll turns off automatically while leaving both panes open for independent reading.

Enabling the duo view does not generate extra pages at build time. The reader fetches the two existing HTML pages inside same-origin frames only when a reader opens it. Pages without a translation, drafts, language-home fallbacks, and translations hosted on another domain are excluded automatically. To disable the duo view for a specific language—both as a host page and as a comparison target—set compare: false under languages.<lang>.params.ui.

Before enabling it in production, check two details:

  • Custom analytics snippets in hooks/head-end.html run inside each pane frame unless guarded with window.OinkComparePane; see Analytics.
  • Your Content Security Policy must allow frame-src 'self', and the server must not send X-Frame-Options: DENY or frame-ancestors 'none'; see Content Security Policy.

Right-to-left languages

Declare the writing direction under the language:

hugo.yml
languages:
  ar:
    label: العربية
    locale: ar
    direction: rtl
    weight: 3

The <html dir> value follows this setting. The theme also loads Bootstrap’s RTL stylesheet. Theme CSS uses logical properties throughout, such as margin-inline-start instead of margin-left. This automatically mirrors the layout. Use logical properties in site CSS too. Otherwise, RTL can position elements incorrectly.

Verify

  1. Build from your site’s root. Confirm that output exists for both languages. These paths assume English at / and Chinese at /zh/. Adjust them for your language settings. Check indexes only if local search is enabled:

    hugo --printPathWarnings
    ls public/index.html public/zh/index.html
    ls public/offline-search-index.*
  2. Check hreflang and canonical URLs against SEO behavior. hreflang lists only actual translations; each blog pagination page has its own canonical URL, and pages after page 1 omit language alternates.

    # Replace with an actual generated page in your site.
    PAGE=public/zh/docs/getting-started/index.html
    test -f "$PAGE" && grep -o '<link[^>]*hreflang[^>]*>' "$PAGE"
  3. On a translated page, expand the language picker and select the other language. Confirm that the same document opens. Repeat on an untranslated page. Confirm that the target language’s home page opens instead of a 404.

  4. Search the same concept once in each language and confirm both return results.

  5. Compare heading IDs in a translated page pair. Before adding this check to CI, adapt the documentation-site script as described above. Do not run it unchanged against a different content tree.

5.10 - Versions

Configure the version switcher and archive banner, and select a deployment layout for multiple documentation versions.

If a product supports several versions, its documentation usually needs versions too. The theme provides a navbar version switcher and an archive banner on older sites. The site selects the deployment layout. The theme does not build across versions. Each version requires a separate Hugo build.

The version switcher

List the menu versions under params.versions. If the list is nonempty, a branch-icon menu appears in the navbar’s utility area. An icon-only upward menu in the footer’s bottom bar shows the same content.

hugo.yml
params:
  # which version this site is
  version: v2.1
  # the accessible menu name; bottom-bar trigger remains icon-only
  version_menu: v2.1
  versions:
    - version: v2.1
      url: https://docs.example.com
    - version: v2.0
      url: https://v2-0.docs.example.com
    - version: v1.9
      url: https://v1-9.docs.example.com

A menu entry shows its version value by default. If name is given, it shows that name. An entry is selected if its version equals params.version or its url equals the site’s baseURL.

An entry without a url is a gray item that cannot be clicked. You can use it as a group heading. name: '---' creates a divider. A url on a divider causes a warning. name accepts inline Markdown:

hugo.yml
params:
  versions:
    - name: '**Current**'
    - version: v2.1
      url: https://docs.example.com
    - name: '---'
    - name: '**Older versions**'
    - version: v1.9
      url: https://v1-9.docs.example.com

The same list supplies “switch version” in the command palette. The menu and palette therefore agree.

version_menu_pagelinks: true appends the current page’s path to the target version’s URL. Version switching thus keeps the reader on the same document.

The target version may not have that page. Documentation structure can change between versions. If an older version lacks a new page, switching to it produces a 404. This site leaves the option off.

A single entry can override the global setting:

hugo.yml
params:
  version_menu_pagelinks: true
  versions:
    - version: v2.1
      url: https://docs.example.com
    - version: v1.9
      url: https://v1-9.docs.example.com
      pagelinks: false # this version's structure differs; go to its home page
Judge by how stable the structure is, not by how far apart the versions are

Enable it where the structure is stable. Disable it where the structure changed. An extra step to a version’s home page is preferable to a 404.

The archive banner

If a version is no longer maintained, identify its site as a snapshot:

hugo.yml
params:
  archived_version: true
  version: v1.9
  url_latest_version: https://docs.example.com

With archived_version: true, a banner appears at the top of every documentation and book page body. It states that the current version is no longer actively maintained and links to url_latest_version. The text uses the site’s language. You do not need to write it. The banner shows the version value as its version number.

The banner appears on documentation and book pages only, not on blog or landing pages.

params.version versus params.versions

These two parameters have different purposes:

  • params.versions is a cross-site list. It identifies the versions available in the menu and their locations. It describes other sites.
  • params.version identifies this build’s version. It selects the menu entry and supplies the archive banner’s version number. It is also the fallback when data/download/*.yaml omits version. See Releases and downloads.

It does not have to be a Git ref. If you need a resolvable release tag, declare a separate parameter. For example, an install command can use that parameter. Do not reuse params.version for this purpose. The full definitions of both keys are in Configuration.

Deployment layouts for multiple versions

Layout baseURL Characteristics
Subdomain https://v1-9.docs.example.com/ Versions are fully independent. Each needs its own DNS and certificate
Subpath https://docs.example.com/v1.9/ One domain concentrates SEO weight. The host must route paths to different artifacts

Each version is an independent build. Check out its content from the branch or tag. Build with that version’s hugo.yml. Publish to the matching address. The current version’s site lists every version. Older sites list them and add the archive banner.

On a subpath deployment, baseURL must include the path segment

Otherwise the search index, page actions and asset links all point at the domain root. The page looks correct, but search returns nothing. This is the most common subpath failure. For deployment details, see Deploy.

Verify

Run from your site’s root after a production build. In the commands below, replace public/docs/getting-started/index.html with an actual generated documentation page in your site (including a language prefix if needed).

  1. After a build, confirm the version menu reached the page:

    hugo --printPathWarnings
    PAGE=public/docs/getting-started/index.html
    test -f "$PAGE" && grep -c 'nav-version-menu' "$PAGE"

    With params.versions empty or unset, the menu is not generated at all.

  2. Check whether the current version is marked selected:

    grep -o 'td-nav-hover-menu__option td-is-active[^>]*' "$PAGE"

    If there is no result, params.version does not match an entry’s version field, or baseURL does not match that entry’s url. Check the trailing slash.

  3. Visit each menu link. With version_menu_pagelinks enabled, switch from a document that an older version lacks. Confirm that the destination is acceptable.

  4. On an archived site, open a documentation page. Confirm that the banner appears at the top of the body. It should use the site’s language and link to the current version.

  5. Press ⌘/Ctrl + K to open the command palette. Confirm that “switch version” lists the same versions.

5.11 - Taxonomies

Use tags and categories to index pages across directories, with automatic term pages, term cards, rail clouds, and navbar panels.

A directory tree gives a page one path. A taxonomy gives it a second. A PostgreSQL backup document can be in an “Operations” directory and on a “backup” tag page. To enable this, set Hugo’s taxonomies: configuration. The theme generates term pages, term cards, the rail cloud, and the navbar panel. You do not need to write a template.

This page has a category. The “Categories: Customization” line appears under the title. Counted chips appear under the outline in the right column. Neither requires configuration on the page itself.

Enabling a taxonomy

Hugo owns taxonomies. The theme adds no switch of its own. Write taxonomies: at the top level of hugo.yml, with the singular name as the key and the plural as the value:

hugo.yml
taxonomies:
  tag: tags
  category: categories

This site uses that configuration. Apply these three rules:

  • taxonomies: defines the complete list. It does not add to an existing list. To retain tags / categories, include them with custom taxonomies.
  • The plural is also the URL segment: /tags/, /categories/.
  • To disable all taxonomies, use disableKinds: [taxonomy, term].

To add a taxonomy for product modules, use:

hugo.yml
taxonomies:
  tag: tags
  category: categories
  module: modules

The six keys tag, tags, category, categories, module, and modules have localized titles in all theme language files. Other taxonomies use the humanized plural (products → Products). To set a custom name, write title / linkTitle in content/<plural>/_index.md and _index.zh.md. The theme gives that name priority:

content/modules/_index.md
---
title: Product modules
linkTitle: Modules
---

Tagging a page

The front matter key is the plural (the value column of taxonomies), and the value is always a list, even with one entry:

content/docs/ha/patroni.md
---
title: Patroni high availability
description: Managing PostgreSQL failover with Patroni.
categories: [High availability]
tags: [PostgreSQL, Patroni, failover]
---

If a whole section shares one category, set it in the section index’s cascade. Do not repeat it on every page:

content/docs/customize/_index.md
---
title: Customization
linkTitle: Customization
icon: fa-solid fa-sliders
cascade:
  categories: [Customization]
---

All six documentation sections on this site use this configuration. A page’s own categories: replaces the cascade. It does not merge with it. To add a category beside the section’s category, write both.

The term line on a page

Documentation and blog pages show assigned terms under the title and summary. Each term links to its term page. This page shows “Categories: Customization”. Its container is .taxonomy-terms-article. Each taxonomy adds a .taxo-<plural> class. Use those two selectors to style it.

By default, it lists every taxonomy except the reserved plurals authors and series. These have their own surfaces: a byline and a series strip. Chips would repeat that information. To include either taxonomy, name it in page_header.

To show only some, in a fixed order:

hugo.yml
params:
  taxonomy:
    page_header: [categories]

The key is listed in Configuration. It cannot hide the line. See Limits.

Two taxonomies the theme knows by name

authors and series are ordinary Hugo taxonomies. Declare them in the usual way. The theme adds no parameter for either. It adds a rendering for each taxonomy. The declaration alone enables that rendering:

hugo.yml
taxonomies:
  category: categories
  tag: tags
  author: authors
  series: series
Plural What the declaration turns on The term page becomes
authors Portraits and linked names in the article head, names on list rows, one <dc:creator> per author in the feed The author’s profile. The display name uses linkTitle, or title if absent. description supplies the short introduction. The body supplies the long introduction. The featured-image resolver selects the avatar
series A strip above the article body naming the series, this article’s position, the next part, and the whole list behind a <details> The series introduction, listing its members in reading order rather than newest-first

For complete details and required front matter, see Writing a blog. Keep these two rules in mind:

  • There is deliberately no data/authors file. The term page owns the profile, which prevents conflicting sources.
  • A series term page does not use reverse-date order. Members with a series_weight come first in ascending order. Other members follow by ascending date. A term page cannot give Hugo an order. The theme resolves the order once. Both surfaces use that result.

Term and taxonomy pages

Each taxonomy generates two levels of page:

Page URL Contents
Taxonomy list /categories/ The heading shows the taxonomy’s glyph, localized name (“Categories”), and term count. Cards show each term, most-used first. Each card shows its glyph (an author’s portrait), name, and page count
Term page /categories/customization/ The heading shows the term’s title and page count. If breadcrumbs are disabled, a “Categories” kicker links to the list. Member pages follow, newest first, in blog-list style

A Chinese term’s URL uses Chinese characters. The address bar shows 定制站点, and the HTML is percent-encoded. Hugo does not transliterate. For ASCII URLs, use English terms. Give each a display name with title in content/categories/<term>/_index.zh.md — Hugo’s term page content file mechanism.

A term page has no fixed location in the content tree. If all members belong to one top-level section, it uses that section’s sidebar tree and root link. A reader who clicks a documentation tag thus remains in the documentation navigation. If members belong to different sections, the page uses the site-level tree.

Term cards appear only on the taxonomy list page. Term pages show the rail cloud instead.

The rail cloud

Documentation, blog and term pages carry one group per taxonomy in the right column (under the outline), with counted, collapsible chips. The group is automatic by default. It appears where a taxonomy is defined and the current scope has terms. Set toc_taxonomies: false in a page’s front matter to hide its rail clouds and taxonomy switcher, or set params.ui.toc_taxonomies: false in hugo.yml site-wide. These display settings leave the page’s tags, byline, series and taxonomy membership intact.

Taxonomy list pages and term pages put a taxonomy switcher at the top of the column. Each declared taxonomy has one row with its glyph, name, and term count. The row links to its list page. The current taxonomy is highlighted. A list page counts its clouds site-wide. It excludes its own taxonomy because the adjacent cards show those terms. A site with a single taxonomy shows no switcher.

The count is per top-level section, not site-wide. The theme first looks for a section named after the page’s type. A type: docs page uses the /docs/ tree. Otherwise, it uses the page’s top-level section. “Tags: release 4” on a blog page means four posts in the blog. It does not mean four on the site.

Icons are configured by plural name:

hugo.yml
params:
  ui:
    taxonomy_icons:
      categories: fa-solid fa-folder
      tags: fa-solid fa-tags
      modules: fa-solid fa-cubes

Those two are the defaults for categories and tags. Other taxonomies default to fa-solid fa-shapes. An icon is one Font Awesome class pair, written as everywhere else on the site.

The taxonomy panel in the navbar

A main-menu entry that points to a taxonomy list page becomes a term-chip panel automatically. It shows counts and sorts by descending usage. No manual dropdown is required:

hugo.yml
languages:
  en:
    menus:
      main:
        - identifier: tags
          name: Tags
          pageRef: /tags
          weight: 60

Both pageRef: /tags and the older url: /tags/ are recognized. The theme resolves a URL-form menu entry to a site page before checking its kind. Migration from an older configuration needs no rewrite. Other ways to write menus are in Navigation and menus.

Bilingual terms

Hugo counts and links taxonomies per language. /categories/ and /zh/categories/ are separate trees. A Chinese page belongs only to the Chinese tree. Terms are written once per language in each front matter:

content/docs/ha/patroni.md
categories: [High availability]
tags: [PostgreSQL, Patroni, failover]
content/docs/ha/patroni.zh.md
categories: [高可用]
tags: [PostgreSQL, Patroni, 故障切换]

Apply these two language rules:

  • The same word in both languages (such as release) still creates two term pages: /categories/release/ and /zh/categories/release/. Each counts only pages in its own language. Do not use English terms on Chinese pages only for consistency. The rail chips would then show English.
  • For the six built-in keys, a taxonomy’s display name follows the language. A term’s name does not. It is the exact front-matter string. The theme does not translate it. If you write 高可用 on an English page, its chip shows 高可用.

The rest of running a multilingual site is in Languages.

Switching by content type

To retain the rail clouds on documentation but hide them on a blog section, set the display switch on that section index and its descendants:

content/blog/_index.md front matter
toc_taxonomies: false
cascade:
  toc_taxonomies: false

Taxonomy membership is a separate choice: it comes from the terms assigned to pages. For example, this documentation site uses:

Content categories tags Effect
content/docs/** Section-level cascade (the six sections) none The term line has one row, Categories
content/blog/** Per post (release, oink) Per post (Oink, Release) Two rows in the term line, two chip groups in the rail

To exclude a whole section from the taxonomy, delete categories from the section index’s cascade. No other change is needed. To exclude one page, write categories: [] in its front matter. The empty list overrides the cascade.

Verify

Choose a page in your own site with taxonomy terms assigned, then check:

  • Its title area shows the terms selected by params.taxonomy.page_header.
  • With toc_taxonomies enabled, the right rail shows term groups and counts. Disabling it hides the clouds but retains the page’s terms.
  • Your taxonomy index (for example /categories/) lists terms whose links open their member pages.

Run from your site’s root, replacing both sample paths with your own page and taxonomy output paths, including language prefixes:

hugo --printPathWarnings
PAGE=public/docs/getting-started/index.html
TAXONOMY_DIR=public/categories
ls "$TAXONOMY_DIR"
test -f "$PAGE" && grep -o 'taxonomy-term' "$PAGE"

Limits

  • page_header: [] does not hide the term line. An empty list is treated as unset and lists every taxonomy. To remove the line, stop tagging those pages. Alternatively, hide .taxonomy-terms-article in assets/scss/_styles_project.scss.
  • toc_taxonomies: false hides the rail clouds. There is no configuration key to limit the number of terms in a visible cloud.
  • Term pages have no cross-language pairing. Language switching does not guarantee the same term in the other language.

5.12 - Repository links and page info

Connect “edit this page”, “open an issue”, and “view history” to your repository. Show last-modified information, contributors, and feedback at the page end.

Repository entries in the action menu use a few github_* parameters. This menu appears at the breadcrumb row’s right end. The “last modified” line at the page end uses Git history. Both features assume that content is in a GitHub-style repository.

Every repository-related entry in the action menu derives from these keys:

hugo.yml
params:
  github_repo: https://github.com/pgsty/oink.pgsty.com # the documentation source repository
  github_project_repo: https://github.com/pgsty/oink # the product repository (optional)
  github_branch: main # defaults to main
  github_subdir: '' # path from the repository root to the Hugo site root

This site uses the configuration above. It gives this page’s action menu the following targets:

Menu entry Target
Edit this page …/edit/main/content/docs/customize/repository.md
View history …/commits/main/content/docs/customize/repository.md
Create child page …/new/main/content/docs/customize?filename=change-me.md&value=<template>
Open a documentation issue …/issues/new?title=Repository links and page info
Open a project issue https://github.com/pgsty/oink/issues/new

Follow these conventions:

  • github_repo must identify the content repository. If you use the theme repository, reader changes go to the wrong place. Without this key, the four documentation actions above are unavailable. The project issue depends only on github_project_repo.
  • github_project_repo identifies a second repository for product bugs instead of documentation errors. Do not configure it if readers cannot distinguish the two.
  • github_branch defaults to main. It identifies the content branch. Do not use the deployment branch or a branch generated by Pages.
  • github_subdir is the path inside the repository. If site source is at the repository root, leave it empty. If source is in a subdirectory, set its path, such as website for a repository with code and website/.

Set these values at site level, per language, in a section cascade, or in page front matter. These scopes support content from several repositories. See the complete definitions in Configuration.

Content from another repository

When content comes from an upstream repository, override the repository parameters with a section cascade. path_base_for_github_subdir removes or replaces the physical source path prefix before appending it to github_subdir. For source files copied inside the site, the path is relative to the site root:

content/reference/_index.md
---
title: Upstream reference
cascade:
  github_repo: https://github.com/OWNER/UPSTREAM
  github_project_repo: https://github.com/OWNER/UPSTREAM
  github_subdir: docs
  path_base_for_github_subdir: content/reference
---

content/reference/api/client.md therefore maps to the upstream’s docs/api/client.md.

Since OINK 1.2.0, the theme normalizes Windows and Unix source paths to / before matching and preserves filename case. For a physical mount outside the site, match its absolute source path rather than Hugo’s virtual mount target:

External source at /srv/upstream/docs/
path_base_for_github_subdir: '^/srv/upstream/docs/'

With github_subdir: docs, /srv/upstream/docs/api/client.md therefore maps to docs/api/client.md. After mapping and cleanup, the source must be a nonempty repository-relative path. Unmapped external files, absolute or drive-qualified results, . and parent-directory escapes suppress Edit, History, and Create child actions. Documentation and project issue links retain their own repository settings. Use / separators in Windows mapping expressions too.

The path_base_for_github_subdir value is a regular expression. If the source filename differs from the local filename, use a from / to mapping. For example, map each section’s _index.md to the upstream README.md:

content/reference/_index.md
path_base_for_github_subdir:
  from: content/reference/(.*?)/_index.md
  to: $1/README.md

OINK keeps .md and .zh.md together in one directory. Both languages share a path prefix, so the expression needs no language directory. After changing it, click “edit this page” from a leaf page, section index, and each language. If the expression removes too much, the URL can appear correct but return a 404.

Turning individual entries off

Every menu entry carries a stable action ID:

Menu entry Action ID
Copy as Markdown copy_markdown
View Markdown source view_markdown
Open in ChatGPT / Claude open_chatgpt / open_claude
View history view_history
Edit this page edit_page
Create child page create_child_page
Open a documentation issue create_issue
Open a project issue create_project_issue
Print the whole section print_section

If a host does not support an action, hide it with CSS:

assets/scss/_styles_project.scss
.td-page-actions__item[data-td-action='create_child_page'] {
  display: none;
}

The command palette uses the same IDs. Hiding a menu entry does not remove it from the palette. If the entire site cannot use a target, omit its configuration key. CSS can hide a link but cannot correct its target.

The whole menu can also be turned off per page with page_context_menu: false in front matter — see Page parameters.

The “create child page” action prefills content from the theme’s assets/stubs/new-page-template.md. To supply a site template, put your own file at assets/stubs/new-page-template.md.

Last modified

The last-modified line uses Git data, not file mtime. Enable Hugo’s Git support:

hugo.yml
enableGitInfo: true
params:
  github_repo: https://github.com/pgsty/oink.pgsty.com
  ui:
    lastmod_commit: subject # subject | hash | none

The page end then reads “Last modified August 17, 2026 · (a1b2c3d)”, with the commit part linking to …/commit/<hash>. The three values of lastmod_commit:

Value What is shown
subject (default) The commit subject plus the abbreviated hash
hash commit a1b2c3d
none The date only, with no commit link

Any other value warns (invalid params.ui.lastmod_commit) and uses subject.

Check these two conditions:

  • CI needs sufficient Git history. A shallow clone (fetch-depth: 1) cannot reach a file’s last commit. Its date can be missing or incorrect. Set fetch-depth: 0 in GitHub Actions.
  • An uncommitted file has no Git time. The line is absent when you preview a new local page.

If Git history is unavailable, do not use build time for “last modified”. Build time does not identify when content changed.

This line belongs to the annotation component, which is on by default and sits after feedback and before the pager. Turn it off for a page with annotation: false.

The annotation block also renders two kinds of provenance. Both use page front matter and require no template override:

  • Upstream attribution: for a derived page, set upstream_link and four required keys: upstream_name, upstream_copyright, upstream_license, and upstream_notice. The page end names the work, copyright holder, and license, with a link to the full notice. Add upstream_modified: true to append a “modified downstream” line.
  • Translation notice: params.ui.translation_notice contains the authoritative version’s language code. A translated page shows a link to that original. For a page authored natively in the current language, set translation_notice: false.

Both families are defined in full in Page parameters.

If you need customization, use one of three override points. Each covers one layer:

Partial to override What it changes
layouts/_partials/annotation-items.html Add, remove or reorder the lines, keeping the theme’s markup, icons, print rules and accessible label
layouts/_partials/page-meta-lastmod.html Replace the markup those lines render as
layouts/_partials/page-annotation.html Replace the block’s outer container

What the page end is made of

The five components have a fixed order. All reading layouts share one implementation:

Order Component Theme default Page switch
1 Share Off (params.ui.share is empty) share: false, or the page’s own list
2 Feedback Off feedback: true / false
3 Annotation On annotation: false
4 Pager On for docs / book / blog pager: false
5 Comments On when fully configured comments: false

This order follows reader actions after the last paragraph: share the page, give feedback, check provenance, open the next page, and join the discussion. Share appears first because it is the only outward action. Readers who intend to share have already made that choice before the feedback question. To configure the bar, see Writing a blog.

Configuring comments is in Comments.

The feedback widget

The widget asks “Did this page solve your problem?” with yes / no buttons. Choosing no expands four optional reasons. The widget is off by default:

hugo.yml
params:
  ui:
    feedback:
      enable: true
      reasons: true # whether to ask for a reason after "no"

To enable it for the documentation section only, use a cascade (a blog usually keeps just comments):

content/docs/_index.md
---
title: Docs
cascade:
  feedback: true
---

The widget has these boundaries:

  • A click completes it. There is no text box, no submit button and no sign-in.
  • The browser stores the choice in localStorage per page and language. Returning readers can see and change it.
  • If the site has Google Analytics (gtag), the widget sends a docs_feedback event with result (solved / not_solved), page_path, and language. Choosing a reason sends a second event with reason and refinement: true. This distinguishes it from the first count. Without analytics the widget still works and sends no reports. It requires no backend.
  • If the page enables comments, an anchor link labeled “add details in the comments” appears below the result. Feedback and giscus have independent data flows. The theme never writes comments for readers.

This page sets feedback: true in its front matter (the docs section defaults it off), so the real widget is visible at the page end.

The contributor wall

The contributors shortcode renders a GitHub avatar grid from a file under the site’s data/ directory. It never contacts GitHub at build time:

data/contributors.yaml
items:
  - github: Vonng
    name: Ruohang Feng
    role: Theme author
  - github: pgsty
    name: Pigsty
    role: Project organization
  - github: gohugoio
    role: Static site generator
    avatar: /icons/logo.svg
Source
{{</* contributors */>}}

The github field is required and must be a valid GitHub username. A duplicate produces a warning and skips the repeated entry. The name defaults to github. The role is optional. The url defaults to https://github.com/<github>. The avatar is optional. Without it, the shortcode renders an initial placeholder and makes no network request. If present, its value must be http(s):// or a site-root-relative path.

For several lists, use separate data files. Select each file with data=:

Source
{{</* contributors data="maintainers" */>}}

In Markdown and RSS output, the grid becomes a list of - [@handle](url) — role.

This site has no data/contributors.yaml

The example therefore does not render on this page. Add a data file to data/ to render it.

Verify

Run from your site’s root. Replace the example PAGE with an actual generated page whose source belongs to the configured repository:

hugo --printPathWarnings
PAGE=public/docs/getting-started/index.html
test -f "$PAGE" && grep -o '<a[^>]*data-td-action[^>]*>' "$PAGE"
  • Open the page’s title action menu. Confirm that “Edit this page” points to github.com/<your repository>/edit/<branch>/<source path>. Every source path segment must match. In command output, inspect the anchor with data-td-action="edit_page".
  • Repeat from a section index (_index.md): it also needs the correct source path.
  • Check the “last modified” line at the page end. It is expected to be absent on a new local page before commit.
  • Page parameters — annotation / feedback / pager / page_context_menu and the other page switches
  • Configuration — full definitions of github_*, ui.lastmod_commit and ui.feedback
  • Comments — the last block at the page end
  • Analytics and SEO — where feedback events are sent
  • AI-agent support — the Markdown and assistant entries in the action menu

5.13 - Print

Print one page with the browser’s Cmd/Ctrl+P. Use the print output format to combine a section into one continuous document.

Printing one page needs no configuration. The sidebar, outline, navbar, and buttons have d-print-none. The browser’s Cmd/Ctrl+P therefore prints only the body. The theme has no per-page “print this page” button.

To combine a whole section or book and its pages into one continuous document, configure print output. This document includes a table of contents. This page explains how to enable the output, understand its structure, and exclude pages.

Enabling whole-section print

The theme declares the custom print output format but does not enable it for the site. Add it to section in the site’s own hugo.yml:

hugo.yml
outputs:
  home: [HTML, markdown, LLMS]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]

This site uses the configuration above. Each key under outputs is a wholesale replacement, not a merge. When you add print, retain every format that the page kind already used (HTML, RSS, markdown). If you omit a format, you lose that output.

After you enable print output, each section has a print URL. The _print segment comes first after the language prefix:

Page Print view
/docs/customize/ /_print/docs/customize/
/docs/ /_print/docs/
/blog/release/ /_print/blog/release/

“Print the whole section” appears in the page action menu. You can also find it in the command palette by action ID print_section. It prints the current section. On /docs/customize/print/, it prints the entire Customization section, not only this page.

The structure of a print view

Each print view contains these items from top to bottom:

  1. A notice bar: “This is the multi-page printable view of this section. Click here to print. Return to the regular view of this page.” It carries d-print-none and appears on screen only, never on paper.
  2. The section title and summary.
  3. A whole-section table of contents, numbered 1:, 2:, 2.1: by level, linking to in-document anchors.
  4. Each page appears in sequence. Its title uses “number - title”, such as “1 - Configuration”. Its description introduces the body. The body is rendered unchanged.

Pages follow sidebar order (weight), with subsections expanded recursively. Each page after the first starts a new sheet. The first page starts a new sheet only if the section index body exceeds 50 words. A one-sentence index therefore does not occupy a separate sheet. You can adjust the threshold:

hugo.yml
params:
  print:
    section_break_wordcount: 120

To remove the table of contents:

hugo.yml
params:
  print:
    toc: false

It can also be turned off for one section, in the section index’s front matter:

content/docs/components/_index.md
---
title: Components
print:
  toc: false
---

Excluding pages

To exclude link-only pages, redirect notes, or pages with one large screenshot, set no_print:

content/docs/about/showcase.md
---
title: Showcase
no_print: true
---

This setting affects only the whole-section print view. It does not affect the page’s HTML or browser Cmd/Ctrl+P. The view automatically excludes sidebar dividers (sidebar_divider). If a divider is a section, its child documents remain in the print sequence.

How components look in print

Print is one of four outputs. Each component has a defined print form. Whole-section print and single-page browser print use the same rule: interactive content becomes static, and collapsible content expands.

Component Print shape
Callouts Static blocks. All collapsible kinds (- / + / DETAILS) expand. Borders become gray, and backgrounds are removed
Tabs The tab bar disappears and every panel is expanded in turn, each with its own heading
Code Blocks Copy and fold controls removed, max height and scrolling dropped, long lines wrapped
Tables Complete static tables with a keyboard-scrollable browser preview; print-media CSS expands them for paper and headers repeat across pages
Images Retains image and caption. Removes zoom attributes and limits width to the reading measure
Gallery The grid becomes a vertical stack
FileTree A static panel with all directories expanded. The split retains its build-time width
Fields A complete definition list, identical in both forms
Math Statically rendered KaTeX / MathML
Mermaid Dedicated Print retains source without an engine; ordinary HTML printing shows rendered diagrams or source when not rendered
Markmap The print view is HTML; its runtime renders normally
PlantUML Dedicated Print keeps source without a runtime; printing ordinary HTML retains rendered images or source when not rendered
ECharts · Infographic Shows the fence source block. Draws no chart
Asciinema · OpenAPI A labeled static link to the recording or specification. None of the three runtimes loads
Cards / steps / badges / keys Static, with content unchanged

Print excludes the page shell: sidebar, outline, navbar, page action menu, feedback widget, heading anchor links, and inline copy buttons.

When printing an ordinary HTML page with browser-rendered diagrams, confirm that they have finished drawing before triggering print.

Browser print styles

The theme ships a layer of @media print rules shared by single-page and whole-section printing:

  • A4 paper with 18mm 16mm 20mm margins; 10.5pt body text; the light palette forced.
  • Fonts use the --td-print-font-family typography token. See Brand and appearance.
  • Headings do not separate from their body (break-after: avoid-page), and paragraphs and list items keep three-line orphan and widow control.
  • Tables, images, blockquotes, callouts, cards, and tabs avoid page breaks where possible. Code blocks may break across pages. Long lines wrap instead of truncating.
  • Links are underlined and dark blue. Print does not append URL text. To append it, add this rule:
assets/scss/_styles_project.scss
@media print {
  .td-content a[href^='http']::after {
    content: ' (' attr(href) ')';
    font-size: 0.85em;
    word-break: break-all;
  }
}
  • A closed <details> always expands. Printed callouts and file tree directories therefore show all content.

Custom print styling goes in a @media print block in assets/scss/_styles_project.scss and needs no template change.

Replacing the print templates

To add a running header or change numbering, override the partial with the smallest relevant scope. Print partials are under layouts/_partials/print/:

Partial Responsibility
print/render.html The whole-section skeleton: notice bar, contents, recursive content
print/page-heading.html The title and standfirst at the top of the document
print/content.html How one page appears inside the whole-section view
print/toc-li.html One row of the table of contents

The last three also support overrides per content type. Create print/page-heading-blog.html or print/content-book.html. The theme gives the type-suffixed partial precedence.

Whole-book printing (type: book) uses a separate path. Chapter numbers, figure numbers, and cross-references remain continuous across the book. See Books.

Verify

In your site’s root, build and check the print output for a section where you enabled print. Replace the example path with that section’s actual output, including its language prefix if applicable:

hugo --printPathWarnings
PRINT_PAGE=public/_print/docs/index.html
test -f "$PRINT_PAGE"

Then open that section’s Print the whole section action on the running site:

  • Confirm the print view contains the section’s pages, excluding no_print: true pages.
  • Press Cmd/Ctrl+P. Confirm that print preview shows no notice bar, navbar, or buttons.
  • Use a page with tabs and a collapsed callout. Confirm that every panel expands in preview. See Tabs for authoring syntax.
  • Print a PDF and check pagination. If necessary, adjust section_break_wordcount.

5.14 - AI-agent support

Publish a .md version of each page and an llms.txt site index. Let readers open the current page in ChatGPT or Claude.

An HTML page includes a sidebar, scripts, and styles. A model must remove that shell before it reads the content. OINK can emit the same content as plain Markdown. It provides one .md per page, one llms.txt index at the site root, and a “copy as Markdown” button on the page. All three use build-time artifacts. They require no runtime service or content negotiation.

The site must declare all three under outputs. The theme does not enable them. Two other optional artifacts help an agent read more than one page: a full-text bundle per section and a navigation tree per language.

A .md per page

markdown is one of Hugo’s built-in output formats. Add it to the page kinds that need it:

hugo.yml
outputs:
  home: [HTML, markdown, LLMS]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]

This site uses the configuration above. Each key under outputs is a wholesale replacement, not a merge. When you add markdown, include every format that the page kind already used (RSS, print). If you omit a format, you lose that output.

The URL rule is the page URL plus index.md:

Page Markdown
/docs/customize/agents/ /docs/customize/agents/index.md
/docs/customize/ (section index) /docs/customize/index.md
/ (site home) /index.md

Each HTML page’s <head> includes a discovery link. A crawler therefore does not need to guess the URL:

<link rel="alternate" type="text/markdown" href="https://oink.pgsty.com/docs/customize/agents/index.md">

What the .md contains

This output contains the source you wrote. It is not a conversion of rendered HTML. Front matter becomes an H1 and a blockquoted summary. The body follows verbatim. The theme expands shortcodes in place into their Markdown forms.

the start of /docs/customize/print/index.md
# Print

> A single page goes to the browser's Cmd/Ctrl+P; a whole section becomes one continuous document through the print output format.

---

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

---

Printing one page needs no configuration: the shell (sidebar, outline, navbar,
buttons) all carries `d-print-none`, so the browser's `Cmd/Ctrl+P` yields a
clean body.

Native Markdown components retain their source in the .md. These include callouts, tables, field lists, image attribute lines, code fences, and data fences. The model reads the source you wrote. A section index also appends a Section pages: list of child links after the body.

Each shortcode has a defined Markdown fallback. A badge becomes emphasized text or a link. A key becomes Ctrl + K. Tabs become consecutive **Label** subsections. Fields become an item list. Each component page describes its fallback in the Output section.

If the site has not enabled LLMS output, the LLMS index: line does not appear. The theme never links to a file that it did not publish.

llms.txt

llms.txt is a plain-text manifest at the site root. It identifies site content and its machine-readable versions. To generate it, add the LLMS output format to the home page:

hugo.yml
outputs:
  home: [HTML, markdown, LLMS]

A multilingual site gets one per language: /llms.txt and /zh/llms.txt. The content is a generated site index:

/llms.txt (excerpt)
# OINK

> A local-first, Hugo-only theme for technical documentation

## Site index

- [Home page](https://oink.pgsty.com/index.md)
- [Docs](https://oink.pgsty.com/docs/index.md): OINK is a local-first Hugo documentation framework…
- [Blog](https://oink.pgsty.com/blog/index.md): Docsy articles, OINK engineering stories, and OINK release notes

## Documentation index

- [Introduction](https://oink.pgsty.com/docs/about/index.md): A local-first Hugo documentation framework evolved from Docsy…
  - [Highlights](https://oink.pgsty.com/docs/about/features/index.md): What separates OINK from an ordinary Hugo theme…
  - [Cases](https://oink.pgsty.com/docs/about/showcase/index.md): Find the production case closest to your site…
- [Get started](https://oink.pgsty.com/docs/start/index.md): Start from the official OINK Starter, establish a local baseline, and customize it in layers.
…

## Site locales

- [English](https://oink.pgsty.com/index.md)
- [简体中文](https://oink.pgsty.com/zh/index.md)

The Site index contains this language’s home page and the site’s main menu (menus.main). It links to an entry’s Markdown version when available. It includes description when present. The Documentation index contains the docs subsections and the level of pages beneath them. It indents entries by level and includes each page’s description. The Site locales section lists every configured language. The index omits off-site menu entries, such as GitHub or an issue tracker. These entries belong to navigation, not site content.

To improve llms.txt, change the main menu and each section index’s description. You do not need to change the template.

Full-text bundle

One .md per page suits an agent that knows which page it needs. To read the whole manual, an agent would otherwise crawl each page. The LLMSFULL output provides one llms-full.txt per top-level section. It contains every included page in reading order. OINK 0.8.0 introduced this output. It remains off until a section enables it.

The switch is the section index’s own front matter rather than the site configuration:

content/docs/_index.md
---
title: Docs
outputs: [HTML, print, RSS, markdown, LLMSFULL]
---

Front matter outputs replaces the page’s site-level list. Include the formats that the section already used. If you omit markdown or print, the section index loses those outputs. Front matter applies per language. For a bilingual site, repeat the line in _index.zh.md to generate the Chinese bundle.

The output contains one file per language at the section root: /docs/llms-full.txt and /zh/docs/llms-full.txt. It uses the sidebar and pager reading order. For a docs or book section with an explicit data/docs_nav.json tree, it uses that tree. Otherwise, it uses the weighted content tree. Pages excluded from the sidebar with toc_hide are also excluded from the bundle.

Each page is introduced by a separator carrying its source URL, and the body that follows is byte-identical to that page’s own .md:

/docs/llms-full.txt (excerpt)
================
Source: https://oink.pgsty.com/docs/customize/print/index.md
================

# Print

> A single page goes to the browser's Cmd/Ctrl+P; a whole section becomes one continuous document through the print output format.
…

================
Source: https://oink.pgsty.com/docs/customize/agents/index.md
================

# AI-agent support
…

Source: points at the page’s Markdown output, falling back to its HTML URL where the page publishes no .md.

Only a top-level section can publish a bundle. If you list LLMSFULL further down the tree, the theme warns (“LLMSFULL output requires a top-level section”) and emits nothing.

If at least one section has a bundle, llms.txt includes a ## Full-text bundles list for that language. An agent can discover bundles in the file it already fetches.

This site’s docs section enables the bundle. https://oink.pgsty.com/docs/llms-full.txt contains the entire English documentation in one file.

Navigation JSON

The sidebar is the site’s table of contents. An agent can read it to plan which pages to fetch. The NAVJSON output publishes the tree as navigation.json, with one file per language at the language root. OINK 0.8.0 introduced this output. It is off by default. Enable it for the home page:

hugo.yml
outputs:
  home: [HTML, markdown, LLMS, NAVJSON]

This produces /navigation.json and /zh/navigation.json. It uses the same tree as the sidebar and pager. For a docs or book section with an explicit data/docs_nav.json tree, it uses that tree. Elsewhere, it uses the weighted content tree:

/navigation.json (excerpt)
{
  "baseURL": "https://oink.pgsty.com/",
  "language": "en",
  "root": {
    "children": [
      {
        "children": [
          {
            "description": "Give every page a .md twin, the site root an llms.txt…",
            "id": "/docs/customize/agents/",
            "kind": "page",
            "markdown": "https://oink.pgsty.com/docs/customize/agents/index.md",
            "title": "AI-agent support",
            "url": "https://oink.pgsty.com/docs/customize/agents/"
          }
        ],
        "id": "/docs/",
        "kind": "section",
        "title": "Docs",
        "url": "https://oink.pgsty.com/docs/"
      }
    ],
    "id": "/",
    "kind": "home",
    "title": "OINK",
    "url": "https://oink.pgsty.com/"
  },
  "schemaVersion": 1
}
Key What it holds
id The page path without its language prefix. The same page has the same id in every language
url The absolute URL of this language’s HTML page
markdown The absolute URL of the page’s .md, present only where the page publishes one
title The navigation title (linkTitle, falling back to title)
description The page’s description, where it has one
kind home, section, or page for real pages. external or link for placeholders
children The ordered children, where the node has any

Array order is part of the contract. The theme never serializes weight because it has already applied the order. If a consumer sorts the array again, its order can differ from the source sidebar.

Placeholder rows retain the sidebar’s structure. A manual_link entry becomes an external node with the authored URL. A manual_link_relref entry becomes a link node with the resolved reference. Neither has page identity, an id, or a markdown URL. The tree omits sidebar dividers and pages that Hugo never renders. Their children remain in place.

The contract has a version. The value of schemaVersion is 1. The theme repository ships its JSON Schema as schema/nav.v1.schema.json. If you consume the file, validate it against this schema. If the site publishes it, llms.txt lists navigation.json for its language in the site index.

This site has it enabled: https://oink.pgsty.com/navigation.json is a live instance of the tree.

Agent actions on the page

Four entries in the action menu at the right of the breadcrumb row relate to agents:

Entry What it does When it appears
Copy as Markdown Copies this page’s .md to the clipboard. Prefetches on hover to avoid a perceptible wait after a click This page has a markdown output
View Markdown source Opens the .md in a new tab This page has a markdown output
Open in ChatGPT Opens ChatGPT with a prompt assistant_links: true
Open in Claude Opens Claude with a prompt assistant_links: true

The first two actions appear when markdown output is enabled. “Copy” is the left half of the split button, with the clipboard icon. After a successful copy, it briefly shows a check mark.

The last two are off by default and must be enabled explicitly:

hugo.yml
params:
  ui:
    page_context_menu:
      enable: true
      assistant_links: true

When the reader clicks an enabled assistant action, the runtime creates a prompt from the address bar’s full URL. It includes the real domain, query string, and anchor. In English, the prompt is “Please read the contents of so that I can ask you about it.” The action then opens the other site. Only the URL leaves this site. The body is never uploaded. The other site fetches the content itself. Do not put confidential information in a URL. Document this third-party boundary in the site’s privacy statement.

A page may restrict the site policy but cannot reverse it. The front matter page_context_menu: { assistant_links: false } disables assistant links for that page. If the site has not enabled them, setting true has no effect. To disable the entire menu for a page, use page_context_menu: false. See Page parameters.

Both assistant actions are also searchable in the command palette, from the same action manifest — see Command palette.

Opting a page out of .md output

Replace outputs in the page’s front matter. This replaces the complete list. Include only the formats that you want to keep:

content/legal/terms.md
---
title: Terms of service
outputs: [HTML]
---

To keep RSS and remove only Markdown, list the remaining formats:

content/blog/_index.md
---
title: Blog
outputs: [HTML, RSS, print]
---

Customizing the output

The theme renders Markdown output with layouts/all.md and generates llms.txt with layouts/index.llms.txt. The two optional formats use layouts/list.llmsfull.txt and layouts/index.navjson.json. To replace a complete template, put a file with the same name under the site’s layouts/. First consider an override with a smaller scope:

  • Per content type: a typed path such as layouts/blog/single.md or layouts/docs/list.md affects only that content kind. The theme specializes its print templates this way (layouts/blog/single.print.html). Check the template lookup order for your combination.
  • Per shortcode: a site shortcode can use an output-format-specific template. This can provide a more machine-readable form in Markdown output.
  • Per page: for a few high-value pages, writing the output by hand requires less work than changing a template.

The content of llms.txt follows the site’s structure. Before changing the template, check the main menu and each description for the problem. If you replace index.navjson.json, you also take responsibility for the nav.v1 contract. Your output must still satisfy schema/nav.v1.schema.json for consumers that validate it.

Verify

Run from your site’s root, with Markdown and LLMS outputs enabled. Replace PAGE_MD with an actual page’s generated Markdown file. The section paths below assume a docs section; adjust them and any language prefix to your site.

hugo --printPathWarnings
PAGE_MD=public/docs/getting-started/index.md
ls "$PAGE_MD" public/llms.txt
# Only if you enabled LLMSFULL on docs and NAVJSON on home:
ls public/docs/llms-full.txt public/navigation.json

Against a running local preview or production, use your own base URL and page path. BASE_URL includes the deployment subpath and, when testing a translated site, the language prefix:

BASE_URL=https://your-site.example/
PAGE_PATH=docs/getting-started/index.md
curl -fsS "${BASE_URL}${PAGE_PATH}" | head -5
curl -fsSI "${BASE_URL}llms.txt" | head -3
# Only if you enabled LLMSFULL on docs:
curl -fsS "${BASE_URL}docs/llms-full.txt" | head -3

Then check four things:

  • Confirm that the selected page’s HTML <head> has rel="alternate" type="text/markdown";
  • Click the copy button beside the title and paste the result. Confirm that it is Markdown instead of HTML;
  • llms.txt contains no off-site links;
  • If you enabled these outputs, confirm that each page in llms-full.txt starts with a Source: line. The same page must have the same id in each language’s navigation.json.

Limits

  • The theme produces four types of machine-readable build-time files: a .md per page, llms.txt, optional llms-full.txt per top-level section, and optional navigation.json per language. Hugo still owns sitemap.xml.
  • A bundle belongs to a top-level section. There is no whole-site llms-full.txt. To read everything, an agent reads the section bundles listed in llms.txt.
  • LLMS, LLMSFULL, and NAVJSON are non-alternative formats. They have no <head> alternate links or page actions. Agents discover them through conventional paths and their entries in llms.txt.
  • Server-side content negotiation (one URL returning Markdown for Accept: text/markdown) is outside the theme’s scope and belongs to the hosting layer.
  • Markdown output uses the source. Content generated only by browser JavaScript, such as a runtime-drawn chart, appears in the .md as fence source. It does not appear as a diagram.

6 - Operations

Preview, deploy, and maintain a site. Configure comments, analytics, and SEO. Upgrade the theme and diagnose problems.

Use this section after you write the content. Preview the site locally, build and deploy the output, configure comments and analytics, track theme versions, and diagnose problems. The previous five sections describe the site’s appearance and content. This section describes its build, deployment, and diagnosis.

Find it by task

What you want to do Where to go
See a change on your own machine Local preview
Build a deployable public/ Local preview
Deploy to GitHub Pages / Cloudflare / Netlify Deploy
Deploy to a subpath such as example.com/docs/ Deploy
Let readers comment at the end of a page Comments
Connect Google Analytics or a self-hosted alternative Analytics and SEO
Configure correct search engine indexing Analytics and SEO
Upgrade the theme, or migrate from Docsy or 0.4 Upgrade
Diagnose a build error, missing search results, or a 404 Troubleshooting

6.1 - Local preview

Preview changes with hugo server and build deployable output with hugo. Neither Node nor a CDN is required.

Use hugo server to preview local changes. Use hugo to produce a public/ directory that you can deploy to any static host. Install Hugo Extended (0.168.0 or newer) on the machine. If you install the theme as a Hugo Module, also install Go. The build does not require Node.js, npm, or PostCSS. Only this repository’s regression checks use those tools.

The preview server

From the site root (the directory holding hugo.yml):

Terminal
hugo server

Open http://localhost:1313/. When you save a file, Hugo rebuilds the site and refreshes the browser. Switching Git branches also triggers a rebuild. The first start is slower. For a Hugo Module, Hugo downloads the module through Go into its cache. Later starts read the cache.

The switches worth knowing

-D / --buildDrafts , defaultoff
Also builds pages with draft: true
-F / --buildFuture , defaultoff
Also builds pages whose date / publishDate is in the future
-E / --buildExpired , defaultoff
Also builds pages whose expiryDate has passed
--disableFastRender , defaultoff
Re-renders the complete site on every change instead of using incremental rendering
-M / --renderToMemory , defaultoff (writes to disk)
Renders only in memory. Writes no public/
-N / --navigateToChanged , defaultoff
The browser opens the page you saved
--bind , default127.0.0.1
The listen address. Use 0.0.0.0 for access from a LAN or outside a container
-p / --port , default1313
The listen port
--minify , defaultoff
Minifies the preview too, to reproduce production rendering
--printPathWarnings , defaultoff
Warns when two pages write to the same target path

The combination used while developing this site:

Terminal
hugo server -DFE \
  --disableFastRender --renderToMemory --minify \
  --printPathWarnings --logLevel info

-DFE means -D -F -E. It builds drafts, future pages, and expired pages together. Newly created pages are therefore visible while you write them.

A change that did not take effect

Hugo enables fast render by default. It rebuilds only the output that it identifies as affected. Changes to layouts, configuration, data/, or a file used by include can escape detection. If the page appears unchanged, follow these three steps:

  1. Restart with --disableFastRender. Check whether the change appears.
  2. Force a browser refresh (Cmd/Ctrl + Shift + R) to exclude browser cache as the cause.
  3. If the change still does not appear, clear the caches and restart.

Reaching it from another device

hugo server listens on 127.0.0.1 only, so no other device can reach it. To preview on a phone or another machine:

Terminal
hugo server --bind 0.0.0.0 --port 1313 --baseURL http://192.168.1.10:1313/

Set --baseURL to an address that the other device can reach. Otherwise, the page opens, but resources with absolute paths point to localhost. These resources include CSS and the search index.

Production build

Build deployable output with hugo, not hugo server:

Terminal
hugo --gc --minify --printPathWarnings

Hugo writes the output to public/. You can deploy it independently of the source tree. The four switches have these functions:

--gc
Clears cached resources in resources/_gen that are no longer referenced
--minify
Minifies the HTML, CSS, JS and XML output
--printPathWarnings
Warns when two pages use the same output path. This is the most common silent error on multilingual sites

Use Hugo Extended 0.168.0 and leave warnings visible. Do not enable --panicOnWarning or HUGO_PANICONWARNING: Hugo’s upstream LibSass deprecation warning is accepted. Invalid giscus settings or unsupported theme values also warn and fall back; review these diagnostics and the rendered output before publication.

This site’s CI uses the production command above, with local module replacements and workspaces disabled for published-pin acceptance.

baseURL and the build environment

baseURL lives in hugo.yml and can be overridden on the command line:

hugo.yml
baseURL: https://oink.pgsty.com
Terminal
hugo --minify --baseURL "https://example.com/docs/"

When deploying to a subpath, --baseURL must include that path segment; the details are in Deploy.

Select the build environment with -e / --environment. The default is production for hugo and development for hugo server. This choice has three visible effects in OINK:

  • Only production emits <meta name="robots" content="index, follow">. Other environments emit noindex, nofollow.
  • In production, robots.txt contains Allow: /. In other environments, it contains Disallow: /.
  • Only production renders Hugo’s Google Analytics template, and only there are static assets fingerprinted with SRI.

For preview deployments, including PR previews and staging, use a non-production environment. The output automatically disables analytics and requests no indexing:

Terminal
hugo --minify --environment staging --baseURL "$PREVIEW_URL"

Previewing in a container

A container is optional. Use one if your team needs a pinned toolchain version or does not want to install Hugo on every developer’s machine.

Dockerfile
FROM debian:bookworm-slim

ARG HUGO_VERSION=0.168.0
ARG GO_VERSION=1.27.3
ARG TARGETARCH

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl git \
    && curl --fail --location -o /tmp/hugo.deb \
      "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.deb" \
    && apt-get install -y /tmp/hugo.deb \
    && curl --fail --location -o /tmp/go.tgz \
      "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" \
    && tar -C /usr/local -xzf /tmp/go.tgz \
    && rm -rf /var/lib/apt/lists/* /tmp/hugo.deb /tmp/go.tgz

RUN case "${TARGETARCH}" in \
      amd64) sass_arch=x64; sass_sha=a530ee08d5792c80bdb9d9614b45fb58219c890cb42a280a80775f0af9c29582 ;; \
      arm64) sass_arch=arm64; sass_sha=40c7a1946f654163daab34d3824cf572d2e72cc48a2efcf9ca0a33140007d374 ;; \
      *) exit 1 ;; \
    esac \
    && curl --fail --location -o /tmp/sass.tgz \
      "https://github.com/sass/dart-sass/releases/download/1.105.2/dart-sass-1.105.2-linux-${sass_arch}.tar.gz" \
    && echo "${sass_sha}  /tmp/sass.tgz" | sha256sum --check \
    && tar -C /opt -xzf /tmp/sass.tgz \
    && rm /tmp/sass.tgz

ENV PATH="/usr/local/go/bin:/opt/dart-sass:${PATH}"
WORKDIR /src
EXPOSE 1313
ENTRYPOINT ["hugo"]
CMD ["server", "--bind", "0.0.0.0", "--disableFastRender"]
Terminal
docker build -t oink-hugo .

# preview: mount the site source, and the Go module cache with it
docker run --rm -it -p 1313:1313 \
  -v "$PWD:/src" \
  -v "$HOME/go/pkg/mod:/root/go/pkg/mod" \
  oink-hugo

# production build: override the default server command
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/src" \
  oink-hugo --gc --minify

The image includes Go because Hugo needs it to resolve and download a Hugo Module. If the site uses a submodule, offline archive, or plain clone, you can remove Go. This makes the image much smaller.

Do not let root write public/

A container process runs as root by default. The generated public/ belongs to root, so the host cannot delete it. In a shared environment, map the user ID with --user "$(id -u):$(id -g)". The production build command above already does this.

The image requires no Node.js, npm, or PostCSS. It should not fetch remote browser assets. For a network-isolated environment, mirror the base image and the Hugo and Go packages in advance.

Clearing caches

Hugo stores intermediate output in three places. Clear the smallest scope first:

public/ , ContentsThe previous build’s output
A deleted page remains live. You can also let the build clear this directory with hugo --cleanDestinationDir
resources/_gen/ , ContentsProcessed images and compiled CSS
Image processing parameters, fonts, or the accent color changed, but the page still shows the old appearance
hugo mod clean , ContentsThe Hugo Module cache
The theme version changed, but the old version still resolves. Add --all to clear the complete module cache
Terminal
rm -rf public resources/_gen
hugo mod clean          # only the modules this project uses
hugo mod clean --all    # the whole module cache; the next build downloads again

Add both public/ and resources/ to .gitignore. Never commit generated output.

Working on the theme alongside

If you change the theme and site together, temporarily select a local checkout with HUGO_MODULE_REPLACEMENTS. Leave go.mod unchanged:

Terminal
HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> /absolute/path/to/oink' hugo server

This site’s Makefile wraps those commands and expects the theme checkout at the sibling ../oink:

Makefile targets
make dev     # development server with ../oink substituted
make check   # full regression suite (npm test) with ../oink substituted
make build   # build with the version in go.mod
make serve   # preview server with the production configuration
Local overrides can mask the published version

CI and production builds can still inherit replacements or workspaces. Do not commit a workspace containing developer-machine paths. To verify a published tag, unset HUGO_MODULE_REPLACEMENTS and set both GOWORK=off and HUGO_MODULE_WORKSPACE=off. Also inspect persistent replacements in go.mod and Hugo configuration, and any _vendor/ copy; confirm the exact version in hugo mod graph under the same environment before building.

Verifying an offline build

For acceptance in a network-isolated environment, check both the build and the browser. Follow these six steps:

  1. Start from a verified theme archive and an empty module cache (hugo mod clean --all).
  2. Block outbound HTTP, HTTPS and the Go module proxy.
  3. Run the production build hugo --gc --minify --printPathWarnings.
  4. Browse pages in both languages: a documentation page, a blog page, the home page, the 404.
  5. Exercise search, the light/dark toggle, diagrams and content components.
  6. Check subresource origins and confirm there is no unexpected remote host.

For the last step, use the output checker from a theme checkout matching your pinned release. It needs Python 3, not this documentation site’s test framework. Replace both absolute paths below and use the same base URL as the build:

Terminal
python3 /path/to/oink/bin/check-output-security.py \
  --public /path/to/my-site/public --base-url https://docs.internal.example.com/

The script scans every href / src / srcset / poster and form action in all four outputs. Each value must be a site-relative path or use http / https / mailto / tel. The script rejects inline on* handlers and javascript: URLs. An <iframe>, <script>, <link>, <img>, <video>, <audio>, <embed>, <object>, or <source> that points to another host is an error. If the site intentionally embeds third-party content, use --third-party to permit it. For a multi-domain language configuration, add first-party hosts with --allow-host.

A passing check validates that commit in that environment. Repeat it for every theme candidate and after every bundled-dependency update.

Verify

A clean production build should look like this:

Terminal
rm -rf public resources/_gen
hugo --gc --minify --printPathWarnings

The build passes when it reports Total in … with no ERROR. Then check the following items:

  • Confirm that the log has no npm, PostCSS, Autoprefixer, or browser-asset download step. Such a step indicates upstream Docsy configuration.
  • public/ has sitemap.xml and robots.txt, and robots.txt reads Allow: /.
  • If local search is enabled, confirm that each language has an index in public/. Production filenames are offline-search-index.<language>.<hash>.json. Development filenames omit the hash. Open search and confirm that the actual data-td-index-src URL returns 200.
  • With hugo server, open one documentation page, one blog page, the home page, and the 404. Check both languages and both color schemes.

For a failing build or a wrong result, see Troubleshooting.

6.2 - Deploy

Publish public/ to GitHub Pages, Cloudflare Pages, or another static host. Set baseURL, review Content Security Policy, verify deployment, and prepare rollback.

An OINK site produces a plain static directory. You can deploy it to any host that serves static files. It requires no Node runtime, server-side rendering, or build plugin. On the host, run one command with the correct Hugo version. Publish public/ and set baseURL to match the final address.

First run a local production build with Hugo Extended 0.168.0 (using its built-in LibSass). Do not enable --panicOnWarning or HUGO_PANICONWARNING; keep warnings visible and review them (the upstream LibSass deprecation warning is expected).

Getting baseURL right

An incorrect baseURL is the most common source of failure. The page can open without an obvious error. However, the search index 404s, page action links use incorrect addresses, and some assets do not load.

For deployment at a domain root, use:

hugo.yml
baseURL: https://oink.pgsty.com

For deployment to a subpath (https://example.com/docs/), include that path in baseURL:

hugo.yml
baseURL: https://example.com/docs/

You can also override this value at build time. This lets you deploy one source to several locations:

Terminal
hugo --gc --minify --baseURL "https://example.com/docs/"
Do not fix a subpath with canonifyURLs

Keep Hugo’s default value false for canonifyURLs. OINK’s templates and content links resolve against baseURL. If the path is incorrect, correct baseURL. Enabling canonifyURLs rewrites relative links that were already correct. This makes the problem harder to diagnose.

If local search is enabled, open search. Inspect its index request in the browser’s Network panel. The request must use the correct language and deployment subpath, and return 200. Read the actual URL from the page’s data-td-index-src attribute. Production filenames are offline-search-index.<language>.<hash>.json. Development filenames have no hash. Do not guess the filename.

Choosing a host

If the source is on GitHub, one Actions workflow is sufficient. Actions builds the site and publishes the output through the Pages deployment API. You do not need a gh-pages branch. OINK Starter includes the file below. Copy it only if you assemble a site manually.

.github/workflows/github-pages.yaml
 1name: Deploy to GitHub Pages
 2
 3on:
 4  push:
 5    branches: [main]
 6  workflow_dispatch:
 7
 8permissions:
 9  contents: read
10  pages: write
11  id-token: write
12
13concurrency:
14  group: github-pages
15  cancel-in-progress: false
16
17env:
18  GO_VERSION: stable
19  HUGO_VERSION: "0.168.0"
20  # a workspace from a sibling checkout must never take part in a CI build
21  GOWORK: off
22  HUGO_MODULE_WORKSPACE: off
23  HUGO_CACHEDIR: ${{ github.workspace }}/.hugo_cache
24
25jobs:
26  build:
27    name: Build Pages artifact
28    runs-on: ubuntu-latest
29    steps:
30      - name: Check out source
31        uses: actions/checkout@v7
32        with:
33          fetch-depth: 0
34
35      - name: Set up Go
36        uses: actions/setup-go@v7
37        with:
38          go-version: ${{ env.GO_VERSION }}
39          check-latest: true
40          cache-dependency-path: go.sum
41
42      - name: Configure GitHub Pages
43        id: pages
44        uses: actions/configure-pages@v6
45
46      - name: Install Hugo Extended
47        uses: peaceiris/actions-hugo@v3
48        with:
49          hugo-version: ${{ env.HUGO_VERSION }}
50          extended: true
51
52      - name: Record build tool versions
53        run: |
54          go version
55          hugo version
56
57      - name: Download OINK
58        run: go mod download github.com/pgsty/oink
59
60      - name: Build
61        run: |
62          hugo --cleanDestinationDir --gc --minify --environment production \
63            --printPathWarnings \
64            --baseURL "${{ steps.pages.outputs.base_url }}/"
65
66      - name: Upload Pages artifact
67        uses: actions/upload-pages-artifact@v5
68        with:
69          path: public
70
71  deploy:
72    name: Deploy
73    environment:
74      name: github-pages
75      url: ${{ steps.deployment.outputs.page_url }}
76    runs-on: ubuntu-latest
77    needs: build
78    steps:
79      - name: Publish
80        id: deployment
81        uses: actions/deploy-pages@v5

OINK Starter ships this workflow. The following settings are required:

  • fetch-depth: 0 — if enableGitInfo is enabled, “last modified” and contributor information require the complete Git history. A shallow clone leaves them empty.
  • setup-go plus go mod download — Hugo needs Go to resolve the theme as a Hugo Module. For a submodule installation, use submodules: recursive instead. For an offline archive, commit themes/oink/. With either alternative, remove both Go steps.
  • GOWORK: off and HUGO_MODULE_WORKSPACE: off — exclude a local development go.work from the CI build. CI then verifies the published tag pinned in go.mod.
  • --baseURL "${{ steps.pages.outputs.base_url }}/" — a project site’s URL is https://<OWNER>.github.io/<REPO>/. Use the value computed by configure-pages instead of a hard-coded address. Warnings remain visible for review. Do not enable --panicOnWarning or HUGO_PANICONWARNING; the upstream LibSass deprecation warning is accepted.

In the repository, set Settings → Pages → Build and deployment → Source to GitHub Actions. Push to main. Check the first run on the Actions tab.

For a custom domain, enter it in the Custom domain field on the same settings page. Configure DNS as prompted. Then set baseURL in hugo.yaml to that domain. If the publishing process requires a CNAME file, put it at static/CNAME. Hugo copies it unchanged into public/.

OINK Starter ships .github/workflows/cloudflare-pages.yaml, a Direct Upload workflow. The production build stays in GitHub Actions and Wrangler uploads the same public/ artifact to a Cloudflare Pages project.

  1. Create a Direct Upload Pages project. Its name defaults to the repository name. To override it, set repository variable CLOUDFLARE_PROJECT_NAME.
  2. Add repository secrets CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN. The token needs Account → Cloudflare Pages → Edit.
  3. Run Deploy to Cloudflare Pages manually once. To deploy every push to main, set repository variable CLOUDFLARE_PAGES_ENABLED=true.
  4. The canonical URL defaults to https://<project>.pages.dev/. Set CLOUDFLARE_SITE_URL when a custom domain becomes production.

The workflow pins Hugo Extended 0.168.0, reads Go from go.mod, disables local module workspaces, and builds with Hugo Extended 0.168.0 and visible warnings before upload. It is the recommended reproducible path for Starter users.

You can also use Cloudflare Git integration as a separate mode. Set the build command to hugo --gc --minify --printPathWarnings, the output directory to public, Hugo to 0.168.0, and Go to 1.27. For one project, use either Git integration or the Direct Upload workflow, and build preview deployments with their own preview URL and a non-production environment so they remain unindexed.

Netlify — build command hugo --gc --minify, publish directory public, environment variable HUGO_VERSION. The same settings can live in the repository:

netlify.toml
[build]
command = "hugo --gc --minify --printPathWarnings "
publish = "public"

[build.environment]
HUGO_VERSION = "0.168.0"

If the theme is a submodule, enable recursive submodule checkout. For a Hugo Module, install Git and Go in the build environment. Use the same Hugo version for production and preview, unless the preview tests an upgrade.

Vercel — set the build command to hugo --gc --minify, the output directory to public, and environment variable HUGO_VERSION. No npm install is required.

Any static server (Nginx / Caddy) — serve an unchanged copy of public/. The example below uses a current symlink to a release directory; create it with the offline packaging steps below. Set the host’s document root to that link when configuring Nginx or Caddy:

/etc/nginx/conf.d/docs.conf
server {
    listen 80;
    server_name docs.example.com;
    root /var/www/oink/current;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    error_page 404 /404.html;
}

The site is purely static; there is no path to forward to an application server.

Object storage — Hugo has a deploy command; put the target in the configuration:

hugo.yml
deployment:
  targets:
    - name: aws
      URL: 's3://www.your-domain.tld'
      cloudFrontDistributionID: E9RZ8T1EXAMPLEID

After a build, run hugo deploy. It compares the remote output with public/ and uploads only changed files. If you supply cloudFrontDistributionID, it also invalidates the CDN cache. Without --target, it uses the first target. Use --dryRun first to inspect the proposed changes.

Two prerequisites apply. The Hugo binary must be built withdeploy, as shown by hugo version. Supply cloud credentials through the standard environment variables or configuration file. On AWS, first confirm access with aws s3 ls.

Offline packaging — for a network-isolated environment, build on a connected machine. Transfer the output as one package. Choose your archive path and use a new release name for every artifact. On the Linux host, you need write access to /var/www/oink and GNU mv. The path current must be absent or a symlink. The path current.next must not already exist. Configure Nginx or Caddy to serve current, as shown above. Configure this on the host, not in Hugo.

Terminal
hugo --cleanDestinationDir --gc --minify --baseURL "https://docs.internal.example.com/" &&
  tar -czf oink-site-20260929-01.tar.gz -C public .

After the build and packaging succeed, transfer the archive to the Linux host. Run the following commands there. Use a new release name for every artifact:

Linux host
release_dir=/var/www/oink/releases/20260929-01
mkdir -p /var/www/oink/releases
mkdir "$release_dir" &&
  tar -xzf oink-site-20260929-01.tar.gz -C "$release_dir" &&
  test -f "$release_dir/index.html" &&
  ln -s "$release_dir" /var/www/oink/current.next &&
  mv -Tf /var/www/oink/current.next /var/www/oink/current

The command chain switches current only after it creates a new directory, extracts the archive successfully, and confirms that index.html exists. Then check the deployed pages. Retain the previous release directory for rollback. Never unpack a release over an existing release directory.

Use the target environment’s baseURL for the initial build. If you change it, build again.

A host without Go — the Hugo Module method requires Go in the build environment. If the platform does not provide Go, use a Git submodule or an offline archive. For a submodule, run git submodule update --init before the build. For an archive, commit themes/oink/. See From scratch and other install methods.

Keeping preview deployments unindexed

Hugo’s -e / --environment selects build-time behavior. It does not change site content. Three OINK outputs depend on this setting. Only production emits <meta name="robots" content="index, follow">, writes Allow: / in robots.txt, and renders the Google Analytics template. Do not build PR previews or staging with --environment production:

Terminal
hugo --gc --minify --environment staging --baseURL "$PREVIEW_URL"

The output then carries noindex, nofollow and Disallow: /, and reports nothing to an analytics service.

Content Security Policy

The bundled runtimes, fonts, and icons are same-origin assets. However, script-src 'self'; style-src 'self' alone does not cover a normal OINK page. The theme emits inline theme initialization and shell prepaint scripts. It also emits inline styles for the initial canvas, theme colors, and font roles. Some components have style attributes. Markmap adds inline configuration and styles.

The theme provides no general policy, automatic CSP hashes, or nonce injection. The deployment must define its policy from the actual built output.

Additional features change the directives needed:

  • Inline HTML and inline scripts written by authors, which are the author’s responsibility under renderer.unsafe: true.
  • ECharts $fn: callbacks: the site registers callback functions on window.OinkEchartsFunctions. Include the registering script’s origin in script-src.
  • Analytics scripts: the script the site inserts, and the destination it reports to.
  • Local PlantUML rendering in OINK 1.4.0 needs same-origin scripts, wasm-unsafe-eval in script-src for WebAssembly compilation, and blob: in img-src. It needs no rendering-service origin.
  • Remote API specifications and self-hosted diagram services: these require permissions in connect-src and img-src.
  • giscus: script-src and frame-src must both permit it.
  • Duo view: panes are same-origin frames, so frame-src (or child-src) must allow 'self', and responses must allow same-origin framing (no X-Frame-Options: DENY or frame-ancestors 'none'). Enabling it adds one inline bootstrap script to every HTML page, which changes the script hashes.

Hash each permitted inline script/style block from the final deployed bytes, or have the host inject a fresh nonce into the response policy and corresponding tags. Because a <style> nonce does not authorize style="..." attributes, handle inline style attributes separately under style-src-attr. Recheck hashes after changes to content, configuration, minification, or the theme. Start with Content-Security-Policy-Report-Only and verify light/dark startup, shell state, menus, and every enabled component in the browser before enforcing the policy.

Start with a minimal policy that covers only reviewed features. Add permissions one at a time. If ECharts needs no callback, keep its options as pure data. Review inline scripts written by authors. Add a remote origin only for an integration that the site explicitly enabled. First, you can scan output subresource origins with the script in Verifying an offline build.

Acceptance checklist

After deployment, complete this table. The first four checks concern build-time output. Check the remaining items at the real URL.

A successful build with reviewed diagnostics
The build command carries --printPathWarnings and the log has Total in …; the upstream LibSass warning is accepted
baseURL is correct
<link rel="canonical"> in the page source points at the real production address, subpath included
Sitemap
<baseURL>/sitemap.xml resolves; a multilingual site has an index pointing at /en/sitemap.xml and /zh/sitemap.xml
robots
<baseURL>/robots.txt reads Allow: / with a Sitemap: line; a preview deployment should read Disallow: /
Search index
With local search enabled, the URL from data-td-index-src returns 200; production filenames contain a hash, and site search returns results
Markdown output
Appending index.md to any page URL returns plain text (where the site enabled markdown under outputs.page)
llms.txt
The primary and every enabled language root publish llms.txt where the site enabled LLMS under outputs.home
Enabled languages
Documentation, blog and home pages open in each, and switching language lands on the corresponding page rather than the home page
Appearance and interaction
The light/dark toggle, the print view and representative components (callouts, tabs, code block copy) all work
404
Visiting a path that does not exist shows the site’s own 404 page

The switches for sitemap.xml, robots.txt, .md and llms.txt are in Configuration, and the agent output details are in AI-agent support.

Rollback

To roll back a static site, republish the last known-good commit. Never edit production files by hand.

  • GitHub Pages: find the last successful Deploy to GitHub Pages run in Actions. Click Re-run all jobs. Alternatively, use git revert on the faulty commit and push again.
  • Cloudflare Pages / Netlify / Vercel: pick the last successful deployment from the list and use the platform’s Rollback / Publish deploy to make it production again.
  • A self-hosted static server: point current to the previous release directory from offline packaging. Do not extract the old archive over the new files. Paths added by the newer release would remain live.

On the Linux host, replace the example path with the retained known-good release. The same symlink and GNU mv prerequisites apply:

Terminal
previous_release=/var/www/oink/releases/20260928-01
test -d "$previous_release" &&
  ln -s "$previous_release" /var/www/oink/current.next &&
  mv -Tf /var/www/oink/current.next /var/www/oink/current

Check a representative old page at the public URL and confirm that a path added only by the rejected release is no longer served.

If a theme upgrade caused the problem, roll back the version pin in go.mod. See Upgrade.

  • Local preview — the full production build command, clearing caches and offline verification
  • Troubleshooting — 404s, empty search, platform-specific faults
  • Analytics and SEO — being indexed correctly after launch
  • Upgrade — upgrading the theme version and rolling back
  • Configuration — baseURL, outputs and the other site keys

6.3 - Comments

Use giscus for page comments through GitHub Discussions. Control comments for the site or a page, and match light and dark themes.

OINK uses giscus for comments. Each page maps to one GitHub Discussion. Readers sign in with a GitHub account to post comments. Maintainers moderate comments in GitHub Discussions. The theme has no comment backend of its own. It bundles only giscus.

You must use a public GitHub repository. Visitors cannot read a private repository’s Discussions.

This is one of the few features in the theme that makes an outbound request

A page with comments enabled loads a script and an iframe from https://giscus.app, which does not work in a network-isolated environment. Comments are off by default. They load only when you explicitly enable them. If the site has a privacy policy, document this external data boundary in it.

Preparing the GitHub repository

  1. Choose a public repository for the comment threads. You can use the site’s source repository.

  2. In the repository’s Settings → General → Features, select Discussions.

  3. Install the giscus GitHub App for that repository. Without the App, visitors cannot comment or react.

  4. Choose a Discussion category. giscus recommends the Announcements type. Only maintainers and the giscus bot can open a Discussion there. Readers cannot start one accidentally.

The repository ID and category ID are public identifiers, not credentials. Never put a personal access token, an OAuth secret or a password in Hugo configuration.

Generating the configuration

Open giscus.app. Enter the repository, mapping, and category. The page generates a <script> block below. Copy four attributes from that block to the OINK configuration:

data-repo
repo
data-repo-id
repoId
data-category
category
data-category-id
categoryId

The mapping determines which Discussion corresponds to each page. OINK defaults to pathname. This suits stable published paths and one repository that serves several domains or preview environments.

If you change mapping or move a page after comments exist, giscus searches for a different Discussion. It does not delete existing comments, but the page can no longer find them. Choose the mapping before launch. If a URL must change, retain a redirect or rename the Discussion at the same time.

Enabling it site-wide

Copy all four repository/category fields from the configuration generated for your repository. Before you enable comments, replace every placeholder below:

hugo.yml
params:
  comments:
    enable: true
    type: giscus
    giscus:
      repo: YOUR_OWNER/YOUR_REPO
      repoId: YOUR_REPO_ID
      category: YOUR_CATEGORY
      categoryId: YOUR_CATEGORY_ID
      mapping: pathname
      inputPosition: bottom
      theme: auto
      loading: lazy

The required fields repo, repoId, category, and categoryId must describe the same repository and its selected Discussion category. If a field is missing or contains only whitespace, Hugo prints one WARNING and skips giscus. The build succeeds with a warning. Review production build diagnostics and correct missing values before publication.

Currently, type accepts only giscus. Any other value also produces a warning and skips giscus. The params.comments key names match Hextra’s. A configuration migrated from Hextra can therefore remain unchanged.

The remaining keys (strict, reactionsEnabled, emitMetadata, term, lang, lightTheme, darkTheme, ariaLabel, errorMessage) all have defaults, defined fully in Configuration. A feature switch takes either a YAML boolean or giscus-style 0 / 1.

Per-page control

The front matter comments value can enable or disable comments regardless of the site switch. The value nearest the page takes precedence.

To enable comments only on selected pages, disable the site switch. Keep the complete repository configuration. Then enable comments on each selected page:

content/blog/2026-roadmap.md
---
title: 2026 roadmap
comments: true
---

To disable comments on selected pages, keep the site switch enabled. Then disable comments on each selected page:

content/about/security.md
---
title: Security policy
comments: false
---

Use a cascade to configure an entire section. This site sets comments: true in the cascade of content/docs/_index.md. Therefore, a giscus section appears at the end of this page.

content/docs/_index.md
---
title: OINK Documentation
cascade:
  type: docs
  comments: true
---

If the site also configures services.disqus.shortname, active giscus suppresses Disqus. The setting comments: false disables both. If a required giscus key is missing, the theme warns and skips giscus. Disqus can then load.

Multilingual text

The giscus interface automatically uses the current Hugo language. Simplified, Traditional, and Hong Kong Traditional Chinese each use the corresponding giscus locale. Unsupported languages fall back to English. Set lang explicitly only if the automatic choice is incorrect.

Translate OINK’s two comment strings: the section’s accessible label and the loading-failure message. Configure them per language. The theme merges them with the global repository configuration:

hugo.yml
languages:
  en:
    params:
      comments:
        giscus:
          ariaLabel: Comments
          errorMessage: Comments could not be loaded. Please try again later.
  zh:
    params:
      comments:
        giscus:
          ariaLabel: 评论
          errorMessage: 评论加载失败,请稍后重试。

A language layer needs only the values that differ. Keep repo / repoId / category / categoryId in params.comments.

Following light and dark

With theme: auto, the giscus iframe follows OINK’s light/dark control and the browser’s prefers-color-scheme. The comment section changes with the page.

To match the site’s palette more closely, set two giscus themes with lightTheme / darkTheme. Each value can be a built-in giscus theme name or CSS hosted by the site. This site uses hosted CSS:

hugo.yml
params:
  comments:
    giscus:
      theme: auto
      lightTheme: /css/giscus-oink-light.css?v=0.4.0
      darkTheme: /css/giscus-oink-dark.css?v=0.4.0

If you set a fixed theme name in theme, giscus no longer follows the toggle.

A custom giscus theme has to be readable cross-origin

The giscus iframe loads from giscus.app. CORS must permit it to read a CSS file from your site. For local preview, this site adds Access-Control-Allow-Origin: '*' under server.headers in hugo.yml. For production, configure this response header on the host.

Privacy and CSP

  • OINK never requests or stores a reader’s GitHub password or access token. All sign-in and posting operations occur on giscus / GitHub.
  • The theme ships the comment initialization script as a same-origin asset. It adds the script only to pages with comments enabled.
  • With loading: lazy, the iframe loads only as the reader scrolls near the comment section.
  • If the site has a strict Content Security Policy, both script-src and frame-src must permit giscus. Merge these permissions into the existing policy. Do not replace other directives. See Content Security Policy for the general rules:
CSP fragment
script-src 'self' https://giscus.app;
frame-src 'self' https://giscus.app;

If the external script fails to load or creates no iframe, OINK ends the loading state. It shows errorMessage in a live status region. The page does not remain on “loading”.

Verify

Terminal
hugo --minify # a missing required key warns and disables comments
hugo server --disableFastRender

Then check the following items:

  1. Open a page that should have comments. Confirm that giscus appears at the end with “Sign in with GitHub”. Its interface must use the current page’s language.
  2. Toggle OINK’s light/dark control. With theme: auto, confirm that the comment section follows it.
  3. Open a page with comments: false. Confirm that neither giscus nor another comment component appears.
  4. Post a test comment. Return to GitHub. Confirm that a Discussion appears in the selected category and that you can manage it there.

Before the first comment or reaction creates a Discussion, a browser console message saying the Discussion was not found is expected.

If comments do not work, check WARNINGs in the build log for the four required keys. Then check params.comments.enable and type, followed by the page’s comments front matter. Confirm that the repository is public, Discussions are enabled, and the giscus App is installed. Finally, check the browser console and response headers for a CSP block on giscus.app. If existing threads are missing, first restore the original mapping and page path.

6.4 - Analytics and SEO

Configure an optional analytics service. Check the canonical URLs, hreflang links, social cards, sitemap, and robots output that the theme generates.

By default, the theme loads no analytics, form, or advertising script. It makes no outbound request until you configure one. You must explicitly configure each integration. Document this external data boundary in the site’s privacy statement.

The theme automatically generates canonical links, hreflang links, the robots meta tag, Open Graph cards, and Twitter cards for each page. Set baseURL and each page’s description correctly.

Connecting Google Analytics

Use Hugo’s built-in service configuration. Replace G-YOUR_MEASUREMENT_ID with your own GA4 measurement ID before enabling this block:

hugo.yml
services:
  googleAnalytics:
    id: G-YOUR_MEASUREMENT_ID

The theme renders this script only in the production environment. A normal hugo server uses development and sends no analytics reports. A hugo build defaults to production, including on a preview host. For PR and staging deployments, select a non-production environment. Set PREVIEW_URL to the deployment’s actual address:

Terminal
hugo --environment staging --baseURL "$PREVIEW_URL"

See preview deployment settings.

Do not also set the deprecated top-level googleAnalytics key. If you do not want analytics, delete the block. Do not enter a fake ID.

This is incompatible with a network-isolated environment

When you configure this integration, it sends page views and events to Google. A strict same-origin Content Security Policy must also permit it. See Content Security Policy. The site must choose to enable this integration. The theme does not enable it by default.

Connecting another analytics service

To connect Plausible, Umami, Matomo, or a similar service, insert its script. The theme provides two injection points. Create a file with the corresponding name in the site repository. You do not need to change the theme:

layouts/_partials/hooks/head-end.html , Insertion pointBefore </head>, ahead of the Google Analytics template
Analytics scripts, cookie consent scripts, meta tags the theme does not provide
layouts/_partials/hooks/body-end.html , Insertion pointLast among the page scripts
Third-party code that affects interaction instead of the first paint

For Plausible, replace your-site.example with the domain registered in your own account before adding this hook:

layouts/_partials/hooks/head-end.html
{{ if hugo.IsProduction }}
<script defer data-domain="your-site.example"
        src="https://plausible.io/js/script.js"></script>
{{ end }}

Do not omit the hugo.IsProduction guard. Without it, each local preview sends reports to your analytics service.

head-end runs before Google Analytics

A cookie consent script must run before the analytics script. This order lets the consent script prevent analytics from running.

The “was this page helpful?” feedback widget is separate. It is off by default and makes no network request. Configure it as described in Repository links and page info.

Duo view panes

With params.ui.compare enabled, opening the duo view loads both pages inside same-origin frames. Theme-managed analytics (services.googleAnalytics.id and params.analytics.*) skips those panes automatically so opening the reader does not double-count page views. When params.ui.compare: true is active, OINK renders the Google Analytics snippet through its own pane-aware template rather than Hugo’s built-in partial, so a site override at layouts/_partials/google_analytics.html will not take effect.

Custom scripts in head-end.html still execute on every frame load. Guard them with window.OinkComparePane before recording a page view:

layouts/_partials/hooks/head-end.html
{{ if hugo.IsProduction }}
<script>
  if (!window.OinkComparePane) {
    var script = document.createElement('script');
    script.defer = true;
    script.dataset.domain = 'your-site.example';
    script.src = 'https://plausible.io/js/script.js';
    document.head.appendChild(script);
  }
</script>
{{ end }}

Server and CDN analytics also see pane requests. They carry the query oink_compare_pane=1 and can be filtered out.

Page descriptions

<meta name="description"> takes the first non-empty value of:

  1. The page’s description front matter
  2. The page summary Hugo computes (.Summary)
  3. params.description in the site configuration

Authors need only write one description per page for SEO. It supplies the search engine snippet, the card subtitle on a section index, and the result preview in site search.

content/docs/admin/analytics.md (this page)
---
title: Analytics and SEO
description: Connect an analytics service (or none), and pair up the canonical, hreflang, social cards, sitemap and robots the theme already generates.
---

For a multilingual site, write one description per language. Do not copy the English description to a Chinese page. Set the site-level default per language too:

hugo.yml
languages:
  en:
    params:
      description: A Hugo theme for engineering docs
  zh:
    params:
      description: 为工程而设计的 Hugo 文档主题

canonical and hreflang

The theme emits one canonical link per page and hreflang alternate links for actual translations. No configuration is required:

rendered output (this page)
<link rel="canonical" href="https://oink.pgsty.com/docs/admin/analytics/">
<link rel="alternate" hreflang="en-US" href="https://oink.pgsty.com/docs/admin/analytics/">
<link rel="alternate" hreflang="zh-CN" href="https://oink.pgsty.com/zh/docs/admin/analytics/">

The hreflang codes use each language’s locale (en-US / zh-CN on this site) and follow Hugo’s translation graph. Since OINK 1.2.0, untranslated pages omit hreflang and og:locale:alternate tags even when the visible language switcher links back to the target language’s home page.

Each paginated Blog index page uses its own canonical URL, and pages 2 and above omit hreflang alternate links because pagination boundaries can differ between languages. (OINK 1.1.0 retained the earlier behavior.)

The canonical URL uses baseURL. An incorrect baseURL directs search engines to addresses that do not exist. This problem is less visible than a build failure. Complete the deployment checklist before launch.

Full multilingual configuration is in Languages.

Social cards

The theme calls Hugo’s built-in Open Graph and Twitter card templates. They automatically supply the title, description, URL, language, and site name:

rendered output (this page)
<meta property="og:title" content="Analytics and SEO">
<meta property="og:type" content="article">
<meta property="og:url" content="https://oink.pgsty.com/docs/admin/analytics/">
<meta property="og:locale" content="en_US">
<meta property="og:locale:alternate" content="zh_CN">
<meta name="twitter:card" content="summary">

To give a shared link an image, set images in front matter:

any page
---
title: OINK v0.8.0 released
images: [/images/releasenote.webp]
---

For a site-wide fallback, write the same key under params:

hugo.yml
params:
  images: [/images/oink.webp]

When an image is present, twitter:card changes from summary to summary_large_image. The output also includes og:image and twitter:image. This site sets neither image key, so the output above has no image tags.

Sitemap

Hugo automatically generates the sitemap. For a multilingual site, it generates an index:

the structure under public/
sitemap.xml        ← the index, pointing at the two below
en/sitemap.xml
zh/sitemap.xml

Both the site default and per-page overrides are Hugo’s own:

hugo.yml
sitemap:
  changefreq: monthly
  filename: sitemap.xml
  priority: 0.5
one page
---
title: Release notes
sitemap:
  priority: 0.8
---

changefreq and priority are hints. A search engine may ignore them. Before publishing, confirm that the sitemap excludes drafts, private content, and non-canonical copies. Confirm that Hugo generated a file for each language.

robots.txt and staying unindexed

Hugo generates robots.txt only when the site configuration turns it on:

hugo.yml
enableRobotsTXT: true

The theme’s template generates one of two results, based on the build environment. You do not need to write the content:

a production build
User-agent: *
Allow: /

Sitemap: https://oink.pgsty.com/sitemap.xml
a non-production build
User-agent: *
Disallow: /

The page’s robots meta tag uses the same switch. It emits index, follow in production, except in print output. Otherwise, it emits noindex, nofollow. Do not build preview deployments with --environment production. A non-production build automatically requests no indexing.

The theme has no per-page noindex switch. To reliably prevent indexing, do not publish the page. Use draft: true or Hugo’s _build options. If you must publish it and request no indexing, emit your own tag through the head-end.html hook. The theme already emits one robots meta tag. Each search engine decides how to interpret two tags.

Checking indexing

One or two weeks after launch, check the following items in order. Confirm that search engines see the expected site:

  1. Crawl permission: open <baseURL>/robots.txt. Confirm that it contains Allow: / instead of Disallow: /.
  2. Page inventory: open <baseURL>/sitemap.xml. Open a language sitemap from the index and check the page count.
  3. Indexed count: search site:yourdomain. Check the order of magnitude. You do not need to compare every page.
  4. Canonical addresses: confirm that results open the canonical URL. They should not use a ? parameter or an old domain.
  5. Active submission: add the site in Google Search Console / Bing Webmaster Tools. Submit the sitemap.xml address to request crawling sooner.

Verify

Run from your site’s root. Replace PAGE with a generated page on your site, including any language prefix:

Terminal
hugo --gc --minify --printPathWarnings
PAGE=public/docs/getting-started/index.html
test -f "$PAGE"

# Canonical should use the real production address; production robots allow indexing.
grep -o '<link[^>]*canonical[^>]*>' "$PAGE"
grep -o '<meta[^>]*robots[^>]*>' "$PAGE"
cat public/robots.txt
head -5 public/sitemap.xml

# No match is expected when Google Analytics is not configured.
grep -o '<script[^>]*googletagmanager[^>]*>' "$PAGE"

In the browser’s Network panel, confirm that configured analytics requests use your measurement ID or registered domain. Repeat this check on a deployment built with --environment staging. It should make no analytics request. If you configured no analytics, neither environment should make an analytics request. Other explicitly enabled integrations may still contact their remote services.

  • Deploy — baseURL, the checklist, and keeping preview deployments unindexed
  • Repository links and page info — the feedback widget, edit links and last-modified time
  • Languages — language configuration decides hreflang and translation pairing
  • AI-agent support — the .md output and llms.txt written for models
  • Configuration — services, sitemap, enableRobotsTXT and the rest

6.5 - Upgrade

Pin a theme version, upgrade to 1.5.1, migrate legacy content or a Docsy site, and keep a rollback path.

To upgrade OINK, change the pinned module version. Then confirm that the site builds with reviewed diagnostics. Most content needs no changes. To convert 0.4 shortcodes to current native Markdown forms, use the migration tool. It starts with a dry run and avoids manual edits to hundreds of files.

An upgrade changes rendered output. Before you start, create an upgrade branch. You can discard that branch if you cancel the upgrade.

Current Hugo baseline

Maintained sites use Hugo Extended 0.168.0 and its built-in LibSass. No external Sass installation is required. Do not enable --panicOnWarning or HUGO_PANICONWARNING; Hugo’s built-in LibSass deprecation warning is expected. Update Hugo workflow pins when upgrading; older-Hugo requirements in the sections below describe those historical releases only.

Upgrade to 1.5.1

OINK v1.5.1 stabilizes the opt-in Duo View introduced in 1.5.0 and corrects RSS, Print, navigation, validation, search, and accessibility behavior. Sites that leave params.ui.compare unset build the exact same pages and outputs as before. See the 1.5.1 release notes.

Verify public module resolution with replacements and workspaces disabled, pin github.com/pgsty/[email protected], and build with --printPathWarnings. If you enable params.ui.compare: true:

  • Keep explicit {#id} heading anchors aligned across translations so Sync Scroll can match sections accurately.
  • Set languages.<lang>.params.ui.compare: false for any language that should neither display the reader nor appear as a target language.
  • Guard custom analytics snippets in head-end.html with window.OinkComparePane so pane loads are not counted twice. Note that a local override of Hugo’s google_analytics.html partial is not used when compare is enabled; see Analytics and SEO.
  • Allow same-origin frames (frame-src 'self') in your Content Security Policy and make sure your server does not block same-origin framing with X-Frame-Options: DENY or frame-ancestors 'none'.
  • Open a translated Docs, Book, or blog page and verify that Open bilingual view loads the target language, syncs scrolling across shared headings, and switches cleanly on narrow screens.

Upgrade to 1.4.0

OINK v1.4.0 keeps the Hugo-only build boundary and adds local PlantUML, unified diagram reading, richer local search, the opt-in table reader, and the gallery blog layout. See the 1.4.0 release notes.

Verify public module resolution with replacements and workspaces disabled. Pin github.com/pgsty/[email protected], build with --printPathWarnings, and review navigation, image viewing, and reader settings. Source-copy consumers must also refresh their fixed theme files; changing go.mod does not update a vendored copy.

OINK v1.3.1 repairs Markmap text contrast and fixed-TOC footer clearance, and adds opt-in layouts for technical tables and long FileTree comments. Existing content keeps its default appearance. Enable technical table layout or FileTree wrap=true only where complete technical text is needed.

Keep preset: paper and the explicit [paper, slate, ink, terminal] menu when the site offers all four styles. The 1.3.1 notes describe these fixes and credit their contributor.

Read the release notes first

Local PlantUML in OINK 1.4.0

Keep params.plantuml: true or plantuml.enable: true and remove svg_image_url to use the two bundled browser scripts. No additional software or rendering service is required. Keep a nonempty endpoint to retain server mode. To disable rendering entirely, set params.plantuml: false or plantuml.enable: false; an empty endpoint now selects local mode. Review unsupported includes, libraries, and CSP requirements before switching an existing diagram collection.

Each version’s release notes describe its changes, breaking changes, and upgrade steps. Before upgrading, read the target version’s notes:

The notes identify required content changes, removed configuration keys, and changed default behavior. Read them to understand differences in the upgraded pages.

Upgrading the Hugo Module

For a production site, pin a published release tag or an explicitly selected immutable commit. Do not follow a branch or use @latest. The example below upgrades to the published v1.5.1 tag. Before you select a later tag, verify that the release is published and its module resolves:

Terminal
hugo mod get github.com/pgsty/[email protected]   # the published release tag
hugo mod tidy
hugo mod graph | grep github.com/pgsty/oink

When selecting a tag, confirm that the module graph shows that exact version. An explicitly selected immutable commit normally uses a Go pseudo-version in go.mod. Commit the resulting go.mod and go.sum. For the published tag in this example, go.mod contains:

go.mod
module github.com/pgsty/oink.pgsty.com

go 1.27.3

require github.com/pgsty/oink v1.5.1
A local module replacement overrides that pin

make dev and make check set HUGO_MODULE_REPLACEMENTS for that command only, using the sibling theme checkout. To verify a release tag, remove environment replacements, disable GOWORK and HUGO_MODULE_WORKSPACE, and check for persistent replace directives or _vendor/. See the preview guide.

For a Git submodule, first confirm that it has no local edits. Fetch the tags. Check out the exact published version. Do not follow its remote branch:

Terminal
git -C themes/oink fetch origin --tags
git -C themes/oink checkout --detach v1.5.1
git add themes/oink

Commit the updated submodule pointer after validation. For an offline archive or clone, replace themes/oink/ with the selected version’s complete tree and confirm that theme: still matches the directory name. The install-method tradeoffs are in From scratch and other install methods.

What to do after upgrading

Terminal
rm -rf public resources/_gen
env -u HUGO_MODULE_REPLACEMENTS GOWORK=off HUGO_MODULE_WORKSPACE=off \
  hugo --gc --minify --printPathWarnings --logLevel info

These commands clear potentially stale caches, rebuild with the new version, and review the visible diagnostics.

--logLevel info includes informational diagnostics, while Warnings remain visible. Review the deprecation messages emitted by the pinned Hugo version before upgrading it; the severity and removal schedule depend on the deprecated feature.

After the build passes, visually check the home page, a documentation page, a blog page, and the 404. Check both languages, both color schemes, the print view, and every site customization.

Upgrading from 1.0 to 1.1

No source migration was required from 1.0.0 to 1.1.0. Its Go 1.27.0 module directive was unchanged from 1.0.0. Validate current upgrades with Hugo Extended 0.168.0. Historical toolchain details remain in the 1.1 release note.

Review the affected surfaces before choosing the new pin:

Languages
All 32 interface catalogs have the same native-message schema. Check the site’s language labels, plural counts, and RTL direction. The site remains responsible for authored translations.
Taxonomies
Root pages become term-card directories with a taxonomy switcher. Review any taxonomy-template or CSS overrides, author portraits, and localized breadcrumbs.
Sidebars
Cached trees preserve effective page settings and remain usable without JavaScript. Test collapse, hover restore, mobile drawer, and keyboard focus. Hidden content must leave the focus order.
Groups
sidebar_divider: true retains a section’s children. Add build.render: never only if you intentionally omit the group’s own outputs. Verify child navigation, breadcrumbs, paging, Print, and Book contents.
Root menus
Explicit sidebar_root_menu: false now applies to self-root sections too. The current linkable root remains a location marker.
Custom scripts
Feature-detect OinkSidebar and OinkCommandPalette.registerSearchTail if an integration must also support 1.0.0. Restore branch state through the API rather than changing classes or ARIA attributes directly.
Copying articles
With image zoom enabled, copy an image and its caption as plain text and rich HTML. Preview instructions must not enter the copied article. Zoom and its keyboard controls must still work.
Print and Redoc
Check page and Book aggregate Print, heading and tab links, and local Redoc specifications under the real deployment prefix. Local specification paths are rooted under static/.

params.ui.image_zoom and params.offline_search remain off by default. The new search hook does not enable a remote provider or add query telemetry. params.ui.scroll_spy and page-level scroll_spy remain accepted no-ops in 1.x; removing the obsolete patch does not disable normal outline tracking.

Compare affected site-level copies of theme code with the new implementation. Then update or remove those copies. Otherwise, an old image-zoom script or sidebar partial continues to override the upstream fix.

To test local theme changes with the documentation site, use its sibling theme checkout without committing a filesystem replacement:

Terminal — from oink.pgsty.com
make check
make browser
make dev

These commands validate the local checkout. For release acceptance, pin the published version, build without a module replacement, then validate the deployed pages. The authoring and API details live in content groups, the sidebar contract, search actions, and image zoom.

Upgrading from 1.1 to 1.2

Default appearance change

Paper is the default in OINK 1.2.0. Set params.ui.preset: slate before adopting this change if the site must retain its existing appearance. preset_menu: true enables reader choice; its default remains false. Ink and Terminal require an explicit preset or menu list. Their buttons do not carry experiment badges; the configuration opt-in remains unchanged. Review custom dark brand selectors as described in Brand.

OINK 1.2.0 is available as a published tag. Apart from the default appearance change above, no content migration is required. Validate current upgrades with Hugo Extended 0.168.0, then verify the site:

  • Check the chosen preset, light/dark icons, keyboard and mobile menus, saved preferences, and custom font/accent overrides. Sun means light; moon means dark. Switching styles must not change the saved light/dark preference.
  • Recheck explicit navigation, hidden subtrees, page links, Blog pagination canonicals, and SEO alternates for pages without translations.
  • Check CJK keyword-only search summaries and outline links with literal percent signs. Compare source-derived Edit, History, and Create child links on Windows or mounted content; mappings must yield repository-relative paths.
  • Check Landing content with JavaScript disabled or blocked, preserved metric formatting, dialogs and keyboard shortcuts, copy fallback, Draw.io controls, and numbered equations at narrow widths.
  • Run a production build for diagram endpoints and resource alt metadata; malformed values now produce a warning and use a safe fallback. To disable rendering intentionally, use params.plantuml: false or params.drawio.enable: false.
  • Use the revised PDF and migration tools when testing publication or content conversion. Review PDF remote-resource opt-ins and migration diffs, including code examples nested in lists. The consumer-upgrade helper ships with 1.2.0 for inventory, module updates and exact-version validation.

The Architecture, Components, Shell, and Migration contracts describe the published 1.2.0 behavior.

The content migration toolkit

Several 0.4 shortcodes now use native Markdown forms. The theme repository ships a conversion tool that requires only the Python standard library:

Terminal
git clone https://github.com/pgsty/oink
cd oink

# 1. read-only inventory: what several sites would change, exportable as Markdown / JSON
python3 bin/migrations/oink06.py report --sites ~/pgsty/oink.pgsty.com ~/www/ddia --md report.md

# 2. dry run: prints a diff and counts per file, writing nothing
python3 bin/migrations/oink06.py migrate --site ~/pgsty/oink.pgsty.com

# 3. apply: written atomically
python3 bin/migrations/oink06.py migrate --site ~/pgsty/oink.pgsty.com --write

# 4. check for residue: exit code 1 while legacy syntax remains
python3 bin/migrations/oink06.py check --site ~/pgsty/oink.pgsty.com

Follow these four rules when you use the tool:

  • A dry run is the default. Only --write changes files. Run the dry run, read the diff, and then write.
  • A second run should change nothing. If a second --write reports changes, a transformation has not converged. Stop and inspect those files.
  • The tool leaves text inside fences unchanged. Documentation examples of old syntax therefore remain intact.
  • If the tool cannot express a construct, it leaves that construct unchanged. It reports file:line and a reason for manual work. This is not a failure.

To convert one class first, use --only with the keys in the table’s last column:

Terminal
python3 bin/migrations/oink06.py migrate --site ~/www/ddia --only callout,tabs --write

After conversion, rebuild, review warnings, and inspect the rendered pages. The tool guarantees correct syntax. You must verify the intended meaning.

The 0.4 → current syntax map

{{%/* alert color= title= */%}}, {{%/* details */%}}, {{%/* pageinfo */%}}, hand-written <details><summary> , The current form> [!TYPE] Title / > [!DETAILS]-
callout
{{</* tabpane */>}} + {{%/* tab header= */%}}, {{</* code-group */>}} + {{</* code-tab */>}} , The current formAdjacent fences with {tab= group= value=}; tabs in running text use {{</* tabs */>}} + {{</* tab */>}}
tabs
{{</* filetree */>}} with filetree/folder and filetree/file , The current formThe filetree data fence
filetree
{{</* gallery */>}} with gallery/image , The current formThe gallery data fence
gallery
{{</* echarts */>}}, {{</* infographic */>}} , The current formData fences of the same name ($fn: is unchanged; a js subfence moves to window.OinkEchartsFunctions)
datafence
doc-cards / doc-card, nav-cards / nav-card, card / cardpane, doc-carousel , The current form{{</* cards */>}} + {{</* card */>}}, or a link list with {.cards}
cards
{{</* imgproc */>}}, {{</* image */>}} , The current form![alt](src) with the attribute line {command= options= caption=}
image
{{</* readfile file= */>}} , The current form{{</* include file= */>}}
include
The fence attribute {filename="x"} , The current form{title="x"}
fencetitle
{{</* badge outline= */>}} , The current formDrop the outline parameter
badge
{{</* example */>}} + a fence, {{</* book-figures kind="tbl" */>}} , The current form{{</* eg */>}}…{{</* /eg */>}}, {{</* book-tables */>}}
eg
{{%/* _param x */%}}, iframe, conditional-text, blocks/*, netlify, a kindless xref , The current formReported only; handle by hand
reportonly

Each page under Components describes the new form and its parameters.

Migrating from Docsy

OINK is a hard fork of Docsy. It retains the content model, td- naming, Sass variables, and most front matter. Migration mainly requires removing site copies of the shared shell. The theme’s implementation then supplies the shell. You do not need to rewrite the prose.

  1. Pin the target version. Change go.mod to an OINK release tag, or use a complete versioned archive. During evaluation, an uncommitted go.work can point at a local checkout.

  2. Inventory the overrides. Classify each site-level file under layouts/, assets/, and static/ into four groups. Delete verified copies of the shared shell. Delete or mechanically rename components that OINK already provides. Retain brand customization in the smallest suitable hook. Keep business-specific data and interaction in the site. Delete files in reference order. Do not empty layouts/ at once. The home page or download page may still call a partial that you plan to remove.

  3. Move the configuration. Keep title, languages.*, github_repo, github_branch, page_width, and params.ui.* in their existing semantic positions. OINK adds no separate namespace. Enable search and the logo with these keys:

    hugo.yml
    params:
      logo: img/product.svg
      offline_search: true

    Docsy’s camelCase search keys have been renamed in OINK: offlineSearch, offlineSearchIndex, offlineSearchMaxResults, offlineSearchOnServe and offlineSearchSummaryLength all become their underscored forms. Rename them deliberately. The old migration registry stopped the build and named the replacement. That registry was removed. The theme now ignores an old key, so search remains off without a message.

  4. Fonts and styling compatibility. The Docsy Sass variables in assets/scss/_variables_project.scss still supply seed values for font roles. You do not need to delete them for an upgrade. Each of $td-fonts-serif, $font-family-sans-serif, $headings-font-family, and $font-family-code supplies its role. The theme no longer reads Docsy’s Google Fonts switches $td-enable-google-fonts, $td-google-font-name, and $td-web-font-path. These unused switches cause no failure and have no effect. OINK bundles Inter, Chakra Petch, and IBM Plex Mono. Neither preset requests Google Fonts. To change fonts, use the token layer. See Brand and appearance.

  5. Convert the shortcodes. Docsy’s alert, pageinfo, tabpane and card families all have a current counterpart; convert them in bulk with the migration toolkit above, one --only class at a time.

  6. Delete one group at a time. Build after each group. First test on a scratch copy. Record the theme commit, Hugo version, removed files, and generated HTML file count. After you confirm equivalent behavior, repeat the process on the production branch.

The “delete after verifying” class in step two is usually these files:

  • layouts/baseof.html and the shared docs / blog baseof*.html;
  • The navbar, footer, sidebar, TOC, search and head CSS partials and their hooks;
  • The old brand documentation shell partials;
  • Copies of the asciinema, echarts, infographic, doc-carousel, details, tab / tabpane, card and param shortcodes;
  • The JavaScript, Lunr copy, carousel code and SCSS that served only those implementations;
  • PostCSS and Autoprefixer steps no site asset needs any more.

Two types of problem can appear after deletion.

A site script can report $ is not defined. OINK does not bundle jQuery, which Docsy loaded in each page’s <head>. The theme does not need jQuery. If your site needs it, load it in the site:

layouts/_partials/hooks/head-end.html
<script src="{{ (resources.Get "js/jquery.min.js").RelPermalink }}"></script>

A home page that uses Docsy’s blocks/* fails an OINK build with template for shortcode "blocks/cover" not found. OINK has no such shortcode family. Use home page sections in data/home/<language>.yaml, or set layout: landing on the page. See Home and landing pages.

Upgrading from 0.4

0.4 changed several defaults. If an upgrade adds or removes a page feature, check these settings first:

  • Sequential paging is on by default. docs, book, and blog pages have previous / next links at the page end. Documentation follows the sidebar tree. Blog pages follow time. For a page intentionally outside a sequence, set pager: false.

  • The navbar appears on every layout. Its compact state has one row of icon navigation and no second mobile accordion menu. Remove local scripts and tests that depend on the old mobile menu. To disable the navbar for a section, set navbar_enabled: false in a cascade.

  • The footer defaults to fat for the site. It accepts only fat / slim / none. Store footer data in data/footer/<language>.yaml, or data/footer.yaml for a single-language site. A remaining footer key in data/home produces a warning with the new location.

  • Single-key navigation is on by default. / opens full search, and \ opens command-only mode. Update training material that describes the old behavior. Page actions now use a split button beside the breadcrumbs.

  • The code block DOM changed. A .td-code wrapper now encloses the original .highlight. Both .highlight and .chroma remain. In site CSS, replace a direct child selector such as .td-content > .highlight with the descendant selector .td-content .highlight.

  • Two ICP footer parameters were removed. The keys footer_icp and footer_icp_url became one string that accepts inline Markdown.

    hugo.yml
    params:
      footer_center_info: '[京ICP备00000000号](https://beian.miit.gov.cn/)'
  • For mathematics, enable passthrough in the site. Hugo does not merge a theme’s markup configuration. If the site uses \(…\), \[…\], or $$…$$, enable Goldmark passthrough in its own hugo.yml. See Math.

The complete configuration for all of these is in Configuration and Layouts and page types.

Verify

After “the build passed”, the upgrade still requires checks. Inspect each surface:

Documentation / Book
Sidebar order, paging, headings, page actions, numbering and cross-references
Blog
Chronological paging, RSS ownership, navbar and footer
Home / landing
Content without JS, the compact menu, print
Release pages
Derived download URLs, checksums, publication state
Components
One page each for the components the site uses most
Accessibility
Keyboard-only operation, focus order, both color schemes, forced-colors mode
Deployment
Internal links and assets all keep the base path prefix

This site’s full gate is:

Terminal
make check     # local sibling theme: build, outputs, translations, and rendered links
make browser   # local sibling theme: accessibility, responsive and interactive behavior
make build     # published theme pinned in go.mod, without a local replacement

Another site runs the equivalent build, link, output and browser checks; the details are in Troubleshooting.

Complete the final checks in the real environment. Deploy a preview. Verify its pages and browser network requests at the real URL. After review, merge the upgrade and run a production smoke test.

Rollback

To roll back the theme, restore the version pin:

Terminal
hugo mod get github.com/pgsty/[email protected]   # example: the site's last known-good tag
hugo mod tidy
rm -rf public resources/_gen
hugo --gc --minify

Three principles:

  • Keep the pre-upgrade module pin, the site commit and the known-good deployment artifact, and restore all three together.
  • Do not perform a partial rollback. Old layout copies over a new theme produce a mixed implementation. This is harder to diagnose than either complete version.
  • Keep the upgrade branch and its acceptance evidence. A rollback first restores production. It does not discard completed upgrade work.

Rolling back the deployed output itself (republishing the previous deployment) is in Deploy.

6.6 - Troubleshooting

Diagnose build, language, search, and platform problems by symptom, cause, and fix. Run checks on your site.

If a problem occurs, first run a clean production build. Read the first error. Later errors usually result from it:

Terminal
rm -rf public resources/_gen
hugo --gc --minify --printPathWarnings --logLevel info

If the log includes an npm, PostCSS, Autoprefixer, or browser-asset download step, the configuration still uses upstream Docsy’s process. An OINK consumer build requires one Hugo command.

The four tables below show each symptom, its cause, and its fix. Find the row for your symptom. You do not need to read every row.

Build

Symptom Cause Fix
The build demands a newer Hugo Hugo Standard is installed instead of Extended, or the version is below 0.168.0 hugo version output must contain extended. If several Hugo versions are installed, check PATH and version pins before installing another
module "github.com/pgsty/oink" not found The theme did not resolve Hugo Module: check hugo mod graph, go.mod, go.sum, and unexpected workspaces or replacements. Submodule: confirm that CI runs git submodule update --init before Hugo. Archive / clone: theme: must match the directory name under themes/
Module download hangs or times out The Go module proxy is unreachable Hugo downloads modules through Go, so GOPROXY applies. In mainland China, use export GOPROXY=https://goproxy.cn,direct. In an isolated environment, use an offline archive or commit themes/oink/
{.cards}, {.steps}, {caption=…} appear as literal text The site has not enabled Goldmark block attributes Put all three settings below in the site’s own hugo.yml. Hugo does not merge a theme’s markup configuration
An image with an attribute line is wrapped in <p> and the caption does nothing wrapStandAloneImageWithinParagraph: false is missing Add all three settings below together
Inline HTML is escaped into text renderer.unsafe: true is missing As above
\(…\) $$…$$ display literally The site has not enabled Goldmark passthrough See Math. The setting math: true does not enable passthrough
shortcode "tabs" must be closed or self-closed A {{< tabs >}} has no matching {{< /tabs >}} The error identifies file:line:column. Add the closing marker at that location
template for shortcode "tabs" not found The body calls a shortcode that does not exist, or quotes shortcode syntax without escaping it Documentation that explains shortcode syntax must escape it. Add /* and */ inside the opening and closing markers. Hugo then treats it as text instead of a call. If the name is misspelled, correct it
... attributes: unknown attribute "witdh" at ... An attribute-line key is misspelled or not permitted The warning lists allowed keys. The theme ignores the invalid attribute and drops style and on*. Correct the attribute
shortcode "field": unsupported parameter "colour" at ... A shortcode parameter name is wrong The warning identifies the shortcode, parameter, file, and line. The theme ignores the unsupported parameter so preview remains usable; correct the parameter name
invalid params.ui.page_width "widee" (allowed: normal | wide | full) -- using "normal" A configuration or front matter value is not one of the accepted ones The theme uses a fallback instead of stopping so a typo does not produce HTTP 500 on every URL under hugo server. The message identifies the key, value, and fallback
A page setting has no effect and nothing is reported The key was written inside a ui: block in front matter Put page keys at the top level of front matter. Remove ui. from the site key. The theme ignores a front matter ui: block without reporting it. See Page parameters
The build passes but production is missing something An unreviewed WARNING Review the build warnings and correct invalid configuration values, missing required giscus keys, or unsupported comments.type values. The upstream LibSass deprecation warning is accepted

The three Goldmark settings:

hugo.yml
markup:
  goldmark:
    parser:
      wrapStandAloneImageWithinParagraph: false
      attribute:
        block: true
    renderer:
      unsafe: true

The two most common shortcode errors look like this. Each ends with file:line:column:

build output
ERROR error building site: assemble: failed to create page from pageMetaSource /a:
  "…/content/docs/x.md:4:1": failed to extract shortcode:
  shortcode "tabs" must be closed or self-closed

ERROR error building site: assemble: failed to create page from pageMetaSource /a:
  "…/content/docs/x.md:4:5": failed to extract shortcode:
  template for shortcode "tabs" not found

Language

Symptom Cause Fix
A translated page does not appear Four possibilities, in order ① hugo.yml has languages.zh with a weight; ② the filename is page.zh.md, with zh lowercase; ③ the translation’s front matter has no draft: true and no future date; ④ routing metadata matches the source file
Switching language lands on the home page Hugo found no translation If no translation exists, the switcher uses the target language’s home page. To open the corresponding page, add its translation file
An anchor link opens the page but does not scroll The translated heading text differs, so the generated ID does too Set the English ID explicitly on the translated heading: ## 安装 {#installation}. If a heading contains a shortcode or inline HTML, read its ID from the English page’s rendered HTML. Do not guess it
Menus / home page sections are untranslated They are not in pages but in configuration and data files Menus are in languages.<lang>.menus. Home sections are in data/home/<lang>.yaml. Interface strings are in i18n/<lang>.yaml. See Languages
A page’s hreflang points at another language’s home page The published 1.1.0 behavior or a copied older SEO partial can reuse the language-switcher fallback The 1.2.0 development implementation omits missing translations from SEO alternates. Check the resolved theme version and template overrides; add the corresponding page when a translation is intended. The visible language switcher’s home-page fallback remains valid
Symptom Cause Fix
A search box that never returns results No index was generated Check params.offline_search and open search. Inspect the page’s data-td-index-src URL in Network. Production uses offline-search-index.<language>.<hash>.json. Development omits the hash. If the index is absent during hugo server, also check offline_search_on_serve
The index file 404s A wrong baseURL For a subpath deployment, an incorrect baseURL is the most common cause of an index 404. Check the requested index URL in the browser’s network panel. See Deploy
Search fails under hugo server but works in a build The site turned the preview index off params.offline_search_on_serve defaults to true, so preview matches production. An explicit false skips preview index generation. Remove that setting or restore true
Chinese queries find nothing Usually not a tokenization problem A CJK query uses the theme’s substring fallback. First, open the Chinese page’s actual data-td-index-src URL. Confirm that the Chinese index contains its content. Then check tokenization
A new page is not found while old ones are The index is build output Rebuild. Under hugo server, wait for the rebuild after editing
params.search.algolia requires explicit appId, apiKey, and indexName values The three Algolia keys are incomplete All three must be given explicitly; the theme will not use another project’s DocSearch credentials. If Algolia is not wanted, delete the block
The command palette finds no content Command actions and full-text search are separate If the index is unavailable, the palette still opens and reports this condition. Page actions and commands still work. See Command palette

Platform

Symptom Cause Fix
A 404 or missing styles on GitHub Pages A project site’s URL carries the repository path and baseURL does not Use --baseURL "${{ steps.pages.outputs.base_url }}/" from the workflow. Do not hard-code the address. See the complete workflow in Deploy
“Last modified” and contributors are empty on GitHub Pages The checkout is shallow Add fetch-depth: 0 to actions/checkout. The setting enableGitInfo requires the complete history
A Cloudflare Pages build says Hugo is too old The build image’s default Hugo is older than the theme requires Set HUGO_VERSION in both the Production and Preview environments, and set SKIP_DEPENDENCY_INSTALL=1
The host’s build cannot fetch the theme The build environment has no Go Hugo Modules require Go. If the platform does not provide it, use a submodule or commit themes/oink/
CI output differs from local go.work took part in the CI build Set GOWORK: off and HUGO_MODULE_WORKSPACE: off in CI so it reads only the version pinned in go.mod
A preview deployment got indexed The preview was built in the production environment too Do not pass --environment production for previews. A non-production build emits noindex and Disallow: /. See Analytics and SEO
macOS reports too many open files Live preview watches more files than the shell limit allows First exclude generated and irrelevant directories from the watch. These usually cause the problem. Then, if necessary, consider ulimit -n
Slow, or missed changes, under WSL Working across a Windows mount point Run Hugo on paths inside the Linux filesystem. Cross-filesystem change notifications and permission behavior can break live reload
Bootstrap / Font Awesome / Lunr / Mermaid assets are missing An incomplete distribution Do not substitute a CDN URL. Confirm that assets/third_party/, assets/js/third_party/, static/webfonts/, and VENDOR.json are present. If a file is missing, download the same pinned version again

Checks a site can run

Run the build from your site’s root. Use the separate output checker from a theme checkout that matches your pinned release. Replace /path/to/oink, /path/to/my-site/public, and the base URL with your values. The remaining commands are examples from this documentation repository’s test harness. They are not supplied to every OINK consumer.

A production build , Commandhugo --printPathWarnings
Duplicate output paths, invalid parameters, incomplete external integrations
Output trust check , Commandpython3 /path/to/oink/bin/check-output-security.py --public /path/to/my-site/public --base-url https://my-site.example/
Checks every href / src in all four outputs for site-relative paths or http(s) / mailto / tel. Rejects javascript: URLs and inline on* handlers. A cross-site <iframe>, <script>, or <img> requires explicit --third-party
Translation parity , Commandnode scripts/check-doc-translations.mjs --public public
Checks that each English page has a Chinese counterpart and that rendered heading IDs match. Detects misaligned anchors
The full gate , Commandnpm test
Runs the six below in sequence

The six checks cover the following items:

  • test:base — builds once, then runs the Markdown style, translation parity, rendered Markdown and link checks.
  • test:hugo-build — build assertions: blog metadata, RSS, content components, and a deprecation-free build.
  • test:md-output — compares Markdown and llms.txt output byte for byte against goldens. A change to a component’s Markdown structure fails this check.
  • test:alt-site — builds once for each alternate configuration in tests/fixtures/*.yml. Confirms that each combination still works.
  • test:favicons — golden comparison of the head output.
  • test:release-pin-contract — whether the version the site advertises matches the one pinned in go.mod.

Browser behavior has a separate suite. The command npm run test:browser runs the Playwright accessibility (axe WCAG AA), responsive shell, keyboard navigation, content component, code block, and scenario component suites in sequence.

check-output-security.py lives in the theme repository

It sits under the theme’s bin/, is a product-level trust check any OINK site can run, and depends on no site test framework. Keep the theme-tool path and your site’s output path distinct; the complete example is in Verifying an offline build.

Diagnostic habits

If the tables do not cover the problem, use these diagnostic steps:

  • Reproduce the problem with a pinned Hugo Extended version. Avoid an environment with an unpinned version.
  • Clear public/ and resources/_gen and rebuild, to rule out stale caches.
  • Compare the development and production configuration layers; many production-only problems are environment differences.
  • Read the first error. Later errors may result from it.
  • Use a minimal page to distinguish “theme behaviour” from “site override”. Put the suspect content on its own page. Re-enable site overrides in batches until a batch causes the problem.
  • Look at the failing page’s browser console and network panel, especially the paths of any 404 resources.

Getting help

When you open an issue, include the complete hugo version output and the theme version from hugo mod graph | grep oink. Include the first complete error and a minimal page or site that reproduces it. This information avoids an additional request for details.

  • Local preview — clean builds, clearing caches, containers and workspaces
  • Deploy — baseURL, the checklist and rollback
  • Upgrade — problems an upgrade introduces, and the migration toolkit
  • Search — index scope, ranking and Algolia
  • Languages — language configuration and the anchor alignment process

7 - Design and development

OINK’s contracts, accepted decisions, dated research, and active proposals in one bilingual section.
OINK 1.5.1 contract

The contracts in this section describe OINK v1.5.1. Theme development uses the sibling checkout; release acceptance verifies the published module with replacements and workspaces disabled.

This section explains how OINK works and why it has its present shape. It brings together current contracts, the reasoning behind accepted decisions, evidence from alternative designs, and ideas that remain proposals. The other guides on this site explain how to build and maintain a site with OINK.

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, authoring, Expo, and CLI rationale
Research Goldmark probes, consumer evidence, CLI acceptance, and October theme reviews
Proposals Active PRDs for knowledge graphs, media convergence, visual presets, and the CLI roadmap

Create each 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. After acceptance, update the implementation, its checker, and the applicable contract, then record the accepted rationale under Decisions. Retire the draft from active use while preserving it in Git history and the changelog.

Expo contract

The Expo contract describes the image-album layout introduced in OINK 1.3.0. Its accepted decision records the content model and the revisions to the earlier Muse experiment. The authoring guide explains album setup and viewer controls. The contract, accepted decision, and authoring guide describe the same released content model and controls.

Authority and maintenance

This directory owns the English and Chinese design text for maintainers; the theme repository owns the executable facts. Within the theme, hugo.yaml defines published defaults, while each resolver and checker defines the input shapes it accepts. layouts/ and assets/ define rendered behavior, and check scripts with tests/goldens/ define validation. VENDOR.json records bundled versions, licenses, files, and checksums.

When public behavior changes, update the implementation, its checker, and both language versions of the applicable contract in the same delivery. Tests should check behavior and output without requiring exact prose.

7.1 - Architecture contract

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

This contract describes OINK v1.5.1.

Hugo toolchain

Use OINK v1.5.1 and Hugo Extended 0.168.0 for maintained sites, development, CI, and release validation. Keep this toolchain pinned.

Do not enable --panicOnWarning or HUGO_PANICONWARNING. Warnings remain visible and are reviewed. The upstream LibSass deprecation warning is accepted; it does not make a successful build fail. Invalid-input tests verify their specific warning and safe fallback output directly.

OINK uses Hugo’s built-in LibSass for main and ReDoc styles. Ordinary consumers need no Dart Sass or other Sass executable. Project-variable and project-style SCSS hooks remain supported. Future upgrades must preserve this dependency boundary. Update requirements, checkers, CI pins, and both language contracts together; historical release and research records retain their original tools.

The module requires Go 1.27.3, also pinned for development and CI. Go resolves Hugo Modules and is not needed for a prepared classic theme copy.

Repository and assembly

The repository root is a complete Hugo Module theme. It is not a site or npm workspace. Hugo Extended compiles SCSS and templates. Committed browser runtimes and third-party assets need no network fetch during normal builds. Public bilingual documentation, examples, and browser tests belong to the sibling oink.pgsty.com repository. The theme keeps only narrow internal regression fixtures under tests/site/. It has no separate public example site.

Generated public/ and resources/ directories are never source. Vendored runtimes, font families, and Font Awesome glyph definitions are supported distributions. Do not remove them as dead code. VENDOR.json and bin/check-vendor.py verify their integrity. OINK includes the complete supported Font Awesome distribution because consumer content can use icons absent from theme templates.

The official compiled Font Awesome CSS is a stable, fingerprinted vendor stylesheet. It loads before fingerprinted main.css, which is compiled from theme and consumer SCSS. Site style changes therefore preserve the icon distribution’s fingerprint, while cascade order still lets sites override its styles. Capability styles, including KaTeX, DocSearch, Swagger, and Asciinema, stay separate and load only when used. Fingerprints permit immutable URLs. The deployment host controls their HTTP cache headers.

Hugo types docs, book, blog, and swagger select reading shells. params.ui.shell_types may add types. Landing uses layout: landing. There is no article type or second blog shell. The shell contract defines immersive pages as a blog presentation.

layouts/_partials/shell/config.html resolves shared shell settings. 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. Keep similar base templates separate when merging them would change Hugo lookup precedence.

Configuration and diagnostics

Theme policy uses params.ui.*. Integrations with multiple settings, such as comments.giscus, plantuml, and drawio, remain top-level. Boolean features use bare booleans unless they have multiple settings. Page overrides omit the ui. prefix. Thus params.ui.image_zoom becomes image_zoom, never a front matter ui map. hugo.yaml declares published defaults. The owning resolver and checker define optional configuration shapes or ranges.

For invalid input, warn with the value, allowed shape, and safe fallback. Then apply the fallback or omit the unsafe feature. Ordinary builds and previews remain usable. The theme never calls errorf. check-params.py enforces this rule. Do not add speculative validation for unreachable states.

There is no generic registry of renamed keys. If a transition needs a migration diagnostic, add a targeted warning to its resolver and a negative test that verifies the warning and fallback. Never read removed keys as a compatibility path.

Network-capable features require explicit configuration and remain disabled when that configuration is invalid. OINK 1.4.0 renders PlantUML locally when it is enabled without an endpoint; an explicit plantuml.svg_image_url keeps server mode. Draw.io requires drawio.drawio_server. Algolia requires appId, apiKey, and indexName. Incomplete configuration warns and sends no request. Draw.io loads only if rendered content has PNG or SVG candidates. It inspects each distinct image URL once. Diagram endpoints must be strings with an HTTP(S) URL containing a host or a same-site path. Unsupported schemes, protocol-relative URLs, backslashes, whitespace, and same-site references without paths warn and disable the integration before runtime selection.

Self-contained PlantUML in OINK 1.4.0

plantuml: true or plantuml.enable: true without an endpoint selects browser rendering. The two pinned local assets (@plantuml/core 1.2026.9 and Viz.js 3.24.0) load only on HTML pages with PlantUML fences. Normal builds and readers need no Java, Graphviz executable, npm installation, CDN or rendering service. A nonempty endpoint still selects server mode and retains its validation and svg behavior; invalid endpoints do not fall through to local mode. No optional libraries, themes, include files or external resources are downloaded by local rendering. Generated SVG is displayed as a Blob image, never inserted into the page DOM. Failed input or loading keeps readable source; dedicated Print, Markdown and RSS keep source without a runtime. This behavior is part of the v1.4.0 release.

Fixed in OINK 1.3.0

Editor URLs retain their query parameters and fragments when embedding options are added. Messages must come from the active editor frame and its configured origin; unrelated or malformed messages are ignored. Diagram data is sent only to that origin. Exports must be PNG or SVG data URLs.

Lazy diagram loading in OINK 1.4.0

The small Mermaid/local-PlantUML scheduler appears only on HTML pages using these diagrams. Large engines load as visible diagrams approach the viewport. Engines are shared, with serial main-thread rendering; no Worker, dependency or build step is added. Scroll/size checks replace IntersectionObserver when unavailable. Ordinary browser printing cannot wait for asynchronous rendering: completed diagrams show images and the remainder retain source. Dedicated outputs are unchanged.

Interface localization

Available since OINK 1.1

OINK 1.1.0 expands the native interface catalogs to the complete locale set below. Consumer-authored content still needs its own translations.

OINK ships native interface catalogs for the 31 locale filenames present in google/docsy@64f51c5, plus generic zh as the Simplified Chinese default:

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

These catalogs cover theme UI strings; sites translate their own content. New Docsy locales do not enter OINK automatically: they need a complete OINK catalog and the same review as existing locales.

i18n/en.yaml defines the message schema. Each of the 32 OINK bundles has exactly these keys and native interface text. An English value may remain only as a reviewed product name, punctuation token, conventional abbreviation, or word shared by the target language. There are no generated English fallback blocks. zh and zh-cn use Simplified Chinese. zh-tw uses Traditional Chinese.

The current Hugo baseline accepts locale: zh for the generic zh language alongside regional Chinese catalogs. A site may also select locale: zh-CN. Neither choice changes the i18n/zh.yaml filename.

Runtime placeholders such as %s, {count}, and {{ .Count }} may move for natural grammar. Their bytes must stay identical. Values are strings or Hugo plural-message maps. Plural maps use supported locale categories: zero, one, two, few, many, and other. other is required. Each form must be a string with the same placeholders. Catalogs contain no hidden bidirectional controls. Arabic, Persian, and Hebrew direction comes from consumer language configuration (direction: rtl), not injected translation characters. bin/check-i18n.py checks locales, schema, value shapes, placeholders, directional controls, and the small reviewed set of English-identical terms. Translate every new visible string in every bundle in the same change. Do not generate fallback text.

Hugo’s images is the single authoring API. params.images supplies 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 explicit or cascaded values but does not disable bundle resource discovery. Only the first resolved image represents the page. Local processable rasters may be cropped. SVG, static, and remote resources remain valid without Hugo image operations.

featured-image-resolve.html defines source priority and relative or absolute URLs. Explicit page images take precedence over bundle resources. Bundle resources take precedence over inherited cascade images. This order also applies when explicit and inherited values are identical. For file-backed pages, authored presence is read from source front matter. Hugo parses YAML, TOML, or JSON. For generated pages without source files, resolved images count as explicit. List thumbnails, Open Graph/Twitter/schema helpers, author avatars, Pinterest media, and blog presentation all use this decision.

params.ui.featured_image applies only to blogs and defaults to none. Page front matter or cascades can override it. banner adds a figure above single-page titles. wash colors the header. hero fills the shell background on single pages and section indexes. Missing images and non-HTML outputs 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
Print Expanded content; no shell navigation, search, or zoom runtime; the shared action layer supports explicit print controls
Markdown / LLMS Source-shaped Markdown without td- component markup
LLMSFULL Opt-in per top-level section: one llms-full.txt per enabled section per language, that same Markdown concatenated in reading order
RSS Safe static summary or explicit omission
NAVJSON Opt-in per site: one navigation.json per language, serializing the navigation authority the sidebar and pager already read
BookManifest Opt-in ordered JSON handoff for EPUB/PDF packaging tools

Output formats run in their defined order. A render keeps its mutable format state stable for the whole output; summary helpers do not restore an earlier format value into an RSS render. Within Print, Hugo can render Book pages and overlapping aggregates in parallel. A cached coordinator for each page creates plain and Book variants in a fixed order. Each caller selects its required form. Plain Print keeps page-local headings and routed xref URLs. Book aggregates keep namespaced headings and in-document xrefs. Relative content links resolve against each source page’s HTML URL before its body moves into Print. This preserves language and deployment prefixes, query strings, and encoded paths in both single-page and aggregate output. Only actual href attributes are rewritten; matching text in title or other non-link attributes is preserved. Fragment-only links remain local for heading and footnote namespacing; canonical and external links keep their destinations. Heading fragments are percent-decoded once for namespace lookup, including Unicode and encoded ASCII. Literal plus signs remain plus signs; malformed escapes remain unchanged. HTML entities in IDs and attributes are decoded separately. The path, query, and external origin are never changed by fragment normalization. Rendered Markdown headings inside Tabs and other Markdown shortcodes receive the same namespace, even when Hugo omits them from Page.Fragments. Their rendered heading IDs own the identity, preserving Chinese text, literal plus signs, and percent sequences without deriving IDs from escaped self-link URLs.

Configured RSS output on a Landing page generates a static feed through the shared RSS renderer. An advertised alternate output must have a generated file; omitting interactive Landing components does not omit the feed itself.

Print is a browser-readable document before it is sent to paper. On screen, wide tables stay inside labelled, keyboard-focusable scroll regions without widening the page. Print-media CSS expands these regions for printing. This requires no table runtime; RSS tables remain non-focusable static content.

Consumers opt into custom outputs. OINK does not force expensive Book aggregates. HTML loads shared action and core layers, plus stable first-party capability chunks selected by page flags. Templated capabilities publish at most one chunk per language. Flags select script tags and never create bundles for feature combinations. Print keeps the action layer and only runtimes needed by rendered print features. Large third-party UMD files stay separate. Unused feature runtimes remain absent.

A top-level section enables LLMSFULL by listing it in its _index front matter outputs. The theme never adds it to site outputs. One renderer creates per-page Markdown and the bundle. The bundle concatenates the same semantic Markdown, without td- component markup, in sidebar and pager reading order. Enabling it below the top level warns and emits nothing. Builds remain usable; the warning and absence of the disabled output are checked directly.

Site outputs.home enables NAVJSON. It publishes one navigation.json per language at the language root. It uses the sidebar and pager authority: explicit data/docs_nav.json when present, or the weighted content tree otherwise. Array order is the contract. weight is never serialized. The output is notAlternative. The manually maintained schema/nav.v1.schema.json versions the format. Edit it with its templates and checker. It is outside the generated configuration schema drift gate. Both outputs default off. Sites that enable neither build byte-identically. bin/check-agent-indexes.py owns their checks.

BookManifest is disabled unless a Book root explicitly lists it in outputs. It references existing per-page Markdown and records derived page order, headings, numbered targets, and xrefs. The theme guesses no publication metadata.

The theme repository provides bin/book-epub.py and bin/book-pdf.py for explicit publication steps. bin/check-book-epub.py and bin/check-book-pdf.py validate their artifacts. The EPUB packager combines BookManifest with whole-Book Print HTML and accepts consumer metadata separately. The PDF runner serves Print output at a temporary loopback address. It invokes an explicit Chrome/Chromium binary with script-src 'none' CSP. It produces A4 pages with CSS page numbers. The PDF server also applies a CSP sandbox, rejects meta-refresh navigation, and refuses symlinks outside the build tree. Without network opt-in, image and media requests allow only the loopback origin and data URLs. This includes requests from CSS or SVG. Both tools refuse missing resources and resources outside the build tree. Network resources and output replacement each need a separate explicit flag. Network opt-in permits only passive HTTP(S) media. Remote scripts and local-file schemes remain invalid. Relative EPUB metadata assets resolve from the metadata file’s directory, not the working directory. Ordinary Hugo builds run no publication steps. PDF remains derived from Print, rather than a separate template output.

Performance rules:

  • Do not walk .Site.Pages for every page if a site-level resource or partialCached result can perform the work.
  • Render .Content once. Read Page Store flags only after rendering it.
  • Emit correct markup. Do not scan 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. First, a named production consumer must measure Sec-Purpose: prefetch requests, useful navigations, bytes, and CSP impact. Use a reversible moderate experiment.
  • Validate reachable author input. Do not validate hypothetical internal states.

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

Single-page Print emits the page description once. Section Print carries mathematics capability from every included descendant to the aggregate root, so a formula-free index still loads local KaTeX styles when a child needs them. Formula-free Print documents do not load those styles.

Trust, CSS, and accessibility

Authors may enable Goldmark unsafe. Configuration and component parameters are not raw HTML. The shared attribute policy uses an allowlist, validates class tokens, and passes data-* and aria-*. It warns and removes style, srcdoc, on*, reserved attributes, 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. They are never fetched at build time.

Fixed in OINK 1.3.0

Image and Gallery URLs are HTML-attribute escaped after URL validation, including full-size Zoom sources. Quotes and ampersands remain part of the URL; they cannot introduce attributes or be decoded as HTML entities.

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

Reading-container focus distinguishes pointer input from keyboard navigation. A pointer-focused main region, table viewport, or code pre does not gain an outline merely when another key is pressed. Tab, blur, or new non-pointer focus clears that exception. Controls keep their focus styles. Scrollable containers keep tabindex. The skip-link destination shows a local outline around its title instead of the whole article. Forced colors retain keyboard focus indication. No global focus-outline reset is used.

Font roles are ui, body, heading, code, display, meta, brand, and print. They use --td-*-font-family properties. ui is the primary face, with body resolving through it and heading through body, so one assignment changes interface text, prose, and headings together. params.ui.typography accepts technical or system. Both compile into one stylesheet and need no runtime. Legacy Bootstrap/Docsy Sass variables still initialize these roles.

params.ui.fonts configures the same roles without mounted SCSS or an added stylesheet. It names faces without loading them, so readers must have the family or the site must declare it through its own @font-face. This key remains outside the network contract. Values must use plain font-family syntax. The emitted :root block is reconstructed from matched parts. Unknown roles or unsafe values warn and are individually removed. The block follows the stylesheet, allowing authored faces to override presets at equal specificity. Shells inherit site faces and define none of their own. Book numbers and captions use the prose face, not a technical face.

Accent roles are separate. Accent text, including links, external URLs, and inline code, uses the Bootstrap link family and --bs-code-color. Theme colors never redeclare these properties. Inline code follows the visual preset: Slate keeps the crimson pair; Paper uses ink text on a quiet chip. Accent backgrounds include selected rows, gray pointer-hover navigation backgrounds, hover washes, outline pills, rails, dots, chip hovers, and hovered card borders. They also include share-button hover fills, selections, and focus rings. These use --td-accent, --td-accent-rgb, and --td-accent-hover, which default to the link family. These are the only properties emitted by params.ui.theme_color. Shell text uses them too: active outline anchors and Book chapter headings under pointer or keyboard focus use the section color, not link blue. theme_color and theme_color_dark accept #rgb/#rrggbb. Page front matter and section cascades override site values. Unconfigured sites emit nothing. Unparseable values warn and keep the default palette. Resolved colors below 4.5:1 against the theme canvas warn with a suppressible ID but are still emitted. This check uses the site default preset canvas and is advisory. Only parse failures remove colors. The light color is required. theme_color_dark without valid theme_color warns and is ignored. A page therefore receives color in both modes or neither. If dark color is omitted, the theme lightens toward white in 4% steps until it reaches 4.5:1 on the dark canvas. Every emitted byte is formatted from parsed integer channels, never author text. One resolver supplies page colors to both the head block and sidebar root switcher.

Visual presets

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

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

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

Paper section headings in OINK 1.4.0

Paper no longer draws a decorative line to the right of content h2 headings, in either light or dark mode. Heading typography, spacing, anchors, and normal wrapping are preserved. Site-authored heading borders remain supported.

Giscus auto palettes follow both dimensions; explicit Giscus theme or light/dark stylesheet overrides remain authoritative. Print uses a light preset palette on white paper even when the screen is dark. Mermaid and ECharts continue to follow mode only; API widgets retain vendor palettes. See the accepted decision and local acceptance record.

Translation reader runtime

The sole setting is the boolean params.ui.compare, default false; a language may override it. Invalid values warn and leave it off. Hugo derives candidates from actual translation pages whose language also enables comparison, and emits a small descriptor. The entry chunk loads the reader on demand; the reader uses two same-origin HTML frames. No language combinations or new content outputs are generated. The retained host makes three live documents. Per-viewer preferences (last target and Sync) live in localStorage under td-compare-reader, read and written inside try/catch; the restorable session (target, Sync, visible pane, semantic bookmarks) lives in history.state.oinkCompareSession on the reader entry only.

oink_compare_pane=1 activates the internal pane state only in a frame owned by an open reader. Visiting that URL at top level leaves the normal page intact. window.OinkComparePane is available before head hooks for custom scripts. Pane state suppresses shell runtimes and shortcuts, search initialization, comments and theme-managed analytics, while keeping content runtimes and styles. bin/check-compare.py classifies every browser runtime as suppressed in panes, running in panes, or host-only, so a new runtime must declare its pane behavior. With comparison on, production builds render Google Analytics through the theme’s pane-aware template instead of Hugo’s embedded partial; a site override of google_analytics.html therefore does not apply. Frames isolate document IDs; same-origin content remains trusted site content. Panes need frame-src 'self' and same-origin framing. Normal canonical links remain the page identity. Print and machine outputs contain no comparison controls or comparison runtime.

A pane is ready when its bootstrap reports DOMContentLoaded, not when every image or embed has loaded; a slow resource never holds back or discards a readable pane. An invalid document fails at its load event or the 15-second document timeout. The iframe’s initial about:blank load is ignored, but a document still blank at timeout fails. Browser geometry is measured at readiness and invalidated on resize, the frame load event, font/image readiness and disclosure/component layout changes (including td:tab-shown). User input assigns the active pane; programmatic follower scrolls cannot take ownership. Animation frames coalesce updates. Continuous owner scrolling is mirrored instantly per frame, because restarting an eased scroll on every frame stalls the follower; a single discrete jump eases when motion is allowed, and reduced-motion or bookmark restoration uses instant positioning. Closing or replacing a pane aborts its listeners, observers and load timeout. Native dialog inertness contains host focus; child keyboard handling preserves nested component dialogs. The reader’s history entry survives a reload or a back/forward restore. The entry runtime binds restoration to that entry’s token and verifies it again after loading the reader; leaving the entry cancels the pending restore. Reopening preserves the token and does not write history. History traversal finishes processing an explicit close or queued user reopen before automatic restoration runs. Moving the language menu across the narrow breakpoint preserves its focused control and open state after updating pane visibility and inertness.

Release states

Keep local validation, module tagging, consumer pin updates, and site deployment distinct during release workflows.

v1.5.1 review corrections

The v1.5.1 release retains Hugo Extended 0.168.0 and Go 1.27.3. Validation reports must identify the actual toolchain and local module source.

Feeds sort by descending date before applying services.rss.limit, and undated items omit pubDate. RSS summaries and page feeds can share a page Store within the RSS pass. Summary rendering keeps that Store in RSS format state instead of restoring stale HTML state, and preserves manual summary dividers.

Focus indicators use the current preset’s opaque link color. Navbar controls and clipped code/FAQ containers retain visible keyboard focus. Forced colors expose current navigation entries and the footer wordmark. Heading and field self-links remain keyboard-reachable; print links retain their underlines. The local Bootstrap RTL artifact supplies directional rules without replacing the theme’s font roles. Prose list spacing does not apply to component lists.

The configuration schema accepts translation_notice as false or a nonempty language-code string; the owning resolver checks that the language exists. Pagination and breadcrumb labels are translated in all 32 locales. Thumbnail alternative text uses the page title without an English prefix.

7.2 - Component contract

The maintainer contract for OINK authoring primitives, validation, Book and release behavior, and output degradation.
OINK 1.5.1 contract

This contract describes OINK v1.5.1.

The reader-facing Components section provides tutorials and complete examples. This page defines the API and behavior used by those guides.

Authoring model

Use ordinary Markdown when one block with attributes can express the component. Use shortcodes for compound bodies or information that Markdown cannot represent. There is no parallel component registry. Native forms require:

markup:
  goldmark:
    renderer: { unsafe: true }
    parser:
      wrapStandAloneImageWithinParagraph: false
      attribute: { block: true }

Only {{%/* steps */%}} uses percent delimiters because its body belongs to the page outline. Other shortcodes use angle delimiters. Compound bodies use content/render-block.html with a unique ID scope. Shortcode and component captions, labels, titles, and names are plain text. Put Markdown in bodies. Landing narrative fields have their own contract. Icons use one Font Awesome class pair. Components expose safe classes and attributes. They do not expose arbitrary colors or inline styles.

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; reader with .expand
Book target image/table/passthrough/fence + {num=} fig, tbl, eq, eg none
Release assets checksums data fence release-assets copy in HTML
Math and chemistry passthrough, math, chem fences eq none; build-time rendering and local styles
Diagram/data mermaid, plantuml, markmap, echarts, infographic fences none selected local runtime only
Unified diagram reading in OINK 1.4.0

Mermaid and local PlantUML share pan/zoom, fit, source viewing/copy and standalone SVG download. Engines load and render only for measurable diagrams within 320px of the viewport. Each engine loads once; both render serially on the main thread. Hidden content waits until revealed, and obsolete theme results are discarded. The viewer reuses rendered results, retains reading position, and supports touch, keyboard, focus restoration and reduced motion. PlantUML retains Blob-image isolation: its SVG markup never enters the live DOM. Source is escaped exactly once, preserving arrows, quotes, ampersands and Chinese. Errors keep source without blocking subsequent diagrams. Copy/export failures show feedback; downloads report initiation only. Exports embed styling and reject external resource dependencies. Ordinary browser printing keeps light Mermaid and PlantUML pictures; dark Mermaid and unfinished diagrams use black-on-white wrapped source, ready without asynchronous print-time rendering. Source disclosures return to their reading state after printing; rendered results and preferences are preserved. Dedicated Print, Markdown and RSS retain source fallback. See Mermaid and PlantUML. Explicit server mode retains its existing behavior.

Optional component presentation

OINK 1.4.0 supports the following opt-in presentation variants. See the presentation examples.

Component Native modifier Full form
Divided card list {.cards .td-cards-list} cards variant="list"
Compact steps {.steps .td-steps-compact} steps variant="compact", with percent delimiters
Command strip A one-line fence with {class="td-code-command"} Existing code renderer; no new shortcode

Unrecognized variant values warn and fall back to the default layout. A command strip with multiple lines, line numbers (including site defaults), or collapse warns and uses the regular block, preserving every source line. Existing copy and wrap policies still apply. No new runtime or component registry is introduced.

Native and full cards share surface and title rules. Only cards with links receive hover emphasis; secondary body links remain operable above a stretched title link. Tabs have transparent individual backgrounds at rest, a subtle hover fill on hover-capable devices, and a thin underline plus link color for selection. Labels keep the same weight and position between states; keyboard focus remains a distinct outline. Native disclosures retain an independently operable summary. Copy controls preserve their target geometry and existing success/error semantics. Coarse-pointer copy and tab controls have at least 44px height.

Preset colors and font roles remain authoritative. Ink/Terminal keep flat surfaces and square/2px geometry. Print, RSS, Markdown, and LLMS keep the content and their existing output semantics; presentation adds no hidden content or extra controls to static output.

Field names use unboxed, emphasized monospace text in all four presets, including Terminal, for both Markdown tables and full shortcodes.

Validation

Invalid author input follows the architecture contract. Warn, then use the documented safe fallback or omit the component. Do not combine named and positional forms. Book target IDs match [A-Za-z][A-Za-z0-9_.:-]*. Book numbers match [0-9A-Za-z.-]+. Classes use token validation. Hooks and shortcodes share one page target registry so collisions cannot produce duplicate output IDs.

URLs use content/url.html. Raw backslashes are invalid because browsers can interpret them as URL separators. Images resolve through page resources, section resources, global assets, then static or explicit remote URLs. Local rasters include intrinsic dimensions. SVG, static, and remote sources remain valid but cannot use Hugo image operations. Resource metadata alt must be a string. Invalid values warn and are ignored, preserving authored alt text.

Empty tabs warn and emit no container in HTML, Print, Markdown, or RSS. An xref without a usable target or with a missing target page preserves its text without a link. If fig supplies both caption and title, the warning and all outputs prefer caption. Generic attributes on block mathematics do not enable Book numbering. Without num, id and caption warn and are ignored; the formula stays unnumbered.

Component behavior

Callouts and tabs

Callout types are note, tip, important, warning, caution, success, danger, question, example, quote, and details. - makes the callout initially collapsed. + makes it expanded. Unknown types render as plain blockquotes, with their markers preserved and no JS.

Adjacent tabs group only when consecutive and of the same block kind. group enables the hash #<group>-<value> and storage td-tabs:v1:<group>. Ungrouped tabs use neither. In OINK 1.4.0, an initial fragment or hashchange targeting a panel or a heading within it opens all ancestor panels from outside in before scrolling to the target. Hash selection takes priority over saved preferences and synchronizes named peers, but does not overwrite storage. Only explicit reader selection persists; ordinary headings outside tabs retain native navigation. HTML exposes all panels before JS. Print expands them. Markdown keeps authored source. RSS provides the rendered text summary. Grouped tab IDs and local links remain unchanged in HTML and single-page Print. Repeated groups and nested tab containers keep unique IDs within each page. Multi-page section and whole-Book Print prefix generated tab container and panel IDs with the source page namespace and update their local links. Reusing a group across chapters must not send a tab link to another chapter. Cross-page links to ordinary HTML keep their destination. Fragment rewriting accepts normal HTML attribute syntax and preserves text inside other attributes. The full form supports arbitrary Markdown. tab.label is required. value is required exactly when the parent has group. Orphan tab shortcodes warn and render nothing.

Steps, cards, fields, and tables

Native steps accept ordinary block content. Use shortcodes only when steps must contain percent-delimited containers. Native cards use link lists. The full form adds bodies, badges, icons, and images. Native fields use the first column for names, the last for descriptions, and middle columns for metadata through meta= or headings. The full form accepts block descriptions. Use card and field only within their parents.

Field anchors use field-<name>, with lowercase punctuation runs replaced by hyphens. Thus params.ui.typography becomes field-params-ui-typography. Duplicate anchors get positional suffixes.

The table hook owns responsive wrapping and captions. Captions have balanced inset spacing inside the frame, aligned with cell text; an available corner action reserves space at the end of the inline caption. .matrix makes the first column row headers; .full-width widens the outer wrapper to the article width. On screens, .wide opts technical tables into intrinsic sizing inside that scrollable wrapper, preserving identifiers with overflow-wrap: break-word and word-break: normal. Body cells have a soft max-width: 40ch bound so prose wraps. Ordinary tables retain their existing layout. The modifier has no effect on .fields, which renders a definition list, or on static and print output. .fields cannot combine with matrix, full-width, numbering, or tabs; numbering and tabs are also mutually exclusive.

.wide in OINK 1.4.0

The short author marker retains the technical-table layout introduced in 1.3.1 and compatibility with existing markup. It composes with .matrix and .full-width; Markdown preserves the authored marker.

Table reading

.expand in OINK 1.4.0

Explicit opt-in for theme-rendered Markdown tables, independent of .wide.

Only .expand tables receive an HTML reader frame and load table-reader.js. The runtime adds a direct top-right corner action, revealed on hover or keyboard focus and visible on touch and in forced colors. It adds no above-table toolbar, global setting, automatic size detection, or whole-table click handler. .fields .expand warns and ignores expansion; layout markers, captions, numbering, and tabs otherwise compose normally. Goldmark supports {.wide .expand} and {class="wide expand"} with block attributes enabled and no intervening blank line.

One native modal moves the original table and scroll wrapper, keeping IDs, links, selection, text size, row order, and native table semantics. Its title receives initial focus; Tab stays inside; Escape and Close restore the original nodes, inline/page scroll positions, and entry focus. Coordinated closes suppress focus return. Plain captions remain native; numbered captions name the dialog and any otherwise unnamed table while open. Single-row headers stick inside the viewport; .matrix retains its existing row headers and pinned first column. There are no sorting, filtering, density, export, or virtualization controls.

Missing JavaScript, blocked assets, or unsupported dialogs leave readable inline content with no dead entry. Tables with references outside their movable subtree stay inline; same-page links to outside targets restore the table before navigation. Before browser printing, restoration is synchronous. Dedicated Print, RSS, Markdown, and LLMS keep their existing content boundaries without reader UI/runtime. The accepted decision records the rationale.

The Markdown image hook is the ordinary image API. Inline images stay inline. Block images become figures with caption or num. Only the native form supports image processing. The full fig source form is a numbered container and deliberately excludes command/options. For processed numbered images, use native block images with num. Allowed image attributes are id, num, caption, width, height, link, command, and options, plus shared safe attributes. command and options must appear together. They use Hugo Fit, Resize, Fill, or Crop on processable local resources. Plain linked images use Markdown syntax. The link attribute therefore requires a caption or number. Linked and decorative images do not load Zoom.

Zoom triggers keep image alt text and the localized preview action in their ARIA accessible name. They insert no helper text into articles. Plain-text or rich-HTML copying must not add preview instructions, even if the editor removes theme styles. Authored images and captions remain intact. When Draw.io and Image Zoom share an image, Edit and Zoom remain separate sibling buttons. The editor control supports keyboards and stays visible on touch devices and in forced-colors mode. The Draw.io editor opens in a modal dialog with a localized host Close button. The button, backdrop, editor exit message, and Escape while focus is in the host dialog dismiss it, release its message listener, and restore focus to Edit. Cross-origin iframe keystrokes belong to the editor and cannot bubble to the host; users can leave the editor’s Tab sequence to reach the host Close button. Opening another editor starts a fresh session.

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. FileTree fence wrap accepts true or false, defaults to false, and changes only screen comment layout: true wraps comments, including unbroken tokens, while rows grow from their existing minimum height. Filenames retain ellipsis. Missing or false values preserve compact desktop/tablet rows; mobile stacking, divider controls, directory folding, and expanded Print output keep their existing behavior. Invalid values warn and fall back to false. Markdown preserves explicit wrap values and tree 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. Mermaid palettes follow light/dark mode, independently of visual preset. The default dark edge-label background is #404040 for AA contrast with its label text; explicit params.mermaid.themeVariables values retain priority.

ECharts uses declarative JSON/YAML. Callbacks use $fn:<name> from window.OinkEchartsFunctions. Embedded scripts are never executed.

In OINK 1.4.0, Infographic SVGs scale with their container width, including orientation and shell layout changes, without repeated DSL rendering. Initially hidden containers wait until measurable. Fence height and full keep their existing meanings. Transparent Infographic output receives a white canvas; an explicit DSL background sets the canvas instead. Site light/dark changes leave authored light, dark, hand-drawn, palettes, and SVG styles intact. The default dark text therefore remains readable without recoloring author content. Authors remain responsible for contrast when explicitly choosing transparent backgrounds or custom text colors.

Markmap node text on screens follows the article text color (--bs-body-color) via --markmap-text-color, dynamically adapting to light/dark mode changes, style presets, and restored user preferences to maintain contrast. Printing retains Markmap’s default dark text on the white canvas. This mapping is scoped to article content across both the wrapper and generated SVG, and can still be overridden by site CSS of equal specificity or SVG inline tokens. (Added in OINK 1.3.1.)

In OINK 1.4.0, Markmap fits on first measurable display and container resize, including hidden panels when revealed. Reader pan/zoom suspends automatic fitting so layout changes preserve their view. The keyboard-accessible Fit to window button restores the whole map and resumes automatic fitting. Layout fits do not animate; reduced motion also disables node transitions. The control is omitted from print. Source normalization removes surrounding blank lines and common indentation before parsing, preserving YAML options and outline levels.

Mathematics uses Hugo’s build-time KaTeX output and local CSS, without a browser mathematics runtime. HTML and Print use the vendored stylesheet and preserve MathML and authored TeX. Validate mathematics with the current Hugo baseline defined in the architecture contract. Markmap uses the matching vendored KaTeX runtime. In OINK 1.4.0, numbered equations reserve readable formula space based on the available column width, including columns narrowed by a sidebar or TOC. Captions move below the formula when they do not fit beside it; long words wrap. Number-only captions, MathML, and TeX remain available. Print gives the formula the full reading width and places the caption below it. Long captions must neither squeeze the formula to zero nor widen the page.

Swagger and Redoc accept HTTP(S) specification URLs or paths rooted under static/. Neither resolves page resources. Redoc handles paths with or without leading slashes identically and joins local paths to baseURL. Only HTML is interactive. Print, Markdown, and RSS provide static specification links.

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. Their default ID is <kind>-<num>. eg requires a caption. eq without num is an unnumbered display formula. xref names exactly one kind with optional page/anchor, or an anchor with explicit text. Numbered examples have one framed body and caption.

Footnotes belong to the page document. Native numbered tables and fences keep them there. Shortcode bodies are separate Goldmark documents, so footnote references in tbl, eg, fig, card, tab, field, or include warn and remain literal. The check ignores text that has code syntax.

book-toc follows navigation order at depth 1–3. Each of the four book-* indexes collects one target kind. Single-page Print keeps ordinary heading, footnote, and tab IDs exactly as regular HTML renders them. Multi-page section and whole-Book Print rewrite cross-page links and namespace page-local headings, footnotes, and generated tab IDs to prevent collisions. Local heading links match after one URL-fragment decoding step, so ordinary Markdown links to Unicode headings retain their target in an aggregate. Explicit target IDs remain intact. Consumers opt into these potentially expensive aggregate outputs. Consumer-authored raw HTML anchors and custom shortcode IDs remain global by default. Add the boolean data-td-print-local attribute to an element with an id to declare a page-local anchor. An aggregate namespaces that ID and its same-page links; ordinary HTML and single-page Print retain the original ID. This lets legacy aliases and custom shortcode headings keep their existing deep links while remaining unique across aggregated chapters. Unmarked authored IDs must remain unique within their aggregate.

Release and download

Release front matter uses one release_url matching https://github.com/<owner>/<repo>/releases/tag/<tag>. The URL supplies owner, project, and tag. The page supplies the date. No remote release state is fetched. Removed release, release_products, and release_group_by_product warn with their replacements. They provide no compatibility path. The section index lists every page. It uses parsed project tag when available, otherwise the page title. For layout: releases, HTML keeps this release index; section Print aggregates the full release bodies with chapter-local IDs, in both development and production builds.

Checksums accept canonical lines or one source resource, never both. Filenames cannot be paths. HTML adds local copying. Static outputs show complete 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, while pinned channels show pending. Markdown renders the complete channel list. RSS omits the component.

Switching tabs remeasures collapsible code blocks in the newly visible panel. Their expand and collapse controls remain usable after fonts settle and when ResizeObserver is unavailable.

Verification

The architecture contract defines shared output rules. Component-specific exceptions appear above. Markdown and RSS set no browser runtime flags. Print keeps 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 features. See the migration contract for migration rules.

v1.5.1 review corrections

The following corrections shipped in v1.5.1 and are part of the current consumer contract.

  • Named-parameter shortcodes reject positional arguments with warnings and continue the ordinary build. Invalid input checks require a successful build and verify the safe output, rather than accepting either exit status.
  • Invalid or missing Book numbers retain unnumbered readable content, without empty fig-, tbl-, eg-, or eq- targets or figure-index registration.
  • Generated IDs include the complete shortcode ancestry. Independently rendered Markdown bodies namespace generated code, chart, FileTree, and field IDs along with their references; authored code IDs remain intact.
  • RSS renders static HTML for Book figures, tables, equations, examples, links, contributors, specification links, recordings, and checksum tables. It does not expose their Markdown source or browser controls. Release assets retain their documented RSS omission. The RSS check uses the ordinary section feed.
  • Copy feedback uses aria-disabled with an operation guard, preserving keyboard focus while preventing duplicate clipboard writes.

7.3 - Shell and navigation contract

Navigation authorities, immersive blog presentation, search, actions, taxonomies, indexes, and page-end composition.
OINK 1.5.1 contract

This contract describes OINK v1.5.1.

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 adds another menu or page tree. One menu child level is interactive. Deeper levels warn and flatten below linked group headings. External links use target="_blank" rel="noopener noreferrer". Internal links retain language and deployment subpaths.

Desktop and drawer navbar views use the same tree. Every dropdown has one moderate-width column of icon and title rows. The mega panel and its columns menu parameter are retired. Configured columns warns and keeps one column. Menu descriptions remain configuration data only. The link tree stays centered when space allows. At phone widths it reserves the brand and utility controls’ widths before centering, so a longer menu cannot overlap the search control. It uses text links from lg and icon links below lg. Between lg and md, the end edge keeps search, version, language, theme, and GitHub controls without a menu button. Below md, appearance stays beside search and the drawer entry; version, language, and GitHub move to the footer dock. Home and explicit Landing pages add a drawer control beside search, opening the complete labeled tree. Shell pages with sidebars use the sidebar drawer control in that position. Explicit Landing layouts use only the Landing drawer, including pages whose content type is Book or Docs. A page never renders both drawer controls. The navbar loads its drawer runtime even when a site supplies a custom homepage or Landing body without the theme’s section renderer. No drawer button appears from md upward. The stock 404 page has no reading sidebar and never advertises its drawer, even when a root cascade assigns it a Docs or Book type. Adding a custom type to shell_types recognizes its shell capabilities; it does not create a sidebar in a custom template. Full-width custom pages without a sidebar set sidebar_enabled: false. Language links target the page translation or the language home. Links stay relative for languages sharing a host/base path. They become absolute only for language-specific baseURLs. hreflang stays absolute and lists only actual translations. It never includes the switcher’s language-home fallback. Paginated blog indexes have their own canonical URLs; later pages omit cross-language alternates because translated archives can have different page boundaries. navbar_autohide applies only to fine pointers from 768px. It never applies at touch or drawer widths. The hidden bar keeps its reserved space, and a pinned bar occupies exactly that space, including its rule. Revealing fades the bar in place without covering resting content. Hero pages use their overlay navbar instead of this policy. The homepage shares their soft boundary: a short background wash below the bar replaces the bottom rule and scrolled shadow.

Sidebar and pager share the navigation root and order. manual_link, build.render: link, dividers, hidden nodes, and placeholders keep their documented behavior. sidebar_icon_policy accepts all, the default, groups, or none. Icons use one Font Awesome class pair. Invalid policies follow the shared warning and fallback contract. At sidebar_cache_limit, both walkers may reuse neutral markup only for the same language, root, and effective settings that affect output. The markup stays visible without JavaScript. The normal shell runtime adds the active path. Book pages with sidebar_headings remain page-specific and bypass the shared tree cache.

Root candidates are linkable, non-divider top-level sections, followed by sections with sidebar_root_for: self. Candidates are deduplicated by URL. Both sources honor sidebar_root_menu: false. Absent or true values preserve inclusion. The current resolved root is appended even if excluded from global choices. Zero entries render no control. One entry renders a static link. Every URL keeps language and deployment prefixes. Dividers and sections with build.render: never cannot be switcher links.

A sidebar_divider leaf keeps its static heading. Divider sections keep children in both sidebar walkers. Their label is not a link. When folding is enabled, they have a real disclosure button. Dividers never become pager targets. Children keep their positions. Use build.render: never to omit the divider section’s outputs without hiding descendants. Breadcrumbs show its label without a link. Search omits it. Navigation JSON promotes its children. Print keeps child documents. Book TOCs keep the group label and child links, but omit headings from the unpublished group body. toc_hide still hides the whole subtree. It is not a grouping option. Explicit navigation keys are independent of language and deployment paths. Rendered links retain both prefixes. The sidebar, pager, and navigation JSON select explicit sections by the resolved page identity. A node belongs to a root when it is that root, a content descendant, or a Docs/Book page that resolves to the same sidebar root. This lets a manual keep files at the content root while the Docs section owns its navigation. The selected node’s children define its navigation hierarchy; they need not be physical content descendants. Custom url values do not change ownership. A tree for Docs does not replace a separate Book root, or vice versa; a nested sidebar root selects its own branch. Roots absent from the explicit tree use the content tree. Only section-root Docs/Book navigation evaluates the explicit tree. Home-root navigation and other shells ignore it, including its warnings. An explicitly empty sections array warns and falls back only where that tree applies. Uncached explicit sidebars mark the current page and expand its authored navigation ancestors from the resolved page identity, even when a node omits url or the page has a custom URL. Listed physical ancestors still expand when the current page is absent from the manifest. Legacy URL and active-path hints remain supported. Both authorities remove toc_hide subtrees. Navigation JSON keeps manual_link_relref as an internal link to its resolved destination, not a page identity.

Book headings and responsive outlines

params.ui.sidebar_headings defaults to false. Book pages show the page tree in the left sidebar and the page outline in the right rail. Set sidebar_headings: true (h2 only) or 2, 3, 4 (deepest heading level) to add headings below the active Book row. Page front matter overrides a section cascade or site setting; explicit false disables an inherited opt-in. This setting does not disable the right outline or its relocation below xl.

Fixed in OINK 1.3.0

The documentation Book no longer opts into sidebar headings. When enabled, the branch follows the parent row’s navigation depth, reserves its icon and chapter-number columns, and adds one indent. Deeper headings indent again; the same logical spacing applies in right-to-left layouts.

From 1200px, the fixed right rail uses 20% of viewport width, clamped to 286–420px. The flow rail uses 22%, clamped to 340–440px, and still honors --td-shell-toc-flow-w. The reserved column and visible panel share one width. Responsive widths follow the viewport immediately, without width transitions: shrinking the window must not make the article rebound between drag steps. Long labels wrap inside the rail; widening it can bring them onto one line. Below 1200px, outline groups move to the left sidebar when enabled; below 768px, they live in its mobile drawer. Pages with sidebar_enabled: false keep the rail hidden below 1200px. Browser zoom follows these CSS breakpoints. Collapsing the right rail releases its full width at once at every desktop size; the panel retains its slide and fade.

Sidebar runtime

Fixed in OINK 1.3.0

With sidebar_menu_foldable: true, each branch controls the visibility of its children. sidebar_menu_compact must not permanently hide leaf entries inside an expanded branch. This applies to cached and per-page trees in both Docs and Book. Non-foldable trees retain compact filtering. Sites still using OINK 1.2.0 can set sidebar_menu_compact: false while keeping folding enabled until they adopt this fix.

Available since OINK 1.1

OINK 1.1.0 provides this disclosure API and explicit hidden-content isolation. Version 1.0.0 does not provide the API.

window.OinkSidebar controls registered tree disclosures and movable TOC, backlink, and taxonomy groups, regardless of their current DOM parent. setExpanded(id, boolean, {source}) returns true for valid targets. It returns false for unknown IDs or non-boolean values. getState(id) returns a new {id, expanded} snapshot or null. IDs are existing aria-controls region IDs. The API cannot change arbitrary elements outside registered OINK groups.

Sources are user, active-path, responsive, and api, the default. Each writer sets aria-expanded, td-is-open, the localized label, and region inert state before emitting one document event. The event is oink:sidebar-disclosure with detail: {id, expanded, source}. Repeated writes of the same state emit no event. API restoration keeps current-path ancestors expanded. Explicit user actions can still collapse them. Before isolating a closing region that contains focus, return focus to its toggle.

ready is a Promise that resolves to the API after initial hydration and responsive placement. isReady and oink:sidebar-ready also expose completion to late consumers. Sites own optional persistence. Await readiness, read storage inside try/catch, then restore valid region IDs through the setter. OINK owns whole-column collapse, width, and scroll persistence. It does not define a version or locale schema for reader-selected branches.

Desktop collapse and closed mobile drawers make panel content inert and mark the panel aria-hidden. Focus leaves before isolation. Opening clears isolation before focus enters. The panel remains the 16px pointer sensor. The external restore control remains active. Hover, Escape, backdrop dismissal, breakpoint cleanup, and scroll unlocking keep their existing behavior. No-JavaScript fallbacks omit these runtime attributes.

Whole-column TOC collapse also isolates hidden panels. If the collapsing control has focus, move focus to the visible floating restore button. Restoring the column returns focus to its visible column control. When the aside moves into the mobile sidebar, clear former column isolation before the drawer controls interaction. The drawer’s Tab trap includes only rendered, non-inert controls. It excludes hidden and collapsed descendants.

Immersive blog presentation

There is no article type or second shell. Four independent keys enable immersive reading on the ordinary blog shell. Set them on pages or section cascades. Repeat values on the section index when it also needs them:

featured_image: hero
toc_style: flow
toc_taxonomies: false
sidebar_enabled: false

Blog shells omit breadcrumbs by default because articles are standalone pieces. The recipe therefore needs no breadcrumb key. breadcrumb remains an ordinary key that pages or cascades may enable or disable on any shell.

hero uses the shared featured image as a decorative full-width background on single pages and section indexes. Without an image, it renders the normal opening. banner and wash remain single-page modes. The navbar overlays a hero with a contrast scrim and scrolls with it.

toc_style accepts fixed or flow. Flow places a wider rail beside the article and fixes its position only after scrolling. Its resting position aligns with the article info line, or the description if no info line exists. docs-shell.js measures this offset because title wrapping is unpredictable. Without JavaScript, the rail starts at the article’s start. toc_taxonomies: false removes term clouds. Rails with neither TOC nor clouds render nothing. notoc remains the page-level TOC opt-out. These switches do not change bylines, tags, series, pager order, feeds, or page-end composition. The rail disappears below the xl breakpoint.

When scrolling down and the footer enters the viewport, the fixed TOC rail (fixed) shares the visible-footer offset (--td-shell-footer-offset) so its scrollable area ends neatly above the footer, keeping the lowest headings and taxonomy links accessible by wheel and keyboard. Footer resizing and content reflow, including tabs, blog view switches and loaded media, update this boundary without requiring a scroll event. This clearance applies even when the sidebar is disabled (sidebar_enabled: false). With no footer the offset is zero, while toc_style: flow relies on its own sticky boundary. Mobile drawer placement and print visibility are unchanged. (Added in OINK 1.3.1.)

Search, actions, and runtime

params.offline_search enables a local index per language. When enabled, it also builds under hugo server by default. Use offline_search_on_serve: false for large editing cycles. HTML search appears on Home, shell pages, and Landing pages with landing_search enabled. Other non-shell pages and Print omit the dialog, Lunr, and Palette.

Search metadata uses search_keywords, search_boost, default 1, and search_exclude. The index includes URL, title, taxonomies, excerpt, headings, description, body/summary, root, section, type, keywords, boost, breadcrumb, and icon. The fixture budget is 2 MiB raw / 512 KiB gzip. Sites may return extra strings from hooks/search-keywords-extra.html. Keywords affect matching and ranking. CJK matches found only through keywords display the description or excerpt, not the keyword list. Body matches keep surrounding text as context.

Local search in OINK 1.4.0

Queries normalize case/Unicode width and split whitespace and CJK/Latin boundaries, up to 24 unique terms, preserving identifier punctuation. A CJK run is a literal substring; no dictionary or remote dependency is added. Literal field matches and English Lunr stemming cover terms across fields; typo fallback applies only to ASCII words of at least five characters (one edit, or two from eight characters). All-term pages suppress partial results. Otherwise coverage is shown and sorted first. A contiguous whole phrase leads within equal coverage; the positive page boost multiplies the strongest-field sum within those tiers. Repeated occurrences of a term in its strongest field add a bonus below 20%, which orders pages within that field without letting them outrank a stronger field; a single occurrence scores the plain field weight. Title/URL break remaining ties. The global result limit precedes existing root grouping and menu group order.

Optional sections entries carry actual rendered id and title; content scope also carries UTF-8 byte start offsets into the single stored body. Read IDs from rendered content, including h1/h6 and shortcodes, instead of reconstructing them from titles or the configured TOC. Index reads temporarily select HTML and restore the previous output context, so RSS/Print traversal cannot poison HTML content. Reduced scopes omit body offsets. Old indexes without sections remain usable. Return one result per page with its best matching section, encoded fragment and nearby text; metadata-only matches may target the page. Preserve language/subpath URLs, exclusions, and scope-specific content. Normalized matches retain the original text in excerpts and highlights. Heading extraction distinguishes id from data-id and ignores script/style content.

The labeled native radio group shows all roots and the page’s FirstSection root as two adjacent scope options, using the index’s existing root authority. Each option has a 44 px hit area and a visible focus ring. Tab visits the selected option; arrow keys change scope, and Tab/Shift+Tab stay within the dialog. Filter before ranking/limiting; another root’s match cannot suppress spelling fallback within the selected root. The default is all roots; the choice lasts only on the loaded page. Scope affects pages only and is hidden in command/choice mode. Counts and partial coverage are visible and live-announced in one footer, alongside desktop keyboard hints. Mobile hides only the hints. Composition suspends delayed and index-load renders as well as Enter activation. No new configuration authority or build step exists. The owning theme search checker and the site’s real-corpus/browser tests cover these additions; the maintained summary indexes keep the 2 MiB / 512 KiB budgets.

Built-in action IDs are compare_translations, 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_preset, switch_theme, switch_language, switch_version, and open_github. Outside the share bar, copy_link appears only in the Palette. Site commands under languages.<lang>.params.ui.command_palette.commands may open safe URLs or invoke built-in IDs. They never inject JavaScript. The legacy clipboard fallback restores previous focus and selection, including selection direction. It does not reclaim focus if another control acquired it during copying.

In OINK 1.4.0, breadcrumbs and page actions wrap onto separate rows when the reading column or enlarged text requires it. Long unbroken path labels wrap without clipping; all breadcrumb links stay available. The action menu is bounded by the viewport and retains keyboard focus and dismissal behavior at 200% text size, including a 320px viewport.

Fixed in OINK 1.3.0

The page-action disclosure closes when keyboard focus leaves its containing controls, including Tab and Shift+Tab. Moving between its controls keeps it open; dismissal leaves focus at the reader’s new destination. ArrowDown on its closed toggle opens the menu and focuses the first action immediately.

Edit, history, and create-child actions require repository-relative source files. Normalize physical filenames and the site working directory to / separators before containment checks. path_base_for_github_subdir matches this normalized path. For local content, it matches relative to the working directory. For external mounts, it matches the absolute path. A string regex removes matches. A {from, to} map may replace them. External sources need an explicit match. After mapping and cleanup, empty paths, ., absolute paths, drive-qualified paths, and paths starting with a .. segment suppress these three actions. Docs and project issue actions remain available under their existing repository settings. Windows mappings must match /, not \. Normalization preserves filename case.

The Palette has empty, text-search, and > command modes. Quick links come from navigation. It provides no history, semantic search, personalization, or remote fallback. Queries stay in the browser. No default telemetry is sent.

OinkSurfaceCoordinator coordinates the Palette, Appearance, Draw.io, Expo viewer, drawer, root, language, and version menus. Each surface controls focus restoration and Escape. Already closed Appearance menus do not restore focus when the coordinator closes them. Keyboard navigation ignores editable controls and modals. The desktop nonmodal Appearance panel permits search shortcuts; its mobile modal sheet and other open dialogs, including fixed-position ARIA dialogs, retain keyboard ownership. Inside the mobile drawer, Escape closes the root switcher first and returns focus to its trigger. A subsequent Escape closes the drawer. Moving focus outside the root switcher dismisses its popover. /, \, f, and c open search or commands. j/k move between headings. q/e move between pages. h changes presentation. l/y, t, and r open language, theme, and root choices. Sidebar WASD/Arrow navigation uses real focus without changing Tab order. Non-link divider buttons participate in tree navigation. From a child, Left/a focuses its parent group. A second press collapses it. Right/d opens closed groups or enters the first visible child of open groups. Previous and next page navigation includes only links, never group buttons.

The outline uses one heading model and computed scroll-padding-top to derive the cursor and visible-heading range. Its SVG line and dot share animated values, preventing drift. Valid URL fragments are decoded. Malformed percent sequences use the literal heading ID. This applies when indexing links and selecting requested headings near the page end. No speculative DOM repair pass is allowed. The normal shell runtime always owns this tracking. params.ui.scroll_spy and page scroll_spy remain quiet compatibility no-ops throughout 1.x. They emit no separate runtime and may be removed only in a future breaking release.

Search-tail extensions

Available since OINK 1.1

This API is included in OINK 1.1.0 and absent from version 1.0.0.

Trusted site JavaScript may call OinkCommandPalette.registerSearchTail({id, rows, activate}). YAML and the action manifest remain data-only. The bundle loads only when local search is enabled. Registration requires a unique ID matching [A-Za-z0-9][A-Za-z0-9_-]* and two functions. Invalid or duplicate registrations throw. The returned unregister function is idempotent. It cannot remove later registrations that reuse the ID. Live changes schedule one owned render. Removal cancels the provider’s pending action.

rows(context) returns descriptors synchronously. Context is a frozen {query, locale, phase, pageResultCount} snapshot. Query is trimmed. Locale is the HTML language tag. Phase is results, empty, or error. Count includes only local page results after the limit. Providers run only for settled, nonempty text searches. They never run in empty, command, choice, or loading states. Rows appear after native results and actions in the localized Actions group, in registration order. Native empty/error messages and input-triggered index retries remain available.

Each descriptor requires a unique per-provider id with the same ID syntax and a nonempty string title. Optional description, icon, and disabledReason values are strings. available is boolean and defaults to true. OINK copies and freezes these fields. Display strings render as text. Invalid descriptors, duplicate IDs, asynchronous returns, or thrown callbacks discard that provider for the render. Other providers remain unaffected. Callback counts are not guaranteed.

activate(row, context) runs only through ordinary row activation. It receives the copied descriptor and original context, with an AbortSignal and handoff() function. While activation is pending, all other row activations are blocked, including entry into native choice menus. Synchronous throws and rejected promises clear pending state, keep the Palette open, and announce the localized action-failed message. Fulfillment values are ignored. Success closes the Palette without taking focus from another surface. Closing, reopening, changing the rendered query, or unregistering cancels pending work. Late settlement cannot modify newer sessions.

Before opening another coordinated surface, call context.handoff(). It closes the Palette without returning focus or aborting the activation. The consumer then owns the new surface’s focus and error interface. Later Palette sessions or unregistration can still cancel unfinished work. Successful completion does not abort handed-off operations. OINK sets no timeout.

rows() must remain pure. This trusted-code contract is not a sandbox. Default queries remain local. No remote provider or telemetry is bundled. Sites own extension network behavior and provider consent.

Desktop nonmodal Appearance permits search shortcuts; mobile modal Appearance and every other open dialog retain keyboard ownership. In the mobile drawer, Escape dismisses the focused root switcher first and returns focus to its toggle; a second Escape closes the drawer. Focus leaving the switcher dismisses it. Search opened from the Landing drawer returns focus to the visible menu toggle. Choosing a visual preset in the Palette keeps it open for the next action.

Share

params.ui.share defaults to empty 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. Page lists replace inherited lists. share: false disables the share bar for that page. Unknown entries warn and are removed. Only regular pages display the bar. Print, Markdown, and RSS omit it.

Targets are plain intent links containing the page permalink/title, plus a local copy_link button. Pinterest media uses the shared featured-image resolver. ChatGPT and Claude receive build-time permalink prompts independently 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. It sends no request until a reader activates a link. It displays one accessible labeled glyph row. share/items.html resolves targets. share/bar.html renders them.

Annotation

annotation-items.html resolves page annotation descriptors. page-meta-lastmod.html renders them. Inline annotation links are underlined so readers can identify them without relying on color, including muted last-modified and upstream attribution lines. You may override either partial 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 and can be inherited through cascades. upstream_link: "" disables it. Other facts resolve from site params → data/upstreams[upstream_source] → front matter: upstream_name, upstream_copyright, upstream_license, upstream_notice, optional upstream_ref, and upstream_modified. A link requires the first four facts. Invalid or incomplete attribution warns and emits no legal notice. Unsupported URLs are refused.

upstream_modified changes the credit verb and links commit history within the existing attribution sentence. The notice page includes complete license and warranty text. params.ui.translation_notice enables translation notices. The page key translation_notice can cascade. Notices skip generated pages and pages without bodies. Natively authored pages can use translation_notice: false to disable them.

Authors and series

Blog article heads show title, info line, term badges, byline, then series strip. The description starts the body below them. article-info.html always shows the date. With reading_time enabled, it also shows word count and minutes. Front matter upstream_link supplies the same per-page fact used by annotation. It adds a localized original-source link, validated by the shared URL policy. Term rows contain badge sequences. The taxonomy name appears on the group label, not as a visible prefix. Inactive badges use pale neutral backgrounds, muted text, and the taxonomy’s term glyph. Linked badges use the current section’s accent background, border, and text on hover or focus. taxonomy-icon.html defines taxonomy and term glyph pairs: folder-open/folder, tags/tag, cubes/cube, users/user-pen, book-bookmark/book for series, and generic shapes. params.ui.taxonomy_icons overrides a pair with one string for both uses, or a taxonomy/term map. Invalid values warn and keep built-ins. Right-rail clouds show the whole-taxonomy glyph only in their heading; repeating it on chips would add noise beside an already announced taxonomy. Their chips therefore contain only text and counts. Standalone taxonomy directory cards show one term glyph. Bylines show only portraits, names, and one-line profile bios. They have no label or date. List rows, cards, and term archives share a metadata line: date, localized author-and-section phrase, then word count and minutes. The same reading_time switch controls these last two values. A wrapping badge line follows, with terms from every taxonomy in alphabetical taxonomy order. Each badge has its term glyph. Cards omit authors because the metadata sentence already names them.

Authors activate only through taxonomies: {author: authors}. Profile term pages define display names, summaries, bodies, and featured-image avatars. Missing profiles use link titles, initials, and archives. authors-resolve.html keeps front matter order for article heads, list rows, and one RSS dc:creator per author. Legacy author stays unchanged when authors is absent. When both exist, authors takes precedence without warning. Custom author-taxonomy plurals remain ordinary taxonomies.

Series activate only through taxonomies: {series: series}. Term pages own the introductions without adding a parameter, data file, cover model, or runtime. Pages use series: [name] with optional series_weight. series-pages.html puts weighted members first, ordered by weight. Unweighted members follow in ascending date order, with Path tie-breaks. The strip and term page share this order. The first named series gets one HTML/print strip. The panel uses translucency and background blur because an opaque surface would obscure the featured image behind the band in hero articles. On plain articles, the tint resolves to the page background, allowing one treatment to serve both.

The summary covers the full bar and trailing caret, while the series name and taxonomy icon remain a sibling link over a hidden width reservation. This keeps nested interactive controls out of the summary. Opening adds a thin rule below the bar and displays reading order in an adaptive grid on the same surface, preserving DOM order.

Each member link aligns its ordinal at the end of a fixed square track so titles keep one edge at every list length. Equal cells stay in one column when narrow and add columns only while titles retain a readable width, allowing desktop panels to use their width without stretching one selected row across the panel. Hover and current-position states reuse the two backgrounds from sidebar navigation; current members also have filled ordinals and heavier titles, so color is not the only cue. Print expands the list in one column. Singleton series and non-HTML outputs omit the strip. Numbering, cross-references, and aggregate outputs remain Book features.

Default article taxonomy chips omit reserved authors and series because dedicated features already show them. Explicit params.taxonomy.page_header restores either.

Blog indexes and page composition

Blog section indexes use params.ui.blog_index. list, the default, cards, and gallery show one flat sequence, newest first, with shared blog_index_size pagination and no year headings. table shows date/title/tag rows; without a layout switch, it lists the complete section without pagination. With switching enabled, it uses the same page as the other layouts. Cards use shared lead images, localized date/author/section metadata, tags, and three-line summaries.

Taxonomy pages such as /tags/ and /authors/, and their term pages, share shell/taxonomy-head.html. Taxonomy heads show a whole-taxonomy glyph in a tinted tile, the localized name, and term count. Term heads show the term title and page count from ui_taxonomy_pages, using the locale’s CLDR plural form. If no breadcrumb appears, a label above the title names and links the taxonomy, since enabled breadcrumbs already provide this context. Generated taxonomy breadcrumbs use the head’s localized label, not Hugo’s plural title. Below the head, shell/taxonomy-cards.html lays out one-line term cards. They use the rail cloud’s order: most-used first, with alphabetical ties. auto-fill creates equal columns, preventing short taxonomies from stretching two cards across the page. Each card contains the term glyph, term, and page count. The whole card is linked. Author cards instead lead with the byline’s small portrait, using the same avatar partial. Cards include no description or newest page because the title and count already identify the term; another line only blurs the grid. There is no filter-chip row or “All” chip because the section root is already in the sidebar and navbar. Term pages stay row lists. Author profiles keep their own head.

Taxonomy and term rails start with shell/taxonomy-switcher.html. Each declared taxonomy has one row with its whole-taxonomy glyph, localized name, and term count, linking to its index. The current taxonomy uses the selected background. The switcher connects taxonomy pages because cloud chips link to terms and cloud headings only collapse. Sites with one taxonomy show no switcher. The switcher and clouds share the toc_taxonomies switch. Taxonomy pages scope clouds to the whole site (taxonomy-root.html returns no root for this page kind) and omit their own cloud because its terms already appear in the cards. Term pages keep section scope and all clouds.

params.ui.blog_index_toggle renders all four forms for the current paginator slice and lets readers cycle between them: list, cards, table, then gallery. The fourth form is included in OINK 1.4.0. The configured form controls initial display. Hidden forms load no images. Stored reader preferences apply only to indexes that publish all four forms. Sections without toggles publish one form and always show it. Front matter or cascades override site mode per section. Tables published without toggles remain complete, unpaginated archives.

blog_index: gallery presents each post as a linked cover. It uses the shared featured-image resolver, the same date order and blog_index_size paginator as rows and cards, and preserves manual_link. It does not change the content type to Expo. Clicking a cover navigates to the article; no image viewer opens.

params.ui.blog_gallery accepts mode (wall, cards, or seamless, default wall) and size (integer CSS pixels, 160–360, default 240). A page or cascade blog_gallery block replaces the site block. Missing or invalid fields use their built-in defaults; invalid values warn. The gallery toolbar changes mode and size independently of Expo albums, using td-blog-gallery-mode and td-blog-gallery-size storage keys. The outer view retains td-blog-index.

Wall uses Expo’s justified row geometry with 6px gaps; Seamless removes the gaps. Cards uses columns filled from the shortest column. Covers keep their proportions within bounded tile ratios and are contained rather than cropped. Below 500px of gallery width, Wall and Seamless bound tile ratios more tightly to keep the caption columns readable, including for very tall images. Processable covers larger than 960px receive a preview within a 960 × 960 box; GIF previews are still images. No build-time remote fetch is introduced.

Wall and Seamless reveal titles and descriptions on hover or keyboard focus. Without hover, captions remain below images, and row spacing includes their height. Cards always shows captions. Covers without images retain a linked, named placeholder; failed images receive the same fallback after enhancement. The title names the link and the cover is decorative. Hidden forms use lazy images. Reappearing forms, resized sidebars, fonts, and late image dimensions trigger layout without overlapping items. Without JavaScript, a responsive grid keeps links and captions visible and hides the inactive controls.

params.logo always supplies the brand mark. params.wordmark, or the site title, supplies text hidden at compact widths. Docs, Book, Blog, and Swagger share one shell model. Page-end order is Share, Feedback, Annotation, Pager, Comments. Docs/Book pagers follow sidebar preorder. Blog pagers use weight, then reverse date. pager: false disables the pager. Static outputs omit pager controls.

Every rendered footer has an icon-only utility dock at the end of its bottom line. Dock order is version, language, theme, then keyboard help. Menus open upward. The version control never shows the current branch or release label. The fat footer’s collapse chevron follows the dock. Below lg, copyright, center content, and dock become three centered full-width rows, with the dock last. These global controls do not appear in sidebar footers. 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 configuration. Feedback sends only docs_feedback through an existing gtag and stores the selection locally. It does not replace Giscus.

An explicit params.ui.quick_links: [] disables quick links instead of restoring the defaults. Invalid sidebar_root_link_self values warn and keep the documented true fallback. Taxonomy and term pages use the shared page_width validator.

Translation comparison

params.ui.compare: true adds an icon-only Open duo view entry, whose glyph differs from every shell control’s, beside the page actions and a localized Duo View action (compare_translations) in the Command Palette on Docs, Book, and blog pages that have an actual HTML translation on the same origin. The reader title, its Sync Scroll switch, close icon and sync notes use the same catalog. The entry exposes its label as its tooltip and accessible name. When keyboard navigation is enabled, B toggles the reader: it opens it from the page and closes it from the toolbar or either pane, under the page shortcut’s guards (no modifier, composition or editable target). The entry and Close declare aria-keyshortcuts, and the help panel lists the shortcut under the view’s name only on pages with a real target. The default is false. This is one site setting; there is no page-pair data, new layout, or output format. A language that sets it to false offers no entry and is never a target. Navigation’s language-home fallback is never a comparison target, nor is a translation whose build policy writes no HTML (build.render: link or never, including cascaded policy). Blog archive pages after the first offer no entry, palette action or shortcut, because they have no counterpart.

The reader fills the viewport with two ordinary pages. The current page is fixed on the reading-start side, so a right-to-left host mirrors the pane order. The target selector uses existing language order and the same icon, rounded trigger, floating menu style and selected and hover option states as the site’s language bar. Its menu supports pointer and keyboard selection. Only the selected sibling is loaded, and replacing it preserves the synchronization preference. Each pane keeps its language, direction, footnotes and content components.

Below 900 CSS pixels, one toolbar row and one switcher row show one retained pane at a time. The title truncates while Sync Scroll and Close stay at the end. The switcher row holds the language buttons and, with more than one target, an icon-only target-language menu, so pane headings are not repeated. A single target is a plain label in the pane heading and the switcher, never a one-option menu. Short viewports tighten both rows: at 320 × 256 CSS pixels (400% zoom) at least 160 pixels of text remain. Forced-colors mode marks the selected pane with a border. Crossing the breakpoint moves the language menu while preserving its keyboard focus, open state and selected pane.

Shared visible heading IDs determine linked scrolling, using interpolation between matched headings. Unmatched headings are skipped. Sync Scroll is a labelled switch (role="switch"): while panes load it keeps the reader’s preference and is disabled, and it reads off when sync is unavailable or a pane fails. A partial match anchors the estimate between matching headings; with no common headings the reader falls back to a bounded beginning-to-end document-progress estimate. In either case an orange exclamation mark beside the switch, without a badge, names the case in a note shown on hover, keyboard focus or tap; Escape dismisses a visible note before it closes the reader. Matching and usable geometry are separate facts: a matched heading that cannot scroll to the reading line adds no anchor without counting as a mismatch, so it never raises the partial-match note. When no shared heading can anchor the map at all, the note says that sync follows overall reading progress. Duplicate or reordered matches are ambiguous, so the switch is disabled and the note explains why; the two panes remain independently readable. While the owner scrolls continuously (trackpad, momentum, a held key), the follower tracks it instantly on every frame; a single discrete jump eases when motion is allowed. Reduced motion and bookmark restoration always position instantly. Turning off Sync Scroll restores independent scrolling; synchronized scrolling aligns sections by heading anchors rather than locking individual paragraph rows. Opening resumes the current passage after reflow. Focus starts on the dialog title so the view is announced; until the reader Tabs into a pane, PageUp, PageDown, Space, the arrow keys, Home and End pressed there scroll the reading pane (the visible one below 900 pixels) and sync follows. Closing, Escape, B or Back restores host scroll and returns focus to the element that had it when the reader opened, such as the reading link where B was pressed, falling back to the entry when that element is gone or hidden. Only an open language menu or a visible sync note consumes Escape before the reader does. A reload, or a back/forward restore, onto the reader’s history entry reopens the reader without adding a record, so one Back closes it and Forward can reopen it. That restore brings back the selected target, Sync Scroll, the visible pane and both reading positions, which the reader keeps on its own history entry once scrolling settles. The last explicitly chosen target language and Sync setting are also remembered in this browser for later openings; without storage the first target and Sync on apply. Back also cancels a reload restore whose reader script is still loading. A pane-local modal consumes Escape before the outer reader does. Fragment links stay in the pane without adding history and, like a native fragment jump, reveal the hidden tab panels that contain their target; another document exits comparison; external links open outside it. A localized loading note covers a pane until its article is readable. The iframe’s initial about:blank load is ignored while the target request is pending; a document still blank at the 15-second timeout is treated as failed. A page that cannot be loaded shows a localized Retry button and a direct link; choosing the same language again also retries it.

The accepted decision records the rationale and boundaries. bin/check-compare.py, the theme’s scroll geometry tests, and the site’s test:compare browser suite own verification. The dedicated eight-language fixture is synthetic evidence, not a downstream adoption claim.

Verification

bin/check-navigation-contract.py, bin/check-shell.py, JS tests, output goldens, and consumer browser tests verify these contracts. They cover navigation, language/subpath links, blog variants, page-end order, keyboard behavior, accessibility, and responsive layout.

Appearance control

The navbar and footer dock share one click/keyboard disclosure. The Landing mobile drawer also provides a labeled Appearance row. The panel offers native Style radios when preset_menu allows a choice and native Light/Dark/System radios when dark_mode.show_menu is enabled. Selection is immediate and keeps the panel open. Enter, Space, or ArrowDown opens it; arrow keys select within a group; Tab moves between groups; Escape closes and returns focus. Sun/moon trigger icons show the resolved current state: sun for light and moon for dark, including changes while following the system.

The English group labels are Style and Light. Style options are independent buttons in a two-column grid, with a colored page, layers, pen-nib or terminal icon and the preset name. There are no Aa previews or experiment badges. The site default is identified in its tooltip and accessible name; selecting it clears the saved preset. A tinted background and border show selection, and keyboard focus has a separate outline.

Desktop uses a non-modal dialog anchored to the trigger; outside press or focus leaving closes it. Below 768 px and inside the Landing drawer, showModal() opens a bottom sheet in the browser top layer with a close button and 44 px option targets. Closing the sheet preserves the underlying drawer. The surface coordinator closes unrelated popovers before opening. The t shortcut still toggles light/dark through switch_theme; switch_preset is the separate command-palette choice.

Fixed in OINK 1.3.0

A queued close event from an earlier appearance dialog must not clear the expanded state of a newly reopened dialog.

The Ink and Terminal experimental presets require explicit configuration; preset_menu: true continues to offer Paper/Slate plus the site default. Both reuse the same state, keyboard, command-palette and bottom-sheet mechanisms. Terminal compacts desktop navigation rows, while prose and mobile touch targets retain their sizes.

Expo albums

The new type: expo reuses this shell and adds image-album presentation. The Expo contract owns its content model, controls, and outputs.

Local component surfaces

OINK 1.4.0 aligns search/command rows, sidebar corners, and Appearance controls with the component geometry. Selected and hovered rows remain distinct, keyboard focus remains immediate, and preference storage, navigation, and palette selection are unchanged. No moving-highlight runtime is added.

v1.5.1 review corrections

These corrections shipped in v1.5.1 and are part of the current consumer contract.

  • Invalid manual_link_relref values warn and use the page permalink. Sidebar, section index, and navigation JSON share resolution, including deployment subpaths. Missing references must not stop an ordinary build.
  • Navigation JSON IDs and section-index lookup keys remove the deployment prefix before the language prefix. Explicit section order and cross-language identity remain stable with or without a directory for the default language. Published links retain both prefixes.
  • Version menus and commands share URL resolution. Page links remove the current deployment prefix before joining the target version URL, retaining the language path without duplicating the old version directory.
  • Blog indexes own their pagination: localized navigation contains ordinary links with aria-current, and head prev/next point to list pages. The article-sequence pager is omitted on blog lists. Archive pagination and its head links apply only to HTML. Print emits one complete section aggregate, without repeating it for each HTML archive page.
  • All navbar-only pages, including immersive blog articles, expose the mobile site drawer. Language, version, and repository links remain reachable there. Closed sidebar drawers cast no shadow into the viewport.
  • The groups icon policy includes the navigation root, top-level entries, and entries with children in both tree walkers. Deeper leaves omit icons.
  • A rail with no TOC, taxonomy output, or backlinks renders no empty panel or controls. Taxonomy-only rails retain collapse/expand controls labelled as sidebar actions, rather than TOC actions. Book TOCs suppress the browser’s list counter alongside authored numbering.
  • Escape dismisses language, version, and utility hover menus and returns focus to the trigger without reopening the menu. Copy, feedback, comparison loading, and Expo boundary controls preserve the focused element.

7.4 - Landing contract

The maintainer contract for landing data, the built-in section registry, language resolution, runtime, accessibility, and outputs.
OINK 1.5.1 contract

This contract describes OINK v1.5.1.

The architecture and component contracts define shared rules. The migration contract defines migration rules.

Shell and data

Any regular page may use layout: landing. It renders the navbar, a full-width content area, and footer without docs sidebars or TOC rails. The homepage can still use data/home/<lang>.yaml with the same renderer.

Non-home pages resolve sections in this order: inline front matter, data/landing/<key>/<lang>.yaml, exact-language entries in data/landing/<key>.yaml, then English or unsuffixed local data. Landing never fetches mutable facts. Commit or generate stars, prices, screenshots, and avatars before Hugo runs.

params.ui.landing_search defaults to true. It enables the existing local Palette only when offline_search is enabled. params.ui.github_stars and params.ui.alt_site are optional local interface 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 an explicit local partial. Authors provide unique IDs. OINK converts them to anchor-safe values. Unknown types warn and use safe fallbacks. They never disappear silently. landing/ partials define built-ins. Removed home/ partial names are not an API.

preview displays Markdown source beside its RenderString output using site hooks. The content registers the same runtimes as docs content. The source pane uses Chroma and a file name, defaulting to page.md. Markdown output uses a four-backtick markdown fence. RSS omits it. Pane labels use theme i18n.

hero.align accepts start or center. Center alignment supports only text. Combining center alignment with an image warns, falls back to start, and preserves the image. download uses the shortcode’s data/download/<key>.yaml schema. It adds 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 - converted to _, then <field>_<primary language>, then the unsuffixed field. camelCase aliases are not accepted. Narrative fields use site hooks to render inline or block Markdown. Values reused as accessible names become plain text. Section text is site data. Only theme controls use OINK i18n.

Each built-in Markdown render has a stable section, item, and field scope. Repeated bodies and marquee copies therefore get distinct generated code IDs, including their copy controls, titles, viewports, and line links. The scope does not depend on render order or output order. Explicit code IDs remain unchanged; repeating one in the assembled Landing output warns and keeps the page available. Custom partials that reuse the Markdown helpers should pass a distinct stable scope for each rendered position.

Block Markdown bodies use the shared .td-content component scope and register the runtimes their rendered content needs. Code folding, tables, images, and other content primitives retain their normal styles inside a Landing section. This scope belongs to the body, not the surrounding marketing headings or grid.

Interactive HTML sets hasLanding, which conditionally adds only landing.js. The runtime uses OinkSurfaceCoordinator for reveal, count-up, copy, compact-menu, and theme-image enhancements. Server output stays complete without JavaScript or if the Landing script fails to load. Reveal candidates are initially visible; only an installed observer may mark one as pending entrance animation. The server renders metrics with configured number formatting, prefixes, and suffixes, and the count-up’s final frame uses the same display.

Landing code plates accept keyboard focus in interactive HTML, including capabilities code visuals and code-plate line lists. Readers can tab to long lines and scroll them with arrow keys without relying on a browser’s implicit scroll-container focus behavior. Static Print output adds no focus target to these line-based renderers.

Marquee duplication uses only CSS. The duplicate is aria-hidden and inert. A localized checkbox preserves pause state without JS. Reduced motion disables animation. Forced colors preserve controls. Theme images follow the shared theme event. The navbar mega panel and columns menu parameter are retired. Menus with columns warn and keep one column. Compact menus use real links and buttons, trap no focus, and do not duplicate the desktop tree.

Outputs and compatibility

Output Contract
HTML Full static sections plus progressive enhancement
Print Static grids and content; controls removed
Markdown Headings, prose, lists, tables, and code without theme classes
RSS Valid RSS document when configured; Landing sections omitted

Non-HTML outputs set no Landing flag and load no Landing runtime. An enabled RSS output uses the shared feed renderer and matches the feed URL advertised in the HTML head. Section and home feeds keep ordinary child-page summaries; an explicitly enabled regular-page feed may have an empty channel. Omitting Landing sections never means emitting an empty file or advertising a missing feed. Root-relative links and assets honor deployment subpaths. Normal builds download no images.

Removed 0.4 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.

Visual presets

Paper removes the hero grid and glow, uses warm shadows, Plex Sans display headings, and a link-colored primary action. Slate retains its technical grid, glow, Chakra Petch headings, and original primary-action colors. Shared section geometry and density remain unchanged. The mobile drawer includes the shared Appearance sheet; see the shell contract.

The explicit Ink/Terminal experiments also remove the grid, glow and shadows. Ink uses heavy Inter headings, square cards and a red primary action. Terminal uses mono headings, 2 px corners and an amber primary action with a static cursor-shaped decoration. Neither adds animation or changes section columns.

Local action refinement

In OINK 1.4.0, button press feedback moves only the background; labels and hit targets stay fixed. Command-box copy labels reserve space for both idle and localized success text. The existing clipboard promise still controls success, and the copy action, live status, data model, and static-output boundary are unchanged.

v1.5.1 review corrections

These corrections shipped in v1.5.1.

HTML and text outputs share validation of record arrays for items, actions, and pricing tiers. Malformed records warn and are skipped. Missing optional pricing features and invalid comparison cells do not stop Markdown builds.

Landing links use the shared content URL policy before language and deployment prefixes are applied. Fragment-only and query-only links remain on the current page. Unsupported schemes warn and lose their link target. HTML and Markdown follow the same link semantics.

Only the first marquee row is exposed to assistive technology; repeated rows and duplicate groups are inert and hidden from the accessibility tree. Landing sections keep their fluid padding without legacy default-page section padding overriding it. FAQ focus indicators remain inside the clipped summary.

7.5 - Expo layout contract

Define album resources, display modes, image viewing, shared appearance, and static outputs.
OINK 1.5.1 contract

This contract describes OINK v1.5.1.

This page defines required behavior. Use Expo albums to create content. Read the design decision for the reasons behind this model.

Content and navigation

type: expo selects the album layout. A section can apply it to descendants through cascade. One album is a Hugo leaf bundle with index.md and an images/ directory. Image metadata belongs in the album’s resources front matter. Separate content files for each image are not required.

Section pages list child topics and albums. The shared navigation tree supports nested sections, the current language, and the mobile drawer. Expo uses the full available page width and has no right-hand outline. An explicit page-level page_width overrides the width. The top navbar follows the shared site and page settings. Expo does not disable it by default. As with Book, an album without a sidebar_root_for ancestor uses its top-level section, not the home tree, as the sidebar and root-switcher root.

Configuration

expo/config.html resolves the configuration in this order:

  1. Use the page’s expo block, including a block supplied by Hugo cascade.
  2. If the page has no block, use params.ui.expo from the site.
  3. Validate each field. Use its built-in default when the field is absent or invalid.

A page block replaces the whole site block. The resolver does not merge fields from those two blocks.

Field Allowed value Built-in default
images A string pattern for bundled image resources images/*
mode wall, cards, or seamless wall
size Integer from 160 to 360, in CSS pixels 240

Invalid configuration produces a warning and uses a fallback. The size is a layout target; it does not set both dimensions of each tile.

Image resources

expo/items.html selects images and resolves metadata for all outputs. It uses Hugo resource metadata and the shared media resolver. Non-image resources are excluded. An optional params.original must name an image in the same bundle. The build does not fetch remote album images.

Value Rule
Title Use the resource title; otherwise derive it from the filename
Alternative text Use params.alt; fall back to the title when it is empty or absent
Caption and credit Read params.caption and params.byline as plain text
Image order Sort by numeric params.weight, lowest first; the default is zero
Equal weights Keep the resource collection’s name order
Explicit ID Start with an ASCII letter or digit; subsequent characters can also be _ or -
Image anchor Prefix the ID with img-; without an ID, use a hash of the resource name

IDs must be unique within the album. An ID may be a string or a YAML integer. Invalid or duplicate IDs produce a warning and use a filename hash; if that hash is also occupied, the fallback appends the first unused numeric suffix. Renaming a file changes its fallback anchor. Keep an explicit ID and the album URL unchanged when a citation must remain valid. expo.images must be a nonempty resource glob, and params.original must name a bundled image even when its value is false or empty.

Processable images larger than 960 pixels receive previews within a 960 × 960 pixel box. GIFs also receive still previews for pause and reduced motion. The animated source remains available. Original files remain unchanged.

Album cards use the first sorted image as the cover. A section without its own images uses the first available image from its descendant albums. Cover frames use a 4:3 ratio and can crop the preview. Authors divide large collections into albums; Expo does not paginate image resources automatically.

Layout and controls

Mode Geometry and captions
Wall Rows of tiles with 6 CSS pixel gaps; captions appear on hover or focus on devices that support hover
Cards Columns filled from the shortest column; captions stay visible
Seamless The Wall calculation with zero tile gaps; the same caption behavior

Tile proportions are bounded for very wide or tall images. Each tile contains the complete image, which can leave space inside the tile. On devices without hover, captions remain visible below the images.

The toolbar has a three-cell radio group on one background. A play/pause button and a horizontal size slider follow it. Small and large image icons mark the slider ends. All controls share a vertical center. Albums without GIFs retain a disabled play button. Each icon control has a localized name, a tooltip where applicable, and visible keyboard focus. Cards and album covers use the preset’s radius token; Wall and Seamless tiles stay square. A focused tile shows a two-tone inset ring above its caption.

Appearance controls stay in the shared navigation. The album body has no appearance picker or article action menu. Without JavaScript, image links and captions remain visible.

Reader preferences and appearance

The browser stores the selected mode and size in td-expo-mode and td-expo-size. Only valid values take effect. A valid stored choice overrides the author default. It applies across albums and reloads on the same origin. Invalid or unavailable storage leaves the author defaults in effect.

Display mode and size are independent of appearance. Paper, Slate, Ink, and Terminal use the shared colors, type styles, and corner settings in light and dark mode. A preset change recalculates the grid after typography changes. It preserves the reader’s display mode and size.

GIFs play by default in the grid and viewer. The play/pause control changes animation for the current album and its viewer. The setting is not stored across page loads. prefers-reduced-motion: reduce starts with still previews. The reader can select Play. A change to the system preference updates playback. The first four tiles load eagerly; later grid images use lazy loading.

Layout stability

Expo reserves document scrollbar space when scrollbars consume layout width. An album near the viewport height must reach a stable layout. A scrollbar must not repeatedly appear and disappear as the grid recalculates its height. Opening or closing the viewer must preserve the underlying grid’s usable width.

Viewer behavior

The viewer uses a native modal dialog. Its image area excludes the toolbar and caption area. Let W and H be the original image dimensions. Let A and B be the available image-area width and height, in CSS pixels.

Action Scale
Open or reset min(1, A/W, B/H)
Expand to fit min(A/W, B/H)
Zoom in Current scale multiplied by 1.25, within the zoom limit
Zoom out Current scale divided by 1.25, within the zoom limit

The smaller ratio is the limiting edge. Both automatic sizing actions contain the whole image and have no image-area scrollbars. Reset keeps a small image at 100%. Expand can enlarge it. Expand does not use the browser Fullscreen API. Only manual zoom can exceed the image area and enable scrolling.

The percentage uses source pixels as CSS pixels. Zoom preserves the viewed center. Reset centers the image. Changing images resets size and position. Previous and Next stop at the album boundaries. A boundary key leaves the current scale and position unchanged.

Pointer and keyboard input

Hold the primary mouse button and drag to move an enlarged image. The image follows the pointer and stops at its bounds. Only overflowing axes can move. Touch input on an enlarged image retains native scrolling. Releasing or canceling the pointer, changing images, resetting, or leaving the window ends the drag. When the image fits the image area, a horizontal swipe of at least 48 CSS pixels changes images; pinches and a zoomed page do not.

Double-click or double-tap alternates between the initial size and a closer look. A shrunk original opens at 100% with the point under the pointer kept in place; an original already at 100% expands to fit. From any other size it returns to the initial size.

Viewer shortcuts apply inside the open dialog. They ignore text input, input method composition, and Ctrl/Command/Alt combinations. Page-level single-key navigation pauses while the dialog is open. keyboard_nav controls that page runtime; it does not disable Expo’s viewer runtime.

Key Action
W / A / S / D Move the enlarged image up / left / down / right, 80 CSS pixels per press
Q or H Previous image
E or L Next image
J or − Zoom out
K or + Zoom in
R or 0 Reset size and position
F Expand to fit and center
Escape Close the viewer

Left and Right arrow keys change images when no horizontal scrolling is needed. Otherwise, they retain native scrolling. Button tooltips show the applicable keys. In right-to-left languages the album order mirrors: the arrow key toward the start opens the previous image, swipes follow the same direction, and the previous and next icons mirror.

Open original links to the original file in a new tab. Locate in album closes the viewer, updates the URL fragment, scrolls to the current image, and focuses its trigger. Closing normally returns focus to the link that opened the viewer. The native dialog contains focus and prevents underlying page interaction. The underlying page cannot scroll while the viewer is open. The viewer registers with OinkSurfaceCoordinator: opening it closes other floating surfaces, and closeOthers() or closeAll() can close it. A handoff keeps focus in the newly opened surface.

While an original loads, the viewer shows the grid preview at the final size; a failed original keeps that preview. After an image loads, the viewer requests one neighbouring image on each side.

Expo loads its viewer and layout runtime only on HTML albums with images. Ordinary Image Zoom retains its single-image behavior and does not wrap Expo links again. Without JavaScript or dialog support, those links open the originals.

When changing images, clear the previous decoded image while the next original loads, and show the new grid preview at the final size. A failed original keeps that preview. Album prose and image headings remain separate Markdown blocks.

Other outputs

All outputs use the same image resolver. Enable each format through the normal OINK output configuration.

Output Image content
Print Still previews, titles, captions, credits, and original links; no Expo runtime
Markdown and LLMSFULL Original-image links and metadata
LLMS Album pages in the existing index
RSS Static image descriptions
Browser print Expanded album content with controls hidden

Authors translate album content and resource metadata. Image controls use the shared interface catalogs. The existing gallery fence remains an inline component with its own contract.

Acceptance and release boundary

Run the focused theme checks from the theme checkout:

python3 bin/check-expo.py
node --test tests/js/expo-geometry.test.js
python3 bin/generate-config-schema.py --check

Run site acceptance from the sibling documentation checkout:

make check
make browser

The bilingual examples are the integration and visual review surface. tests/browser/expo.spec.mjs checks interaction and accessibility. expo-stability.spec.mjs samples consecutive frames near the viewport-height boundary. It forces width-consuming scrollbars independently of host settings.

For the additional Chromium, Firefox, and WebKit checks, install the matching Playwright browsers and run from the documentation checkout:

HUGO_MODULE_REPLACEMENTS="github.com/pgsty/oink -> $(cd ../oink && pwd)" \
  npm run test:expo:engines

The separate Graph copy supplies additional large-collection evidence.

7.6 - OINK migration boundary

Supported source, configuration, and validation boundaries for OINK migration, including the 1.5.1 upgrade.
OINK 1.5.1 contract

This contract describes OINK v1.5.1.

This page defines the maintainer migration toolkit and configuration boundaries. See Upgrade for the reader-facing upgrade guide.

Toolkit scope

bin/migrations/oink06.py scans and automatically rewrites only Markdown in a site’s content directory, including supported YAML front matter. It does not rewrite Hugo configuration, data, layouts, assets, modules, or generated output. It reports TOML/JSON front matter and ambiguous Markdown with source positions for manual review.

Dry-run is the default. Completed migrations are idempotent:

python3 bin/migrations/oink06.py report --sites <dir>... --md report.md --json report.json
python3 bin/migrations/oink06.py migrate --site <dir>
python3 bin/migrations/oink06.py migrate --site <dir> --write
python3 bin/migrations/oink06.py check --site <dir>

Code-fence contents are not rewritten, including fences starting on the same line as ordered or unordered list markers, with optional blockquotes. An extra literal quote prefix within a code example does not close the fence. book_figures.py keeps narrow TPME, DDIA v1/v2, and pg-internal profiles. It is not a generic parser.

Before deleting existing output, bin/measure-baseline.py and bin/sites/build-all.py reject snapshot destinations that overlap protected locations. These locations include input sites, the running tools checkout, the selected theme checkout, and other snapshots. The check includes --keep destinations through symlink aliases and alternate --theme checkouts.

Updating consumer repositories

After publishing a theme release, inventory maintained consumer checkouts and update their exact pins. bin/update-consumers.py scans primary project directories under supplied roots and their site/ directories. It does not recurse into archives, generated sites, caches, or theme fixtures.

The tool ships with OINK 1.2.0. Run it from the theme checkout to inventory consumers and upgrade them to the published tag.

python3 bin/update-consumers.py v1.5.1 --roots ~/www ~/pgsty
python3 bin/update-consumers.py v1.5.1 --roots ~/www ~/pgsty --write --check --check-appearance

The first command only reports adoption. The second updates go.mod and OINK’s go.sum entries, verifies the exact module graph, and builds selected sites with fatal warnings. It disables GOWORK, Hugo’s module workspace, and environment module replacements. Logs and original module files go to a temporary report directory or the explicit --report-dir. Failed updates restore module files. Failed builds keep the new pin for diagnosis and return failure. Unreadable roots or malformed consumer modules become failed entries. Inventory continues through other sites and exits nonzero. Explicitly selected missing or non-consumer directories also fail visibly.

Linked worktrees, hidden copies, and non-default branches are skipped. Review every skipped or blocked entry. An explicit project root resolves to its site/ directory when that directory contains the Hugo site and the root is not itself a Hugo configuration or OINK consumer. An unrelated Go application’s root go.mod does not prevent this resolution. An actual root consumer takes precedence. --sites <path>... selects a reviewed site explicitly, including checkouts with existing module edits and source directories without Git. Source-only directories are skipped during automatic discovery until selected explicitly; they receive the same file backups and build checks. Classic themes/oink consumers are listed for separate maintenance: update a clean Git submodule to the published tag, or back up and replace a verified upstream source copy. Preserve offline builds and site-level overrides, then run the owning site checks. The module updater does not rewrite classic theme directories. Manually resolve OINK replacements in go.mod. Review vendored themes separately before --refresh-vendor, which backs up and regenerates _vendor/. Changing a module pin alone does not update vendored themes. Hugo’s resolved module graph determines whether OINK actually uses vendor output. A retained snapshot bypassed by ignoreVendorPaths is preserved and does not block a published-module upgrade.

To offer all four styles with Paper as the site default, set the site configuration explicitly:

params:
  ui:
    preset: paper
    preset_menu: [paper, slate, ink, terminal]

preset_menu: true offers the stable styles and the site default; the explicit list also opts into Ink and Terminal. These settings persist across later pin upgrades. Review sites with a custom shell separately: configuration alone does not add appearance controls to templates that bypass OINK’s shell.

The maintainer checkout also supports --check-appearance together with --check. It checks the fresh rendered home pages, including localized entries, for Paper as the default and all four style controls. Sites that disable Home are checked at their documentation entries; redirect aliases are omitted. Browser interaction and each site’s owning checks remain separate.

Preserve unrelated work. Update current theme-version references in site READMEs and configuration. Run each site’s owning checks and visual review. The tool updates module pins and runs build checks without editing content or committing.

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

Make these configuration changes manually. 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 No behavioral replacement; ui.scroll_spy remains a quiet 1.x compatibility no-op
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 uses search.algolia. Page overrides omit ui.. 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_attribution with upstream_link plus upstream_name, upstream_copyright, upstream_license, and upstream_notice; rename downstream_modified to upstream_modified.
  • Replace the release map with one GitHub release_url; remove release_products and release_group_by_product from release indexes.
  • Blog and default dates now default to ISO 2006-01-02; retain explicit time_format_blog or time_format_default for prose dates.

Removed names warn and use the documented safe fallback or render nothing, allowing ordinary previews to continue. blog_index_toggle, featured_image: hero, toc_style, and toc_taxonomies are additional opt-ins. They add no content type. Immersive reading remains on the ordinary blog shell.

Prerequisites and validation

Enable Goldmark unsafe rendering, block attributes, and standalone block images as specified in the component contract. Enable passthrough explicitly for \(...\), \[...\], or $$...$$. Hugo does not merge theme markup configuration.

Run the smallest source and output checks for the changed contract with pinned Hugo Extended 0.168.0. For runtime changes, also run JS tests. Run production root and subpath builds. For maintained sites, inspect representative EN/ZH Docs and Blog routes at desktop and narrow widths.

7.7 - Design decisions

Accepted decisions that explain why OINK’s current contracts and implementation have their present shape.
Accepted rationale

A decision explains why OINK selected one compatible design instead of another. The five contracts define the required current behavior. The implementation and its checkers define the executable facts.

OINK previously kept reviews, PRDs, and execution notes in a local plan/ directory, which made useful reasoning hard to find and allowed abandoned designs to appear authoritative. Accepted rationale now lives here, beside the contracts it supports, in the same bilingual, versioned site.

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
Optional CLI and result contract Independent Go executable, versioned diagnostics, coverage, and explicit write boundaries for the local CLI candidate
Visual presets Paper default, Slate compatibility, opt-in Appearance menu, independent mode and font boundaries
Expo albums Resource bundles, optional metadata, shared appearance, and album viewing
Blog gallery Article covers, shared Expo geometry, independent preferences, and readable captions
Table reading Explicit .expand, independent .wide, and a direct corner action with native table restoration
Translation comparison One switch, real same-origin translations, a lazy two-pane reader, and heading-based sync

| Component refinement | Shared styling, divided cards, compact steps and command strips | | Unified diagram reading | Shared Mermaid and local PlantUML viewing, source export and demand loading | | Local search refinement | Mixed queries, real section anchors, partial matches and section scope |

Record format

An accepted decision records its context, choice, consequences, and evidence that the choice remains current. It does not repeat a parameter reference or tutorial. Each decision links to its contract and verification checks, 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 previous decision in Git history and the release changelog rather than leaving two active answers in the navigation tree.

7.7.1 - Warnings and safe fallbacks

Invalid input produces visible warnings and safe fallbacks. Checks verify diagnostics and rendered output without aborting on every warning.
Decision

OINK does not call Hugo’s errorf. Invalid author or site input produces a warning. OINK then uses a documented safe fallback or omits the invalid fragment. Development, CI, release, and deployment use Hugo Extended 0.168.0 without --panicOnWarning or HUGO_PANICONWARNING. Warnings stay visible; checks verify the specific diagnostic and the rendered fallback.

Context

Hugo builds the whole site as one transaction, so an errorf from one edited page makes every URL served by that rebuild return an error, including unrelated pages and the home page. The server process continues and recovers after the input is corrected, but other authors cannot use the preview in the meantime.

A warning allows the affected value to use a safe fallback while other pages remain available for inspection and the author receives a precise message. Production builds keep these warnings visible for review. The upstream LibSass deprecation warning is accepted; unexpected theme or content warnings need review.

Decision

Validation follows four rules:

  1. Name the invalid key and value, the allowed shape, and the fallback.
  2. Include a page position when the value came from page front matter; avoid repeating one site-wide warning for every page.
  3. Never pass an invalid value into a later operation. Validate first, then render from the normalized value.
  4. If no valid fallback exists, give a warning and render nothing. Do not invent content, make a network request, or emit an unsafe URL to continue.

The shared enum, boolean, CSS-length, and number shapes are in layouts/_partials/validate.html. Domain resolvers may add narrower checks, but they preserve the same warning/fallback contract.

Safety boundary

A build must never continue with unsafe output. OINK replaces a rejected CSS length before it reaches a style attribute, omits a component with incomplete remote service configuration before the browser can make a request, and removes unsafe action URLs. These fallbacks protect the reader without terminating Hugo.

Editing and publication share the warning and fallback behavior:

Stage Invalid input
hugo server or an ordinary local build Warn, fall back or omit, keep other pages available
CI, release validation, deployment Warn and fall back; review diagnostics and verify the output

Consequences

  • Every fallback is part of the public contract and must match the default declared by the theme.
  • When failure behavior changes to a fallback, update its tests. A negative test must check build success, warning text, and the rendered fallback or omission. A warning alone must not fail the Hugo build.
  • Checkers must test rejected output directly. For example, a URL security test must confirm that the unsafe URL is absent and that the warning is emitted.
  • Rendered markup checks verify the DOM, attributes, order, and emitted tokens, while the browser suite checks computed color, size, spacing, breakpoints, and interaction. When the public result is directly observable, a checker does not require exact Sass text.
  • Source-level checks detect forbidden constructs such as errorf and structural requirements that output cannot prove, such as one authority, one resolver, or a deliberately restricted caller set.

Verification

The owning references are the architecture contract, bin/check-params.py, and production builds of both the theme fixture and this integration site with Hugo Extended 0.168.0.

The warning policy was updated on 2026-10-18. Do not restore a warning-fatal publication gate.

7.7.2 - Configuration model

Hugo and Docsy-compatible configuration, with a narrow OINK interface and no parallel configuration namespace or global resolver.
Decision

OINK retains Hugo’s native keys and useful Docsy-compatible keys. Theme presentation and behavior use params.ui.*. A page override uses the matching top-level front-matter key. OINK does not add a params.oink.* tree or a second registry for Hugo configuration.

Context

OINK extends existing configuration with shells, content output, and local interaction. Earlier designs attempted to move all theme-owned keys to a new namespace and resolve a complete configuration dictionary for each page. That approach added a second configuration language beside Hugo’s keys, complicated section cascades, and required a large migration for limited behavior changes.

The current model defines these responsibilities:

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:

  1. Site facts remain at the established top level. Interface choices belong under params.ui.*.
  2. A page override drops the ui. prefix and otherwise keeps the same name. A section cascade can apply that top-level key to its descendants.
  3. If a scalar describes the complete policy, use it for a boolean feature. Use a map only when a feature has subordinate settings. An established map may accept a boolean shorthand.
  4. Use positive, snake_case names grouped by function. Closely related settings share a prefix without requiring another nested resolver.
  5. Declare theme defaults in the theme’s hugo.yaml. Templates may derive a default only when one static value would remove a deliberate distinction between shells.
  6. Each feature family owns its normalization and validation, using a shared helper for common shapes. There is no global compatibility registry that silently rewrites arbitrary old keys.

The complete current key list, types, and defaults are in the configuration reference. This decision records the placement rules rather than maintaining a second parameter catalog.

Compatibility

For each public rename, the owning resolver gives a targeted warning, supported by a migration note and a negative test. Removed or misspelled keys do not require a permanent alias layer. If a rename would break the native API, retain Hugo and third-party camelCase keys. OINK-owned additions use snake_case.

Hugo’s ordinary front-matter and cascade model resolves page values. OINK does not ask authors to build a nested ui: tree in front matter or 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 checks declared defaults, page aliases, warning behavior, and the no-errorf requirement. The integration site’s bilingual and rendered-link suites check the public reference and its Chinese peer.

7.7.3 - Markdown-first authoring

Native Markdown carries common semantics, shortcodes fill capability gaps, and new content models extend shared shells.
Decision

If Goldmark can preserve the intended meaning, prefer a native Markdown form. Keep a shortcode only when the native form cannot provide its capability. For a new content scenario, extend an existing shell and data model. Do not create a parallel rendering system.

Context

OINK supports short manuals, large references, release archives, landing pages, and books. A survey of eleven consumer sites examined more than five thousand Markdown files and found 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, while one limited to 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:

  1. 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.
  2. 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.
  3. One semantic implementation. Native and full forms share normalization, partials, and an output contract, making them one component rather than two components with the same appearance.
  4. 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.
  5. 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 it defines a result for its semantic content in every enabled output:

Output Requirement
HTML Semantic server-rendered content; JavaScript only enhances it
Print 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 HTML-only component from removing or corrupting content in agent output, feeds, or a printable book.

Trust and presentation

Render hooks and shortcodes use explicit allowlists to remove unsafe URL schemes, inline event handlers, and arbitrary style input. Author-provided classes are accepted only where the documented contract permits downstream site CSS. Icons use one Font Awesome class pair, without 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

Theme component, Book, output, and golden checkers test the authoring contract, followed by this site’s bilingual examples and browser suites for the integrated result. The Goldmark behavior that supports native forms is recorded in block-attribute research.

7.7.4 - Generated configuration schema

Editor schemas generated from existing configuration sources, with a CI gate that prevents drift.
Decision

bin/generate-config-schema.py generates the two JSON Schemas under schema/. It reads the theme’s hugo.yaml and the template read-point scan. CI rejects manual schema changes. The schemas are read-only projections of the existing configuration authorities. They are not a third authority.

Context

The theme already has two configuration authorities: hugo.yaml declares each default beside an explanatory comment, and the read-point scan in check-params.py identifies each key that the templates consume. Editors do not read either source, so authors have to enter params.ui.* keys and front matter from memory.

A JSON Schema gives editors completion and hover documentation, but a manually maintained schema can become a third authority that drifts from the implementation. Incorrect completion can cause more problems than no completion, so the schema must follow the existing sources.

Decision

bin/generate-config-schema.py generates two files under schema/. site-params.schema.json validates a site’s hugo.yaml, taking types and defaults from the theme’s own hugo.yaml and descriptions from its comment blocks. front-matter.schema.json validates page front matter and includes each key that the templates read as an authoring input, with descriptions from the matching site key. The generator excludes, by name, keys read only to warn about a rename or removal.

Two deliberate restraints are part of the decision:

  • The front-matter schema has no type constraints. Several keys accept a bare-boolean opt-out in addition to their site type (share: false, theme_color: false). An incorrect editor warning for valid input would be worse than no warning.
  • The hugo.yaml reader parses only the shapes that the file uses: nested maps, scalars, and inline lists. If the parser cannot read an input, it fails. The drift gate then fails instead of generating an incorrect schema.

The parameter checker identifies maps with documented boolean shorthand. The generated site schema preserves both boolean and object forms for those maps, including ui.dark_mode; pure configuration maps such as print and ui.expo remain objects. Front-matter read points such as feedback.enable produce nested properties under feedback, never literal dotted keys. Those properties do not exclude the supported boolean page override.

Consequences

To change a schema, change hugo.yaml or the templates that the scan reads. When public configuration changes, regenerate the schemas in the same commit. Maintainers do not need a second parameter inventory, but the generator and read-point scan constrain the public configuration surface: a new parameter key must use a shape they understand, or CI fails.

Verification

python3 bin/generate-config-schema.py --check regenerates schemas in memory and fails if schema/ is stale or missing. Theme CI runs this check with the parameter contract checker. Editor configuration and schema behavior are documented normatively in Configuration.

Visual preset enum values are also derived from preset-config.html. The preset_menu union accepts a boolean or a list of those resolver-owned values; the schema does not maintain its own list.

7.7.5 - Optional CLI and result contract

Executable ownership, versioned diagnostics, isolated validation, and guarded maintenance plans for the current local CLI candidate.
Current scope; local candidate

This contract describes the reduced local 0.1.0-dev command surface on 2026-10-04. Keep site diagnosis, real Hugo checks, initialization, builds, upgrades, and guarded maintenance plans. Cobra provides command help; colored English text is the default, with JSON/YAML results available. Studio, general editing, context, snippets, editor setup, and CI generation are retired. Historical R1–R8 acceptance applies only to its recorded source and binaries; it does not replace validation of the current implementation. The CLI is currently a local candidate and is not yet published as a standalone release or Homebrew package.

Context and ownership

The theme is a Hugo module, while consumer tooling is an optional executable with a separate installation and release lifecycle. pgsty/oink-cli owns that executable, named oink, and its Go tests. It invokes an external Hugo binary without importing Hugo’s private runtime or depending on a sibling checkout, Python, Node.js, or unpublished theme scripts at runtime.

Hugo owns configuration resolution, rendering, routes, and anchors. The CLI inspects Hugo’s effective configuration, module graph, mounts, and rendered files. It does not create a second route resolver, navigation authority, or configuration namespace. Theme-only regression scripts remain maintainer tools. Configuration preprocessing relocates workspace, replacement, and cache paths only in the temporary copy. Hugo still owns defaults, configuration merging, language selection, validation, and rendering semantics. The public architecture contract continues to own theme behavior; this page owns the initial CLI boundary and result envelope.

The CLI introduction provides a short overview and local build steps. The dated acceptance record separates executed checks from open limitations and release states. The maintenance acceptance record preserves the earlier R1–R8 program and its source-bound evidence. The roadmap retains future proposals and adoption hypotheses rather than duplicating the current command reference.

Command and mutation boundary

Help groups commands by daily, maintenance, and release work. Run oink COMMAND --help for the exact options.

Command Behavior and mutation boundary
doctor Read-only toolchain, configuration, module source, workspace/replacement/vendor diagnosis
check [links|translations|style] Actual Hugo output and declared source/translation policy in isolated copies
init DIRECTORY Validate the fixed Starter before creating a new or empty site
dev, build Ordinary Hugo processes. Hugo owns normal output/cache writes
upgrade --to TAG Preview by default. Only --write applies verified module changes
translations status, translations diff PAGE Read-only relationships, hash-review states, and differences
translations review SOURCE TARGET, baseline capture Requires an explicit reviewer/reason. Preview with an optional new --plan
new BUNDLE --title TEXT, move SOURCE TARGET Validate a candidate and preview its full diff. An optional new --plan saves the plan
plans apply FILE Revalidate supported saved plans. Write only selected files in the selected site
inspect PAGE, impact --since REF Read-only actual page and historical/current impact facts
workspace list, workspace check [GROUP] Select only explicitly registered sites
build --check Check, seal, and export the same isolated production render
artifacts verify Compare local artifacts with a manifest offline
verify Compare deployed HTTP responses with a manifest after explicit --network

Network access is off by default. All commands are non-interactive. Only dev and build accept Hugo arguments after --. Removed commands and their old plans cannot apply. Supported plan kinds are authoring.new, translations.review, baseline.capture, and content.move.

Versioned result envelope

Option Output
Default Concise colored English text
--json, -J One JSON oink.result/v1 object
--yaml, -Y One YAML document with the same result fields and types
--verbose, -v All findings, coverage details, and tool logs
--no-color Plain English text

Choose one structured format. Nonempty NO_COLOR or TERM=dumb also disables text colors. Structured output adds no terminal colors. Tool logs go to stderr. --format json|yaml and --non-interactive remain hidden compatibility options. Every command is non-interactive.

Default text shows status, counts, up to eight active findings, and explicit coverage omissions. Detailed facts and reviewed findings remain available in structured results. Plan and upgrade previews retain their complete diffs. Cobra owns command dispatch and focused help. CLI messages use short, active English sentences inspired by ASD-STE100. User content and external tool evidence retain their original language.

Field Type and meaning
schema_version String; oink.result/v1 for this contract
version String; CLI build version, including a development suffix when applicable
command String; requested command, or help / version
site Optional string; selected source or generated target directory when available
exit_code Integer; the CLI result code defined below
diagnostics Array of findings; an empty array means no recorded findings
coverage Array of scoped coverage statements; callers must inspect these alongside findings
evidence Array of subprocess records; empty when no subprocess ran
data Optional command-specific JSON value; current commands return objects such as inspected facts, initialization provenance, or an upgrade plan

The JSON Schema describes this envelope.

The first version permits additive fields and new rule IDs. Consumers should ignore unknown fields and treat IDs as opaque strings rather than parse their spelling. A change to the meaning or type of an existing envelope field requires a new schema version. Command-specific facts and raw tool output serve as evidence, not an SDK for importing internal Go packages.

The machine-readable schema is shipped in the CLI repository as schema/result.v1.schema.json.

Each evidence record has command (an argument array), optional directory, stdout, stderr, and the subprocess’s exit_code. Captured inspection and build output remains available in the result. Directly streamed dev/build output is sent to the logging stream rather than buffered again in evidence. The subprocess status remains separate from the CLI’s 0/1/2 result; a negative subprocess status can mean that no normal exit code was obtained.

Findings, severity, and locations

Every diagnostic has rule_id, severity, message, and action, plus an optional location. Stable rule IDs identify the condition. Raw Hugo wording, translated messages, paths, and build-specific details are not stable IDs. Existing IDs must not be reassigned to a different condition.

The optional incomplete: true identifies a required-work failure that policy cannot downgrade. Reviewed exclusions and baseline acknowledgements retain the finding in diagnostics with disposition: "excluded" or "baseline" and review containing reason, reviewed_by, and RFC 3339 reviewed_at. An excluded finding remains visible with its recorded severity. It does not block the completed policy check. Any uncompleted required coverage, including not_checked, determines exit 2. Only complete or not_applicable satisfies required coverage.

The severity vocabulary is info, warning, and error. error is blocking. info and warning are advisory unless the underlying required Hugo build or its owning output check fails, in which case required work is incomplete. Informational completion and scope explanations belong in coverage details. Automation must use the result exit code and coverage, not only count severities.

When present, location contains file, with optional kind, line, and pointer. kind distinguishes source from output. A rendered finding points to the actual artifact and may include an element, attribute, or JSON location hint in pointer. This field does not always use RFC 6901 syntax. A line number is included only when known. A rendered link failure does not justify inventing a Markdown source line.

The CLI does not reject unknown valid front matter merely because a generated editor schema omits it. Configuration validity continues to follow Hugo and the owning theme resolver/checker. See the generated schema decision.

Coverage and exit semantics

Each coverage entry contains id, status, required (boolean), and detail. Entries describe the scope that actually ran. A report may contain several statements about the same broad area. Inspect all of them.

Status Meaning
complete The stated operation, inspection, or artifact-check scope completed
not_checked This run did not inspect the stated scope
not_applicable The stated check is unnecessary for these inputs
unsupported The stated contract or required input shape is unsupported
incomplete The stated work was required but could not finish
CLI exit code Meaning
0 Requested required work completed without blocking findings
1 Completed checks identified a policy violation, such as a broken local link or an unsafe requested write
2 Required work is incomplete, including argument, tool, build, I/O, cancellation, or required unsupported-contract failures

Incomplete work takes precedence over policy findings. Required coverage that has not completed cannot produce success. Only complete or not_applicable can satisfy it. Hugo build failure preserves the raw evidence and stops output acceptance. The CLI does not report stale or partial output as a passing check. Disabled optional machine outputs do not become missing-output errors.

The rendered-reference scope includes supported HTML URLs and anchors and supported emitted machine contracts. Coverage explicitly excludes browser interaction, accessibility, visual presentation, external URL availability, hosting redirects, and production deployment. It also identifies uninspected dynamic resources and content semantics. Static output checks inspect declared translation pairs and rendered files; browser execution and semantic translation review remain separate.

Hugo’s public Page.OutputFormats supplies expected artifact names and URLs per page and per enabled language. The isolated copy adds an unlisted probe with a unique per-run identifier. Each enabled language must emit its own verified manifest. Identified probe files are removed before artifact checks. Existing authored pages retain their output selections. Static content hidden from ordinary page lists is resolved through Hugo’s GetPage, without deriving its route or output filename from source syntax. Effective per-language base URLs and local-search settings accompany that enumeration. Enabled supported machine artifacts are checked against these exact expectations. Optional disabled outputs remain optional.

The same Hugo probe supplies data.pages through public Page.Path, Page.File, Page.Translations, Page.Aliases, and Page.OutputFormats. Facts retain language, actual URLs, publication settings, translation relationships and declared outputs. sourceKnown: false and sourceScope: "unknown" identify pages without proven source provenance. Generated sections do not receive invented source files. Site-owned known paths are relative to the selected site. Known copied dependency inputs have explicit dependency scope. These facts describe the production view. A separate internal analysis view can include drafts, future, and expired pages. It does not change production artifacts or claim publication of those pages.

data.references records observed HTML and machine-output references, their actual resolved URL, output file/pointer, local target when present, and anchor status when checked. It does not infer a Markdown source line. Page and reference data remain additive command evidence. They are not a public Go SDK.

Project check policy

An optional regular oink.yaml at the selected site’s root uses schema_version: oink.policy/v1. It owns check selection, severity overrides, reviewed finding exclusions, reviewed external URL scopes, translation scopes, protected prose declarations and the optional baseline file path. Hugo inputs continue to own languages, titles, URLs, menus, and configuration. Module files own dependency versions. A symlink, unknown key/group, unsupported version, invalid review metadata or multiple YAML documents is required input failure (exit 2). Diagnosis and checks only read this policy.

Without a policy, links, translations and style are enabled and required. check links, check translations, or check style explicitly selects one required group, regardless of its policy selection. Unselected or disabled optional groups report not_checked. Every check invocation retains the required Hugo build and diagnostics and output-enumeration prerequisites. Standalone translation/source checks also use an explicit nonpublishable draft/future/expired analysis view. It never replaces production artifacts. Managed build --check renders only the production view and returns 2 for unknown required scope identities.

The rules mapping assigns error, warning or info to exact opaque rule IDs. A reviewed exclusions entry requires rule_id, a clean relative file glob, reason, reviewed_by, and RFC 3339 reviewed_at. ** patterns and path escape forms are unsupported. Findings remain visible with review metadata. Site-local source locations match their relative site path. Source paths outside the selected site cannot match an exclusion. Required build, input, tool or coverage failure cannot become success through severity changes or exclusions.

Same-origin HTML references outside the configured base path are policy findings unless a reviewed external_scopes URL declares a separately deployed path scope. Each scope needs the same review metadata and an absolute HTTP(S) URL without credentials, query or fragment. Matching uses complete path segments. The scope cannot exempt missing targets inside the project or required local machine-output targets. Different-origin references remain explicitly unverified by offline static checks.

Translation policy and review evidence

translations.scopes selects source pages using clean absolute Hugo Page.Path prefixes, then finds targets through Hugo’s translation identity. It does not infer a public route or language from a filename. Each scope has path, source_language, required_languages, mode and drafts. mode defaults to localized. strict is also supported. drafts defaults to include. ignore excludes draft sources/targets. require-published requires the selected source and required targets in production. Known disabled Hugo languages are not_applicable. Unknown languages make the policy invalid. The most specific matching path owns a source page.

Without scopes, existing pairs rooted in Hugo’s enabled default language and duplicate relationships are inspected. Universal localization is not required. translations.coverage records the unconfigured language scope as optional not_checked. Missing required targets and duplicate selected relationships are policy findings. Draft/publication state is independent of review state.

Constraints require explicit selection. explicit_ids compares the full recognized explicit-ID map in strict mode. Localized mode requires a selected ids list. ids requires each named ID in both files, placeholders compares exact declared prose-literal counts, and code_labels protects fenced blocks with each named language/info token. required_fields requires nonempty dotted front matter fields in both files. equal_fields compares their actual values. No rule requires matching heading counts, translated prose or all code blocks by default.

.oink/translations.json uses oink.translations/v1. Explicit review records bind Hugo source/target IDs, source language, complete source/translation byte SHA-256 values, reviewer, reason, and RFC 3339 time. An absent record is unknown. Equal hashes are current. Source-only, target-only, or both changes are source_changed, translation_changed or both_changed. These states track byte changes since the recorded review rather than file modification times. Unproven sources stay unknown. If a recorded review or protected constraint cannot be verified, required work is incomplete.

Native content rules and coverage

Source rules extract evidence from Markdown structure and independent enabled Hugo attributes, preserving original UTF-8 bytes, CRLF/BOM, source offsets and YAML/TOML/JSON front matter with unknown fields. Actual effective Hugo attribute switches and configured math passthrough delimiters control recognition. Fenced/inline code, shortcode bodies, raw HTML and passthrough contents do not become prose or invented headings. Unsupported body syntax remains visible. Required source coverage cannot silently pass.

Generic rules detect duplicate recognized explicit IDs and evaluate declared style.protected entries with file, exact prose literal and expected count. The bounded OINK v1.1.0 and v1.2.0 catalogs add advisory code/table attribute, deprecated front matter and dropped unsafe-attribute findings. Each rule records module, version, immutable revision, module sum, license and exact source-file SHA-256 provenance in data.native_rule_provenance.

The catalog runs only when the actual mounted public v1.1.0 or v1.2.0 module-cache inputs match that release’s recorded hashes. Later releases need an explicit source audit before they gain native-rule coverage. A replacement, vendor copy, other version or unknown source does not select a latest-theme fallback. native-theme-rules is optional not_checked. Generic syntax checks still run.

Baselines and reviewed file plans

baseline selects a clean relative file, default .oink/baseline.json, using oink.baseline/v1. Capture requires completed work and explicit review metadata. The fingerprint binds exact rule ID, normalized location/pointer and condition message. It excludes severity. Acknowledged findings stay visible with disposition: "baseline" and their original severity. New conditions still block according to policy. Required incomplete findings/coverage cannot be acknowledged.

Review and capture preview oink.plan/v1 with selected edits, readable diffs, base existence/bytes/modes, after bytes and read guards. The plan ID excludes mutable validated/applied/recovery state. --plan FILE exclusively saves the plan. These commands do not accept --write. plans apply FILE --site DIR requires that exact selected site, fresh isolated candidate validation through the same checks and rechecked source guards before any write. Escapes, .git, symlinks, and nonregular files are protected. Overlapping candidate/source trees are refused. Stale plans fail safely. Optional external_inputs_hash binds captured non-site input bytes, full modes and inventory into the plan ID. This opaque SHA-256 grants no external paths or permission to read them. The owning validator compares fresh proven inputs. Trusted original external guards are rechecked around selected writes.

Exclusive installation preserves a file created during commit. Partial failures restore owned unchanged writes. Subsequent editor bytes, modes, or deletions remain intact. The reported recovery directory keeps original bytes/modes and actual concurrent captured evidence. Unrelated files and new editor children are preserved. No file content authorizes shell execution or publication.

Captured page inspection and impact

inspect PAGE selects an actual Hugo page by exact language:path ID, Hugo Path, permalink or proven site-owned source file. A known default language resolves a multilingual Path. Remaining ambiguity or an unknown selector is required incomplete (2). data.inspection exposes source byte hash/full mode, actual output identities, observed inbound/outbound references, translations and physical bundle attachments. Physical attachments are distinguished from observed published resources.

impact --since REF compares captured current inputs and an isolated committed Git tree rendered by the same Hugo engine. It retains deleted prior pages and their inbound edges, unchanged referring pages, translation peers, attachments and actual derived outputs. Global or uncertain inputs expand causal scope. An actual HTML output without proven ownership, such as an alias, also forces conservative full scope. Alias declarations never create guessed route ownership. Historical inputs in proven Git mode scopes compare only Git’s executable bit. Other module/foreign inputs and current facts retain full modes.

Historical materialization reads bounded Git objects without checkout hooks, filters, smudge execution or document execution. The limits are 10,000 files, 16 MiB per file, and 128 MiB per tree. Required private history is bounded at 256 MiB. Symlinks, submodules, capped or missing objects, required incomplete history and unsupported monorepo GitInfo are explicit incomplete states. Committed site-owned dependencies can be proven. Current external local replacement/workspace bytes cannot substitute for historical evidence.

data.impact.baseline_state is complete, incomplete or unavailable. When the required baseline is unavailable, the result returns 2, retains all known current pages/attachments/references/outputs and expands full scope. It creates no prior pages or invented changes. Only an actually resolved commit is recorded. check [GROUP] --since REF deliberately performs the full current check and declares data.check_scope: full so validation coverage stays complete. data.impact.full_scope describes causal uncertainty separately from that validation scope.

Completed inspect and impact fact queries return 0 even when their separately reported data.current_check has completed quality findings (1). Required capture failures remain top-level 2. check --since keeps the current policy’s quality exit code and required completion precedence.

context is removed. Read page facts through the JSON/YAML report from inspect.

Content move plans

move SOURCE TARGET [--plan FILE] previews a physical site-relative file or bundle relocation. Actual Hugo identities determine translation peers and old/new outputs. The plan includes byte/full-mode-preserved files and binary attachments, readable diffs, proven Markdown destination rewrites, observed route changes and alias advice. Front matter is not rewritten to install aliases. Raw HTML, shortcode output, transformed destinations and ambiguous source/output ownership remain visible as manual actions. Opaque source spans are not changed. Repeated ordinary Markdown destinations can also lack a unique source/output occurrence proof, including aggregate/print appearances. Matching URLs alone do not authorize rewriting those occurrences. A relocated physical attachment does not prove its new published URL. Resource URL changes require paired actual rendered edges and equal emitted bytes. A proven processed image URL does not prove an absolute original-resource URL. Unproven original URLs stay manual and are not constructed from the directory move.

The original before check and provisional route_probe are separate from the final candidate check. The provisional relocation may expose findings 1 from stale inbound links. A plan is validated or saved only after the final isolated candidate and reference proof pass. Unsupported identity or required capture failure returns 2. An actual final quality failure remains 1 and cannot save an applicable plan.

Content move plans must be saved outside the selected site. Their additive oink.plan/v1 move selectors and source_inputs_hash bind the complete raw source inventory, bytes, and full modes. External-input and fresh-directory guards also apply. Saved apply regenerates the original/relocated Hugo proof and requires exact expected plan ID and files before final reference verification. It rechecks current guards before writes, restores raw original modes rather than private-copy modes, and preserves later editor bytes/modes on refusal or guarded recovery. Stale inputs or an occupied fresh target lack the required proof and return 2. Only explicit plans apply writes selected files. Previews never stage or commit Git changes.

Supported input boundary

Initial full validation supports materialized files in a normal checkout or without Git metadata, including supported local module replacements copied into the isolated tree. It does not follow mounted symlinks or external mounts back into the user’s workspace. Auxiliary symlinks outside effective mounts are omitted rather than validated. Linked Git worktrees with a .git file need a materialized review copy. Git-dependent behavior needs a copy with its own Git metadata.

The snapshot excludes top-level public, resources, node_modules, tmp, and the Hugo build lock. A mount that needs excluded input cannot silently pass. Root configuration and the standard config tree are supported. Explicit configuration files must be inside the selected site. Custom HUGO_CONFIGDIR locations are refused. Supported configuration relocation is not a second implementation of Hugo validation.

Content adapters (_content.gotmpl) can create unlisted pages that cannot be enumerated completely through the supported public Hugo APIs. Full output validation therefore reports incomplete work for those inputs. A disabled page kind or render segment that omits an enabled language’s probe is also incomplete. Multihost language configuration is outside the first full-check scope and returns incomplete work. Multilingual paths on a single host remain supported. These cases must not be presented as a successful partial check.

Ordinary content plans

new BUNDLE --title TEXT [--language LANG] [--translations LANGS] [--kind page|docs|blog|book] [--plan FILE] previews an ordinary Hugo leaf bundle through captured site-owned content mounts. The primary language falls back to the effective default. Selected peers must be distinct enabled languages. Shared filename and language-directory layouts follow actual Hugo mounts, including observed sites.matrix.languages selection rather than an assumed legacy lang field. Ambiguous, filtered or unsupported mappings require manual authoring. Existing bundles or sibling files owning that page are refused.

The primary index has draft: false. Selected peer indexes have draft: true. The title is a supplied literal, not translated text. No review record is created. Full quality analysis and isolated candidate validation precede the shared guarded plan. Every proposed new file must map to exactly one actual site-owned Hugo source page with actual rendered outputs, including translation drafts in the explicit analysis view. Link-only/no-output, ignored, hidden or build-never content cannot pass merely because the existing site renders without errors. Required identity is checked even when ordinary source-check groups are disabled. Saving --plan creates only a new plan file. Explicit plans apply FILE --site DIR revalidates and checks source bytes/modes, existence, fresh directories and later attachment conflicts before applying. Fresh-directory state is bound into the plan identity and checked before/after validation, between writes and at completion. Rollback preserves later editor attachments and reports recovery. It does not delete unrelated directory entries.

Editor settings and Markdown snippets belong to the site editor. editor and snippets are removed.

Initialization profiles

init DIR [--profile project|docs|blog|book] [--languages en|en,zh|all] composes one embedded MIT-licensed Starter archive. The default project retains the previous complete language projection byte-for-byte. The language selection is independent of the content profile. all means English, Chinese and French.

Explicit docs, blog, and book retain their corresponding archived content section and shared home, assets, examples, workflows and license. Native section front matter defines their documentation, blog or sequential book model and navigation. Each language’s site title/description comes from its archived section. Existing localized home cards/actions/CTA are projected to that section. Only their generated hugo.yaml and data/home YAML files are serialized. Retained content/license bytes stay unchanged. There are no four copied Starter trees or runtime template downloads.

Unknown profiles are policy refusals (1) before candidate validation or writes. Missing/failed required Hugo validation remains incomplete (2). New and empty-target, candidate-before-publication, exclusive creation and concurrent-edit recovery protections apply to every profile. Ordinary Hugo builds each generated site with provisioned dependencies. Archived workflows remain source examples. init does not generate or execute the checksum-bound R3 CI templates.

Bounded upgrade comparison

upgrade --to TAG now captures baseline and candidate views from the same original site inputs and returns a readable module diff with full mode changes. Its comparison records actual Hugo pages/outputs/language settings, emitted file hashes/sizes/modes, raw alias declarations and separately observed alias redirect files. It reports removed/added URLs, proven redirects, alias target/ byte changes and enabled language/output/search changes.

A previous URL is preserved only when an observed redirect at its exact old output file targets the corresponding actual candidate page. Unknown/relative custom alias identity remains required incomplete (2). Removed previously emitted routes or outputs are blocking findings (1). Both resolved theme versions must match the explicitly selected pins. Unknown/substituted pins, unknown/different normalized Hugo versions or environments, or unexpected other input changes remain incomplete. Comparison supports one HTTP(S) base origin/path. Multihost inputs stay incomplete. Manual review remains explicit optional unchecked coverage. Observed aliases that retarget a different unique Hugo page are blocking independently of same-page URL moves.

The v2 upgrade plan ID binds the selected module plan, copied source bytes/full modes/file inventory and normalized actual comparison. --expect-plan ID checks a fresh capture/comparison, not a previously saved successful build. Because emitted file hashes are bound, nondeterministic templates can require a refreshed preview even when source files appear unchanged. Guards are rechecked before returning a preview, before each write and after writes, including proven local dependency/workspace inputs read-only. Only selected module files are written. Rollback restores only unchanged files owned by the operation and preserves later editor bytes. Existing dirty target, replacement and vendor safeguards remain in force.

File preservation and release checks

Initialization embeds the complete Starter commit and preserves its license. The provenance records its hash and every projection: independent content/language selection, the exact public theme pin/checksums, and disabling Git metadata for a fresh directory. The default project retains prior bytes. Selected content profiles share the same archived source and license. Candidate validation precedes target writes. Exclusive creation refuses existing files. Rollback removes only unchanged files created by that invocation and preserves concurrent user edits with recovery evidence.

Upgrade owns only a single site’s selected go.mod and go.sum changes. It preserves unrelated dependencies and directives, refuses dirty target files for --write, checks the reviewed plan ID when provided, rechecks targets before writing, and records backups/recovery. Unrelated dirty source files do not block read-only diagnosis or justify overwriting them.

check --release disables GOWORK and HUGO_MODULE_WORKSPACE and removes the environment replacement for the subprocess. It preserves go.mod replacements, reports conflicting local OINK replacement policy, and keeps vendor evidence separate from the public requirement. Upgrade refuses an OINK replacement and refuses _vendor. Vendor refresh remains a separate explicit workflow. No module pin change is described as updating vendor bytes. If Hugo actually selects vendored OINK, --release reports required public-source verification as incomplete (exit 2). Matching version metadata is not proof that the vendor bytes match the public tag. Ordinary check still validates the actual vendor build.

Configured Hugo module replacements are also disabled only in the release snapshot. If Hugo changes module files in that snapshot during resolution or build, the CLI reports that dependency inputs need explicit preparation and review. It preserves the original bytes rather than silently accepting a build that depended on an unreviewed generated module-file change.

Checked builds and artifact identity

build --check --destination DIR --manifest FILE uses an isolated, production build that keeps warnings visible for review. Hugo renders once. The check engines inspect that output, then seal and export those same bytes. It never invokes a second renderer to create the publication tree. The source checkout remains unchanged. Only an outcome of 0 with complete or inapplicable required coverage can be sealed. Blocked or incomplete checks do not create a verified export.

This production view does not include the separate nonpublishable maintenance render. An explicit scoped translation policy whose required Hugo identities are unknown because publication excludes their sources returns 2. It does not infer missing translations from filenames or silently skip the scope. Standalone check and translations retain the full maintenance view.

The destination must be new or empty, with an existing parent. The local manifest must be a new file outside that tree. Export uses exclusive creation, preserves exact bytes and full regular-file modes regardless of umask, and rechecks source and destination against the manifest. Existing entries, symlinks and overlapping trees are refused. Failed partial exports remain unverified evidence and are preserved. Empty directories and directory modes are outside the published file inventory.

--marker is optional and off by default. It adds .well-known/oink-build.json with only oink.build-marker/v1 and the artifact ID. That file’s entry is excluded from artifact-ID calculation to avoid a circular hash, then its exact digest is included in the final inventory. An existing marker path is refused. The manifest is never copied into the public tree automatically.

The separately saved oink.artifact/v1 manifest records the source-input hash, known source Git revision and dirty state, actual resolved theme identity, CLI/Hugo versions, effective environment/base URL/release settings, required coverage, actual Hugo route contexts and each file’s relative path, size, full mode and SHA-256. Canonical URLs and HTML language values come from emitted HTML. Hugo language keys remain separate. Unknown Git state stays unknown. Original input bytes and modes are captured before temporary probe overlays or workspace/replacement path rebasing. The public manifest omits absolute local paths, arbitrary arguments, process logs and free-form coverage details. Hashes record file byte identity.

Managed builds accept only the boolean Hugo flags --minify, --gc, --ignoreCache and --noTimes after --, including =true/=false forms. Other passthrough flags are unsupported inputs. Ordinary build keeps its existing transparent passthrough behavior. Effective example/local addresses are warnings during ordinary diagnosis and errors for checked release builds. --release still requires separate evidence for actual public theme resolution.

Local and deployed verification

artifacts verify --artifact DIR --manifest FILE is read-only and offline. It compares the exact file set, bytes, sizes and full modes. Changed, missing, additional, unsafe or mode-changed files invalidate identity (1). Invalid manifests, unreadable inputs and unsupported or interrupted inspection return 2. Verify the export again immediately before upload. Later edits cannot inherit a previous successful result.

verify --site URL --manifest FILE --network explicitly authorizes HTTP reads. It checks every declared file and distinct actual Hugo route URL, including all language/subpath contexts, against the manifest’s bounded response size and decoded-byte digest. Recorded HTML canonical/language values and an enabled marker are checked when available. Shared URL/file requests may be coalesced without removing their recorded contexts. HTTP cannot verify local file-mode bits.

A wrong body, soft-404, wrong route, changed captured canonical/language value or wrong marker is a conclusive finding (1). Timeouts, authentication failures, rate limiting, server unavailability and an absent required marker leave work incomplete (2). Redirects outside the selected origin/base path are blocked. The command does not discover or send credentials. Static build checks do not perform this deployment check. Network permission for a build does not authorize a later verification request or an upload.

Retired CI generation

ci init is removed. Keep CI configuration in the site or Starter. Previously saved CI plans are rejected by plans apply.

Explicit workspace registry

R6 supported local scope accepted

The registry and optional-tool boundaries passed frozen owning/runtime, actual protocol, four-consumer parity/preservation and canonical source/render gates. A07 adapter and A15 workspace supported scope is accepted locally in the maintenance record.

A workspace is one explicitly supplied YAML registry, independently versioned as oink.workspace/v1. It contains only site names and directories:

schema_version: oink.workspace/v1
sites:
  - name: docs
    directory: ../docs-site
  - name: blog
    directory: ../blog-site

The registry must be a regular nonsymlink file containing exactly one YAML document with known fields, 1–64 entries and at most 256 KiB. Names match [A-Za-z][A-Za-z0-9_-]{0,63} and are case sensitive. Directories are literal relative paths from the registry’s actual parent, or absolute paths. Variables, globs, and sibling discovery are not evaluated. Explicit directory symlinks and operating-system aliases resolve to their canonical identity. Duplicate names, duplicate or overlapping actual roots, filesystem roots, dangling symlinks and nondirectory ancestors are rejected. Missing directories with a proven existing ancestor remain listed. Checking one returns that site’s 2 without preventing later selected sites from being checked.

workspace list|check [GROUP] --workspace FILE [--sites NAME,NAME] selects all registered sites when --sites is omitted. Explicit selections require exact, nonempty, distinct registered names and retain registry order, including when the names were supplied in another order. list needs no Hugo renderer. check reuses the single-site engine and each site’s own Hugo inputs and oink.policy/v1 policy. It never duplicates Hugo configuration in the registry.

The existing oink.result/v1 envelope contains data.registry, selected_sites, sites: [{name, path, result}], completed_sites, finding_sites and incomplete_sites. Each child is a full single-site result. Completed sites include exits 0 and 1. Finding sites are the 1 subset. The aggregate exit is 2 if any selected site is incomplete, otherwise 1 if any has blocking findings, otherwise 0. Human output includes each site’s result and findings. This aggregation does not infer completion for unselected sites.

Supported single-site commands accept --workspace FILE --site NAME, with one explicit registered name and no default site. init, artifacts and verify do not accept this selection. A saved plans apply FILE must bind to the selected canonical directory. Selecting another registered site returns 2 before source writes. There is no automatic multi-site apply or upgrade. Existing candidate validation and source/dependency byte and mode guards still apply. Registry listing/checking neither provisions missing sites nor installs tools, commits or writes consumer configuration.

Optional check adapters

Explicit tools entries in each site’s oink.yaml select already provisioned executables. These entries extend oink.policy/v1. They are not a second Hugo configuration or an installer. Each kind has enabled (default true), required (default false), command (default the kind’s name), config (a clean site-relative regular file when supplied) and timeout_seconds (default 60 seconds, with bounded nondefault values 1–300). Commands are one executable name or absolute path, not shell expressions.

Kind Owning check group Current supported protocol Configuration boundary
markdownlint style markdownlint-cli 0.49.1 Optional declarative JSON/YAML/TOML; no JS, JSONC, custom rules or extends
vale style Vale 3.24.0 Explicit INI plus captured styles from the supported declarative subset
lychee links lychee 0.24.2 Optional bounded request settings; explicit network consent

Unconfigured tools are not discovered. A tool outside the selected check group is visibly not_checked. A configured optional tool that is unavailable, unsupported or cannot complete leaves an omission. Required incompletion returns 2 and cannot be downgraded by rule severity, exclusions or a problem baseline. Required and disabled cannot be combined. Completed typed findings still follow policy. Blocking findings return 1. An unrecognized tool version or invalid protocol output does not count as a completed check.

data.adapters records each kind, requirement, status, typed diagnostics, adapter.KIND coverage, raw process evidence, omissions and provenance. Provenance includes the observed supported version, executable SHA-256, captured configuration/style paths with SHA-256 and full mode, and pinned public protocol sources. Tool logs stay in stderr and evidence. JSON stdout remains one result. Per-process time and output are bounded, and changed executables, captured configurations or tool-modified private inputs invalidate their evidence.

Prose tools receive private masked copies of proven site-owned Markdown. Front matter, BOM/CRLF and UTF-8 offsets, code, shortcodes, raw HTML, configured math and attributes retain their source boundaries. Code prose is outside source attribution. Markdown structure and fence/inline code boundaries remain available to markdownlint. Vale receives the prose-only mask. Findings touching excluded or synthetic mask text are not attributed to original source. Source locations are emitted only for proven original lines/ranges. Unsupported syntax and suppressed findings remain visible omissions. These adapters do not format or rewrite original content.

Markdownlint uses an unpredictable generated JSON pointer to isolate the captured rule object after upstream rc merging. Executable configs, custom rule loaders and recursive extends are refused. Vale uses an explicit captured INI, --no-global, and copied declarative styles. Sync, packages, actions, scripts, conversions and style pipelines are unsupported. Lychee accepts bounded timeout, max_retries and max_concurrency settings, plus the literal cache = false. Caching remains disabled, and cache = true is refused. Preprocessors and arbitrary command options are refused.

Offline is the default. Lychee is not invoked, including its version probe, unless --network is explicit. Optional coverage is not_checked. Required coverage is incomplete 2. It receives only observed external HTTP(S) references from actual Hugo output. Local links remain the native check’s responsibility. Definitive failed 4xx responses are policy findings, except 401, 403, 408, 425 and 429; those, 5xx, DNS/TLS failures and timeouts are inconclusive, returning 2 when required and an omission when optional. External fragments, browser behavior and remote content identity are not verified. Findings retain the rendered output file and DOM pointer. No Markdown line is invented from an external URL.

Child processes do not receive caller proxy-URL/credential settings or Node preload variables. Literal NO_PROXY/no_proxy host-list data may be forwarded for the qualified runtime. Tool preparation and any network operation remain separate explicit actions. No tool is installed by these commands.

Offline and compatibility boundary

Offline is the default for managed subprocesses. Dependency misses are incomplete work. --network explicitly enables network use for the current operation. It conflicts with --offline. Isolated checks may seed disposable caches from already provisioned local modules. Downloading into a disposable cache does not promise a persistent cache for the next invocation.

Only module download artifacts are seeded. Isolated resource caches start fresh. The CLI does not reuse a global GetRemote cache to promise offline remote-resource builds. Required resources must be available as local inputs, or that operation must explicitly enable network access.

The CLI does not download a Go toolchain, install packages, alter global configuration, or enable telemetry.

The current development and validation baseline is Hugo Extended 0.168.0; the version gate requires 0.168.0 or newer. Do not enable --panicOnWarning or HUGO_PANICONWARNING; warnings stay visible, including Hugo’s accepted upstream LibSass notice. Initialized sites use the CLI’s separately maintained Starter snapshot; theme upgrades follow the upgrade guide. Initialized sites retain ordinary Hugo inputs and need only their documented dependencies after removing the CLI.

Retired local Studio

studio is removed from the CLI. Use an ordinary editor and oink dev for a site preview. Read maintenance facts through inspect and structured reports. The dated R7 acceptance remains historical evidence for its identified inputs.

Retired management API

The CLI no longer serves a management API. Earlier Studio API acceptance does not describe the current executable.

Historical capture limits

Historical R7 limits belong to the dated acceptance record. Current command coverage and supported inputs are defined in this contract.

Retired general editing

edit and Studio editing are removed. Edit source with an ordinary editor, then run check. new, move, review records, and baseline plans retain candidate validation and byte/mode guards. Previously saved editing plans are rejected. The dated R8 record remains historical evidence.

Retired text and field editing

The CLI no longer owns general text or front matter editing forms.

Retired snippets and attachment editing

Write Markdown and add attachments with the site editor. The CLI no longer provides a snippet catalog or general attachment editing command.

Retired Studio editing

The CLI does not serve an editor or accept browser Apply requests.

Verification and remaining scope

Use make test for offline Go tests and vet. Use make test-hugo for actual Hugo integration and make test-tools for configured optional tools.

Local integration gate rechecked on 2026-10-14

make test (offline Go tests and vet) and make test-hugo passed on macOS arm64 with Go 1.27.1 and Hugo Extended 0.166.0. The cold-cache configuration failure is fixed: both JSON readers accept leading hugo: collection notices, retain raw evidence, and reject unexpected prefixes or malformed JSON. A deterministic regression forces the notice on every configuration call. Native-rule checks also passed against the audited public v1.2.0 sources. Optional tool adapters require their separate make test-tools gate.

The dated maintenance acceptance record preserves earlier R1–R8 and A18 evidence. Declared targets are macOS arm64 and Linux arm64/amd64. Darwin amd64 is experimental and unqualified. Windows is unsupported.

7.7.6 - Component refinement across four visual presets

Consistent component surfaces and controls, with three optional Markdown presentation variants.
Accepted in OINK 1.4.0

Shared component styling and three optional presentation variants ship in 1.4.0. The component contract owns their behavior; the comparison guide shows them together.

Context

Cards, disclosures, Tabs and code controls had accumulated different spacing, borders and feedback treatments. A shared refinement keeps familiar authoring syntax while making related components feel consistent. The dated review records implementation and acceptance evidence, including the original staged plan.

Accepted style rules

Use the existing page, content and floating surface tokens. Independent cards have their own boundaries; grouped lists share a frame and internal dividers. Nested code and tables should not accumulate redundant borders.

Keep labels and hit targets fixed during press feedback. Reserve space for localized copy feedback, show success only after copying succeeds, and preserve visible failures. Keyboard focus is immediate; touch controls have usable hit areas. Reduced motion keeps state changes clear without travel.

Paper and Slate retain rounded surfaces. Ink keeps square, flat geometry; Terminal keeps compact, flat geometry. Paper’s decorative heading tails are removed. Ink and Terminal remain opt-in experiments; component refinement does not promote them to stable presets.

Optional authoring variants

The default grid, steps and code blocks remain available. Authors select a variant only when it suits the content.

Divided card list

- [Installation](/docs/start/) — Set up a local site.
- [Configuration](/docs/customize/config/) — Configure the site.
{.cards .td-cards-list}

This produces a single-column group with internal dividers. The full shortcode accepts cards variant="list". Item order, links and descriptions are preserved.

Compact command block

```bash {class="td-code-command"}
hugo server
```

A marked one-line command becomes a compact strip with a dedicated copy control. Multiline, numbered or collapsed blocks warn and retain regular presentation. Literal source and copied text are preserved.

Compact steps

1. Prepare configuration.
1. Run the local preview.
1. Review the generated page.
{.steps .td-steps-compact}

The full shortcode accepts steps variant="compact". Smaller markers and spacing suit short procedures while retaining numbering, nesting and anchors. Unsupported shortcode variants warn and use the default presentation.

Scope and compatibility

The same style rules cover tables, Fields, badges, Landing actions and shell rows. Existing renderers, clipboard behavior and tokens remain the owners. There is no new JavaScript dependency, font download, global density setting or external Sass executable. Native Markdown, no-JavaScript reading, Print and machine-readable outputs retain their content.

A moving selection background was evaluated but deferred. OINK 1.4.0 keeps static selection feedback. Any later motion trial needs its own acceptance.

Validation and delivery

Focused theme checks cover components, content primitives, code blocks, reading, shell, presets and namespace boundaries. The real site’s component presentation and appearance suites cover EN/ZH, light/dark, desktop/mobile, keyboard, touch, reduced motion and printing. The three variants and shared styling ship in 1.4.0.

7.7.7 - Paper and Slate visual presets

Accepted phase-one visual identity and appearance controls, with separate reader style and mode state.
OINK 1.2.0

Paper and Slate ship in 1.2.0. Ink and Terminal are included as explicit opt-ins; their remaining design work is recorded below.

Decision

Paper is the default, with warm paper/ink colors, blue links, IBM Plex Sans, heading hairlines and framed tables. Slate retains the v1.1.0 palette, Inter/Chakra/Plex Mono roles and Landing grid/glow. This gives reading sites a quieter default while preserving an explicit compatibility choice. The cost is a visible default change: existing sites can set params.ui.preset: slate. The 1.2.0 release notes and upgrade guide call out this default change.

The reader menu is opt-in (preset_menu: false). The docs site enables it. One Appearance disclosure combines native Style and Light radio groups; mobile uses a modal dialog in the browser top layer. It is reachable by touch and keyboard without relying on hover. The cost is replacing the old one-click mode toggle with a selection panel; the t shortcut still toggles mode.

Style and mode use separate attributes and storage keys. Choosing the site default preset clears the style key. Hugo renders the default without JavaScript; an allowlisted inline script restores reader state before CSS. This prevents the common initial preset mismatch, while keeping blocked storage usable. Presets ship in one stylesheet, at the cost of additional CSS bytes.

brand separates the wordmark from display headings. Paper adds the local OFL IBM Plex Sans variable font, including normal/italic and the six supported small writing-system subsets. Font files download on use; system typography and explicit role overrides retain priority. Chinese uses the system stack. Phase 1 includes no serif face and no external font request.

Page task determines density: Landing keeps display scale, long articles keep their reading measure, and navigation/configuration tables remain compact. No global spacing increase, new shell, or geometry abstraction is introduced. Giscus and print follow the preset; API vendors and charts retain their current mode-only behavior. This keeps the first implementation bounded.

Later work

On 2026-10-24, the maintainer accepted removing Paper’s trailing heading hairlines because the decoration adds visual clutter. The OINK 1.4.0 implementation removes the line in both light and dark mode while retaining heading typography and anchors. This revision ships in OINK 1.4.0; the 1.2.0 decision above records the original appearance.

A subsequent October 5 experiment implements Ink and Terminal behind explicit configuration; see the experiment record. They are not stable defaults. preset_menu: true offers Paper/Slate and the site default; an explicit list can expose either experiment. The menu uses the same compact icon-and-name buttons for all four, without experiment badges. This gives reviewers actual theme output without changing ordinary menu choices. The cost is additional scoped CSS and a larger menu when experiments are enabled.

The experiment uses owned component rules for square/2 px geometry and compact desktop navigation instead of introducing a global density framework. It reuses existing local fonts, state handling and accessibility controls. Charts and API vendors stay mode-only; comment palettes and print follow the experiments. Remaining work is visual acceptance, wider device review and any decision to promote them into the stable set.

Evidence

On 2026-10-25, the maintainer authorized component refinement using the existing four presets. Shared surfaces, grouped rows, stable labels, and background-only press feedback reuse current tokens and CSS. Markdown remains the primary authoring path; the three local presentation variants are explicit opt-ins. Paper/Slate keep rounded surfaces, while Ink/Terminal retain flat, square/2px geometry. The moving selection trial is deferred: static states provide the needed feedback without a second geometry or runtime layer. These changes ship in OINK 1.4.0; the component contract owns their supported syntax and fallback behavior.

The architecture contract and shell contract own behavior. check-presets.py owns token parity, AA palette checks, the frozen Slate v1.1.0 palette and strict configuration output. Font, parameter, vendor, namespace, action and runtime checkers retain their existing ownership. The documentation site’s appearance.spec.mjs tests real output; the dated acceptance record distinguishes executed checks from remaining experiments.

7.7.8 - Expo image collections

Record the reasons for album bundles, shared appearance, three display modes, and the image viewer.
Available in OINK 1.3.0

Expo is included in OINK v1.3.0.

The Expo contract defines required behavior. The authoring guide explains how to create an album.

Context

Docs organizes explanations. Blog organizes posts. Book organizes chapters. Expo organizes image collections into topics and albums. Readers need to scan an album, select an image, and inspect its details.

The Graph experiment used a separate copy of an existing collection. It contained 7,658 originals in 149 albums. The images included mixed dimensions, long images, GIFs, captions, and existing citation anchors. This collection supplied the initial content and scale requirements.

Decisions

One bundle per album

An album is one Hugo leaf bundle: index.md and image files. Filenames provide default titles. Optional resources metadata adds descriptions, credits, order, explicit IDs, and paths to separate originals.

Model considered Effect on authoring
One directory and metadata file per image Many small files to manage for a large collection
One Markdown page per image Useful when each image needs an article, but adds a page model to every image
One bundle per album with optional resource metadata An album can start with image files and one page; metadata can be added later

Expo uses the third model. The album owns the URL and reading sequence. Each image has a fragment anchor. Authors use explicit IDs for lasting citations. They split large collections into albums themselves. This version has no resource pagination or separate image database.

Shared appearance and compact controls

Expo is a layout. It uses the existing Paper, Slate, Ink, and Terminal presets and their light/dark modes. Shared colors, type styles, navigation, and corner settings keep the album consistent with the rest of the site.

Three adjacent radio cells expose all display choices: Wall, Cards, and Seamless. The play/pause button and size slider follow them. Icons keep the toolbar compact. Localized names, tooltips, and focus indicators make the controls usable with keyboards and assistive technology.

Reader display choices persist across albums and appearance changes. Appearance controls stay in the navigation. The image toolbar supplies album actions, so the album body omits the article action menu. The gallery fence remains the component for image groups inside text pages.

Animation and image viewing

GIFs play by default. Pause and the system’s reduced-motion preference provide still previews. Readers can start playback explicitly. Lazy loading reduces initial requests, but large GIFs still transfer the original files. Authors must consider album size when they group animated images.

The viewer starts at original size and shrinks large images to fit. Expand to fit uses the smaller width or height ratio. This rule keeps the whole image visible. Manual zoom, mouse dragging, and WASD movement let readers inspect details. Q/E and H/L move through the album. J/K control zoom, R resets, and F expands to fit. These shortcuts apply inside the viewer so they do not also move the page.

Original-file and album-location links preserve access and citation. A native modal dialog supplies focus containment. Expo loads its own album runtime only where needed. Ordinary Image Zoom keeps its existing single-image behavior. Without JavaScript, readers can still use the image links and captions.

Stable geometry and shared outputs

A width-consuming scrollbar changes the grid width. A new width can change the row count and total height. Near the viewport boundary, these changes can make the scrollbar appear and disappear repeatedly. This feedback caused the reported flicker even when the viewer was closed.

Expo reserves scrollbar space and preserves the grid width during modal scroll locking. The regression test forces a width-consuming scrollbar and samples successive frames. A single screenshot cannot detect this failure.

One image resolver supplies HTML, Print, Markdown, LLMSFULL, RSS, and album covers. This keeps image order and metadata consistent across outputs. Static formats remain usable without viewer JavaScript. Changes to the resolver require checks for each affected output.

Validation and ownership

The theme repository owns the resolver, layouts, runtime, schema, and focused checks. The documentation site owns the bilingual examples, browser tests, and desktop/mobile visual review. Acceptance includes all four presets, light/dark mode, keyboard input, focus, animation, and static outputs.

The copied Graph collection supplies additional evidence at a larger scale. Its original acceptance checked 7,658 file hashes, image anchors, and the unchanged source collection. Personal image data, generated output, and screenshots remain outside the theme and documentation source repositories.

Use the commands in the Expo contract for current acceptance.

7.7.9 - Blog posts as a gallery

Keep articles as the content model while reusing Expo geometry for a fourth blog index view.
Accepted in OINK 1.4.0

This decision records the released gallery blog view and its compatibility boundaries.

The shell contract defines the behavior. The design blog explains the reading experience, and the authoring guide gives configuration.

Context

Some blog sections are most easily scanned by their covers. The existing row, card, and table views prioritize article information. Expo already supplies image-wall geometry, but its album resources and image viewer have a different content model and destination.

Decisions

Gallery is a fourth blog_index form. It consumes the same paginator slice and featured-image resolver as the other blog views. Articles retain their URLs and metadata; authors do not duplicate them in an album manifest.

Share Expo’s row geometry and control styling. Keep the blog runtime and reader preferences separate, because a blog cover opens an article rather than an image viewer. The wall, cards, and seamless arrangements are choices within the gallery, while the outer view switch selects among the four blog forms.

Contain each cover within bounded tile proportions. This preserves the image at the cost of some blank space for extreme aspect ratios. Narrow layouts use tighter bounds so captions retain a readable width. Touch captions stay below the images and contribute to row height; keyboard focus reveals desktop captions. Missing covers keep an article link, and a no-JavaScript grid keeps titles and descriptions visible.

Consequences

An enabled view switch publishes markup for all four forms. Hidden views use lazy images, but preparing their thumbnails still costs build time. Authors can disable switching to emit only their chosen form. This change adds no remote image service or new browser library.

Native blog section indexes own the automatic integration. Custom home and term templates must explicitly render the gallery and its controls. A site configuration setting does not replace custom templates.

Verification and ownership

Theme checks in bin/check-shell.py compare article order, pagination, external links, missing covers, and invalid configuration fallbacks. The configuration schema follows the theme defaults and resolver. Runtime tests verify the four-form cycle and saved preferences.

The documentation site’s tests/browser/blog-gallery.spec.mjs owns browser acceptance with synthetic, bilingual content. It covers layout changes, resizing, independent preferences, keyboard navigation, touch captions, failed images, no-JavaScript output, and gallery accessibility.

7.7.10 - Explicit table reading

Separate technical table layout from an optional corner action for reading in a larger viewport.
Accepted in OINK 1.4.0

This decision records the released .expand table-reader behavior.

The component contract defines the behavior. The table guide gives author syntax; the dated research retains the earlier comparisons.

Context

Most documentation tables fit ordinary reading. A few package, parameter, or compatibility tables benefit from more room. A toolbar above every table adds visual weight to an occasional task. The reviewed prototypes compared a corner expand action and a hover-revealed ellipsis menu.

Decision

Use .expand to opt individual Markdown tables into a native reading dialog. .wide controls intrinsic table sizing independently; {.wide .expand} combines both. Do not infer expansion from size, column count, or .wide, and do not add a site-wide switch.

A single expand action needs one click. Place it at the top-right corner, revealing it on desktop hover or keyboard focus and keeping it visible on touch devices. The visible icon stays small; its touch target is at least 44px. Do not add an above-table toolbar or a one-item ellipsis menu.

Move the original table and its scroll wrapper into one modal dialog; preserve text size, native semantics, links, selection, and authored order. Restore that same node, inline scroll offsets, page position, and entry focus on close. Numbered captions supply the dialog title without duplicating IDs. Restore the table synchronously before browser printing. Without JavaScript or dialog support, readers retain the original inline table.

Boundaries

This delivery has no sorting, filtering, TSV export, density/wrapping switches, new column-pinning controls, virtualization, separate reading route, or Fullscreen API. Existing .matrix semantics remain available. Only theme-rendered Markdown tables opt in; .fields warns and ignores .expand. Tables with external headers or ARIA references stay inline because the modal makes their external targets inert. Custom ancestor-dependent styles need review when a table moves to the dialog.

The dialog inherits existing preset and light/dark tokens. Paper is the reviewed entrance design; other presets receive compatibility checks rather than new visual variants. Mobile reading still uses horizontal scrolling when the data is wider than the viewport.

Ownership and validation

The theme owns table rendering, the capability-gated runtime, localization, and scoped SCSS. bin/check-content-primitives.py covers opt-in, invalid combinations, localization, and static outputs. The documentation site’s tests/browser/table-reader.spec.mjs owns keyboard, touch, links, selection, focus/scroll restoration, printing, failures, and accessibility checks. Runtime checks and both site Make suites remain delivery gates.

The original draft is retired from active proposals and retained in Git history. The research describes dated experiments, including features intentionally excluded here.

7.7.11 - Translation comparison

Compare a page with an existing same-origin translation in a two-pane reader that loads only when asked.
Accepted in OINK 1.5.0

This decision records the released params.ui.compare translation reader.

The shell contract defines the reader and its controls. The architecture contract defines the browser boundary. The multilingual guide gives site setup; the dated research retains the earlier source review.

Context

Translated documentation is often read and reviewed side by side. Without built-in support, readers had to open two browser tabs and scroll each one manually. Generating a dedicated layout or prebuilt language-pair pages at build time would multiply the output tree and duplicate content. Because OINK sites already publish each translation as a standard HTML page with aligned heading IDs, a lightweight reader can pair those existing pages directly in the browser.

Decision

A single boolean parameter, params.ui.compare (default false), enables comparison. Hugo lists only published, same-origin HTML translations of Docs, Book, and blog pages. Drafts, non-HTML output formats, external hosts, and the language-home navigation fallback never qualify. Neither do translations whose build policy writes no HTML (build.render: link or never, set directly or by cascade), nor blog archive pages after the first, which have no counterpart. Any language that sets compare: false under languages.<lang>.params.ui is excluded as both a host page and a comparison target. Authors do not need to add front matter, custom layouts, or extra output formats.

The reader loads on demand: from an icon button beside the page actions, via the B keyboard shortcut when keyboard navigation is enabled (pressing it again closes the reader), or through the Command Palette action compare_translations. It opens two existing HTML pages inside same-origin frames within a modal dialog. The current page stays on the reading-start side, so right-to-left host pages mirror the pane order. Across the UI, the feature uses a single name: Duo View, with the trigger labelled Open duo view (it shipped in 1.5.0 as Bilingual View; see Revisions).

The Sync Scroll switch matches shared visible heading IDs and interpolates smoothly between them. Partial matches use the available anchors; when two pages share no headings, the reader falls back to overall document progress. An orange exclamation mark beside the switch explains either case. Matched headings that cannot scroll to the reading line do not count as a mismatch. Duplicate or out-of-order heading IDs disable synchronization while leaving both panes independently scrollable. Clicking Close, pressing Escape or B, or navigating Back restores the host page’s scroll position and focus. Reloading an open reader or returning to its history entry reopens the view in place so a single Back step still closes it.

On viewports narrower than 900 CSS pixels, the reader displays one retained pane at a time beneath a compact toolbar row and a language switcher bar (which also holds the target-language menu). At 320 × 256 CSS pixels (roughly 400% zoom), at least 160 CSS pixels of vertical reading space remain visible.

Pane documents suppress shell navigation runtimes, search, comments, and theme-managed analytics. When params.ui.compare is enabled, the theme renders Google Analytics through its own pane-aware template so pane loads are not counted as separate page views; a site’s local override of Hugo’s google_analytics.html partial is not used. Custom scripts in head hooks can check window.OinkComparePane.

Boundaries

This release does not include cross-version diffing, manual page-pair maps, paragraph-by-paragraph row locking, text diffs, or cross-origin targets. Panes require frame-src 'self' and same-origin framing: X-Frame-Options: DENY or frame-ancestors 'none' blocks frame loading and falls back to a direct link. While open, the host page and its two panes exist as three live documents; nothing extra is fetched until the reader opens.

Ownership and validation

The theme owns discovery, the lazy entry and reader runtimes, the 32 interface catalogs, and scoped SCSS. bin/check-compare.py covers discovery, Docs, Book and blog pages, the per-language opt-out, keyboard hints, analytics, output isolation, and how every runtime behaves inside a pane. tests/js/compare-scroll.test.js covers scroll geometry. The documentation site’s test:compare suite owns sync, lifecycle, reload, narrow and zoomed layouts, right-to-left order, eight languages, failures, and accessibility.

Revisions

  • v1.5.1 (2026-11-06): The interface name changed from Bilingual View to Duo View (Chinese 双列视图). The reader sets any two pages side by side, including same-language script pairs such as Simplified and Traditional Chinese, so a short name for the two-pane layout fits better than one about two languages; each locale uses its own concise equivalent. The entry, Command Palette action, title and help panel follow; the parameter and action ID are unchanged. B now toggles between the page and the reader, and scroll keys work as soon as it opens. The last chosen target language and Sync setting are remembered in the browser, and a reload or back/forward restore also brings back the visible pane and both reading positions. Sync Scroll became a switch. Its warning became an orange exclamation mark without a badge, with a note on hover, focus or tap that distinguishes a partial match from overall-progress sync. A matched heading that merely cannot reach the reading line no longer reports a mismatch; when no shared heading can anchor the map at all, the note says that sync follows overall reading progress.

The proposal draft is retired from active proposals and retained in Git history.

7.7.12 - Unified diagram reading

Shared Mermaid and local PlantUML viewing, source export and demand loading.
Accepted in OINK 1.4.0

Shared diagram viewing, source export and demand loading ship in 1.4.0. Components, Architecture and browser printing own the behavior.

Context

Before this change, Mermaid had a pan/zoom dialog but loads its engine at page startup and renders a second SVG for the dialog. Local PlantUML loads both engines at startup and offers only horizontal scrolling. Both preserve authored source.

Scope

Share the existing dialog and styles, source viewing/copy, and standalone SVG download. Defer large engines and rendering until a diagram is within 320px of the viewport. Do not change pinned libraries, server-mode PlantUML, other diagram families, the CLI, or ordinary consumers’ Hugo-only build requirements.

Behavior and boundaries

One lightweight scheduler serializes both engines on the main thread. Each engine loads once. Hidden or zero-width containers wait until visible. Theme changes invalidate old results; the open viewer retains its reading position. The viewer reuses a rendered snapshot, supports touch pan/pinch, wheel and keyboard controls, fit, Escape, focus restoration and reduced motion. Source is available even after an error. Copy and export report failure, and downloads report initiation only.

PlantUML stays in Blob images; its SVG is never inserted into the live DOM. SVG exports embed their styling, remove active content and reject external resource dependencies. No includes, themes or optional libraries are downloaded. Browser printing shows a rendered diagram or readable source synchronously; it cannot wait for an asynchronous engine in beforeprint. Dedicated Print, Markdown and RSS retain their existing source fallback.

Compatibility

No configuration or build step is added. Local PlantUML remains opt-in; explicit endpoints retain the legacy renderer. Without IntersectionObserver, bounded scroll/resize checks perform the same visibility test. Source survives disabled JavaScript and blocked assets.

Implementation and acceptance

Extract shared scheduling and viewing from assets/js/mermaid.js; retain engine adapters and the existing dialog partial. Update focused output checkers, runtime tests, real EN/ZH documentation and browser tests. Verify multiple and wide diagrams, hidden tabs/disclosures, rapid themes, errors, blocked resources, source fidelity, standalone exports, touch, keyboard, accessibility and printing. Compare requests before/after.

Decision log

  • 2026-10-25: Initial bounded design. Main-thread serialization and Blob isolation are retained. Immediate browser printing uses source for unrendered figures. Local implementation and focused browser acceptance are complete; broader site validation is recorded with the delivery evidence. This behavior ships in OINK 1.4.0.

7.7.13 - Local search refinement

Mixed-language queries, technical identifiers, section results, and palette scope in OINK 1.4.0.
Accepted in OINK 1.4.0

Owner: OINK maintainers. Affected contract: Shell. This search behavior ships in OINK 1.4.0.

Evidence and scope

The previous CJK path required one contiguous query in one field. English uses Lunr, and both return page links without section context. Preserve the one local index per language, existing commands, input-method composition, exclusions, keyword weights, and positive page boosts. No dictionary, service, account, external executable, configuration key, or extra build step is introduced.

Accepted behavior

Split whitespace and Latin/CJK boundaries while preserving technical identifiers such as shared_buffers, pg_basebackup, --baseURL, and /pg/data. Match terms across indexed fields. Return all-term pages first, with contiguous phrases ahead of other matches; only when no page covers every term show partial matches with visible coverage. Apply the page boost within those tiers. Keep English stemming and bounded spelling fallback through the bundled Lunr runtime.

One result per page selects its best matching section and excerpt. Read actual rendered heading IDs, never reconstruct a slug. Preserve language and base paths. Store body text once, with UTF-8 byte offsets for section boundaries. Heading and summary scopes keep heading anchors but do not silently index full bodies.

Add a labeled native radio group inside the existing palette, offering all pages or the current top-level content root. Default to all pages; commands and actions remain available. Announce result counts and partial coverage. Preserve keyboard selection, focus trapping, and deferred search during IME composition.

Compatibility and checks

Existing page metadata and scope configuration remain valid. Sections are additive index metadata; the engine continues to accept old indexes without sections. Theme owners: check-search.py, search-engine/runtime tests, and palette checks. Site owners: bilingual real-content query expectations, root/subpath anchor checks, and browser keyboard, composition, scope, responsive, light/dark, and axe checks. Compare old/new results and latency on identical real corpora. Record page and section counts, tool versions, warmup/repeats, and raw/gzip bytes. Every language must remain within 2 MiB raw and 512 KiB gzip. Synthetic cases cover ambiguous identifiers and duplicate, Unicode, explicit, and special-character heading IDs.

Decisions and acceptance

2026-10-25: Implemented locally after the initial design. The Shell contract and Search/Command palette guides now own the behavior. Theme checks exercise all four index scopes, actual UTF-8 section offsets, multilingual/subpath IDs, legacy index compatibility, boosts, and root-aware dialog caching. The site owns tests/search/queries.json and scripts/check-search-corpus.mjs; its summary query gate runs in root and subpath integration builds.

On the frozen documentation source ab2747a (149 pages and 1,321 headings per language), the 43-query set includes two negative summary cases. Top-five expected-page hits are EN summary 7/7 → 7/7, ZH summary 2/7 → 7/7, EN content 10/10 → 10/10, and ZH content 6/17 → 17/17. These are curated regression expectations, not general relevance scores. 连接池 超时 has no full match in this corpus and deliberately checks labeled partial results; a synthetic owner case checks the full match.

Frozen summary indexes are 185,183/184,281 bytes raw and 39,529/43,208 bytes gzip (EN/ZH), within budget. Full-content diagnostic indexes are 1,640,294/1,516,283 bytes raw and 536,971/578,674 bytes gzip, over the gzip budget. The maintained site retains summary scope; enabling all historical research text is not a budget-compliant configuration. The implementation never silently truncates it.

Browser acceptance covers EN/ZH, 390/1440 px, light/dark, real anchor navigation, visible/announced partial states, native scope selection, Blog/Book roots, composition and command retention. The artifact directory for this local run is /Users/vonng/tmp/oink-search-20261025/; it contains exact commands, JSON timings, logs and screenshots. These dated measurements describe that corpus; 1.4.0 includes the accepted behavior.

2026-10-26: Fixed cross-output contamination during index reads, added multi-output owner coverage, and completed the failed-route accessibility and file-tree retries.

2026-10-26: Refined the palette after visual review. The two scope choices are visible as compact segments with native radio keyboard behavior and 44 px hit areas. Reduced the desktop panel and row spacing, softened the selected result, and combined live status and keyboard hints into one footer.

2026-10-26: Adversarial review corrected normalized excerpt/highlight offsets, distinguished heading IDs from data attributes and script strings, and scoped English spelling fallback to the selected root. Regressions cover compatibility characters, combining marks, shortcode headings, and literal HTML in result text.

7.8 - Design research

Dated experiments and consumer evidence that inform OINK design decisions.
Dated research records

Research records capture measurements, inputs, and tool versions from specific studies. Current behavior is defined by the Design contracts and validated on Hugo Extended 0.168.0 per the toolchain contract.

Publish research in the Design tree only when another maintainer can inspect its method, understand its scope, and repeat the applicable check. Keep raw agent transcripts, temporary build logs, and local absolute paths out of research records.

Research map

Record Evidence
Goldmark block attributes Render-hook visibility and CommonMark container limits on the historical Hugo versions recorded in the study
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
Community issue and PR review, 2026-09-19 Reproductions, PR acceptance advice, and remedies for sidebar, focus, and search feedback
OINK 1.1 release review, 2026-09-20 Five runtime repairs, documentation readiness, validation evidence and publication boundaries
CLI acceptance snapshot, 2026-09-29 Executed Starter, real-site, offline, upgrade, and reproducible-archive checks for the local CLI candidate
Visual preset acceptance, 2026-10-05 Paper/Slate local implementation, actual output and bounded browser evidence
Ink and Terminal experiment, 2026-10-05 Explicit experimental presets, design tradeoffs and real-site verification
OINK 1.2 pre-release review, 2026-10-05 Final local candidate checks, cleanup, local resources, compatibility and publication boundaries
CLI maintenance acceptance, 2026-10-03 Historical R1–R8 and A18 source-bound runtime and archive evidence
Theme review, 2026-10-13 Reproduced output and interaction defects, repairs, and local validation scope
Component refinement, 2026-10-25 Final component acceptance, 409 Chromium tests, targeted three-engine visual review, CSS/JS size comparison, and separate gallery baseline failures
Document comparison, 2026-10-29 Dated source review behind the translation comparison decision; historical, not the released contract
Review repairs, 2026-11-06 Reproduced v1.5.0 defects, bounded development repairs, validation and remaining work
OINK 1.5.1 readiness, 2026-11-06 Three repaired candidate regressions, owning checks, browser acceptance, publication evidence and release gates

Publication rules

A research record states its date, inputs, applicable versions, method, result, and known limits, with changing counts labeled as snapshots. External framework comparisons should be refreshed from primary sources before publication and focus on conclusions that affect OINK.

When a result becomes an accepted product choice, link it from an accepted decision. If it proposes behavior that does not exist, move the design question to Proposals.

7.8.1 - Goldmark block-attribute evidence

Repeatable probes of lists, images, tables, passthrough blocks, fences, callouts, and nested containers on Hugo 0.160.1 and 0.164.0.
Verified snapshot

These probes produced identical relevant output bytes on Hugo Extended 0.160.1 and 0.164.0. They explain OINK’s native component forms. The current component contract defines required behavior.

Method

The probe used a minimal Hugo site without OINK templates, with render hooks printing their context fields and .Attributes as visible markers. The site enabled Goldmark block attributes and passthrough delimiters for inline and block math, allowed unsafe rendering to inspect raw HTML, and set wrapStandAloneImageWithinParagraph: false.

Each source shape was rendered with the minimum supported Hugo and the then current Hugo version, comparing relevant output byte for byte to record Goldmark’s structural behavior.

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 outer wrapper 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. Its template must put a blank line before and after the inner Markdown. Without both blank lines, Hugo may treat a following list as literal HTML-block content instead of Markdown.

A multi-line % container inside a CommonMark list item has another limitation: its generated HTML lacks list-content indentation, so the list closes before the container and restarts after it. OINK therefore keeps a full Steps form for steps that must contain another full container. Ordinary rich blocks, fences, and < shortcodes do not have this limitation.

In the applicable collector shape, nested % shortcodes receive inner HTML that is already rendered. If a collector requires the child’s original Markdown, it uses < delimiters and renders the captured body through the shared scoped block renderer.

Attribute ownership

Each hook enforces an explicit attribute allowlist, rejects style and inline on* handlers, and validates URL-bearing values through the shared URL policy. A hook retains a site class only where downstream CSS is an established extension mechanism.

The experiment showed that gallery images inside list items can be block images, but the image hook still does not receive their parent list’s marker. A runtime therefore relies on a theme-emitted marker or a structural fallback.

Limits and verification

These results cover Hugo 0.160.1 and 0.164.0 with the stated Goldmark settings. Before updating this snapshot for a minimum-version change, rerun the focused component, Book, table, gallery, and Markdown-output checks.

7.8.2 - OINK 1.3.2 and Hugo 0.168 consumer inventory

Local inventory of 37 maintained OINK sites using v1.3.2 and Hugo Extended 0.168.0, with visible warnings and separate publication evidence.

This dated inventory records local checkouts under ~/pgsty and ~/www. The current build policy is owned by the architecture contract and diagnostics decision.

Baseline

OINK v1.3.2 was published on 2026-10-18 at 13:37 UTC. Its tag resolves to bd2e856e6e2f7027a9189af009701f5b8cf562cf. All maintained sites use this release and Hugo Extended 0.168.0. CI, local checks, preview, and deployment commands omit the warning-fatal flag; HUGO_PANICONWARNING must also remain unset. Warnings stay visible for review. The upstream LibSass deprecation notice is accepted. No external Sass compiler is required, and older Hugo versions are no longer development targets.

The observed local tools were Hugo 0.168.0+extended+withdeploy on macOS arm64 and Go 1.27.2. Module verification disabled environment replacements and both Go and Hugo workspaces. Classic consumers were compared with the release’s runtime sources. All 37 builds preserved Paper and the explicit four-preset reader menu.

Sites

Paths are relative to the home directory. Every row uses OINK v1.3.2 and Hugo Extended 0.168.0. “Module” means the resolved public module; “copy” and “submodule” identify classic themes/oink consumers.

Checkout Theme source Local production build
pgsty/barn.pgsty.com module Passed
pgsty/boar.pgsty.com module Passed
pgsty/caps.vonng.com module Passed
pgsty/cloudexit.org module Passed
pgsty/exp.pgsty.com module Passed
pgsty/oink-starter module Passed
pgsty/oink.pgsty.com module Passed
pgsty/pgetcd.pgsty.com module Passed
pgsty/pgext module Passed
pgsty/pgredis.pgsty.com module Passed
pgsty/pgsilo.pgsty.com module Passed
pgsty/pgsty.com module Passed with local report data; see below
pgsty/pgsty.pro module Passed
pgsty/pig.pgsty.com module Passed
pgsty/pigsty.cc module Passed
pgsty/pigsty.io module Passed
pgsty/repo.pgsty.com module Passed
pgsty/silo.pgsty.com module Passed
pgsty/sow.pgsty.com module Passed
pgsty/wok.pgsty.com module Passed
www/aiempire module Passed
www/ca.vonng.com submodule Passed
www/ddia module Passed
www/dharma module Passed
www/doc.pg.center module Passed
www/fzb/site copy Passed
www/lzj module Passed
www/muse.vonng.com copy Passed
www/pg-internal module Passed
www/pg36g module Passed
www/pgweekly module Passed
www/piglet.run module Passed
www/sg.vonng.com submodule Passed
www/tpme module Passed
www/us.vonng.com submodule Passed
www/vonng.com module Passed
www/zj module Passed

Boundaries

  • pgsty.com could not fetch its separately published report manifest: the remote request returned an unexpected EOF or timed out. Its build passed with the existing local report dataset and HUGO_REPOORT_USE_LOCAL=1.
  • sow.pgsty.com was checked on its existing publication-review branch. Non-Git and source-copy consumers were validated in place.
  • pgsilo/work/silo-docs is a frozen delivery baseline, as its owning repository documents. It is excluded from maintained-site upgrades.
  • This inventory records local source, module-resolution, build, and rendered-menu verification against the published v1.3.2 tag.

7.8.3 - Ink and Terminal experiment, 2026-10-05

Explicit experimental presets in real theme output, their design tradeoffs, checks and remaining work.
Local experiment record

Ink and Terminal compile into the theme stylesheet and use the Appearance control. They remain explicitly enabled experiments, pending design acceptance.

Inputs and method

This extends the Paper/Slate implementation on the October 5 working trees. Tools: Hugo Extended 0.166.0, Go 1.27.1, Node 26.9.0, and Playwright 1.62.1 on macOS ARM64. The documentation site’s local sibling-theme build is the integration surface; its published pin remains v1.1.0.

The same Home, configuration, callout, and tab content is compared across four presets, EN/ZH, 390/1440 px, and light/dark mode. Checks inspect real rendered fonts, overflow, appearance controls, and local font requests. Further component checks cover code, parameter fields, Blog, Book, API, Mermaid, ECharts, search, and print. API vendor DOM is excluded from axe under the site’s existing policy.

Design choices

Choice Improvement Cost / limit
Ink: black/white canvas, Inter, red markers, underlined prose links, strong heading rules Clear hierarchy and link affordance with little decoration Heavier headings and repeated rules need long-page editorial review
Terminal: mono controls/headings, sans prose/tables, teal links and amber emphasis A recognizable technical interface while retaining paragraph readability Long Latin navigation labels wrap sooner; CJK uses platform fallback faces
Square Ink geometry, 2 px Terminal geometry, no component shadows Visibly different surfaces using the same content and layout Scoped component rules add CSS; no global spacing/radius token layer
Compact Terminal desktop navigation only More useful navigation rows without shrinking article text Density is fixed per preset
Existing local fonts and state handling No new font files, external font service, framework or persistence mechanism All preset CSS remains in one stylesheet
Explicit experimental menu entries Reviewers can switch immediately without changing ordinary menu choices Four cards make the enabled menu taller

Ink uses #ffffff / #0b0b0b canvases, #141414 / #ededed text, and #c8102e / #ff5c4d accent. Terminal uses #f4f5f2 / #0c0f0e canvases, #1d211f / #d3dbd6 text, #0a6560 / #4cc9bd links, and #935400 / #f0a73a accent. Site/section accent overrides still win. Terminal’s heading markers use empty accessible alternatives; unsupported engines omit them. Its hero cursor is a static shape, with no typing, blinking, scanlines, or glow.

Try it

params:
  ui:
    preset: paper
    preset_menu: [paper, slate, ink, terminal]
    dark_mode: true

The local docs site enables this list. Select Ink or Terminal in Appearance, then choose light, dark, or system independently. Following the October 5 menu revision, all four options use icon-and-name buttons without experiment badges. A site may set either as preset without enabling reader choice. preset_menu: true remains Paper/Slate plus the site default. Selecting the site’s default preset clears the saved preset. Font overrides, system typography, and pre-CSS initialization use the same contracts as Paper/Slate.

Verification

Executed check Result and scope
check-presets.py AA text/link/accent contrast on three surfaces, light/dark token parity, advisory canvas luminance, frozen Slate v1.1.0 palette; seven strict builds and 28 document roots
Theme checks Parameters, font roles, 32 catalogs with 205 keys, generated schemas, component/output contracts, runtime isolation and namespace passed; 52 existing output goldens unchanged
Runtime tests 49 Node tests passed
make check 57 non-browser tests passed; EN/ZH coverage 142/142, Markdown, rendered content and internal links checked
Standard browser suites Eight suites passed 170 tests; the appearance suite passed all 49 after fixing the default-Terminal build issue below (219 checks total across the initial run and focused rerun)
Appearance coverage Four presets × EN/ZH × 390/1440 px × light/dark on Home, configuration, callouts and tabs; local font requests and menu axe checks; state, keyboard, print and Giscus asset checks; four experiment/mode checks over 11 page types plus search, and shared Mermaid contrast
Font/configuration builds Actual docs site rebuilt with Terminal as default: system fonts with/without explicit overrides, plus explicit technical-font overrides; all three passed
Browser engines Six checks passed on Chromium, Firefox and WebKit at 390/1440 px, including pre-CSS state, keyboard selection through Ink/Terminal, persistence and focus return; desktop Chromium also used 4× CPU throttling
Visual review 96 actual-output viewport captures; representative Home, Docs and mobile menu images inspected. The local comparison gallery selects content, language, size and mode without injecting styles

The standard sitemap axe pass used 15 routes: EN/ZH Home, configuration, callouts, tabs, and OpenAPI; English search, Mermaid, ECharts, Blog, and /book/04-design/. The existing responsive axe matrix also ran. Standard browser checks used the 4173 fixture server; the engine gate used the sibling-theme dev server at port 1313. No external font request was observed in the appearance matrix.

The first default-Terminal font build found an omitted entry in the advisory canvas-luminance map, causing false accent-contrast warnings. Both experimental canvases now participate, with checker assertions and authored-accent fixtures. Only the affected appearance suite was rerun after this correction. The added research index entry and updated proposal description were reviewed before refreshing the corresponding two changes in the LLMS golden.

The experiment exposed two new styling faults: the global underline suppression hid Ink’s links, and Terminal’s selected search row retained dim summary text. Both received scoped fixes. A separate inherited Mermaid dark-label pair (#cccccc on #585858, 4.43:1) reproduced in Paper and Slate. The shared mode-only default label background is now #404040; authored Mermaid values retain priority.

Remaining work

Before stable promotion, review the look on real Windows and Android devices, including CJK fallback faces, underlines, mono heading wraps, and long parameter tables. Manual screen-reader speech and first-paint filmstrips remain unverified.

Mermaid/ECharts keep mode-only palettes, API widgets keep vendor styling, and Giscus coverage checks generated palette assets. The decision to promote Ink/Terminal or redesign chart palettes remains open.

7.8.4 - OINK 1.2 pre-release review, 2026-10-05

Local 1.2.0 candidate review, cleanup, compatibility, resource provenance and publication checks, with release boundaries.
Local candidate review

This review covers the October 5 working trees prior to tagging v1.2.0. The public theme tag and the documentation site’s consumer pin were still v1.1.0 at review time.

Scope and inputs

The theme starts at a1979a4 and the documentation site at ed2d0e3. The reviewed working trees also contain the CJK keyword-summary, literal-percent outline, and repository-source-path fixes, the site’s existing English editorial changes, and the cleanup below. The separate optional CLI is outside this theme release.

Most checks used Hugo Extended 0.166.0, Go 1.27.1, Node 26.9.0, and Playwright 1.62.1 on macOS ARM64. Official, checksum-verified Hugo Extended 0.160.1 and 0.165.0 binaries were used for selected compatibility checks.

Review findings and cleanup

Finding Correction Impact
The 1.2 release draft omitted the new default and appearance controls Update both release drafts and upgrade guidance with Paper, the Slate compatibility setting, independent persistence, current-state icons and experimental preset selection Readers can identify the visible upgrade change before adoption
Current proposal, decision, experiment and source comments still described preview letters, experimental badges, a separate Default card or an undecided release target Align current descriptions with compact icon/name buttons, site-default reset and 1.2 release preparation; preserve dated test evidence Guidance agrees with the accepted interface while keeping historical results intact
A working-tree ignore rule hid all site tests/ except two files Remove that broad rule; retain existing generated-output exclusions New regression tests remain visible to Git
Development previews do not expose production-only analytics Inspect strict production output and distinguish core local resources from explicitly configured services The local-first boundary is verified against production output

These cleanup findings required no runtime change. Earlier working-tree runtime fixes are covered by their owning checks and the final integration run.

Executed verification

The local candidate passed the following technical pre-release checks. No release-blocking theme defect was found within this scope. Counts are dated snapshots of this working-tree review.

Check Result and scope
Preset checker Passed: seven warning-strict configuration builds, 28 document roots, light/dark token parity, AA text/link/accent checks on three surfaces and the frozen Slate v1.1.0 base palette
Runtime tests 49 Node tests passed, including current-state icons, search summaries, outline tracking, clipboard and dialog focus
Theme regression and tooling checks 40 checker/tool commands passed, including 90 migration tests, snapshot/consumer safety and current output goldens; the PDF browser case was then rerun with an explicit browser, with all three isolation tests passing
Documentation checks Final make check: 57 tests passed; 143/143 bilingual files, 1,197 source headings, 228 rendered content pages and internal links checked; only the new research index entry and changed release title/description required reviewed Markdown-golden updates
Standard browser suites One complete make browser run passed all 219 checks across nine suites, including the full 372-route sitemap axe scan; no failed, flaky or skipped cases
Documentation follow-up A fresh build passed a separate axe scan of 14 updated EN/ZH routes, including this new report pair
Browser engines Six appearance checks passed on Chromium, Firefox and WebKit at 390/1440 px; pre-CSS restoration, keyboard selection, persistence and focus return; desktop Chromium also used 4× CPU throttling
Visual spot checks Current-output captures inspected for the Chinese mobile Paper menu, English desktop dark Paper menu and Chinese Terminal reading on mobile/desktop; two-column icon/name options, current-state icons and reading layout confirmed
Hugo compatibility floor 0.160.1 passed preset and reading/math checkers plus a strict minified production build of the actual documentation site
CI Hugo version 0.165.0 passed a strict minified production build of the actual site, Hugo Module/include/static/print checks, system typography, legacy Sass font overrides and expected rejection of invalid typography
Production resources 28 page visits across four presets and seven routes; core fonts and scripts served from the site’s origin; configured external services recorded separately
Production output security Passed on 921 files in the final strict production build with the documented third-party integration policy
Book publication Root and subpath EPUBs passed the theme checker and EPUBCheck 5.3.0 with zero errors/warnings; both PDFs passed the 23-page, five-chapter structure checks; the root PDF script-isolation probe passed
Published consumer pin Existing v1.1.0 resolved and passed the site’s release-pin check with environment replacements and both workspaces disabled

The full sitemap scan follows the site’s existing axe policy: OINK-maintained surfaces are checked, Giscus requests are blocked, and Swagger UI/Redoc vendor DOM is excluded. Responsive checks cover 360, 768, 820, 1024, 1200, and 1440 px in English/Chinese and light/dark mode.

Appearance checks use the same Home, configuration, callout, and tab content in four presets, both languages, 390/1440 px, and light/dark mode. They also cover search, code, tables, input/focus states, Blog, Book, API, diagrams, and print. Ink and Terminal remain explicitly selected experiments.

Publication used local Pandoc 3.11, Java 26, and Chrome headless-shell 151.0.7922.34. The first attempt with the full Chrome for Testing application timed out on this Mac. Selecting headless-shell explicitly, as CI does, produced the verified PDFs. CI pins Pandoc 3.10 and Java 21 on Linux.

Local-first resource boundary

IBM Plex Sans, Inter, IBM Plex Mono, Chakra Petch, icons, KaTeX fonts, and core browser libraries are bundled locally. Preset switching introduces no runtime font-service or CDN-script dependency. System typography and explicit font-role overrides retain their documented precedence.

The production audit loaded Home, Chinese configuration, math, Mermaid, Markmap, ECharts, and OpenAPI under each preset, then switched dark/light mode. The production base origin was preserved while built files were served locally. Request tracing recorded and blocked off-origin requests; local fonts and diagrams still loaded without uncaught JavaScript or local HTTP errors.

Two configured services requested external scripts: Giscus and Google Analytics. Giscus is an optional comments integration; its OINK palette files are local. This documentation site configures an analytics ID, so production output includes Google Tag Manager’s script. Neither service is required by the new presets. Authored remote media and explicitly selected diagram services retain their opt-in boundaries.

Repeating the checks

Run owning theme checks before the real-site checks. These commands use the sibling theme through the documented Make targets:

python3 bin/check-presets.py
node --test 'tests/js/**/*.test.js'
make -C ../oink.pgsty.com check
env -u A11Y_PATHS -u PLAYWRIGHT_BASE_URL make -C ../oink.pgsty.com browser

The full theme checker set and publication commands are defined in .github/workflows/ci.yml. Run all owning checkers with fresh fixtures, including parameters/schema, vendored assets/fonts, navigation/search/actions, components, output/namespace/goldens, migrations, snapshot protection, consumer tooling, and PDF isolation. The engine suite is npm run test:appearance:engines in the site repository.

For compatibility checks, put the selected Hugo binary on PATH, disable inherited Go/Hugo workspaces, and identify the sibling replacement explicitly. Build the real site with --environment production --minify --printPathWarnings --panicOnWarning into a separate output directory. For published-pin checks, disable the replacement as well.

Remaining release steps and limits

Local technical pre-release acceptance passed. Completing the release requires assembling reviewed changes into commits, running CI on those commits, publishing the tag and module archive, updating downstream pins, and verifying the hosted deployment.

Real Windows/Android font rendering, manual screen-reader speech, and first-paint filmstrips remain unverified. Windows source-path behavior was checked through deterministic fixtures. Ink/Terminal design follow-up remains in the experiment record.

7.8.5 - Visual preset acceptance, 2026-10-05

Local Paper and Slate verification, real theme output, and bounded browser evidence.
Local verification record

This record covers sibling-checkout theme output for the Paper and Slate presets prior to tagging v1.2.0.

Inputs

Theme and documentation working trees on 2026-10-05; Hugo Extended 0.166.0, Go 1.27.1, Node 26.9.0, and Playwright 1.62.1 on macOS ARM64. The ordinary browser suite uses Chromium; the additional engine gate uses Chromium, Firefox, and WebKit. The site pinned v1.1.0 at the time of this run; make check, make browser, and make dev select the local sibling theme.

Executed checks

Evidence Result and scope
check-presets.py Paper light/dark token parity and AA text/link/code/copper contrast; frozen v1.1.0 Slate base palette; four strict configuration builds and 16 HTML roots including 404 and print
Existing theme checkers Parameters, font roles, vendor inventory, 32 locale catalogs, actions, shell, output, namespace, Landing and runtime isolation passed; generated schemas match their sources
check-goldens.py 52 surfaces passed after reviewing and updating the 34 HTML/print expectations affected by root attributes, prepaint colors, the menu and its action/runtime; other output formats were unchanged
Strict site build Real sibling-theme EN/ZH site built with --panicOnWarning; translations, rendered Markdown and internal links passed
node --test 'tests/js/**/*.test.js' 49 runtime tests passed
appearance.spec.mjs 25 tests passed: Paper/Slate × EN/ZH × 390/1440 px × light/dark on Home, configuration, callouts and tabs; menu axe checks; keyboard, persistence, default reset, language navigation, cross-tab sync, blocked storage, invalid values, no JS, print, command palette, reading-anchor/breakpoint handling and generated comment stylesheets
appearance-engines.spec.mjs Six checks passed: Chromium, Firefox and WebKit at 390/1440 px; stored state and browser chrome color restored before CSS, native keyboard selection, focus return and language navigation. Desktop Chromium also used 4× CPU throttling
Font requests All observed fonts were local. Paper requested no Inter; Slate requested no Plex Sans. Two real-site overlay builds proved system typography requests no bundled text face and explicit font roles override both presets
make check Complete non-browser suite passed: 57 tests, 141/141 translated pages and the existing Markdown/rendered-content/internal-link checks
make browser 197 tests passed across all nine standard suites; sitemap axe scan scoped to the 15 routes below
Visual inspection Actual Paper desktop Home, mobile long-form Docs, English/Chinese light/dark Appearance panels and Slate dark Home reviewed from browser test captures

The browser suite’s sitemap axe pass is scoped with A11Y_PATHS to 15 representative routes: EN/ZH Home, configuration, callouts, tabs, and OpenAPI; English search, Mermaid, ECharts, Blog, and a Book chapter. The existing responsive axe matrix runs in addition. Cross-origin Giscus and vendor API widget DOM retain the suite’s established exclusions.

The integration run found and fixed Paper dark highlighted-line gutter contrast and smooth-scroll interference with reading-anchor restoration. Theme-color checks now assert both presets: Paper’s opaque warm selection surface and Slate’s existing translucent selection surface.

Limits and next checks

Manual screen-reader speech and visual filmstrip/paint traces remain unverified. The blocked-stylesheet and throttled-CPU assertions verify initialization order. Slate comparison freezes base palette values and verifies rendered font behavior.

Mermaid/ECharts retain mode-only palettes. Ink and Terminal remain research; serif display headings, full geometry/density tokens, and preset-colored charts remain future work.

7.8.6 - Consumer and migration evidence

A dated consumer corpus survey that informed OINK shells, authoring primitives, and deterministic Book migration rules.
Dated corpus snapshot

These counts describe repositories inspected in August 2026 to inform OINK’s shell, component, and Book migration design.

Corpus

The authoring survey scanned the content/ trees of eleven OINK consumer sites and found 5,325 Markdown files, of which 5,293 had YAML front matter. The survey included single-language English and Chinese references, bilingual product sites, release archives, custom landing pages, and separate Book consumers.

The survey measured source Markdown, counting shortcode calls, fenced-code attributes, callouts, table markers, raw HTML, front-matter keys, content types, and site-local layouts. A later Book-focused survey 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 ruled out several additions: documentation sites did not require a second Landing system, Book sites did not need a new cover component, and a serial archive did not require a new shell type. Remote API collection belongs in site-side CI, outside a Hugo theme built for local rendering.

Block and table evidence

A focused survey of eleven sites plus Book consumers found 11,484 pipe tables, but only eleven matched the strict Field-table heading vocabulary. Roughly 874 were reference-style tables and about 1,300 were compatibility matrices. OINK therefore uses explicit .fields and .matrix markers rather than inferring these components from table shape.

The same survey found eighteen Steps blocks in the eleven-site corpus, all using the full form with headings and rich content. Platform probes showed that a native ordered list could contain most of that content, but not another full % container inside a list item. OINK keeps both forms because their technical capabilities differ.

Deterministic Book migration

Three dated dry-run profiles checked whether migration rules accounted for every recognized source without inventing meaning:

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 first ran without writes, and writes began only after reviewers understood its ambiguous inputs. A second run produced zero changes. Each profile 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.

Publication adoption snapshot

An isolated 2026-08-24 check tested the released generic Book publication path with two consumers:

Consumer Generic publication evidence Downstream status
DDIA 23 ordered pages, 131 typed targets, and 292 resolved cross-references; EPUBCheck, internal, and PDF checks passed At this snapshot it still retained a semantic preprocessor pending independent acceptance of the new gate
TPME 18 ordered pages, 41 typed targets, and 1,062 resolved cross-references; the same generic checks passed A second consumer confirmed portability; it created no upstream migration gate

These results record downstream adoption of the generic publication pipeline.

Limits

Repeating this survey requires a fresh repository list and a new dated snapshot. This public record omits local filesystem paths, uncommitted content, private repository names, and generated build artifacts.

7.8.7 - OINK comprehensive review, 2026-08-26

Evidence-based review of post-v0.7.0 implementation, configuration, outputs, security, tests, performance, bilingual contracts, and the real integration site.
Historical review snapshot (2026-08-26)

This page records the 2026-08-26 audit of github.com/pgsty/oink and its integration site. OINK 0.7.1 subsequently fixed code findings F01–F06 (see the 0.7.1 release notes). Current behavior is defined by the Design contracts.

Review verdict

OINK’s main-line quality was substantially higher than that of a typical Hugo theme: the default path built successfully, bilingual coverage was strong, and component tests were broad. The project treated output and trust boundaries seriously. The real site showed no general breakage on desktop, mobile, light/dark modes, or primary accessibility paths. Theme and site worktrees were clean. Their latest remote checks succeeded, and every locally rerun first-party suite passed.

While those checks passed, this review found 4 P1, 9 P2, and 5 P3 findings, where some early or peripheral features fell short of OINK’s modern contract. The gates preserved selected positive scenarios very well, but did not systematically cover configuration combinations, static-output behavior, or the semantic accuracy of public documentation.

Before the next release tag, at minimum:

  1. Disable Swagger UI’s default online validator. Verify no implicit outbound requests with a non-localhost browser test.
  2. Apply common type, range, URL, and CSS-value validation to all public configuration and Landing data.
  3. Redesign Swagger, Redoc, and Asciinema behavior in static outputs and their runtime gates.
  4. Repair generated schemas. Update public configuration and front matter references to match 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/site Hugo build passed.
  • The real bilingual site’s npm test passed: 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, and 45 responsive/navigation cases. It also passed 16 keyboard, 10 content-component, 18 code-block, 4 PRD5, and 5 theme-color cases.
  • Extra visual review at 320 CSS px covered EN Home, ZH configuration, ZH Book, and OpenAPI/Redoc, with no page-level horizontal overflow.
  • npm audit found no advisory among the site’s 79 npm dependencies. An OSV Query API batch for 26 exact VENDOR.json versions found no known advisory.
  • measure-baseline.py assets --fixture-site passed 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. Vendored swagger-ui-bundle.js defaults to https://validator.swagger.io/validator. It suppresses the badge only when specification URLs contain localhost or 127.0.0.1. On deployed hosts, it creates an online-validator badge whose request includes the specification URL.

This violates the promises of disabled-by-default theme network features, local same-origin specifications, and local-first operation, and can disclose internal hostnames or specification URLs on intranet deployments. The localhost exemption explains why every local browser test missed the request.

Set validatorUrl: null explicitly. Any future online validator should require an explicit opt-in URL, use the shared URL policy, and have privacy/CSP integration documentation. Test a production-like, non-localhost origin while intercepting every request. Require same-origin specifications to fetch only first-party resources.

F02 — Invalid configuration does not consistently warn and fall back (P1)

ui-param.html says callers validate types. Some callers do not. Minimal builds produced these 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 invalid types, including print.toc and offline_search_max_results, silently change behavior.

This directly contradicts the Diagnostics decision: ordinary hugo server can become unusable, while some invalid input passes strict publishing gates without warnings. Add shared validators for integers, positive integers, ranges, paired ranges, and grid counts. Normalize values before arithmetic or output. For every public key, test valid site and page cases, invalid ordinary cases with warnings/fallbacks, and invalid strict cases with failures. Domain resolvers should enforce cross-field rules such as min <= max, pager size >= 1, and integer grid counts.

F03 — OpenAPI and Asciinema remain HTML-only islands (P1)

Architecture and Components require Markdown/LLMS without theme markup, static Print, and safe static RSS or explicit omission. The observed behavior conflicts with these requirements:

  • Redoc emits <style>, <div class="td-redoc">, and <redoc spec-url=...> into generated .md.
  • Swagger puts an executable inline initializer in its shortcode.
  • Asciinema .md contains the complete td-asciinema tree and JSON script.
  • Asciinema Print loads about 185 KB of player JS/CSS and prints only an incidental frame.
  • Swagger/Redoc leave empty Print containers and can select 1–2 MB runtimes.
  • These shortcodes are absent from the Markdown/RSS/Print golden matrix.

The result is theme HTML in agent output, empty shells for paper/EPUB readers, useless runtime in Print, and a strict-CSP violation from Swagger. Reader-facing guides describe these defects as output behavior, conflicting with normative contracts.

Make all three branch on tdOutputFormat. Provide full behavior in interactive HTML. For Print/Markdown/RSS, provide a titled static link with the specification or cast address, or explicitly omit the component. Only interactive HTML should set capability flags. Move Swagger initialization to a stable chunk and Redoc styles to a stylesheet. Add four-output goldens and assertions that unwanted runtimes are absent.

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 unchanged before marking the full string safeCSS. This input:

sections:
  - type: hero
    data:
      title: Probe
      image: /icons/logo.svg
      media:
        ratio: "1fr; background-image: url(https://example.invalid/x)"
        max_width: "240px; color: red"

passes a strict build without warnings and emits:

style="--td-hero-columns: 1fr; background-image: url(https://example.invalid/x);
       --td-hero-media-max: 240px; color: red;"

Landing accepts inline sections in page front matter, so authors can supply these values directly. Other sections handle columns, rules, dimensions, styles, icons, and URLs inconsistently. A javascript: URL often becomes #ZgotmplZ without warnings. Invalid columns become ZgotmplZ. Some direct integer casts abort builds.

Add section normalization with common handling for classes, icons, URLs, CSS lengths, grid counts, booleans, and enums. hero.media.ratio should accept two constrained track values, not arbitrary CSS. max_width should use the length validator. All Landing actions should use content/url.html. 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 directly to safeURL. url_latest_version is also treated as trusted configuration and marked safeURL. Neither path validates schemes, hosts, whitespace, or protocol-relative URLs. Warning-free builds can produce:

<a class="td-page-actions__item"
   href="javascript:document.body.dataset.pwned=1;undefined">

Clicking executes JavaScript, which violates the published safe-URL model and gives copied configuration fragments unnecessary execution capability.

Use the shared resolver to allow only HTTP(S) and explicitly supported first-party relative URLs. Also validate archived-version URLs. The browser action registry’s second check provides useful defense, but progressive-enhancement anchors must not bypass it.

F06 — Generated schemas disagree with actual YAML (P2)

The small parser in generate-config-schema.py does not remove inline comments. At least 11 defaults become strings. These include print.toc, which becomes "true # ..." instead of a boolean, and print.section_break_wordcount. Both index column counts and enum defaults such as footer_style, blog_index, and typography are also affected.

Comment association is also incorrect. Breadcrumb commentary attaches to section_index; quick-link commentary attaches to sidebar_icon_policy. Taxonomy-icon commentary attaches to pager_types; local-interface commentary attaches to image_zoom.

The front matter schema offers removed detector keys: release, upstream_attribution, and downstream_modified. It incorrectly treats navbar-menu Params.columns as page front matter. The drift check compares the same defective generator with committed output, preserving the error.

Use a real comment-preserving parser or explicit machine metadata markers. Do not extend the ad hoc parser. The scanner must distinguish page, menu, shortcode, and legacy-detector contexts. Tests should compare schema defaults with Hugo’s actual parsed values and exclude removed keys from completion.

F07 — Public configuration and front-matter references are not current (P2)

Both reference pages claim to list every key read by the theme. Material differences include:

  • Long English date defaults, although hugo.yaml uses ISO 2006-01-02.
  • Blog docs omit hero, table, toggle, size, toc_style, and toc_taxonomies.
  • Removed release maps and filters are presented as current, while release_url is absent.
  • images: [] is described as disabling featured images, although bundle discovery continues.
  • upstream_modified is described as adding a line, although it changes the attribution verb.
  • Invalid-input claims conflict: immediate failure versus warnings in ordinary previews and failure only at strict gates.
  • The Book guide says OINK stops at Print HTML, although v0.7 shipped BookManifest/EPUB/PDF tools.
  • Asciinema/OpenAPI guides present static-output defects as product contracts.
  • Features lists 28 vendor dependencies, although the authoritative manifest has 26.

English and Chinese usually share the same outdated answer. Translation parity cannot detect that error. Treat both references as a focused contract migration. Derive comparable key inventories from implementation/schema. Review semantics manually. Check current-key coverage, placement of removed keys, enums, and defaults.

F08 — The Design tree contains conflicting authorities and unretired proposals (P2)

Shell retires navbar columns/mega panels and promises a warning with one column. Landing still says navbar mega-menu columns accept 1–4. Implementation and tests follow Shell.

The lifecycle is also incomplete. config-schema is marked implemented but remains an Active proposal. Book publication has shipped the manifest, EPUB, PDF, and most CI work, but a Draft proposal duplicates the Architecture contract. Media convergence keeps implemented milestones and open M4 in the original design record.

Correct Landing. Move stable config-schema facts to Architecture/Decision and retire the proposal. Reduce Book publication to the remaining consumer-migration question, or replace it with a focused follow-up. Active proposals should not define a second current API.

F09 — OpenAPI accessibility claims conflict with test exclusions (P2)

The axe suite excludes .td-swagger-ui and .td-redoc. Its comments identify Swagger’s unnamed server selector and version stamp that cannot scroll by keyboard. They also identify Redoc operation-description contrast. The guide discloses only Swagger defects and presents rendered Redoc as the alternative. This implies that Redoc meets the site’s zero-violation gate.

Publish the actual boundary in both languages. Fix Redoc contrast in theme CSS where possible. Use a narrow post-render adapter for fixable Swagger DOM. For remaining upstream defects, provide versioned waivers, upstream issue links, and a separate axe report. Do not exclude the entire supported feature while claiming zero site-wide violations.

F10 — The current theme does not directly support a strict CSP (P2)

Deployment guidance says strict CSP is workable. It lists only author scripts, ECharts callbacks, analytics, remote specifications or diagram services, and Giscus. Ordinary Docs pages already emit two theme-owned inline scripts for initial theme display and shell prepaint, plus inline styles. 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 initial theme display and shell-state restoration. style-src 'self' blocks theme colors, font roles, Landing, and several inline custom properties. Consumers must add 'unsafe-inline', maintain hashes, or override templates. The guide states none of these requirements.

Move stable initializers to same-origin chunks with data/JSON configuration. For unavoidable inline content, provide a generated hash manifest or one nonce hook. Publish policies for minimal core, Markmap/OpenAPI, and third-party integrations. State the style-src requirements.

F11 — Browser compatibility has no baseline or cross-engine proof (P2)

CI installs only Chromium. Product documentation gives no minimum Chrome, Firefox, or Safari version. The implementation uses or enhances content with :has(), dialog, inert, color-mix(), @property, logical properties, and discrete display transitions. Some paths have fallbacks, but there is no engine matrix.

RTL checks mostly use source markers, small JS tests, and one element-level geometry change. They do not use a complete RTL-language site. Most forced-color checks verify SCSS strings instead of computed behavior.

Publish a small support matrix. Test core shell, navigation, content, and dialogs on Chromium, Firefox, and WebKit. Add a real languageDirection: rtl integration configuration. Also test forced colors, reduced motion, 320 px, and 200% zoom.

F12 — Output-security and Markdown gates do not inspect every claimed surface (P2)

For .md, check-output-security.py checks only Markdown links. It does not send raw HTML through the HTML scanner. Redoc/Asciinema scripts, spec-url, and raw href are therefore invisible. It also ignores CSS and JSON URLs. The fixture uses a broad --third-party allowance.

check-rendered-markdown.mjs has a misleading name. It scans generated HTML text for leftover Markdown syntax, not generated .md. The actual Markdown golden set covers 15 pages and omits OpenAPI/Asciinema.

Use clearly named gates for HTML trust, machine-output purity, and rendered-text residue. Give generated Markdown a narrow raw-HTML allowlist. Deliberately parse CSS URLs, form actions, JSON URLs, and non-executable JSON scripts. Every public shortcode should have 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 in go.mod. Real EN/ZH and Playwright validation for theme PRs depends on local maintainer HUGO_MODULE_REPLACEMENTS. Changes in both repositories cannot be committed atomically.

Both repositories can pass checks while public references differ from implementation, as this review shows. The written separation of release states is correct, but automation does not enforce delivery of implementation, owning checker, and paired contract together.

Add a read-only candidate workflow. Check out a theme PR SHA and a declared documentation-site SHA. Apply a temporary module replacement, then run npm test and critical browser suites. Allow Design-contract PRs to identify candidate theme SHAs. Tag, pin, and deployment remain distinct. The candidate pair gains one traceable joint result.

F14 — Checker maintenance cost and source coupling are high (P3)

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. Many assertions preserve template/SCSS spelling, nearby comments, or complete file equality instead of observable behavior.

Some helpers hard-code theme: oink with --themesDir <repo-parent>, making the checkout or worktree basename an implicit requirement. Without a common Python lint/type gate, checkers are quick to add but can retain shared testing omissions.

Create a common fixture builder and assertion library. Put negative cases in table-driven data. Keep source checks only for actual topology rules. Move other checks to parsed output or computed styles. Load themes through explicit symlinks or module replacements instead of repository basenames.

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 removed bundles for feature combinations correctly. Large third-party runtimes remain page-local. Bootstrap/theme/Landing CSS and the complete Font Awesome distribution are the remaining common cost.

Do not prune Font Awesome based on theme template usage. That would violate the authoring contract. Measure whether Landing, Book, or Swagger CSS can be independently cached and limited to relevant pages. Inspect fonts requested on first visits. Maintain a trend report instead of 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 found no known advisory in this snapshot.

The custom manifest is outside common SBOM/advisory gates. npm audit cannot inspect vendored browser packages. Two documentation-site workflows download Hugo .deb files and immediately run sudo dpkg -i without checking a checksum. Actions use movable major tags. Theme CI uses floating Python 3.x.

Generate CycloneDX/SPDX from VENDOR.json. Schedule OSV scans. Pin Hugo archive/deb SHA-256 values and trusted release actions to commit SHAs. Select a specific Python version or matrix.

F17 — Design and release records have lost signal (P3)

CHANGELOG.md has 1,768 lines. Its v0.7.0 section has about 300 lines, and Unreleased uses about 20 lines for one checker retry. These narratives provide useful engineering history, but make breaking changes, migrations, and observable behavior harder for upgraders to find.

Contracts recognize book_kind and book_part, and content front matter repeats them, but templates explicitly do not read them. They impose API-like authoring work without behavior. Implemented proposals that remain active provide another duplicate answer.

Keep changelog entries focused on observable changes, breaking/migration notes, and concise fixes. Move long design accounts to Blog/Research and link them. Give metadata without behavior a consumer/schema, or make it site-owned.

F18 — The Print isHTML FIXME is no longer accurate (P3)

hugo.yaml says to leave isHTML unset until Hugo fixes issue #14381. Hugo closed it on 2026-01-17, and the fix shipped before OINK’s 0.160.1 floor. Enabling isHTML: true still produces missing page/section/landing Print-layout warnings in this theme, which fail strict builds.

The dependency changed from “waiting for an alias fix” to “the current Print template names rely on non-HTML lookup rules.” Do not simply remove the workaround. First complete the HTML-classified Print lookup matrix and alias/subpath tests. If false remains intentional, explain the actual reason in the comment. Add a test to prevent cleanup based only on the 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

  1. Set Swagger validatorUrl: null and add a production-origin no-network test.
  2. Build the public-parameter inventory and validate every F02/F04 field with negative cases.
  3. Redesign four-output behavior and runtime gates for Swagger, Redoc, and Asciinema.
  4. Validate custom action and archived-version URLs.
  5. Repair the schema parser/scanner and regenerate both schemas.
  6. Synchronize paired Config, Front matter, OpenAPI, Asciinema, Book, Features, and Landing-contract pages.

Phase 1: contract gates

  1. Create a minimum HTML/Print/Markdown/RSS coverage map for all 29 shortcodes.
  2. Split and strengthen output-trust and machine-output-purity gates.
  3. Normalize all Landing section input centrally.
  4. Externalize theme-owned inline initializers and publish CSP guidance.
  5. Add a cross-repository candidate workflow.

Phase 2: compatibility and structure

  1. Add Firefox/WebKit, real RTL, forced colors, and 200% zoom.
  2. Consolidate the Python checker harness and source-string assertions.
  3. Evaluate surface-specific CSS and actual font requests.
  4. Generate an SBOM, schedule OSV, and pin CI download digests.
  5. 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 .md contains no td-*, 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 review did not individually audit consumer repositories, production response headers/CDN caches, real Firefox/Safari, or screen readers. 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 covered DDIA/TPME EPUB/PDF consumers.

7.8.8 - Community issue and PR review, 2026-09-19

Evidence, acceptance advice, and focused remedies for community issues 40, 41, 42, 44 and pull request 43.
Historical review snapshot (2026-09-19)

This page records source inspection, GitHub status, local builds, and targeted browser observations from 2026-09-19, followed by the same-day implementation and acceptance follow-up.

The initial review remains below; the maintainer later authorized merging PR #43 and implementing the other items directly on main. See the same-day implementation and acceptance follow-up.

Verdict

The reports identify useful problems, but they are not all defects of the same kind. Fix hidden navigation focus first. Accept PR #43’s direction as a small correctness fix after clarifying its boundary and adding coverage. Treat search-tail registration and public sidebar state as additional APIs requiring their own acceptance work.

Item Finding Recommendation
PR #43, MagicFollower Self-root collection ignores an explicit sidebar_root_menu: false. Reproduced. Conditional acceptance: the patch correctly filters the global candidate list. Document the current-root exception, add regression tests, and obtain successful CI.
#41, imbajin Hidden whole-sidebar content remains focusable. Several controllers write disclosure state, and no public API exists. Split a correctness repair from an optional API. The former has higher priority.
#44, lloydsun Pointer focus followed by a key produces the reported outlines. Reproduced on another platform. Improve the main-content focus treatment. Retain useful keyboard cues for scrollable content. The browser heuristic itself is expected.
#42, aucru Existing hiding and divider options do not provide a complete non-link section group with its children. Explain the options separately and obtain the author’s exact minimal example. If the missing group behavior is confirmed, implement it.
#40, imbajin There is no supported way to append a query-dependent action to local search. This is a reasonable small extension proposal. It does not identify a failure of existing local search. Correctness repairs have higher priority.

Baseline and method

GitHub API reads found four open external issues and one open external PR. The other open issue, #37, was the maintainer’s release tracker. The external items had no discussion comments or submitted PR reviews at the snapshot time.

Input Verified snapshot
Remote theme main 93ac292014a3cd81f7c41caec4df98ed9d2dc45a
Local theme 75ddc95. Only the release text in CHANGELOG.md differs from remote main.
PR #43 head 8eeb8ecaf525097cc56572fe22234db381bfc16a, one changed template line
Local documentation ff0ba39. go.mod still requires OINK v1.0.0.
Published release GitHub’s latest release is v1.0.0. The remote query returned no v1.1.0 tag.
Build tools Hugo Extended 0.166.0, Node 26.9.0, npm 11.19.1
Browser observation macOS, Chromium 153.0.0.0, light theme, real sibling documentation site using a command-scoped module replacement

The method combined complete issue/PR bodies and comments, the exact diff, and bilingual Design contracts with inspection of owning templates and JavaScript. It also used a temporary bilingual site under the /sub/ base path and targeted documentation-site browser interactions. No theme implementation changed in the shared checkout.

PR 43: accept the small fix with a precise boundary

The first pass in root-menu-roots.html filters top-level sections by sidebar_root_menu. The second pass collects sections with sidebar_root_for set to self, but omits that filter. Sections excluded in the first pass can therefore return in the second.

The PR adds the same explicit-false predicate to the second pass:

{{- if and .IsSection (ne .Params.sidebar_root_menu false) -}}

The patch keeps the default for absent or true values, the section constraint, and URL deduplication, while preserving navigation-tree order, pager order, and language-specific caching. These properties make it a focused correctness fix with no need for a broader navigation refactor.

However, root-menu-entries.html subsequently appends the current resolved root when absent. This existing behavior is documented in the navigation guide, so it is not a new PR regression; it does prevent the broader claim that false hides the root on every page.

The local probe used a top-level Blog root and nested Docs root, both with sidebar_root_for: self and sidebar_root_menu: false. It also included a visible Docs root and a self-root without a visibility override. English and Chinese results were identical and retained language-aware /sub/ URLs:

Viewed page Before the PR With the PR
An unrelated Docs page Both hidden self-roots appear Both disappear
A page inside the hidden Blog root Blog appears Blog still appears through current-root fallback
A page inside the hidden nested root Nested root appears Nested root still appears through current-root fallback
A visible self-root Appears Still appears without duplication

Recommended contract: false excludes roots from site-wide selectable candidates, but the current root may remain for orientation. Keeping this exception is the smallest compatible interpretation. State it explicitly in both languages. If the intended contract requires absolute exclusion, change the current-root fallback and switcher trigger together. Another predicate alone is insufficient. Check zero and one-entry states.

Before merging:

  1. Add output-based bin/check-shell.py cases for hidden top-level and nested self-roots, absent/true values, deduplication, and current-root behavior. Cover one-entry degradation and EN/ZH subpaths.
  2. Update the Shell contract and navigation guide together. Change the PR description’s YAML comment from // to # so readers can paste its example.
  3. Resolve the workflow’s action_required result and run required checks on the final head. At this snapshot, the PR head had no completed check runs (MERGEABLE, UNSTABLE).

The maintainer can add these small changes while preserving the contribution. Do not make acceptance depend on #40 or all of #41.

Issue 41: repair isolation, then expose state

There are two separate findings.

Whole-sidebar hiding uses transforms and, on desktop, opacity. Drawer and collapse controllers leave hidden controls in keyboard navigation. In the browser probe, Collapse sidebar left focus on the transparent collapse button. Tab then moved focus to the hidden root switcher. The panel had zero opacity and no effective inert or aria-hidden ancestor. This is a reproducible keyboard usability defect. Closed mobile panels use similar off-screen positioning without explicit isolation.

Disclosure writes are duplicated across the click controller and responsive relocation and cached active-path hydration. There is no public setter, getter, or committed-state event. Existing storage for sidebar collapse, width, and scroll position does not persist per-branch disclosure state across page loads. The authoring guide’s local expansion-state claim needs this distinction.

Recommended repair:

  1. Centralize whole-sidebar isolation at initialization and every open, close, collapse, hover-overlay, restore, and breakpoint transition. Remove isolation before moving focus inside. Return focus to a visible external control before making content inert.
  2. Isolate content while keeping the external restore button and desktop edge hover target active. Applying inert to the pointer sensor would break hover interaction. aria-hidden alone does not prevent keyboard focus. The HTML inert contract controls interaction and accessibility exposure.
  3. Route disclosure changes through one commit function separately. Update aria-expanded, the open class, and localized label before emitting one event. Repeated writes of the current value should have no effect.
  4. Expose a small setter/getter and event only after defining stable IDs, invalid-ID behavior, initialization readiness, and restoration order. Keep version/locale storage policy downstream-owned. Let the active path take precedence after restoration.

The proposal needs a scope correction. Responsive TOC/backlink/taxonomy groups can move from the sidebar to the right rail. A controller searching only current-sidebar descendants cannot control wide-layout writes. Register OINK-owned targets independently of their current DOM parent. Restrict the public sidebar API to its intended registered subset.

Acceptance must check real Tab order and the accessibility tree in hidden states. It must also check restored desktop collapse on first load, hover entry/exit, focus return, Escape, backdrop close, and scroll unlock. Cover the 768/1200 breakpoints and preserve #24’s visible no-JavaScript fallback.

Issue 44: real symptom, partly expected behavior

On the documentation configuration page, clicking the article heading focused main#td-main-content. Clicking table headers focused div.td-table-scroll; clicking code focused pre.chroma. In each case, :focus-visible was false after clicking and true after pressing unbound z. Main/code outlines changed from none to browser auto outlines. Tables used the theme’s solid outline. This reproduced the mechanism without the reporter’s Linux compositor or Super key.

The Selectors specification explicitly describes keyboard activity changing focus indication while focus stays on the same element. The report therefore provides useful UX feedback. Browsers are not required to keep mouse-focused elements free of focus rings after keyboard activity.

Recommended treatment:

  • Keep the main skip-link target and focusability. Replace its oversized outline with a local, visible content-entry cue, such as a title-area indicator. Verify actual skip-link activation.
  • Keep visible keyboard focus on scrollable tables and code. Normalize its appearance if needed. Focusability enables keyboard scrolling.
  • Do not apply global outline: none, remove all tabindex attributes, or blur focus on arbitrary keypresses.
  • If OINK suppresses only pointer-origin reading focus, define that behavior explicitly for these non-editable containers. Test pointer, Tab, skip-link, and programmatic focus, including dark and forced-colors modes. A global input-modality framework is disproportionate to this report.

This review recommends a focused presentation improvement while keeping table and code keyboard focus cues.

Issue 42: distinguish hiding from grouping

The question names _index.json and supplies screenshots instead of a source fixture. This review could not visually inspect the original screenshots. The author’s intended first change remains unresolved. When replying, request a small directory tree and actual index/front matter. Without that evidence, do not assume _index.json is a supported page source or a typo.

The existing options have different meanings:

Option Current behavior and limitation
no_list: true Removes the child list from the section’s content body. It does not hide the section’s sidebar row.
hide_summary: true Removes an item from a parent section’s body list. It does not change sidebar grouping.
toc_hide: true The content-tree walker filters out the node before recursion. This also removes its subtree from that tree.
sidebar_root_menu: false Controls root-switcher candidates. It does not control the node’s row in the reading tree. See PR #43.
sidebar_root_link_self: false Redirects a self-root’s row to its parent. It does not make the row a non-link group.
sidebar_divider: true Emits a non-link heading. The shared renderer does not emit the supplied children in that branch.
build.render: link Suppresses the section HTML and retains its permalink. The current sidebar still emits a link to it. This option alone is insufficient.

The temporary site confirmed that divider sections keep child HTML while child sidebar links disappear. Sections with only build.render: link have no section HTML, but keep clickable sidebar rows and children. This matches Hugo’s build-option semantics and the theme’s shared node renderer.

For a real “group label with child links, but no directory-page navigation” requirement, first consider completing sidebar_divider for sections. Keep leaf dividers and section children. Use real disclosure buttons when folding is enabled. Check existing consumers before adopting this interpretation. Add a separate node switch only if the divider contract cannot express it compatibly. Keep section publication separate from whether its navigation label is linked.

The change must preserve hierarchy and active-path expansion in both walkers, and keep children in the pager. If a section is intentionally unpublished, the change must avoid dead targets in breadcrumbs, search, root switching, Print, and machine-readable navigation. Hiding whole nodes with CSS is not a solution.

Issue 40: a narrow extension is reasonable

Source inspection confirms the gap. groupsFor creates only built-in page/action groups. The public Palette object has no provider registration. registerExecutor accepts only built-in action IDs. Static URL commands cannot replace rows carrying the current query.

The proposed search-tail slot is a useful upstream boundary if its scope stays small. It uses synchronous data-only row creation, asynchronous activation, and local results first. OINK controls rendering, selection, keyboard handling, and ARIA. It needs no bundled AI provider, credentials, remote search, or generic plugin system.

Before adopting the proposed API, settle and test:

  1. Define which settled text-search states call providers. Preserve empty, command, choice, loading, and default no-extension behavior.
  2. Snapshot the query/locale used for each rendered row. Preserve native empty/index-error messages, retry behavior, and the difference between local page counts and all selectable rows.
  3. Validate and copy descriptors. Render titles and descriptions as text. Isolate provider exceptions, duplicate IDs, and invalid descriptors.
  4. Handle synchronous throws and rejected promises. Clear pending state and reject duplicate activation. Cancel stale sessions. Ensure unregister handles remain safe when IDs are reused.
  5. Test handoff to another dialog. Palette close already avoids returning focus when focus moved outside. Preserve this guard. Define how successful handoff differs from cancellation. An “every close aborts activation” rule can cancel the assistant being opened.
  6. Preserve default local-only network behavior and conditional bundles. Document that registered extensions run as trusted first-party scripts in the page context.

Before implementation, record the accepted API in a bilingual Design proposal. Downstream Ask AI wrappers can continue until a tagged release provides the hook. This is not required for the small correctness fixes.

Delivery order and ownership

Order Delivery Owning checks and documentation
First PR #43 completion and hidden-sidebar isolation as separate small changes check-shell.py; site responsive/keyboard/accessibility cases; EN/ZH Shell contract and navigation guide
Next Main-content focus styling and clarified grouping behavior Content/reading and navigation checkers as appropriate; browser focus/scroll/skip tests; EN/ZH architecture, shell, and authoring guidance
Later Public disclosure controller, then search-tail API Theme JS tests and check-navigation-contract.py / check-palette.py; real site fixtures; accepted bilingual API contracts

For each behavior change, run its owning checker first. Then use the sibling site’s make check, make browser, and make dev for relevant integration and visual review. Do not combine unrelated proposals into one large sidebar/search rewrite. Do not delay small fixes until all features exist.

Suggested response content, not posted: acknowledge #43’s filter defect and explain its current-root exception. Accept #41’s isolation defect separately from the API request. Acknowledge #44’s reproduction and explain standard focus behavior. Explain #42’s options and request minimal input. Describe #40 as a scoped enhancement, not a local-search failure.

Historical external issues are closed. #22 was resolved by enabling Goldmark passthrough, with reporter confirmation. #21 received the Mermaid viewer and corrected centered presentation. Arbitrary right alignment was explicitly excluded. Do not silently count either issue as a new open defect.

Validation and limits

Executed for this review:

  • python3 bin/check-shell.py passed on the local baseline and in an isolated checkout at PR #43’s exact head.
  • Palette controller and model tests passed: two test files, no failures.
  • Strict temporary Hugo builds before/after the actual PR diff reproduced EN/ZH self-root filtering and fallback under /sub/. The same fixture demonstrated grouping limitations.
  • The real bilingual site built with --panicOnWarning using the local theme. Targeted Chromium interactions reproduced all three focus outlines and the desktop hidden-focus defect.
  • Bilingual, rendered-content, and link checks passed, along with all 57 non-browser tests. Initial make check stopped at the llms.txt snapshot because this report added an index entry. After verifying that one-line addition and updating the golden, affected and remaining groups passed on rerun.

Local verification used Hugo 0.166.0 on macOS. Linux Super-key behavior, mobile accessibility-tree isolation, dark/forced-colors cases, and the exact #42 screenshots are covered in the follow-up acceptance below.

Implementation and acceptance follow-up

The maintainer chose to merge the contributor patch first, then complete repairs and extensions directly on main without another PR. PR #43 merged as 6e814089. The merge was pulled while preserving the local release-note commit. The implementation follow-up is 56bfe37.

Item Implemented behavior Owning acceptance
#43 Both root collectors honor explicit false. Current-root orientation remains compatible. Dividers and unpublished sections do not become switcher links. Strict EN/ZH subpath fixtures cover hidden top-level/nested roots, absent/true values, deduplication, current-root fallback, zero and one entry.
#41 One disclosure controller commits ARIA, classes, labels and inert state. A late-safe API supports downstream persistence. Hidden whole-sidebar content is isolated. Hover and drawer restoration remain usable. Runtime tests plus browser checks for atomic events, no-op writes, scope, active paths, blocked storage, responsive relocation, focus return, real Tab traversal, Escape, backdrop and breakpoints.
#44 A pointer-origin mark suppresses later incidental container outlines. Tab and fresh programmatic focus retain visible cues. The skip destination outlines the title. Browser checks for article/table/code in light, dark and forced-colors modes, plus keyboard and skip-link regression coverage.
#42 Divider sections keep their children under a non-link label. build.render: never suppresses their own page. Breadcrumb, search, pager, navigation JSON, Book TOC/Markdown and Print agree. Explicit navigation also works under bilingual subpaths. Strict generic/data-tree fixtures, Book depth-three heading checks, EN/ZH browser fixtures and no-JavaScript traversal.
#40 Trusted site scripts can register synchronous data-only search-tail rows and asynchronous activation. OINK continues to own native ordering, ARIA and validation. It also owns error isolation, cancellation, unregistering and focus handoff. Runtime lifecycle tests and a Chinese browser scenario cover pointer/keyboard selection and literal display text. They also cover context snapshots, external-dialog focus and unregistering.

Acceptance used the sibling theme checkout on macOS with Hugo Extended 0.166.0, Node 26.9.0, and Chromium:

  • All 44 theme JavaScript tests passed.
  • check-shell.py, check-reading.py, check-palette.py, and check-keyboard.py passed. A broader run passed 29 of the other 31 theme CI commands. Two local failures involved the media checker’s version-specific processed-image hashes and four goldens with Hugo 0.166’s changed KaTeX output. Ordinary navigation markup matched after preserving its existing whitespace. Fixed-toolchain verification is recorded below. Unrelated expected outputs were not rewritten.
  • make -C ../oink.pgsty.com check passed bilingual source/rendered/link checks and all 57 non-browser tests.
  • make -C ../oink.pgsty.com browser passed all 141 tests: 30 accessibility, 45 responsive/blog/palette, 16 keyboard, 10 content, 18 code-block, 4 scenario, 5 theme-color, and 13 community regressions. Accessibility included the complete multilingual sitemap scan.
  • Strict theme fixture output and namespace checks passed. Local Book packaging produced an EPUB with five chapters and zero checker errors. It also produced a 23-page PDF with all five expected Book pages and zero checker errors.
  • make dev served the real site for desktop-light, Chinese-dark, collapsed-sidebar restore, and 375px mobile-drawer review. Escape returned focus to the visible drawer opener. The temporary viewport and development server were cleaned up afterward.

Accepted contracts are in Shell and Architecture, with matching Chinese sources. Navigation, organization, palette, and Print guides were also updated. The documentation repository owns regression suites. The theme keeps focused checkers and synthetic inputs.

The merged PR’s fixed-toolchain CI passed all three jobs. The final implementation’s CI run also passed at exact revision 56bfe37092a43fc12c0e16f865d3d3407c55cbde. Hugo 0.165.0, browser runtime tests, and Book publication succeeded. This included media and four-state golden checks that differed locally on Hugo 0.166.0, plus root/subpath publication. The supported 0.160.1 floor was not separately retested. The declared continuous-test toolchain remains 0.165.0.

At this checkpoint, the documentation repository stayed pinned to v1.0.0 while awaiting the v1.1.0 tag, and issues #40, #41, #42, and #44 remained open pending release publication.

Image-copy follow-up

The maintainer also reported preview instructions below images after copying blog articles to rich-text editors. The blog pins OINK v1.0.0 and enables params.ui.image_zoom. That release and the reviewed main revision inserted a visually hidden text span after every eligible image. Native Chromium copying reproduced Open image preview and its Chinese equivalent in clipboard content. This is a theme defect independent of the destination editor.

Theme commit 75052f8 puts image descriptions and localized actions in the button’s aria-label, keeping helper text nodes out of the article. Image alt text, authored captions, native button behavior, and dialog focus return remain intact. The component contract and image guide document this copying behavior.

check-image-zoom.py and all 57 non-browser site tests passed. The focused browser run passed 16 tests: 14 content-component cases and two desktop-light/mobile-dark dialog accessibility cases. Component cases included four new EN/ZH image/gallery clipboard regressions. The regressions read plain-text and HTML clipboard data and check text without its original styles. They verify retained image URLs, alt text, captions, and accessible names.

Downstream blogs pick up the fix once they upgrade their theme dependency past v1.0.0.

7.8.9 - OINK 1.1 release review, 2026-09-20

Five reproduced runtime defects, documentation corrections, validation evidence, and the OINK 1.1 publication follow-up.
Historical release review (2026-09-20)

This record captures the pre-release validation and five runtime fixes for OINK 1.1, followed by the v1.1.0 publication record.

Scope and baseline

The review starts at theme commit 75052f8a3106d13ef313644836a5ad545135f484, after the community fixes and image-copy repair. It examines the v1.0.0..main changes and behavior requested by issues #40, #41, #42, #44, and merged PR #43. It also examines bilingual documentation and the release boundary. The previous review records those original reports and their implementation.

The method inspects owning JavaScript, templates and contracts, then exercises transition boundaries with focused regressions that compare failing assertions before each fix with the repaired implementation. Theme checkers and the real sibling documentation site’s integration and browser suites provide the subsequent acceptance evidence.

At this snapshot, the public release, documentation go.mod pin and configured public version are still v1.0.0. The blog consumer’s theme pin is unchanged.

Findings and repairs

Five P2 correctness defects were reproduced and repaired in theme commit 08f6563, pushed to main. They concern ordering in the new APIs and existing focus/navigation behavior. They require no new configuration format or content migration.

Finding Trigger and observed failure Minimal repair
P2: sidebar readiness fires before hydration A consumer awaits OinkSidebar.ready or handles oink:sidebar-ready. The readiness microtask can run between DOMContentLoaded listeners. The cached sidebar’s active path is not yet hydrated, so consumers see incomplete initial state. After all initialization listeners and aside placement finish, resolve readiness in the next task. Preserve the existing ready Promise and event contract.
P2: a pending action can enter a native choice menu Start an asynchronous search-tail action, then activate a native choice such as theme selection. The pending guard ran after the choice branch. This allowed that menu to replace the pending action’s rows. Check pending activation before any row-type branch. After completion, ordinary choice activation is available again.
P2: a collapsed right TOC rail remains focusable Collapse the desktop right rail. Its hidden control and links remain keyboard targets. Focus can stay inside the hidden panel. Apply inert and aria-hidden to the rail panel. Move focus to the visible restore control. On restoration, return focus to the column control. When relocated, keep the movable aside outside that isolation.
P2: arrow navigation skips non-link groups From a child of a divider-only group, Left/a cannot consistently return to the parent disclosure and fold it. The group button is absent from the tree’s focus sequence. Include group disclosure buttons in tree focus navigation and direct-parent traversal. Right/d opens or enters the group. Previous/next page navigation still uses links only.
P2: drawer focus wrapping counts inert descendants In the mobile drawer, collapse an aside group and wrap with Shift+Tab. Hidden descendants still count as focusable. They can make the wrap fail or leave focus stuck. Exclude controls under inert or hidden from the drawer’s focusable set. Also exclude controls with hidden/collapsed visibility.

Implementation and regression ownership:

Finding Theme implementation Owning regression
Readiness assets/js/sidebar-state.js tests/js/sidebar-state.test.js; site tests/browser/community-feedback.spec.mjs snapshots the active path from both readiness signals in EN/ZH
Pending choice assets/js/command-palette.js tests/js/command-palette.test.js exercises pending extension → native choice → completion → available choice
Right rail assets/js/docs-shell.js Site tests/browser/community-feedback.spec.mjs covers EN/ZH collapse, Tab traversal, restoration, reload and aside relocation across desktop/tablet/mobile
Group keys assets/js/keyboard-nav.js tests/js/keyboard-nav.test.js covers LTR/RTL, arrows/WASD and link-only paging. Site community tests exercise real EN/ZH groups.
Drawer trap assets/js/docs-shell.js Site community tests collapse relocated groups and wrap Shift+Tab and Tab. They assert that focus never enters an inert or hidden subtree.

The readiness and pending-choice regressions failed against the previous implementation before their fixes. The right-rail browser assertions also failed in both English and Chinese before isolation was added. The repairs remain small: they change timing, move one pending guard earlier, add explicit rail isolation, and adjust tree focus targets and the drawer’s visibility filter.

These changes preserve the already accepted behavior: both root collectors honor sidebar_root_menu: false, and non-link groups retain their children. Pointer focus avoids incidental article outlines while keyboard cues remain visible. Search-tail callbacks retain their cancellation and handoff contract. The image preview fix continues to use an accessible name without inserting helper text into copied article content. Final regression results are recorded below.

Documentation readiness

The current documentation update covers 28 files in 14 EN/ZH pairs:

  • The six Design contract pairs use candidate-v1.1.0. They describe implemented main behavior without announcing a published release.
  • Navigation, layout and front-matter guides match current root filtering, divider groups and bilingual deployment paths. They also match the centered navbar, narrow-screen drawer and in-place navbar reveal behavior.
  • Organization and palette guides explain runtime load order, readiness and feature detection. They also explain site-owned persistence/integrations. Keyboard and image guides explain the fixes and the older-version boundary.
  • Installation guidance distinguishes the validation toolchain from the public version. Existing heading IDs remain stable. The new sidebar API heading has a matching Chinese ID.

The two 1.1.0 release-note files and two upgrade-guide files are also prepared. The release note remains a draft/candidate. Both home-page release entries point to the published 1.0 version.

Validation snapshot

Counts below describe the 2026-09-20 snapshot for theme 08f6563. Site acceptance uses the sibling checkout through a command-scoped module replacement. Local site checks used Hugo Extended 0.166.0, Node 26.9.0 and Playwright 1.62.1. Candidate CI used the pinned Hugo 0.165.0 toolchain. The 0.160.1 floor was checked separately with the official binary.

Check Result and scope
Baseline 75052f8 CI Passed all three jobs: pinned Hugo toolchain, browser runtime tests and Book publication. Exact baseline run.
JavaScript unit suite after the fixes Passed: 44 tests.
Official Hugo Extended 0.160.1 Passed the focused i18n and shell checkers. i18n covers 32 catalogs × 194 messages. The real documentation site also passed a production build against the local candidate with --panicOnWarning: 376 pages per language. The draft release is absent, and both home-page links point to 1.0.0.
Bilingual source and style checks Passed including this report: 129/129 page pairs, 988 source headings, Markdown style and git diff --check.
Final focused theme checkers Passed: shell, palette, keyboard and image zoom.
Final real-site non-browser suite make check passed all 57 tests. It checked 200 rendered content pages, 331 linked HTML pages, 39,803 internal links and 3,863 fragment links. Only the two expected Markdown goldens changed, for the release summary and documentation index. The floor production build also passed link checks: 327 pages, 39,059 internal links and 3,833 fragments.
Final browser suite make browser passed all 149 Chromium tests in eight suites. The suites contain 30 accessibility, 45 responsive/blog/palette, 16 keyboard, 14 content components and 18 code blocks. They also contain 4 scenarios, 5 theme-color and 17 community regressions. Coverage includes the full multilingual sitemap and six viewport widths. It also includes light/dark, forced colors, clipboard and no-script cases.
Agent documentation sample 93/100 (A) across 50 same-origin sampled pages. Sampled links resolve. 49 pages provide Markdown, and all 270 sampled code fences close correctly. The checker warns that the HTML llms.txt discovery hint is missing or too deep.
Rendered review Reviewed the Chinese release note in a desktop dark view and English in a narrow light view. Right-rail collapse removes its descendants from the accessibility tree. It also moves focus to the restore button. Restoration returns focus to the visible rail button.
Final theme revision and its CI 08f6563 passed all three jobs: Hugo 0.165.0, browser runtime tests and Book publication. Exact candidate run.
Public v1.1.0 tag, consumer upgrade and deployment Not performed.

Within this review’s scope, no unresolved implementation blocker remains. The candidate is ready for the publication steps below.

Repeat the checks from sibling checkouts. During development, keep the published dependency pin intact:

# From the theme repository
node --test tests/js/*.test.js
python3 bin/check-shell.py
python3 bin/check-palette.py
python3 bin/check-keyboard.py
python3 bin/check-image-zoom.py

# The site Make targets apply a command-scoped sibling module replacement.
make -C ../oink.pgsty.com check
make -C ../oink.pgsty.com browser

Remaining publication steps

Publication has not been executed. After the final revision passes acceptance:

  1. Finalize CHANGELOG.md, the release date and the release record. Publish the v1.1.0 tag and GitHub Release from the verified theme revision.
  2. Verify that the module proxy resolves that tag to the intended revision.
  3. Update the documentation consumer pin and version configuration together with its home-page release entry and contract status. Set the release-note state to draft: false in the same update.
  4. Rebuild and accept the documentation site using the published dependency, without HUGO_MODULE_REPLACEMENTS. Deploy it and verify the public routes.

A future improvement is an earlier, consistent llms.txt discovery hint for agents entering through HTML.

Limits

Browser verification ran in Chromium. The automated accessibility gate covers theme-owned surfaces while retaining its existing exclusions for vendored Redoc and Swagger UI, and Hugo 0.160.1 floor checks ran on the focused i18n, shell, and production build paths.

Publication follow-up

After this review, the v1.1.0 release was published on 2026-09-20 from 3a18234. Only the changelog differs from the accepted 08f6563 implementation. All three release-commit CI jobs passed before the annotated tag and stable GitHub Release were published.

A download with a fresh cache, using only the official Go module proxy, resolved the tag to that exact commit. Its .info, .mod, .zip, version-list entry and signed checksum record were verified. The module checksum is h1:121L5g57ChRCPyidzEBBcln2Co+0zYRQ+XDDXjymd0Q=. The go.mod checksum is h1:pHvbUhJCfseB41n5RGwsF7abT3i32VSTpofLQoq4b7Y=. The public records are the proxy version and checksum entry.

The documentation publication update pins v1.1.0 in go.mod and go.sum. It aligns the advertised version and both home-page release entries, and publishes both release notes. It also promotes the six contract pairs to released-v1.1.0. The historical acceptance tables above continue to describe the earlier sibling-checkout run. The site’s Site checks and Browser quality workflows track published-dependency validation separately, with both Go and Hugo module workspaces disabled.

Local validation of this published module passed all 57 non-browser tests and 26 focused Palette/community browser tests. It also passed the strict production build with 378 pages per language. The checks ran with GOWORK=off, HUGO_MODULE_WORKSPACE=off, and no HUGO_MODULE_REPLACEMENTS. The publication commit’s Browser quality workflow also runs the complete 149-test browser suite.

7.8.10 - CLI maintenance acceptance on 2026-10-03

Dated source and binary evidence for the local R1–R8/A18 program, preserving the initial audit, failed trials, and final acceptance scope.
Historical CLI maintenance acceptance ledger (2026-10-03 to 2026-10-04)

This record preserves the initial audit, intermediate trials, and local R1–R8 and A18 runtime/archive acceptance for the CLI maintenance roadmap. On 2026-10-04, the CLI surface was simplified to the Cobra/text/JSON/YAML command set defined in the current CLI contract and CLI introduction. These results apply only to the recorded historical source and binaries; later changes to the reduced CLI require separate validation.

Scope and evidence rules

The maintenance roadmap defines the authorized R1–R8 scope. The current CLI contract defines its compatibility baseline; the original roadmap does not add Docsy migration, version lifecycle, OpenAPI, theme publication, or the conditional E1–E4 extensions to this program. Hugo remains an external renderer. Generated sites remain ordinary Hugo projects.

Stages are accepted in dependency order. Each stage needs a complete usable flow supported by owning tests and relevant actual Hugo integration, together with known limits, a reviewable diff, and accepted EN/ZH contract and guide updates. New public behavior moves from the proposal into the owning contract only after its implementation and acceptance evidence exist.

In the tables below, existing, not rerun means code or a named test was inspected but its current runtime outcome was not established. Partial means the first candidate provides a reusable part of the required behavior. Open means new implementation or decisive acceptance evidence is missing. Passed, failed, unverified, and unsupported describe a specific executed input and scope when later runs are recorded.

Inspected inputs and tools

The initial 2026-10-03 audit read both repositories’ instructions and the documentation README and translation rules, both maintenance PRD languages, the original proposal, and the current CLI contract. It also inspected existing Go packages and test names. Execution was limited to version and Git inspection commands, without running the owning suites or writing consumer sources.

Input Observed initial state
Host and Go darwin/arm64; go version go1.27.1 darwin/arm64
Hugo hugo v0.166.0+extended+withdeploy darwin/arm64, Homebrew build dated 2026-09-09
Node and npm v26.9.0; 11.19.1; contributor/documentation tools, not CLI consumer requirements
Git 2.54.0 (Apple Git-157)
CLI source e623d93d589c49e5c58b8fae1bd5db720fc904cb, main; clean initial tracked/untracked status; generated bin/, dist/, tmp/ ignored
Documentation source 907d873eb05cfc2e194f492462dfa94849e93474, main; 184 initial porcelain entries, including existing proposals, contracts, guides, and unrelated content changes
Embedded Starter 137843b25bacd76ddd1f7ce71330bf2e3155b954; provenance and license already recorded by internal/starter
Declared theme baseline github.com/pgsty/oink v1.1.0 in Starter and the three selected sites; effective resolved bytes still require each acceptance run

The 2026-09-29 acceptance record provides baseline reproduction inputs from the first candidate.

Stage requirements and implementation evidence

Stage Required complete flow and invariants Initial implementation evidence Acceptance evidence still needed
R1 Shared page identity, languages, publication state, source provenance, actual outputs, translations and observed references from Hugo; oink.yaml owns check policy only; links/translations/style share analysis; severity and exclusions cannot hide required incompletion; trustworthy locations Partial: internal/site isolated snapshots and Page.OutputFormats probe, internal/outputcheck, internal/report; no shared translation/page facts or policy commands at initial audit Real Hugo routes, aliases, mounts, unlisted/generated-source cases and language relationships; public focused-check/policy cases; required unknown/tool/build/input failures remain 2; source locations only when reliable
R2 Three language layouts; strict/manual and localized policies; duplicate, missing and draft states; explicit versioned review records bind source language and source/translation hashes; bounded native syntax rules; effective-theme coverage; visible versioned baseline; reviewed fixes validate before narrow apply Open: no translation/review/native-rule/baseline public command at initial audit; rendered-reference checks remain reusable A04–A07; valid/invalid reviewed content corpus; no mtime review inference; disabled/localized languages handled; acknowledged findings stay visible; missing required checks stay incomplete; fix preservation
R3 Preserve thin default build/dev; build --check checks and manifests one strict Hugo output, exports only to new/empty target; digest/provenance manifest and optional minimal public identity; both local CI templates upload the same tree; release diagnosis; explicit-network public verification Partial: direct wrappers, strict isolated checks and licensed workflow inputs exist; managed build/export, digest verification, CI plans and public verify are absent at initial audit A08–A10; exactly one Hugo build; stale-byte rejection; revision/dirty/input/theme/tool/settings/coverage provenance without secrets or machine paths; workflow customization/conflicts/provenance and immutable source input; example address policy; fallback/language/resource/canonical/timeout/auth/rate-limit HTTP fixtures
R4 new, snippets and editor setup create ordinary inputs without overwrite; docs/blog/book/project profiles compose one licensed Starter; upgrades provide readable diff and old/new routes, aliases and enabled outputs; unsupported migrations give manual action; existing protections survive Partial: fixed archive language profiles and hash-bound single-site module upgrade with candidate validation, backups, dirty/workspace/replacement/vendor protection A11–A12; all new profile/language combinations build with ordinary Hugo; unknown editor settings retained; upgrade route/capability regression and readable diff; source provenance and licenses retained
R5 inspect, impact --since, bounded context, preview move; shared plans include touched files, diff, base hashes, translations, attachments, output/route changes and alias advice; candidate validation and stale/concurrent-safe recovery; ambiguous references require review Partial: module-specific upgrade plan/apply primitives; no shared content plans or inspect/impact/context/move flow at initial audit A13–A15; deleting B includes unchanged inbound A; translation/attachment/derived-output impact; uncertain/global changes force full checks; no content execution; candidate/stale/failed-write preservation and ambiguous-link handling
R6 Explicit versioned site registry reuses single-site engine; per-site and aggregate completion; writes only to selected sites; configured preinstalled markdownlint/Vale/lychee adapters normalize findings and declare syntax/network coverage Open: no workspace/adapter public command at initial audit A07/A15/A18; direct/per-site parity; no sibling discovery, implicit installation or default formatting writes; required missing tool 2, optional omission visible, external network uncertainty distinct
R7 Read-only loopback Studio with overview, issues, translation comparison, page relationships and publication views; filters, known sources, actual Hugo preview, comparisons and copied actions; CLI parity; prebuilt assets; explicit allowlist, separate preview origin, Host/Origin/session protection Open: no Studio server or assets at initial audit A16; browser/keyboard/screen-reader/mobile/light/dark/long-list flows; same underlying results as CLI; unauthorized hosts/origins/sessions and preview-to-management requests rejected; Node unnecessary for consumer runtime
R8 Markdown/text and front matter forms, selected components and collision-safe attachments reuse plans; authorized allowed writes with visible diff, hashes and candidate validation; no-op bytes and unknown fields/comments/order/encoding/whitespace retained; unsupported form syntax stays text Open: editing follows accepted read-only R7; no editor API at initial audit A17/A14; byte-identical no-op, surgical YAML field updates and text fallbacks; stale external-editor saves, traversal/symlink escapes and preview requests fail safely; attachments never overwrite; no management API in static publication

Evidence pages

The stage-by-stage receipts are kept on two companion pages, moved there unchanged on 2026-11-06 so each page stays small enough for people and agents to read whole:

  • R1–R5 evidence: local validation for R1 to R3, authoring and upgrade acceptance (R4), and implementation and documentation acceptance (R5).
  • R6–R8 and A18 evidence: workspace and adapter acceptance (R6), the read-only Studio candidate (R7), accepted reviewed editing (R8), and the 2026-10-04 runtime completion supplement (A18).

Acceptance case ledger

This ledger combines the initial audit with accepted R1–R7 evidence and the qualified R8 candidate gates.

Case Required outcome Code or checker evidence Status and missing decisive evidence
A01 One oink.result/v1 JSON result. Stderr logs. Policy 1, required incompletion 2 Protocol/public R1–R8 commands, frozen owning tests and exact-binary CLI/API reports. Unchanged result schema. Current eight owning gates/Linux qualification plus unchanged-runtime carry-forward of the preceding corpus in #a18 Passed supported current command scope
A02 Hugo resolves slug/url/permalinks/aliases, mounts, unlisted pages and language roots Real PageFacts/manifest/custom-mount/translationKey fixtures. Preserved ordinary artifacts. Final consumer facts R1 scope passed
A03 Definite local missing routes fail. Outside origin/path and declared external scope classified honestly Actual rendered-reference fixture plus subpath/policy regressions and final real sites Passed the required A03 scope. External availability remains unchecked
A04 Filename, directory and translationKey. Duplicate/missing/draft cases. Strict/localized policy R2 translation engine, actual Hugo/public commands, final reports and numeric supplement Passed R2 required scope
A05 Absent record unknown. Changed source/translation hash visible. No mtime inference R2 hash/status/diff, public preview/apply and final reports Passed R2 required scope
A06 Real fences, inline code, shortcodes, HTML, attributes, unknown fields and protected text boundaries R2 actual syntax/provenance fixtures, reviewed corpus and final reports Passed R2 required scope. Catalog and unsupported-source limits remain explicit
A07 Acknowledged findings visible. New findings block per policy. Required unavailable tools cannot pass R2 baseline/public plans. R6 fake/actual protocol, missing/unsafe/offline/network-uncertainty and required-precedence fixtures passed Supported scope passed. Required unavailable/uncertain tools remain 2
A08 Post-check bytes invalidate manifest. Provider uploads verified tree without another build R3 manifest/export/tampering/public one-build tests. Final ordinary-Hugo comparison and provider rehearsal Passed R3 required local scope
A09 Both CI templates. Custom workflows preserved. Permissions/variables/provenance and stale plan protection R3 offline generation/bootstrap, public preview/apply/stale-input tests, custom workflow supplement and local rehearsal Passed R3 required local scope. Custom workflows remain unknown and unchanged
A10 Reject HTTP 200 fallback, wrong language/build, missing resource/canonical mismatch. Incomplete timeout/auth/rate-limit R3 explicit-network local HTTP and public result fixtures, including required identity absence Passed R3 required fixture scope
A11 All declared profiles/languages. Target protection. Ordinary Hugo. Unknown editor settings retained R4 24 ordinary Hugo/public profiles, full Starter authoring/editor flow, snippets, actual mounts, source identities, JSONC preservation and external schema reproof Required supported R4 local implementation/corpus scope passed. Documented unsupported editor inputs remain explicit
A12 Readable diff and route comparison. Dirty/workspaces/replacement/vendor. Recovery/concurrency Frozen actual-Hugo seven synthetic pinned cases, public upgrade, source/external guards, observed alias retarget and guarded partial rollback Required bounded R4 local implementation/corpus scope passed. Unknown redirects/multihost remain incomplete
A13 Deleted B finds unchanged inbound A. Translations/attachments/derived outputs. Global full scope R5 committed Git/actual Hugo deletion, alias-inbound, global/uncertain input and unavailable-baseline fixtures. Exact-binary consumer reports Passed required supported R5 scope. Unavailable or unproven historical inputs stay explicit 2
A14 Candidate before apply. Stale/hash/write failures preserve later edits. Ambiguous references unchanged R2/R4 shared safety, R5 full-mode/inventory moves and R8 regenerated intent/fresh-input candidate validation, guarded writer and stale/late-editor/attachment tests. Current eight owning gates/Linux qualification plus unchanged-runtime carry-forward of the preceding corpus in #a18 Passed supported R5 CLI and R8 CLI/Studio editing scope. Ambiguous or unavailable required inputs still block
A15 Workspace/direct parity. Selected writes only. Bounded context with paths/versions/reasons. No content execution R5 bounded captured-source/context fixtures and four-site queries. R6 registry/direct/aggregate parity and explicit-name saved apply with other sites preserved Supported context/workspace scope passed. No implicit batch writes
A16 Five useful CLI-parity views. Keyboard/mobile/light/dark. Source/preview isolation Accepted R7 evidence retained. Historical source-bound R8 read-only 14 axe/screenshots and Editor 12 axe/screenshots with unchanged UI bytes. Current backend gates, ARM and corpus separately verified, native/API parity, preview isolation, four consumers and canonical render passed. Current eight owning gates/Linux qualification plus unchanged-runtime carry-forward of the preceding corpus in #a18 Passed supported local views. Required partial-preview incompletion/native findings remain visible
A17 No-op bytes. YAML unknown/comment/order preservation. Stale-save and attachment collisions rejected Corrected frozen core/public/guarded-writer race, actual Hugo/tools, source-bound Editor/read-only browsers, exact-binary four-consumer proposal parity/preservation and guarded canonical source/render passed in #r8. Current eight owning gates/Linux qualification plus unchanged-runtime carry-forward of the preceding corpus in #a18 Passed supported local editing scope. Required partial preview/native findings still block Apply. Final A18 separate
A18 Actual declared macOS/Linux runtimes. Child signals. Provisioned offline runs. Honest unsupported inputs Current freeze/source and repeated five-archive reproduction. Darwin arm64, native Linux arm64 and emulated Linux amd64 nonroot ext4/full offline unit-vet/selected Hugo/native archive/signal smokes passed in #a18 Passed current declared runtime/archive scope. Optional guest prerequisites remain explicit skips. Darwin amd64 is experimental/unverified and Windows outside scope

Candidate sites and source preservation

The selected acceptance inputs include the embedded Starter and three distinct maintained consumer sites, reusing the historical corpus without writing consumer sources. The Starter source checkout provides provenance, while generated profile trials use disposable directories.

Input in the sibling checkout layout Initial observed identity and purpose Current candidate acceptance
oink-starter / generated Starter Source 137843b, two initial status entries. Licensed fixed archive, language/profile/root/subpath trials R1 bilingual init/check and R4 all-profile ordinary/public authoring flows passed. Archive/license unchanged
oink.pgsty.com Source 907d873 with existing changes. Bilingual documentation/regression and explicit local-theme trial Final R1 check and source preservation passed. Local-theme evidence remains distinct from public-pin evidence
pig.pgsty.com Source 75050c0, five initial status entries. Root Docs/Blog rewrites and nonrendering sidebar entries. Declared v1.1.0 Final R1 check and source preservation passed
repo.pgsty.com Unborn main, no HEAD revision. Materialized untracked sources, generated catalog and declared v1.1.0 Final R1 check and source preservation passed. Revision remains unknown

For each run, record effective module source and versions, flags/network policy, exit/result/coverage, raw evidence location, and preservation outcome. Before/after inventories must include all tracked and non-ignored untracked source bytes and modes, Git status/index state, workspace/replacement files, and effective vendor inputs. Compare exact inventories. Keep reports, isolated candidates, output and caches outside consumer sources and outside Git. Full-build timing comparisons must use the same current input baseline.

Owning checks and documentation gate

Start with the smallest affected Go packages and public behavior tests. The existing repository gates are make test (offline tests and vet) and make test-hugo (actual Hugo Starter, snapshot, manifest and public command fixtures). The owning Hugo gate now runs all owning packages without the old narrow test-name filter. New fixtures must remain in that gate. Use a race run for concurrent plan/server changes when the focused tests justify it. Unit fixtures remain offline. Networking requires explicit invocation.

For documentation, preserve EN/ZH heading number, order and stable explicit IDs. Use these narrow source checks:

node scripts/check-markdown-style.mjs content/docs/design/research
node scripts/check-doc-translations.mjs

Both source checks passed after this record and its Chinese peer were added. Eight Chinese research files passed the style checker. Translation source coverage was 137/137 pairs with 1,082 source headings.

After building the relevant site, complete the rendered documentation gate:

npm run _check:markdown-style
npm run _check:translations
npm run _check:rendered-markdown
npm run _check:rendered-links

make build validates the declared published pin. make check selects the sibling theme for the complete non-browser regression suite. Studio uses its own actual browser and accessibility checks.

Delivery state and remaining limits

State Current completion evidence. History retained above
Local implementation Finite R1–R8 supported implementation completed locally, including Studio/read-only and opt-in reviewed editing. Current A18 runtime/archive scope passed. Canonical lifecycle rendering is bound separately to these exact bytes
Local validation Historical R1–R8 owning/browser/corpus/render records retained. Current 2026-10-04 backend correction and eight current owning gates, actual three-target runtime/archive checks and unchanged-runtime carry-forward of the preceding four-consumer preservation/parity passed in #a18. Required repository findings/partial preview remain visible. Rendered navigation/URL checks have a separate exact-byte receipt boundary
Commits Baseline CLI commit identified
Archive and runtime qualification Current corrected source: Darwin arm64, native Linux arm64 and QEMU-TCG-emulated Linux amd64 passed installed archive/offline/signal/filesystem flows. Two fresh builds reproduce all five archives. Darwin amd64 remains experimental/unverified after actual failed execution
Public distribution and consumer adoption Not performed in this local validation run
Deployment and public-content verification Verified against local HTTP fixtures

The finite R1–R8 implementation and required current A01–A18 runtime/archive scope have local acceptance evidence. Known repository findings and required preview incompletion remain visible diagnostic outputs.

7.8.11 - CLI maintenance acceptance on 2026-10-03: R1–R5

R1–R5 local validation and acceptance evidence from the dated CLI maintenance record.

These R1–R5 sections of the dated record were moved here unchanged on 2026-11-06 so each page stays small enough to read whole. The scope, evidence rules, case ledger and delivery state remain on the main page.

R1 local validation

R1 now provides shared Hugo page/translation/source facts, rendered reference and anchor evidence, strict oink.policy/v1 input, check links, --format json, visible reviewed exclusions/external scopes and required-work precedence. Translation and style selections explicitly return required unsupported coverage while those engines remain unimplemented. Default build/dev remain direct Hugo operations.

Requirement Executed evidence Current outcome
Public result/policy and incomplete precedence make test: all packages and vet; public severity/exclusion/unimplemented-group/JSON-alias tests; TestEveryRequiredUncompletedCoverageFails Passed R1 scope; any required uncompleted status, including not_checked, remains 2
One build and shared facts TestPublicCheckSharesOneBuildAndRenderedFacts Passed; one strict Hugo build supplies page and observed target/anchor facts
Hugo authority and source mapping Actual TestPageFacts* fixtures: translationKey, actual routes/aliases, unknown generated nodes, custom mounts, excluded-page analysis and failure preservation Passed; separate analysis preserves production facts/artifact bytes and source bytes/modes
Repeatable real Hugo gate Corrected make test-hugo includes TestPageFacts* and TestHugoRendered*, alongside Starter and manifest fixtures Passed; scoped route/reference, reviewed external-scope and original-output preservation cases
Fresh Starter Bilingual init, check links, ordinary strict Hugo using isolated provisioned v1.1.0 module archives Exit 0; 223 files, 4,461 references, 66 page facts; dependency preparation remains explicit
R1 documentation source and schema Markdown style under content/docs; bilingual source checker; JSON parse and equality of CLI/docs result schemas; scoped diff whitespace check Passed: 88 Chinese docs, 137/137 source pairs and 1,085 headings; schemas remain additive oink.result/v1 with exits 0/1/2
Final candidate reports and rendered EN/ZH Refreshed current-binary consumer reports; actual rendered source/Markdown/link owning checks below R1 scoped gate passed; existing draft-release omission in production is recorded separately

Earlier offline R1 trials returned 0 without diagnostics on three consumers:

Earlier trial Source files Built files HTML files References Page facts Bytes/modes/Git inventory
OINK documentation 421 1,139 512 74,689 341 Exact before/after equality
PIG project site 858 1,392 424 64,440 248 Exact before/after equality
Repository catalog 2,294 3,287 1,635 851,535 1,572 Exact before/after equality

These earlier reports spell optional unselected coverage not_selected, outside the existing result-schema enum. The final code corrects it to not_checked and includes project.pages coverage. Their measured counts and exact inventories remain valid observations of the earlier binary. The final reports below establish final JSON conformance. Raw evidence stays in task-named local acceptance directories outside consumer sources.

The final R1 binary was rebuilt from the dirty CLI working tree based on e623d93d589c49e5c58b8fae1bd5db720fc904cb. The recorded input inventory includes file hashes, modes and Git-status identity. Its SHA-256, computed over sorted JSON serialization, is 518260f07f3c916468ee3d56c4eeca03c131514155aa82539564ccd2f3c1f664. The exercised binary SHA-256 is 3deb7e357fc86f6907df60da0769d93f2d41ba5e01949b641548a67d7f459d12.

Final current-binary trial Source files Built files HTML files References Page facts Acceptance
OINK documentation 421 1,139 512 74,755 341 Exit 0, valid result, complete page facts, exact source bytes/modes/Git preservation
PIG project site 858 1,392 424 64,440 248 Exit 0, valid result, complete page facts, exact source bytes/modes/Git preservation
Repository catalog 2,294 3,287 1,635 851,535 1,572 Exit 0, valid result, complete page facts, exact source bytes/modes/Git preservation

Each final result has required check.links: complete and project.pages: complete coverage. Unselected translation/style coverage is optional and reports not_checked. The final offline make test and vet passed; the corrected actual-Hugo owning target also passed. The recorded tools remain Go 1.27.1, Hugo Extended 0.166.0, Git 2.54.0 on macOS arm64. The three exact before/after inventories were independently compared while preparing this record.

Production output passed rendered Markdown and link checks. Its global translation checker returned 1 solely because the pre-existing draft content/blog/release/1.2.0.md / .zh.md pair is absent from production. The changed R1 pages rendered in both languages. A separate explicit analysis build set HUGO_BUILDDRAFTS, HUGO_BUILDFUTURE and HUGO_BUILDEXPIRED to true. All three owning checks passed: 137/137 paired sources, 1,085 headings, rendered Markdown and rendered links. That view checks excluded sources in isolation while keeping production output unchanged.

R2 local validation

The local candidate now implements translation policy/status/diff/hash review, syntax-bounded native content rules, visible reviewed baselines and shared oink.plan/v1 preview/validate/apply. Default check requires links, translations and style. Production output and the explicit draft/future/expired analysis are separate. Stable behavior and examples are in the contract. The R2 local gate passed owning checks, final frozen-input consumer reports and rendered bilingual documentation. The exact exercised binary and the subsequent narrow equality fix are recorded below.

Requirement Executed owning evidence Outcome and limit
A04 translation relationships/policy Actual TestHugoFilenameDirectoryAndTranslationKeyLayouts; scope, duplicate/missing/disabled-language, draft, strict/localized and selected-constraint tests Passed owning tests; no universal heading/code/localization parity
A05 explicit review and diff Full byte hash/current/source/translation/both-changed, mtime-independent, unknown/unreadable/ambiguous and malformed-record tests; public status/diff/review preview/apply fixtures Passed owning tests; review state is change evidence, not semantic judgment
A06 source boundary and provenance Actual Hugo enabled/disabled canonical title/block attributes and configured passthrough fixtures; front matter/CRLF/BOM/shortcode/code/HTML tests; every public v1.1.0 source/license SHA verified Passed owning tests; unsupported syntax remains incomplete and custom hooks remain outside catalog attestation
A07 baseline scope Capture/visible acknowledgement/new finding/incomplete precedence and malformed-record tests; public baseline preview/apply fixtures Passed R2 baseline scope; external tool adapter acceptance belongs to R6
A14 shared metadata plans Stale bytes/modes/existence/guards; edits during validation; exclusive commit collision; partial restore; later editor bytes/modes/deletion; old open inode write; new-directory children; confinement/identity/diff tests Passed owning tests and vet; candidate/source overlap refused; later move/reference ambiguity remains R5 scope
Frozen runtime gates macOS arm64 make test/vet, owning actual Hugo and focused race runs Passed; logs /tmp/oink-r2-frozen-go-gate.log, /tmp/oink-r2-frozen-hugo-gate.log, /tmp/oink-r2-frozen-race-gate.log; final all-owning-package Hugo gate /tmp/oink-r2-owning-hugo-final.log explicitly includes configured passthrough
Bilingual documentation Narrow source style/pairing/IDs, equal result schemas, scoped whitespace and actual production/analysis node checks Passed scoped gate: 88 Chinese docs, 137/137 source pairs and 1,092 headings; production draft omission separately recorded below

The frozen parser corpus at /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r2-source-corpus-lqx25kwr/summary.json records parser input SHA-256 a601200ec4fe275d2bd4baf11d4db7d46a2cc6f1674900c1fd801769e55d12de. Each scope parsed completely with zero findings. The core uses actual Hugo site-source identities from the recorded configuration. Supplemental Markdown includes disabled/unpublished files. It does not invent routes or relationships. These captures precede the authorized R2 documentation edits.

Corpus Unique actual Hugo source files Supplemental local Markdown Source inventory files Bytes/modes/Git
Starter 52 78 97 Exact before/after equality
OINK documentation 272 274 421 Exact before/after equality
PIG 212 212 858 Exact before/after equality
Repository catalog 1,568 1,572 2,294 Exact before/after equality

Preliminary public-command reports at /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r2-final-qu_zprps/summary.json used binary c8d87e6d73d3101fefcb62c5d6845518573c02c400f474dc9f4603afafc774d5 and CLI input inventory d8a75be0e074365a4164b7aaaa27d82a1e844e04406a36c3dd6d39ff2b6e873f. They precede the final parser/doc freeze and are not final acceptance evidence. The initial Starter invocation selected the enclosing evidence folder and returned 2. This was a validation setup error. Selecting its actual site child returned 0, with evidence in /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r2-starter-27vmi0w5.

Preliminary check Exit Page facts Built files References Translation statuses Outcome
Correct Starter child 0 66 223 4,461 28 Complete; 97 source files/inventory unchanged
Documentation 0 341 1,139 74,755 144 Complete; 421 source files/inventory unchanged
PIG 0 248 1,392 64,440 120 Complete; 858 source files/inventory unchanged
Repository catalog 1 1,572 3,287 851,535 788 Completed policy check: 10,462 actual HTML_ID_DUPLICATE findings in existing merged-print output; 2,294 source files/inventory unchanged

The repository catalog run completed with policy findings from existing duplicate HTML IDs in merged-print output; informational review states remain visible.

The final frozen-input reports at /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r2-candidate-6xzcs7fk/summary.json exercise binary SHA-256 ff88b407a6cddb9007f94275c65a80ed4c9c4fd13f5e821f9b7a4a8973abaa56 from CLI input inventory bd8c71b55250a89dc15c7534924bb82a5447d6f2628eba23c8cb3d864309ee9f. All four exact source byte/mode/Git inventories were independently compared equal before/after. These reports supersede preliminary public-command trials:

Final check Exit Source files Page facts Built files References Translation statuses
Starter 0 97 66 223 4,461 28
Documentation 0 421 341 1,139 74,825 144
PIG 0 858 248 1,392 64,440 120
Repository catalog 1 2,294 1,572 3,287 851,535 788

No final report has incomplete diagnostics. The repository catalog retains 10,462 actual merged-print HTML_ID_DUPLICATE findings and 788 informational review states; the other sites retain informational unknown review states.

Kept production docs at /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-site-2201601475/public passed rendered Markdown and links. Global translation checking returned 1 only for the existing draft release 1.2.0 pair absent from production. The separate draft/future/expired analysis at /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-site-3698773232/public passed all three node checks: 137 pairs/1,092 headings, 216 content pages/41,586 text nodes, and 347 pages/48,682 internal links/4,171 fragments. Evidence logs are /tmp/oink-r2-docs-production-{translations,markdown,links}.log and /tmp/oink-r2-docs-analysis-{translations,markdown,links}.log.

A final review identified optional equal_fields comparing JSON 7.0 with YAML/TOML numeric 7 by representation. The narrow supplement now normalizes decoded numeric values recursively to an exact rational number tag, preserving strings versus numbers, map keys and array order. Tests cover decimals/exponents, negative zero, integers beyond float64 precision, and nested differences. They also cover source byte preservation and required incompletion for unrepresentable values. Actual-Hugo translation tests passed in /tmp/oink-r2-numeric-translations-gate.log; all public maintenance actual-Hugo cases passed in /tmp/oink-r2-numeric-public-gate.log; owning vet and whitespace checks passed. Supplemental source SHA-256 values are:

Source SHA-256
internal/translations/check.go 24664377e14b4ae2fc554d0d7fde2ec33cc987707250e130fd88d9a25d5e1637
internal/translations/translations_test.go f58f4a305fe9fe3f5500ddfcf85faf3cfa37d72f8c220a1cb16ce4ccfbddb74d

The frozen real-site reports and Linux qualification predate this supplement; because those sites configured no numeric equality constraint, the fix does not affect their recorded outputs.

At this stage, macOS arm64 was exercised; an attempted Darwin amd64 runtime on this host failed with arch -x86_64 / posix_spawn: Bad CPU type in executable (/tmp/oink-r2-darwin-amd64-gate.log) because Rosetta was not installed. Both native Linux arm64 and Docker Desktop Rosetta-emulated Linux amd64 executed the same runtime/schema/license inputs. Their SHA-256 is 0f786df68ef3c4844c983a51595f79242d1cb1d2bf6c5b5eb7f2c6415fb8d861. Evidence is retained at /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-a18-linux-ajbbnvki in arm64-results, amd64-results, commands.json, candidate-inputs.json, preparation.json and qualify.sh. Each target passed 270 test/subtest cases with zero failures and two optional external corpus/provenance skips. Checks included full actual-Hugo go test ./..., vet, and built CLI version/bilingual init/doctor/full check/translation status, plus missing-Hugo exit 2 smoke. JSON stdout and source byte/mode inventories were verified. Go 1.27.1 ran on Linux arm64; Hugo Extended 0.166.0 architecture assets were SHA-verified.

R3 local validation

R3 adds managed build --check, oink.artifact/v1 sealing/export/local verification, explicit-network HTTP verification, release diagnosis and guarded local CI generation. The default build/dev path remains ordinary Hugo. The executed runtime and paired contract/guide gates passed.

Requirement Executed owning evidence Outcome and limit
A08 one checked artifact Public fake/actual Hugo one-renderer tests; exact export, manifest/marker, post-check byte/mode/missing/extra/symlink tampering, failure/concurrency and source-preservation tests Passed local scope; a failed/incomplete check cannot seal/export; local artifact verification does not rebuild
A09 both CI providers Offline deterministic generation, pinned source/Hugo archives and action revisions; safe bootstrap archives; guarded public preview/apply/stale-input cases; actual-Hugo original-input binding Passed local configuration scope; every existing generated target is refused and custom workflows remain unchanged
A09 upload identity Both local provider rehearsals and TestProviderUploadRehearsalPreservesActualSealedManifestIdentity Passed: one managed build, separate verification, then the same tree; GitHub tar includes the hidden marker, Cloudflare rehearsal receives that verified directory; no provider upload executed
A09 custom workflow diagnosis Generated-plus-other-custom and standalone-custom/no-metadata public tests, actual-Hugo preview and owning vet Passed supplement in /tmp/oink-r3-ci-custom-owning-gate.log and /tmp/oink-r3-ci-custom-vet-gate.log; each unrepresented workflow stays unknown, informational and optional release.ci: not_checked, including beside valid generated metadata
A10 deployed identity Local HTTP fixtures for all recorded files/routes/languages, marker, canonical/base/inert-template behavior, HTTP 200 fallback, wrong bytes/language/build, missing resources/Markdown/search JSON Passed local fixture scope; definite mismatches return 1; browser JavaScript is explicitly unchecked
A10 unknown network state Explicit network/credential refusal, timeout before headers/during body, authentication/rate-limit/server errors, required marker absence, bounded body/gzip and redirect/no-cookie fixtures Passed local fixture scope; incomplete states return 2 with remaining requests unknown; no public deployment was contacted
Frozen runtime gates Full tests/vet, owning actual Hugo and focused race checks Passed on macOS arm64 in /tmp/oink-r3-frozen-go-gate.log, /tmp/oink-r3-frozen-hugo-gate.log, /tmp/oink-r3-frozen-race-gate.log; the subsequent custom-CI change has the focused supplement above

The latest single-binary corpus is retained at /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r3-ci-final-ahc4csjk/summary.json. It compiled an exact captured CLI input copy, with binary SHA-256 425845c1d2db7b1cd3c3cdb5f28475cb06ba6f656054909759e2359a39925dd2, 67 runtime/schema/license inputs SHA-256 6789a3a0235ff8d81453b9bfde37824979eac7d56af3710da390e4d2ef8479dc, and 108 broader CLI inputs SHA-256 4ca473a4cb586d232baeb4cee029b281469c5bb03c831cc199b95451e6832c60. Runtime inputs remained exactly equal after all runs. Live tools were Go 1.27.1 and Hugo Extended 0.166.0 on Darwin arm64. The manifest’s normalized Hugo version excludes vendor build text and private paths.

Each run used offline build --check. It used fresh external destination/manifest paths, the optional marker, and retained isolated work. These existing local consumer inputs were checked without --release. Their configured workspaces were preserved. Every raw report records exactly one strict Hugo renderer, zero incomplete diagnostics and zero required unfinished coverage.

Final managed build Exit Source files Copied source inputs Page facts Built files References Exported files Local artifact verify
Starter 0 97 94 66 223 4,461 224 0
Documentation 0 421 427 341 1,139 74,825 1,140 0
PIG 0 858 861 248 1,392 64,440 1,393 0
Repository catalog 1 2,294 2,299 1,572 3,287 851,535 None Not exported

Both inventories compare exact bytes, modes and file types before/after; the primary inventory also compares logical Git state. Git sites include tracked and non-ignored untracked sources; the non-Git Starter includes its existing generated files and lock. The supplemental copied-source inventory also includes ignored workspace/editor metadata read by snapshots, excluding existing generated output/cache trees. All four comparisons were exactly equal.

The repository catalog retains 10,462 existing merged-print HTML_ID_DUPLICATE findings, so neither destination nor manifest was created. Production review states number 28/143/120/786. Analysis includes unpublished pages, which explains the earlier R2 144/788 counts. Existing custom workflow information remains visible, and Starter’s example address is a warning in this non-release run.

The three fresh exports match the retained independent ordinary-Hugo trees in every original file’s SHA-256, size and mode. The sole extra file is .well-known/oink-build.json. Their original source inventories and complete manifest input hashes match the earlier capture, so reusing those ordinary trees does not substitute different inputs. The helper source SHA-256 is 13e4957a3d7847eb28c8b1eeba3588a4f4a9982c2bfca2ebc729ab2827159607; its binary SHA-256 is b2699fe7a7aa3a34c41f9e4aba4b22d39cf8d0c156a369f3dfc4ca8c8c0fbce5. Raw helper and ordinary evidence are in /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r3-candidate-wb643dhh; the temporary compilation source was removed after building the helper.

Earlier R3 captures preceded the runtime freeze (oink-r3-final-pisrr21h preceded custom-CI diagnosis), and an initial /Users/vonng/pgsty/PIG target selection returned 2 before switching to pig.pgsty.com.

The scoped R3 documentation gate passed at /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r3-docs-render-pljj5aqd/summary.json. Ten paired contract/guide/roadmap/index/overview edits were installed only after the original bytes/modes matched. Their recorded hashes are at /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r3-doc-drafts-s0b3g48l/applied-files.json. Source style passed 88 Chinese docs; translation coverage passed 137 pairs and 1,099 headings; schemas remained equal and scoped whitespace passed. Actual production Markdown passed 214 pages/41,871 text nodes; links passed 345 pages/48,344 internal links/4,171 fragments. Production translations returned 1 only for the unchanged draft release 1.2.0 absent from production. A separate draft/future/expired analysis passed Hugo and all three owning checks: 137 pairs/1,099 headings, 216 pages/42,177 text nodes and 347 pages/48,720 links/4,199 fragments. All 421 canonical and 488 copied source files retained exact bytes/modes throughout these rendered checks.

R4 authoring and upgrade acceptance

The supported R4 implementation and read-only corpus scope are locally accepted after frozen owning/full gates. This record covers profiles, ordinary authoring/editor/snippets and bounded upgrade views, along with guarded canonical documentation promotion and fresh scoped rendered validation.

Executed profile evidence Outcome and limit
One fixed licensed archive Snapshot verification against commit 137843b25bacd76ddd1f7ce71330bf2e3155b954 passed without --write; archive SHA e55bde279715f6d8d19d3d88671a2cf7561b515be46915b0f12c640d0ce1d958 and MIT license unchanged; projection metadata/script match
Composition and preservation Default/explicit project byte parity; selected archived model/localized home, invalid profile, nonempty target, concurrent validation/publication and cancellation recovery tests passed; unit/vet/race gates passed
Ordinary Hugo All four profiles × three language choices × root/subpath passed 24 actual warning-strict offline builds using provisioned public OINK v1.1.0; complete source byte/mode/no-extra-file and rendered-reference checks passed
Public init workflow Four profiles with en/en,zh, actual subsequent root/subpath Hugo URL facts/checks, workflow/license preservation and default parity passed; unknown/nonempty refusals 1, missing/failed Hugo 2, empty/absent targets and pure JSON/separate logs verified
Public authoring and source identity Actual candidate/apply/ordinary Hugo, review-unknown and source preservation passed in /tmp/oink-r4-authoring-public-gate.log; fresh-directory/site guards and vet passed in /tmp/oink-r4-new-input-race.log and /tmp/oink-r4-public-core-vet.log. Actual ignored input refuses 2 without a saved plan or source writes even when source groups are disabled; selected draft peers still force analysis identity in /tmp/oink-r4-authoring-sourceproof-gate.log. Supported owning scope passed
Bounded upgrade owning gate Seven actual-Hugo synthetic pinned module-fixture cases, observed-stream digest/inventory fidelity, independent cross-page alias-retarget blocking, source/concurrency/exclusive installation and later-edit rollback protection passed under race; vet passed. Final hardening logs /tmp/oink-r4-hardening-owning-gate.log, /tmp/oink-r4-hardening-final-focused.log, /tmp/oink-r4-hardening-vet.log; final public/full frozen gates passed
Integrated authoring/editor hardening Actual-Hugo language-directory plan/apply/ordinary builds, link/never new-source refusal, external schema/license/full-mode/module identity and legacy schema reproof, shared translation/baseline/CI regression and full Starter docs→new draft peer→editor→check→ordinary Hugo flow passed. /tmp/oink-r4-app-authoring-hardening-gate.log (58.241s), focused race and vet passed; post-candidate external mutation proof /tmp/oink-r4-app-external-during-validation.log passed. Opaque saved external-input hashes and canonical workspace-origin guards passed /tmp/oink-r4-external-plan-binding-final.log, /tmp/oink-r4-workspace-origin-gate.log and their vet logs. Frozen full-stage, corpus and scoped canonical rendered documentation gates passed
Actual language mounts Standalone ordinary per-language contentDir and explicit site-matrix fixtures each passed config/mounts/strict-render with source bytes/modes unchanged; Hugo0.166 emits sites.matrix.languages and distinct physical files with reciprocal public translations. /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r4-language-mounts-lgmve1sk/summary.json; public actual language-directory plan/apply/ordinary-Hugo integration passed

Owning evidence is retained at /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r4-starter-owning-0pv41lw5/summary.json. Logs are /tmp/oink-r4-starter-{unit,hugo,vet,snapshot,race}-gate.log and /tmp/oink-r4-public-init-gate.log, /tmp/oink-r4-public-init-vet-gate.log. Generated source counts are project 94, docs 58, blog 40 and book 34.

Final frozen gates all returned 0: make test/vet /tmp/oink-r4-frozen-go-gate.log, make test-hugo /tmp/oink-r4-frozen-hugo-gate.log and actual-Hugo core race /tmp/oink-r4-frozen-core-race-gate.log. The final public flow includes Starter docs → primary/translation draft → editor → check → ordinary Hugo; post-candidate external schema mutation still refuses before source writes. Seventeen owning and three final gate logs are retained verbatim with hashes in the final corpus’s owning-gates.json.

The exact four-site evidence is /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r4-corpus-lw2cjyq6/summary.json with bounded summary.compact.json, raw JSON/logs and per-command inventories. Binary SHA is c169b3d4d046c811dca80867068b86fb66ada5c8ce6910dd5cda7353c406f377; 82 runtime-input files bind SHA fdff7f50d49b44f03fa1db79eec6b6c9b9bd5e52f8967a88aed84b3207a7b3c6, which equals the final root inventory with no runtime changes. The broader 139 CLI inputs bind SHA 562e838d9eccb628eac86ae59b9b9587c1e23ad52991ec50eafb1e604e3924da. Driver SHA is d3ac41dc2e18295bfb26134d1a696935c8174913e2801a5766dbf7a1139d89f8. Actual tools were Go 1.27.1 and Hugo 0.166.0 Extended on macOS arm64.

Frozen consumer Primary/copied source files Pages; output files; references Managed build / artifact verify Read-only upgrade / new / editor
Starter 97 / 94 66; 223; 4,461 0 / 0; 224 exported files including marker 0 / 0 / 0
Documentation 421 / 427 341; 1,139; 74,937 0 / 0; 1,140 exported files including marker 0 / 0 / 0
PIG 858 / 861 248; 1,392; 64,440 0 / 0; 1,393 exported files including marker 0 / 0 / 0
Repository 2,294 / 2,299 1,572; 3,287; 851,535 Completed finding 1; no export/manifest Completed finding 1; new/editor not attempted after blockers

Each managed build used exactly one strict production Hugo render and had no required incompletion or uncompleted required coverage. Primary Git-visible source bytes/full modes/logical Git state, supplemental copied inputs and source directory modes matched exactly before/after every command and each complete site flow. Repo’s 10,462 existing merged_print duplicate HTML IDs remain visible.

Consumer upgrade previews selected the available public v1.1.0 pin and did not apply writes. Cross-version route/alias/output regressions use explicit synthetic fixture pins. New/editor plans were validated previews without saving or applying consumer plans. Multi-host and unknown relative-alias identities stay incomplete, and nondeterministic output requires a fresh v2 plan preview.

Fresh canonical documentation acceptance passed after applying the ten guarded files. Evidence is /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r4-docs-render-v01eima0/summary.json; the promotion manifest is /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r4-doc-drafts-3i8bw994/applied-files.json. The frozen c169b3… CLI performed one strict production render and its focused link check returned 0.

Fresh canonical documentation gate Executed result
Source owners Translations 0: 137/137 pairs and 1,104 headings; complete canonical style 0: 137 Chinese files, 181 strong spans, no emphasis; ten-file whitespace check and public JSON schema equality passed
Production rendered Markdown/links Both 0: 214 content pages / 42,214 text nodes; 345 pages / 48,360 internal links / 4,187 fragments
Production translations 1 solely for the pre-existing draft content/blog/release/1.2.0.md absent from production; no new pairing/heading finding
Separate nonpublishable analysis Fresh ordinary Hugo with the actual original snapshot environment/rebased paths and explicit draft/future/expired flags returned 0; all three owners 0: Markdown 216 pages / 42,520 nodes, links 347 pages / 48,736 links / 4,215 fragments, translations 137/137 pairs / 1,104 headings
Source preservation Canonical 421 Git-inventoried files and 427 copied inputs retained exact bytes, modes, Git state and directory modes; production copied 428 and analysis copied 427 files remained unchanged through their checks; analysis build also retained copied full modes

The temporary helper copied the frozen core without modifying it: helper source SHA 7faea7e726a6c6fb2e0747be1a4428f4c5fb5734fa52b6f981157a5fe37d9989 and helper binary SHA 532638e76f96f8b173c122e512b3bf5fc2c4d4a7130f59c99c2c69e135e87073 are retained with raw logs.

R5 implementation and documentation acceptance

R5’s supported local scope is accepted after focused public/core, corrected frozen full-stage, exact-binary read-only consumer and guarded canonical source/rendered documentation gates.

The actual public Git/Hugo flow at /tmp/oink-r5-public-final-flow.log passed in 53.963 seconds. Its committed synthetic site owns its local theme, bilingual pages and binary attachment. Ordinary modes 0640 and 0600 remain full current facts. Historic Git comparison uses only executable bits. Deleting B selects unchanged inbound A, the remaining translation, removed attachment and actual RSS output. Actual alias-inbound uncertainty, global configuration/template/data and unknown-input changes expand full scope.

Completed inspect/impact/context returns 0 with separate current-check findings 1. Check-since retains current quality 1 and full validation scope. Missing/unborn/foreign history returns 2. It retains every known current page/attachment/reference/output. It does not invent a prior identity or change. Malformed selectors/limits, missing tools and failed renderer logs are tested. Bounded context gives reasons/versions/source and excerpt hashes, visible omission/truncation and no execution of literal document instructions.

Saved move preview/apply and subsequent ordinary Hugo passed, retaining binary bytes, raw full modes, unrelated files and Git index/revision. Actual opaque HTML/shortcode references remain manual. Inline/fenced/opaque spans stay unchanged. Their broken final candidate returns 1, with no saved plan or source writes. Source/config/attachment/mode/fresh-target drift returns 2 and preserves the later edit. A deterministic mutation after the actual candidate renderer also refuses before writes and preserves editor bytes/mode. Focused actual move race passed in 8.286 seconds at /tmp/oink-r5-public-move-race.log; app vet passed at /tmp/oink-r5-public-vet.log.

Before the cached-module supplement, frozen parent make test/vet and make test-hugo both passed at /tmp/oink-r5-frozen-go-gate.log and /tmp/oink-r5-frozen-hugo-gate.log (actual app fixtures 185.709 seconds). Actual move/source race and vet passed /tmp/oink-r5-move-hugo-gate.log, /tmp/oink-r5-source-move-race-gate.log and its vet counterpart; full inventory/mode/selector plan safety passed /tmp/oink-r5-plan-owning-final.log.

The first frozen consumer trial exposed a cached-public-module guard gap. The original graph contained resolved module inputs that were absent from a fresh outer candidate hash. It returned false incomplete 2, without source writes. Content plans now resolve/capture the same module inputs before comparison. Legacy metadata/authoring plan scopes remain unchanged. The separate checksum-verified public OINK v1.1.0 regression passed preview, fresh saved apply and ordinary bilingual Hugo in 27.42 seconds (package 28.220) at /tmp/oink-r5-public-cached-module-move.log, including raw modes, binary bytes, unrelated inputs and Git preservation. Corrected current-binary corpus and supplemental race evidence remain separate from the earlier unaccepted trial.

The corrected current candidate passed full make test/vet at /tmp/oink-r5-corrected-frozen-go-gate.log and actual make test-hugo at /tmp/oink-r5-corrected-frozen-hugo-gate.log (app 278.787 seconds). The cached/public and committed/in-site move safety race passed in 38.578 seconds at /tmp/oink-r5-public-cached-seam-race.log; its app vet also passed.

/tmp/oink-r5-corrected-runtime-freeze.json records 96 runtime inputs with SHA-256 e5b6e0eda972116dbb94a8086668e6ef34bfaa56138cf31f1f71f4832c477842 and 165 broader CLI inputs with SHA-256 4965a0c92cb6126f67a6dabd548c7c25ee5d7c9c57e14cce5e55ebb7a22fca2d. The corrected binary SHA-256 is d7675aecca2f77b1eb37bb4f664c3314cf5207149e6abbb86523686c5c50bff0. The corrected four-consumer capture completed 16 commands in 831.825 seconds at /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r5-corpus-corrected-y2eue81h. Its bounded final-receipt.json has SHA-256 b86e0e6c7dbfbaed62c845c03a55d963068f7d9a16771de5ff3b0974e76fce3b; the receipt retains 12 copied owning gate logs and all 20 observed move routes. It links to the complete raw JSON/logs and per-move classifications.

Site Primary/copied inputs/directories Inspect/context Current check Impact Move preview
Starter 97/94/21 0/0 0 2: no Git baseline 0: validated, unapplied
Documentation 421/427/109 0/0 0 0: complete historical comparison 1: six candidate missing references
PIG 858/861/52 0/0 0 2: historical foreign-input provenance incomplete 1: 32 candidate missing references
Repository 2294/2299/48 0/0 1: 10,462 existing duplicate HTML IDs 2: unborn HEAD baseline unavailable 1: the same existing duplicate IDs

Starter and Repository impact retain known current facts without inventing prior pages or changes. PIG’s actual baseline is complete (theme v1.0.0 versus current v1.1.0). Required foreign-input provenance is incomplete. The comparison therefore expands full scope and returns 2. These are distinct outcomes. Documentation impact completes with 192 captured input changes, 343 affected prior/current pages and full scope. All completed fact queries expose current quality findings separately. Repository inspect/context remain 0.

Documentation move proves 18 rewrites and four routes. Two ordinary literal /docs/admin/comments/ occurrences in content/docs/customize/repository.md at lines 216 and 313 remain manual because repeated source/output occurrences cannot be attributed precisely across ordinary and print outputs. Their six missing candidate references block validation. PIG moves two Markdown files and four binary attachments, with eight proven page/processed-resource routes. Equal-byte paired outputs prove processed featured_hu_* resources, but not new URLs for the four original absolute image references /article/pgext-day/{featured,topic,venue,schedule}.webp. Their 32 candidate missing references block validation. Original-asset rewrites are not guessed. These ordinary Markdown limits are separate from opaque HTML/shortcode limits.

Repository move proves ten rewrites and four routes. Its candidate retains only the same 10,462 existing duplicate-ID findings. It has no new missing reference or required incomplete finding. Starter’s zero-link bilingual move is validated. All four moves remain unapplied. No consumer plans were saved. No consumer source writes occurred. Failed candidates have validated: false. All JSON stdout is pure. Primary/copied inputs, full modes, directory inventories and logical Git/index state are unchanged. These comparisons include ignored copied inputs. The Git metadata inventory excludes immutable object storage. The runtime and broader CLI inventories still match the captured identities.

The first ten-file guarded canonical promotion was qualified in 62.37 seconds with the corrected frozen binary and runtime hashes above. The separate rendered receipt is /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r5-docs-render-ks2tw82c/summary.json, SHA-256 06ae844a4b3f1c01bb5faa8a21091d5461c28aab592d12c4310fb34bc176c5d4.

Canonical documentation gate Executed result
Source owners Translation 0: 137/137 pairs, 1,109 headings; style 0: 137 Chinese files, 181 strong spans, no emphasis; scoped whitespace and public JSON schema equality passed
Frozen CLI Production check links returned 0 using the exact corrected binary
Fresh ordinary production Hugo Build 0; Markdown 0: 214 pages/42,571 nodes; links 0: 345 pages/48,376 links/4,203 fragments
Production translation owner 1 only for the pre-existing draft release-1.2 omission from ordinary production output; no new R5 discrepancy
Separate ordinary analysis Hugo Fresh nonpublishable -DFE build 0, without the CLI probe; Markdown 0: 216 pages/42,877 nodes; links 0: 347 pages/48,752 links/4,231 fragments; translations 0: 137 pairs/1,109 headings
Input preservation All per-command and overall guards passed: 421 primary files, 427 copied inputs, 109 directories and 36 mutable Git files retained bytes/full modes/logical Git state; both isolated source copies remained unchanged

The permanent first-promotion applied-files.json in /var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r5-doc-drafts-t3_klnck retains the ten authorized files and their original modes. Core owning logs /tmp/oink-r5-frozen-core-hugo.log, /tmp/oink-r5-owning-race.log and /tmp/oink-r5-owning-vet.log passed. A13 impact, A14 move safety, and A15 bounded context passed their required supported CLI scope.

7.8.12 - CLI maintenance acceptance on 2026-10-03: R6–R8 and A18

R6–R8 and A18 workspace, Studio, editing and runtime evidence from the dated CLI maintenance record.

These R6–R8 and A18 sections of the dated record were moved here unchanged on 2026-11-06 so each page stays small enough to read whole. The scope, evidence rules, case ledger and delivery state remain on the main page.

R6 workspace and adapter acceptance evidence

R6 supported scope is accepted locally after frozen owning/runtime, exact-binary consumer parity/preservation and guarded canonical source/render gates. The supported registry/tool fields are documented in the contract and command reference.

The registry has an independent version, oink.workspace/v1. It requires strict one-document regular YAML, 1–64 entries, and at most 256 KiB. It requires exact ASCII names, literal relative/absolute directories, and proven canonical identities. It refuses overlap. Missing-site results stay per-site incomplete while later selected sites run. Selection preserves registry order. There is no default named site, sibling discovery, Hugo settings duplication or automatic multi-site apply. Optional tools extend oink.policy/v1, with pinned protocol versions, configuration/full-mode provenance and typed omissions/coverage.

Workspace owning receipts

Focused gate Executed local evidence
Registry core Strict fields/document/bounds/names/literal paths, existing aliases/case-inode ancestry, duplicate/overlap refusal, missing-directory listing and exact subset order; go test -race ./internal/workspace -count=1 passed in 1.414 seconds, /tmp/oink-r6-workspace-core-race.log
Public actual Hugo OINK_TEST_HUGO=1 go test ./internal/app -run '^TestPublicR6Workspace' -count=1 -v passed in 10.498 seconds, /tmp/oink-r6-workspace-public-hugo.log
Public race The same workspace public suite with -race passed in 12.426 seconds, /tmp/oink-r6-workspace-public-race.log; excluded commands specifically reject registry selection
Vet go vet ./internal/workspace ./internal/app completed with exit 0, /tmp/oink-r6-workspace-vet.log
Public outcomes Actual bilingual committed fixture sites retain direct diagnostics/coverage/exit parity for links and full checks. A missing first site yields 2 while later clean/finding sites yield 0/1; explicit subsets preserve registry order, invalid unregistered siblings remain untouched and human output retains findings
Selected application Saved translation-review preview is validated but unapplied; a different registered name is refused before writes and preserves plan/source bytes/full modes/Git. Explicit matching-name apply writes only its planned review file; other registered and unregistered sites remain unchanged

The inspected core workspace.go SHA-256 is cf2cbc9509e8c83eedf6d8833c9eb0ea6492de9a85c959798112fa3f105213f4; its owning test is 9070a8e2c3e58680f6567f2394160ec682bf0457c068c2addf354921e7612d6b; the public test is 3e57a6417ae2e7604f7cb06933759bb06a2f40758ff7059848593cedbaa6570a. All three inspected files retain mode 0600.

Corrected protocols and stage gates

Protocol or gate Recorded status
Actual markdownlint-cli 0.49.1 and Vale 3.24.0 Corrected public trial passed: exactly one finding mapped to the original UTF-8/BOM/CRLF line; excluded front matter/shortcode/math/raw HTML/enabled attributes/code produced no false original attribution
Actual lychee 0.24.2 Corrected trial actually reached the local HTTP fixture: 200 → 0, 404 → 1, 401/403/429/503/timeout → required 2. Optional offline → 0, required offline → 2, both with zero HTTP requests
Final focused actual-tool receipt /tmp/oink-r6-public-actual-tools-final.log passed in 15.192 seconds; the earlier corrected 14.686-second run is retained as prior evidence. Node preload and discovered JS configuration did not execute; source full modes/Git were preserved
Fake/protocol failure receipt /tmp/oink-r6-public-fake-tools-final.log passed in 13.089 seconds: malformed output, version mismatch, timeout, unsafe configs, required missing/optional/group omissions and raw stderr normalization
Focused public race/vet /tmp/oink-r6-public-tools-race.log passed in 30.273 seconds across fake and actual cases; /tmp/oink-r6-public-tools-vet.log completed with exit 0
Frozen runtime inputs Parent freeze at 2026-10-03T10:58:01.807947Z, /tmp/oink-r6-runtime-freeze.json: 103 runtime inputs bind b85affd96378b45bfc56a996b0c5672d02ee4c6cc9bc95335fa5072f6c42a03b; 179 broader CLI inputs bind fbb8176ebc58f1aa26336f4e6036cf9bd5f7a0d62b142f16532b50a8071e9fbe. The exercised 0.3.0-r6-local binary SHA-256 is aa8b347fbe01071f9da729f4d98aa2f50d7264456be6c5f05771bcfadadc371f
Frozen owning suites Full Go/vet completed with exit 0, /tmp/oink-r6-frozen-go-gate.log; full actual Hugo plus pinned tools completed with exit 0, /tmp/oink-r6-frozen-hugo-gate.log (app 382.832 seconds). Workspace/core/protocol/source-mask/policy/report race and vet receipts passed and are copied into the final receipt
Four consumer sites Qualified: exact-binary direct/aggregate diagnostics, coverage, exit, identity and registry-order parity for all four sites; per-command/overall source byte/full-mode/type/logical and mutable Git/ignored-input/directory guards passed. Aggregate completed 4, finding 1, incomplete 0, exit 1
Canonical EN/ZH Passed: guarded first ten-file promotion, source owners and fresh ordinary production/nonpublishable rendered evidence; only the known production draft-release omission remains
Stage decision Supported R6/A07/A15 scope accepted locally after the required receipts

The durable focused-tool receipt is /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r6-tools-6bf67ltv/r6-public-tools-acceptance.json, SHA-256 16b6e47fc0618c76d2f9e3680a4112b6e47b478af8aabd3f2fc84821f840cc8a. It binds the provision record, executable/configuration evidence and 1,422 resolved Node package files. Markdownlint reports original content/tools.md line 7, bytes[80:92] (ppears here.); Vale reports the same line, bytes[71:78] (BADTERM). Each of the seven network cases actually makes one HTTP request.

The exact-binary consumer receipt is /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r6-corpus-59_asiyr/final-receipt.json, 42,212 bytes, SHA-256 0ad86afaf235bdcff0c474e76b08e0591591a7b22c7992b02e20fb17975d029e; the completed summary binds 467b66eb6d178829508115050d4243909313acf1b4d59317ecda37ab7383ca55. It retains 14 copied owning/completion gate logs. The six original operations sum to 314.912887 seconds, excluding candidate compilation and receipt-only correction. workspace list returns 0; four direct full checks return 0/0/0/1; the aggregate returns 1 with all four sites completed.

Site Preserved source files Direct/aggregate child exit Diagnostics/coverage Recorded finding boundary
Starter 97 0/0 28/29 Translation review information only
Documentation 421 0/0 144/34 Translation review information only
PIG 858 0/0 120/41 Translation review information only
Repository 2,294 1/1 11,250/29 Existing 10,462 duplicate-ID findings and 788 translation review information items

Direct and aggregate child identities, order, every diagnostic and coverage record match. After every operation and overall, all four consumer sources retain full modes/types, logical and mutable Git metadata, ignored copied inputs and directory inventories. The complete root CLI inventory still equals its frozen capture. The 478,603,149-byte direct repository JSON and 635,470,795-byte aggregate JSON were validated streamingly rather than truncated. Separate pinned-tool fixtures qualify optional-tool protocols. The consumer registry is task-local and writes no consumer policy.

The initial acceptance driver overwrote a summarized result’s command string with invocation argv, producing a false parity exception after all six CLI operations and their per-operation guards had completed. The failed driver and summary remain preserved as pre-correction.r6_qualify.py and pre-correction.summary.json. Receipt completion corrected only invocation metadata. It verified unchanged raw-result SHA-256 values and header commands. It retained all original full-stream diagnostic/coverage digests and rechecked overall consumer/root guards. No CLI runtime correction or Hugo/CLI rerun was needed. The receipt-only completion took 2.002 seconds and exited 0 in /tmp/oink-r6-corpus-receipt-completion.log.

The executed driver SHA-256 is 4d2a360c6f7f6f96c38698bd189bc4d4b2cb02a7509858920d752897fdd85988; the corrected driver is 4870f5c0374fcc11ad1a6b2e3aefe36f493b6f9f4666c293993dc6a59df8a11b; the receipt-completion driver is cd50d3fe704370f73fa4e7d94ce8e4bc925d11ec8ef04d463aacec37c2053daf. The streaming helper binds 144f778cdb7907372797b47b97f817f340e70423701a2a958dee589281a9a11c, and the inventory helper binds d3ac41dc2e18295bfb26134d1a696935c8174913e2801a5766dbf7a1139d89f8. This receipt qualifies local darwin/arm64 with Go 1.27.1, Hugo Extended 0.166.0, Node 26.9.0 and Apple Git 2.54.0.

The initial actual-tool trial exposed Darwin /var versus /private/var staging identity, actual loopback proxy routing, and an invalid inline-block-attribute/line assertion in the Vale fixture. Staging is now canonical. The Vale fixture uses a real standalone block attribute. The source-mask boundary is unchanged. The qualified child environment forwards literal NO_PROXY/no_proxy host-list data while omitting proxy URLs/credentials and Node preload settings.

Supported source diagnostics require proven original ranges. Rendered lychee locations remain output file/DOM pointers without inferring a Markdown line. Offline lychee is not invoked. Authentication/rate-limit/server/transport uncertainty cannot become required success through severity, exclusions or baseline acknowledgement.

The first guarded ten-file promotion and its fresh rendered qualification are complete. The receipt is /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r6-docs-render-dcwtcmyl/summary.json, 555,297 bytes, SHA-256 ee153932900dc6f1ec62beef1a75927fc60b857efccfbcc558bf0e2c2b12cc04. The 64.17-second run used the exact qualified aa8b347f…371f binary and unchanged 103-input b85affd9…a03b runtime inventory recorded above.

First-promotion documentation owner Actual result
CLI production links 0; one strict production Hugo renderer, no analysis build
Ordinary production Hugo / Markdown / links 0 / 0 / 0; 214 content pages and 43,376 text nodes; 345 HTML pages, 48,438 internal references and 4,259 fragments
Ordinary production translations 1 only for the existing draft content/blog/release/1.2.0.md absent from production; not a new R6 failure
Independent ordinary nonpublishable Hugo / Markdown / links / translations All 0; 216 content pages and 43,682 text nodes; 347 HTML pages, 48,814 internal references and 4,287 fragments; 137/137 pairs and 1,118 headings
Source owners / schema Translation, style and whitespace all 0; 137/137 pairs, 1,118 headings; 137 Chinese files, 181 strong marks, zero emphasis marks; CLI/documented result schema both bind 7468c2d04cde8a368ce0ba44a1f27125b5fca364b6d4672353519b9545b3bdda
Preservation All 12 owner commands, CLI/schema checks and overall comparison preserve 421 primary files, 427 copied inputs, 109 directories and 36 mutable Git files with full modes/types/bytes and logical Git; both private ordinary source copies and all 103 runtime inputs unchanged

The first-promotion installer receipt, oink-r6-doc-drafts-ymjop499/applied-files.json, binds 13c965592d64056d8365aed1927d2d422fadec8adc54ee7050b22e2ea0ad6270. It retains captured actual original inodes in private temporary storage, preserving later old-open-handle writes. Recovery also preserves later target edits or deletion.

R7 read-only Studio candidate evidence

R7 implements the embedded five-view browser and authenticated loopback API candidate described in the contract. Frozen core/browser, exact-binary four-consumer qualification, and guarded canonical rendered gates passed within the declared scope.

Focused native and browser evidence

Owning boundary Evidence status
Native/public parity Actual shared check reports preserve diagnostic/coverage/exit identity for 0/1/2; explicit selected workspace startup, cleanup/signal and no-source-write proofs are recorded separately by the owning test receipts
HTTP management/source/preview Literal-loopback selection; exact Host/origin/Bearer checks; no arbitrary request paths/writes; captured source/diff bounds and source-mode/output inventory guards; focused core/new browser and current corpus receipts below bind this supported scope
Initial held browser 14 axe checks with zero violations and 14 screenshots; five desktop light views, captured BOM/CRLF source/diff, desktop dark, mobile dark and all five 320-pixel light views plus capture changes. Synthetic actual-Hugo fixture retains 228 native diagnostics and coverage parity; copied suggestions use a private test clipboard, leaving the host clipboard unchanged
Browser preview attack Actual attack script executes in the isolated preview, but parent access, management fetch and popup are blocked; token query refused. Actual draft-only page remains production 404
Snapshot preservation Captured source instructions/HTML stay literal data; the initial capture retains its old bytes before refresh after an explicit task-fixture external edit. Source full modes/Git/directories preserved except that declared fixture edit; changes show the actual modified captured input
Preceding held browser Passed refreshed held UI/backend receipt: all 14 axe checks zero violations and 14 screenshots, including declared/captured theme rows
New partial-preview browser Passed new frozen partial-preview runtime: 14 axe checks with zero violations and 14 screenshots; actual Hugo normal HTML 200 and 67,108,865-byte output 413; native 1/228 diagnostics and coverage retained, required partial coverage visible and Studio/refresh 2

Initial browser receipt: /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-studio-browser-AfwaYi/summary.json, SHA-256 930fbd1ab86806069b963bff2e3e95aaa07e3634400cec65e2b8c7922e2a1707. Its exact binary binds 92e5b962e40fbe828a0b006f3ae76a2bddf4ad7e8b1c5b6967e365b8f1827879. The qualified local versions are Node 26.9.0, Playwright 1.62.1, @axe-core/playwright 4.13.0 and Chromium 151.0.7922.34.

The refreshed held UI/backend browser receipt is /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-studio-browser-vn0ofb/summary.json, SHA-256 520603779f712539865c6e9ef7a9ad3ad21ec1906adfb067ec607cc69d071b7e, with exact binary 73bf90c69dce84849ee20ddfbfe825b9f2dd46f0cd37a228ce1b041a83afa33f. All 14 axe runs and 14 screenshots passed, including declared/captured theme metadata and all five views at 320 pixels.

The initial parallel whole-suite trials in /tmp/oink-r7-frozen-go-gate.log and /tmp/oink-r7-frozen-hugo-gate.log failed due to ten-second CI-test deadlines under parallel package load and a graph test observer refreshing its own Git index. The isolated CI target sets then passed in 12.149 and 2.291 seconds. The controlled Git-observer graph run passed in 1.354 seconds. Only internal/projectgraph/hugo_test.go changed: its read-only observer disables Git optional locks, filesystem monitoring and the untracked cache. The ordinary actual-Hugo graph run passed in 1.562 seconds after that test-only correction. No runtime or embedded UI bytes changed.

The corrected freeze is recorded in /tmp/oink-r7-corrected-runtime-freeze.json: the 113 runtime inputs retain 15a7de85a1ae9e6a73d8ea6570aa4f97bdd0ad5677ad7ca996fdd081ad43f7b5; the 193 broader inputs now bind 67c6d36cf91d175f208f79cdd4d337aab6d2ef71e43453b20394b677678725e8, with only the test-observer file changed from the preceding 85ad60d24c93e899020fbdcd34f8252c578253ce5afaf8652e561a432ecc8067 freeze. Corrected serial Go tests and vet passed in /tmp/oink-r7-corrected-go-gate.log. The corrected serial actual-Hugo/pinned-tool suite also passed in /tmp/oink-r7-corrected-hugo-gate.log, SHA-256 2aed822ff6fc8be04919aa74ca6ada721789232c14c1d77f1d44113bc0d235a7; the application package took 220.782 seconds. The independent post-Go source-preservation audit is /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r7-postgo-audit-qygh7bcc/receipt.json, SHA-256 5ee45fe041536243bc1229054516835c61b27909a4f296ac226b1a138e4bc8dc.

The durable corrected owning-gate receipt is /tmp/oink-r7-corrected-owning-gates.json, SHA-256 c7f94a740e33a7349886b3f3689419719f857f39031375e80ca384a3dac36e67. The prepared documentation renderer requires the completed consumer receipt to prove a private captured-source rebuild byte-identical to the final browser binary. Its source-only independent audit, oink-r7-docdriver-audit-ig_r8ud1/receipt.json, binds SHA-256 858cc48bf602fbdb26fcbda03c78ca485338b1295d64257d32cfb14b24f1ade3 and prepared driver f25d9ac4bf1a7ef43d5526b7b3cbadf84dd64ad8a57fd82d1c82e76fdb2b3435.

The first consumer driver trial, oink-r7-corpus-h1lOFl, stopped with KeyError('preview_base_path') because it indexed a field omitted when the actual preview base path is empty. Its private rebuild was byte-identical to the final browser binary 73bf90c69dce84849ee20ddfbfe825b9f2dd46f0cd37a228ce1b041a83afa33f, and all four consumer source inventories and the root inventory were preserved. The fresh oink-r7-corpus-corrected-cByXTa driver changes only those two accesses to get(..., ''), with SHA-256 4c409acacbb9b82e658e6705eddefd9a3541def5c63c340b65678a6c9a8354e4. That fresh run subsequently failed when the repository produced an inventoried file larger than 64 MiB: the preceding runtime refused all production preview. Its native check retained outcome 1, while required unavailable preview made Studio outcome 2. Starter, docs and PIG completed that run with outcome 0. All four source inventories and the root inventory were preserved.

A narrow runtime/test correction for partial preview keeps the 64 MiB limit, exposes guarded production files within the limit, returns 413 for the exact skipped oversized paths and keeps required studio.preview coverage incomplete. Native check outcome remains unchanged. Studio still returns 2 for required incomplete preview. The embedded UI is held unchanged. The bounded coverage detail records actual omitted relative paths, sizes and count. A prepared real-Hugo browser fixture adds static/oversized.bin at 64 MiB plus one byte to exercise an available guarded HTML preview, an exact skipped-file 413, native outcome 1 and required partial-view outcome 2.

The new partial-preview freeze is /tmp/oink-r7-partial-preview-runtime-freeze.json, SHA-256 c426ce3e641ed7b39bb711a26006306cab22e761c5062f2164f10deb4bea8765. Its 113 runtime inputs bind 4900ae05abbdf4409b0be54f276fb4135269cf0a49e9071013ccf42544d35c84; 193 broader inputs bind 8b172cef2b228e2642f0139d6cc569136e86843f818e52e412fa4a2d56add25d. Only internal/studio/preview.go changed among runtime inputs. The broader changes also include its test and scripts/test-studio.mjs. All three UI files retain their exact bytes and modes. Focused core final race passed in 1.748 seconds, with vet and scoped whitespace checks also passing. Its receipt, oink-r7-partial-preview-owning-a56dunn4/receipt.json, binds SHA-256 7b6ecb491f283d04fe54347e564dba426b1a84d152040a1d945af54bc67756ac. In the initial sparse-fixture mode trial, host umask 0077 made a requested 0640 file actually 0600; explicit fixture chmod to 0640 corrected the setup. Focused proof covers normal 200, oversized GET/HEAD 413, changed identity 409, private path 404 and refusal for other unknown output errors.

Whole serial Go tests for the new partial-preview freeze then passed in 61.481 seconds, and vet passed in 0.571 seconds. Completed logs are /tmp/oink-r7-partial-preview-go-gate.log, SHA-256 be7d6eccf99a6f4c1b8f09d1fb782455c7cbd3bad4a2f37e2f0e9da916bcb313, and /tmp/oink-r7-partial-preview-vet-gate.log, the empty SHA-256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. The independent held-input audit oink-r7-partial-held-audit-o6p98i1l/receipt.json, SHA-256 e567efc5f580db9395afab8ad36c4db842c3db95db442eb1cb1c740dbd43ec31, verified all 113/193 inputs and physical/logical identities during that parent Go/vet run. The new whole actual-Hugo/pinned-tool invocation subsequently completed with exit 1 after 285.649 seconds. Its sole failure was the parent’s unavailable Markdownlint preparation path /md/node_modules. All other actual cases passed. The log is /tmp/oink-r7-partial-preview-hugo-gate.log, SHA-256 1ab6b8cfd399d484e08a1d1f05d25475754caa731991dd1eec1cca03cf6ce970. The sole owning case was rerun with the exact provisioned /markdownlint/node_modules executable. Source/runtime did not change. The case passed: application package 2.317 seconds, wall 3.265 seconds. Its receipt is /tmp/oink-r7-partial-preview-corrected-tools-gate.json, SHA-256 62b75e563e8074995ed9dd354434e653b2f5f2c6d20767226286d0c08d4c667c; log SHA-256 is e9bddac210654d219d9c5d6ebabaa3b91a0f5f4de4daf228b3ae21aeaac7673a. The executable comes from provision receipt 268e601e81bc03a263296d57257b85635371bda642d0632532a7d9318c981461. The independent case-matrix/held-source audit verifies cumulative executed actual-owning-case coverage 0 from the whole invocation plus that corrected case. Its receipt, oink-r7-partial-case-matrix-audit-16_bl9hr/receipt.json, binds SHA-256 0a9e4a1a1e1a08f597becb2f27e743c9f23df672c713c2757241704edb16b51e. All 113/193 physical/logical inputs remain frozen. Optional TestArtifactCorpus and TestPublishedRuleSourceProvenance cases were explicitly skipped.

The new post-Go input audit, oink-r7-partial-postgo-audit-g1hcqdtl/receipt.json, SHA-256 b369737ec48456f673c850ea702cb3cb7efffb8ecc129d87e00dc03af82b2e3b, then confirmed the complete held 113/193 physical/logical inputs after Go/vet.

The new partial-preview browser passed against exact binary f39d6754f7ad13599e4e849394e0f470b2c6f26edf96ce40f199d27b65a8030e, version 0.4.0-r7-local, 16,000,578 bytes and mode 0700. Its summary is /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-studio-browser-cE9be3/summary.json, SHA-256 9099e6c407fd0f9de3c29ce80e03f034a4223d7d7a7c1f1378052e8b9084e0ae; provenance SHA-256 is 85b9f537fb09eecbb09d133b53a297c78184c11200ab0938734c0d10f3449095. The private oink-r7-browser-partial-ZZIqzZ/source-binding.build.json, SHA-256 7a89ab8318c3a38455ab6ce12bcdbc53ae5ce0674fb5aaaa1df0fcf68a093399, binds all 113 runtime and 193 broader source inputs plus physical identities before capture/build/after to the new freeze; root inputs stayed unchanged. All 14 axe checks had zero violations and all 14 screenshots passed, retaining the keyboard/mobile/light-dark/source/clipboard/security checks described above. Actual Hugo emitted oversized.bin at 67,108,865 bytes. A request through its rendered /sub/oversized.bin link returned 413. Ordinary actual HTML returned 200. Required partial preview coverage stayed visible. The CLI/API/UI matched 228 typed native diagnostics and native coverage/outcome 1. Studio and the subsequent refresh returned 2. Source preservation still excludes only the declared task-fixture external edit.

Another prepared corpus driver had assumed that an available normal preview always appends a studio.preview coverage row. Actual normal Starter/docs/PIG Overviews do not emit that row, so the driver assumption was corrected without changing native coverage. The repaired fresh oink-r7-corpus-partial-pZLwY0 driver, SHA-256 47778df62505beeb7432985be927f1b001e03824e9dee3a6dbed9d9b2dbe049c, was reviewed against those three retained actual Overviews and the current partial browser capture. Normal availability still requires its actual preview URL and independent HTML 200. A partial capture retains its actual required row, omitted count/identities and 413. The preparation audit is oink-r7-partial-driver-correction-audit-sa98cm6k/receipt.json, SHA-256 a519a5bc6ae83438146ff4710d53f5edb0e656a05d0532c02123e5771416f07e.

The fresh four-consumer qualification completed with driver outcome 0 in 234.6425 seconds. Its current summary is /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r7-corpus-partial-pZLwY0/summary.json, SHA-256 d7b5a4f1607b6f75ae6a596c19cbab28685fb67dac750096173060ed097c8bf5; log /tmp/oink-r7-partial-corpus-gate.log binds SHA-256 a7b724500569bd594d8e01502ec1956eb089cc9153b04b693992a9322013c811. The durable current corpus qualification.receipt.json binds SHA-256 4e5df7c3fda9f0b763091af3e6cb85c68c736c319a1de68030b81d5cd5b384bc; primary source inventories contain 97/421/858/2,294 files respectively. The private captured-source rebuild is byte-identical to the new browser binary f39d6754f7ad13599e4e849394e0f470b2c6f26edf96ce40f199d27b65a8030e. Runtime 113/broader 193 inputs and root physical identities stayed frozen. Every operation and the overall boundary preserve all four consumer bytes, full modes/types, logical/mutable Git, ignored copied inputs and directories.

Consumer Native outcome Typed diagnostics Studio outcome Actual captured pages
Starter 0 28 review-info records 0 66
docs 0 144 review-info records 0 343
PIG 0 120 review-info records 0 248
repo 1 10,462 existing duplicate-ID findings plus 788 review-info records 2 1,576

Nested native headers, typed diagnostics, coverage and exit match direct CLI checks exactly for all four sites. All issues were paginated. Other views used bounded samples. Captured physical source and translation diff were available on all four. The first three actual production previews returned HTML 200 and emit no studio.preview omission row. The repository normal HTML returned 200 with 60,100 bytes. Its current capture exposes four oversized print paths, where each actual HEAD returned 413 with zero response-body bytes:

Captured omitted relative path Captured byte size
_print/pkg/index.html 73,976,221
_print/pkg/pgsql/index.html 69,903,999
zh/_print/pkg/index.html 73,086,240
zh/_print/pkg/pgsql/index.html 69,052,754

Required studio.preview remains incomplete, so repository Studio 2 retains native 1. Actual analysis-only draft routes returned production 404 in docs and repo (Starter/PIG had no unique captured draft route). Workspace subset/full/healthy-subset sessions selected only registered sites and closed listeners without captures. Unknown or selected missing sites returned 2 before startup. Independent final corpus audit oink-r7-final-corpus-audit-xr3_u5dt/receipt.json, SHA-256 b8a8eedf5c899fe5830bdde959783c46b3f191ab144c0d3798077555d55238fc, verifies raw typed native/API/shutdown parity, all 132 operation preservation comparisons and four overall guards.

For temporary disk capacity, the parent retired three private Go build caches totaling 366,184,826 bytes, as recorded in /tmp/oink-r7-private-cache-retirement.json.

Remaining stage gates and promotion boundary

Required gate Current status
Frozen runtime input/binary identity New partial-preview freeze binds 113 runtime inputs 4900ae05abbdf4409b0be54f276fb4135269cf0a49e9071013ccf42544d35c84 and 193 broader inputs 8b172cef2b228e2642f0139d6cc569136e86843f818e52e412fa4a2d56add25d; all UI bytes/modes unchanged. Source-bound browser binary f39d6754f7ad13599e4e849394e0f470b2c6f26edf96ce40f199d27b65a8030e passed; fresh private consumer rebuild is byte-identical
Full Go/vet and actual Hugo New whole serial Go/vet and browser passed. New actual-Hugo/pinned-tool whole invocation remains exit 1 for a preparation path; sole corrected owning case passed 0, yielding independently verified cumulative executed actual-case coverage 0; two optional cases explicitly skipped
Four consumers Completed exact-binary CLI/API qualification; native 0/0/0/1, Studio 0/0/0/2, exact nested native parity and source byte/full-mode/type/Git/ignored-input/directory guards; repository partial preview remains required incomplete
Canonical paired sources/render First guarded TEN promotion and scoped actual render passed
Stage decision R7/A16 supported local scope accepted

The documentation installer retains the actual captured old inode outside canonical source storage on both success and recovery, without unlinking its last name after an earlier target identity check.

The completed first-promotion rendered gate is /private/var/folders/df/bfm8q07d7bv3kpjf1fjchq4m0000gn/T/oink-r7-docs-render-n8tw2tbw/summary.json. Its SHA-256 is 35e79f51d39803b3e4cdf134ed277957dd627acba42e0e0dc785e4745ec3c481. The log SHA-256 is de3a07eac661c15805070e0ed2e364a71ebbd38e15d8907aab3bdf716e95131d. The run took 63.33 seconds. Exact qualified binary f39d6754f7ad13599e4e849394e0f470b2c6f26edf96ce40f199d27b65a8030e passed production CLI links with one strict Hugo build. Ordinary production Hugo without a probe passed rendered Markdown (214 pages/44,075 text nodes) and links (345 pages/48,482 internal links/4,303 fragments). Its translation owner retained exit 1 only for the existing nonpublished release 1.2.0 draft. Independent draft/future/expired analysis passed Markdown (216 pages/44,381 nodes), links (347 pages/48,858 internal links/4,331 fragments) and translations (137 pairs/1,129 headings). Source translation/style/whitespace checks passed and CLI/docs schema 7468c2d04cde8a368ce0ba44a1f27125b5fca364b6d4672353519b9545b3bdda remained identical. All twelve operations, schema and overall guards preserved 421 primary files, 427 copied inputs, 109 directories, 36 mutable Git files and 113 runtime inputs.

The first guarded promotion receipt is oink-r7-root-promotion-p9g1u7pz/summary.json, SHA-256 323a5ce267e39aaf8f97dc4a12cccbdde83730155a199efb5f3815e29b334e4a, verifying that actual original inodes are retained outside canonical source.

R8 accepted reviewed editing evidence

R8/A17 supported editing scope is accepted locally after the corrected frozen owning/browser/corpus and guarded canonical rendered gates recorded below. CLI edit text, field, snippet and attachment preview the same bound oink.edit/v1 intent. Explicit studio --edit uses this intent. Saved-plan apply or explicit acknowledged Editor Apply owns selected source writes. The default Studio session remains read-only.

Candidate scope and preservation

Known site-owned UTF-8 Markdown is bounded to 1 MiB. Full text and supported ordinary top-level YAML scalar forms retain the declared BOM/line-ending and source-span preservation boundaries. Unsupported form shapes remain text. Exact value_json numeric tokens avoid browser Number rounding. Scalar forms limit numeric literals to 4,096 bytes and absolute decimal exponent 10,000. Larger/nonfinite constructs remain manual text. Field JSON is at most 1 MiB. Escaped lone surrogates are refused. Valid Unicode pairs are supported. Catalog components use original UTF-8 body byte offsets. Attachments require actual leaf-bundle identity, at most 4 MiB and an exclusive new clean basename. Source hashes, full modes, all site/external inputs, regenerated intent and fresh actual Hugo validation bind the same shared guarded application path.

The Editor displays the complete UTF-8 review, selected-file base/after identity and native candidate result. The review is capped at 2 MiB. Its literal bytes are hash-checked before acknowledgement. The actual selected candidate HTML is draft/future/expired analysis, visibly nonpublishable and separate from the original production preview. Required candidate-view incompletion may raise the proposal/session to 2 without changing native findings. For page-file edits, the selected candidate source hash/full mode matches its reviewed After state. Attachment/no-op proposals retain the selected page’s reviewed Base state. Stale/replayed plans, attachment collisions and untrusted preview requests are refused. Applied-with-refresh-error remains explicitly applied.

Focused preparation receipts

Candidate evidence Current observation and boundary
Pure editing core Owner’s focused exact-numeric tests passed for 18446744073709551615 and 7.12345678901234567890123456789, exact no-op raw bytes and changed final decimal digit; broader frozen receipt pending
Actual DFE output ownership Live ordinary output is copied through a confined os.Root, exclusive target files and guarded streaming reads; files over 64 MiB can be captured while serving limits remain unchanged
Copy cancellation/race Context-aware helper focused 0 in 0.703 s, race 0 in 1.856 s, vet/whitespace 0; actual first-chunk cancellation retains partial output, source bytes/modes/identity unchanged; source FIFO replacement cannot block before descriptor proof
Helper log identities Focused c227a88210ab0dc46b24eaff50a347d5c494e9ce23f5bdef5d5b822efab4976f; race 13f0616d55fd4df791ecded0712a18096392c88cb9b849383414c305e50b6779; vet is empty SHA-256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Read-only candidate integration review Selected actual HTML URI/base prefix and inventory, retained private DFE lifetime, source SHA/full-mode equality, native versus view coverage and cancellation reviewed; no new material defect found within this code review scope
First Editor browser trial Harness stopped on ambiguous global Open editor selector; retained as failed trial
Corrected-selector Editor trial Desktop field/component/binary apply checks and axe checks passed before 320 px draft-review horizontal overflow failed; retained original trial
Narrow layout correction Editor review hashes/receipt text wrap and intrinsic widths are bounded; prior CSS and failure evidence retained separately. Development rerun passed 12 axe checks/screenshots, including real 320 px dark review and light receipts/refusals; final frozen-source/binary rerun pending

The preceding preparation rows were captured before the first complete R8 freeze. The first whole frozen gates later passed for binary 84b804d3246a5be581e44884ed910fa3f45d8be29734b8babdeeb763a11fa882 (0.5.0-r8-local), runtime 123/58517b8e98b80df6642be4ee6275a0074ec187768b20e009f41da2607b635d46 and broader 212/d81335c78413acc60e27adee0ac794862285e41cb2a3a7687ec820c1065ba003. The whole-gate summary is b49f4a3272af3e3dcc92e7e9b38d4289bb49cb19aa3e9354ea148d2d8cb2cea8: Go tests 0/56.523 s, vet 0/0.904 s, whole actual Hugo plus all three pinned tools 0/320.044 s, core race 0/16.610 s and public R8/helper race 0/48.319 s. Its source guards passed.

The first frozen browser receipt eb6977235ef9ae6cec28651b5654eb101685af67abe0cbed0acb0f8375458d9c binds that same binary and source freeze. Editor had 12 axe runs with zero violations and 12 screenshots. Retained read-only Studio had 14/zero/14. The actual form preserved the literal 1e400, its planned after hash and diff. It then discarded the proposal. Exponent ±10,001 and a 4,097-byte numeric literal were refused locally without an API request. Four acknowledged field/component/binary/draft applications occurred only in disposable fixtures. Default read-only refusal, stale preservation, no-op source bytes, preview isolation and native-result independence passed.

The first exact-binary consumer trial then stopped at Starter after 37.533 s. Its immutable failed-trial receipt is 853a8397ba4c527c03aa3cc9ac7cacc41c0ab0379549d145b874f1d1ecc3901c. Two failures are recorded separately. A driver event hash depended on JSON object-key order. Recursive comparison nevertheless proved API/CLI arrays equal: 28 diagnostics, 29 coverage rows and native exits 0/0. Separately, repeated resolved cache capture produced a genuine duplicate module input, .gitattributes. Required candidate graph capture became incomplete. Mutation outcome was 2, and native check remained 0. There was no actual selected DFE HTML or applicable lease. The public published-cache fixture reproduced that defect; its retained failing log is 50ab704e715665096e0f36391bb1364841c3a2b2ac88dd262915ce53fb66afa6. All 48 recorded per-operation source proofs and four overall consumer guards preserved bytes, full modes, types, Git, ignored copied inputs and directories; root inputs stayed exact. No Apply or saved plan was performed.

The narrow runtime correction gathers complete resolved-module rows before committing additions. Identical repeated or reordered captures retain the original inventory. Changed hashes/full modes and added or removed paths within an existing scope are refused. Missing scopes, conflicting identities and capture errors are also refused. Refusal does not refresh prior evidence or append partial additions. Non-module rows remain exact. Site owning receipt e7b284b9dece1c5f2b696cd76166d2286fcbec69e6c48912ab9a78204bdb980b records focused 0/0.746 s, site 0/2.123 s, focused race 0/1.958 s and vet 0/0.167 s.

The corrected published-cache receipt 6358dc81f06e34b789cb47a0f4442d6d036d2e33423a06f5dbe85b3064766da6 records actual github.com/pgsty/[email protected] from a task-local copied checksummed archive, with no downloads or replacement. Original and candidate graphs each have 1,256 unique inputs, including 1,198 module inputs. Full API/CLI typed diagnostics, coverage, exits and plan identity match. Actual selected DFE HTML returns 200. Source bytes/full modes/Git are unchanged. No Apply or saved plan occurred. Owning race passed in 30.255 s; vet passed. Its corrected race log is c7d21a30b8af141d9d9604a80ddf9cf3f608320b97a441a06a742376e2119551.

The current complete corrected freeze is fb276500a3d2643bd0aa220f8bebb380fce2c98493d62b0502b6497b2f02949f, runtime 123/cdf629eeb4bbef6d4d88ee27fe3fb0a73b07b6bf6438336e033a18fb7feb1c17 and broader 212/f6e305e792733a550814eb841615d12fa14a9a6bb2a97c4ada85f7275183e579. Only source_inputs.go, its owning test and the public published-cache test differ from the first freeze. Held UI/helper bytes and all full modes remain unchanged. The rebuilt candidate is bd25f9e0b35ec10e227aabf9582ae40b0b367390f64b93668de6ae85222c3d71 (0.5.0-r8-local). Its observed corrected source-bound browser receipt 33a698985a55c14c3e64e981da1f8e74c686497083dc0edb241406f185e3eeb8 again reports Editor 12/zero/12 and read-only 14/zero/14 with complete 123/212 pre/post source preservation.

At the next evidence observation, the corrected whole gates completed for that held bd25f9e0…22c3d71 binary and fb276500…02949f freeze. Summary 208f156c0954e803eccbada678a4689683dc1576c543c379e5cc04b3497ef772 records Go tests 0/57.826 s, vet 0/0.521 s, whole actual Hugo plus all three pinned tools 0/378.847 s, core/site/Studio race 0/17.937 s and public R8/attachment/output-helper race 0/85.159 s. Every gate’s source pre/post guard passed. The actual-Hugo log has 434 top-level passes and zero failures. The two optional external fixtures TestArtifactCorpus and TestPublishedRuleSourceProvenance remained explicitly skipped.

The corrected whole actual-Hugo log is 4eb1a2afdce2adbe570b10922fd53b6d8954f7c95747370c3c661e94d2f71a05; Go log ea59463e9649ffe2f8aff9da66c91cf6895c86fde96a524db23c89cd4eb35925, core race cdd3d761b5ca7b5e986b25aee3129d65663e3e5ebb83eecb6fbb080387298a58 and public race bb610bdcd7a299cb9b66f4c69e30e246c20546efae47653c01d350b1026ea2de.

The 434 passes and two optional skips above are top-level counts. The same whole invocation also skipped the nested Unix-socket refusal fixture because the Darwin temporary pathname exceeded the socket limit. A first shorter private-path trial still skipped. Receipt 93106854ca890b497d3c74522b895f597ac60cec55ced42cad7b187d334da200 retains process exit 0 and records no actual socket execution.

A subsequent nonresolved short private TMPDIR executed the same frozen socket fixture under race detection without a skip: 0/2.954 s. Receipt 531a503b3b91e1b423c2be61b92ed806d3a813738c38d57e5ec122577b4337f9 and log 236c84f1842ffce76174c834f3888718a109cec6377ada6f3242b02f551f00b3 bind fb276500…02949f and all 123/212 logical/physical/Git inputs unchanged before/after.

The corrected four-consumer trial subsequently completed in 845.705 s. Summary af4fc53326163c4a03aa2982c1f01363fbbdd5a447c9baed3639bd8599d46370 and qualification receipt 4d6fd02543c1920497e1a1bb0a68fcf89b0546130fd9cc12c1df391b7e673e75 bind a byte-identical private rebuild of bd25f9e0…22c3d71, the complete held 123/cdf629ee…feb1c17 runtime and 212/f6e305e7…5183e579 inputs. All 2,383 entries of the first failed trial retained their exact bytes/full modes/types.

Corrected consumer Native/current and native candidate exit API candidate outcome Full diagnostics/coverage Actual selected analysis HTML
Starter 0 / 0 0 28 / 29 200, 48,149 bytes, /blog/design/content-model/
Documentation 0 / 0 0 144 / 34 200, 61,738 bytes, /blog/oink/immersive-reading/
PIG 0 / 0 0 120 / 41 200, 55,641 bytes, /404/
Repository 1 / 1 2 11,250 / 29 200, 92,352 bytes, /blog/infra/2020-12/

These are actual selected Hugo HTML routes in the separate, visibly nonpublishable draft/future/expired candidate view. Each expected candidate marker was present. API and CLI matched full typed diagnostics/coverage, native exits, and plan ID. They also matched Base/After hashes and full modes, unified diff, and the selected page’s proposed source. The complete literal API review and its hash were independently validated. No consumer Apply or saved plan occurred, and no listeners remained. The repository retained 10,462 existing duplicate-ID findings and 788 information records. Its four actual PRINT outputs remained required partial-preview incompletion: _print/pkg/index.html 73,976,221 bytes, _print/pkg/pgsql/index.html 69,903,999 bytes, zh/_print/pkg/index.html 73,086,240 bytes and zh/_print/pkg/pgsql/index.html 69,052,754 bytes. Each bounded HEAD request returned 413 with zero body. Selected in-limit HTML remained 200. Native 1 remained unchanged. Proposal/session 2 and Apply refusal stayed visible. The other three complete previews had no invented explicit complete-coverage row. Actual guarded HTML 200 supplied the evidence.

Exactly 53 protected operations each checked all four sources: 212 per-operation source proofs plus four overall proofs, with source bytes/full modes/types, copied ignored inputs, directories and logical/mutable Git unchanged. Each source proof compared four inventory categories, yielding 864 raw inventory pairs including the overall comparisons. All 53 root guards and the final complete 123/212 logical/physical inputs also matched. All issues and pages were paginated. The other five view endpoints were sampled to their first 50 records. Each site included one known source and one bounded diff. Full native/CLI record parity used the declared bounded complete-record codec. Object order was canonicalized. Array order, types, null and field presence remained significant.

Independent audit receipt 6b72ca06d8392a5271fc40757f176f26e1144c21eec93dbd035e0a1bd645657b verified 69 artifact hashes, complete bounded API/spool/shutdown typed records and the large native/CLI raw-file digest bindings. Its additive receipt 335f137663d4ec0b2a0d3e49c8d70b9918f86078ab6264855171a2644d8aa6c6 also verified the fresh current root’s complete logical Git inventory against the freeze. The original audit stayed immutable.

The reviewed first TEN promotion subsequently passed through the guarded retained-inode installer, root receipt 7cd9b4604d2340b9e46965a26281c921b967060909d518b8b4b31e5f42d0120c. Its actual original source inodes were retained outside the documentation site. Unselected source/copied inputs, directories and Git, and complete CLI 123/212 logical/physical inputs stayed unchanged.

The canonical render then completed in 67.21 s, summary bb0d0710294f810fb14284f7b5b0329befbd290b66c21397b9a45e8382287fb6, qualification receipt 32d3ffeca43bc9ad4615edcca0d3cc47cc932bbc93c6576724c800e0a62405b1. It used the exact qualified bd25f9e0…22c3d71 binary and corrected 123/212 freeze. Actual CLI production links passed 0 with one strict Hugo build. Independent ordinary probe-free production Hugo/Markdown/links passed: 214 Markdown pages/44,691 nodes and 345 link pages/48,532 internal references/4,351 fragments. Its translation owner retained 1 solely for the existing release/1.2.0 draft absent from production. Separate explicitly nonpublishable draft/future/expired Hugo analysis passed Markdown (216 pages/44,997 nodes), links (347 pages/48,908 references/4,379 fragments) and all translations 0.

Source translations passed 137/137 pairs and 1,143 headings. Chinese style passed 137 files/181 strong spans/zero emphasis. Whitespace passed. Schema SHA-256 7468c2d04cde8a368ce0ba44a1f27125b5fca364b6d4672353519b9545b3bdda matched exactly. All 12 commands, schema and overall guards preserved 421 primary source files, 427 copied inputs, 109 directories and 36 mutable Git files, plus all 123 runtime/212 broader CLI logical/physical inputs. The qualification receipt binds 60 canonical inventory pairs, 15 CLI guard pairs and six private-copy source pairs.

R8/A17 supported local editing scope is accepted after corrected owning, socket, source-bound browser, exact-binary four-consumer preservation and these guarded canonical gates. R1–R8 are accepted locally. Native repository findings and required partial-preview 2/Apply refusal remain visible. The separate platform-authority audit 762571dab9a07651ac8e4c71764bfef292f8d5eba729a089e72d9d755b7e2d7c confirms that the initial contract qualifies actually exercised architectures: macOS arm64, native Linux arm64 and emulated Linux amd64. Darwin amd64 remains an experimental archive with failed actual execution/unverified runtime.

Required gate matrix

Required gate Current status
Final immutable runtime/source freeze and exact CLI binary Corrected complete 123/212 freeze and bd25f9e0…22c3d71 bind completed owning/browser/corpus/canonical scope; first-freeze trials separate
Public CLI/JSON/exit, stale source/config/external-input and guarded writer tests Corrected public R8/attachment/output-helper race, whole Go/vet/actual Hugo and canonical stage gates passed
Frozen whole Go/race/vet and actual Hugo/ordinary Hugo after selected application Corrected whole Go/vet/actual Hugo and core/public race passed; 434 top-level passes, zero failures, two optional external fixture skips explicit; first trials remain separate
Editor browser five-view parity, text/forms/components/binary attachments, exact numeric/no-op, stale rejection and preview isolation Corrected source-bound Editor 12 zero-violation axe runs/12 screenshots and read-only 14/zero/14 passed; bound completed corpus and canonical acceptance
Exact-binary four-consumer read-only qualification Corrected all-four completed in 845.705 s; full typed native/API/CLI candidate parity, 212 per-operation source proofs plus four overall, no Apply/save/source writes; first failed trial preserved
Protected canonical TEN promotion, EN/ZH source/schema/style/whitespace and actual production/analysis render Guarded first promotion and independent exact-binary 67.21 s render passed; only known draft translation omission in production; rendered and status bytes separately bound
R8/A17 stage decision Supported local scope accepted after corrected whole owning/browser/corpus/canonical gates; R1–R8 accepted locally
Final A18/platform/archive delivery Completed in the 2026-10-04 supplement below

Prior R1–R7 sections, whole-invocation failures and scoped acceptance receipts remain unchanged. Temporary qualification files stay outside canonical content and Git.

Current runtime completion supplement on 2026-10-04

This supplement records the current candidate on 2026-10-04 (Asia/Shanghai). The dated page URL and all initial 2026-10-03/R1–R7 records remain unchanged. The three UI files retain the browser-qualified bytes and full modes. Current Go, Hugo, platform, archive and four-consumer checks refresh the changed backend.

The first current ARM offline unit run exposed an output-copy integrity gap. On ext4, adding a directory entry could preserve the parent’s allocation size and observed timestamp. That failed run stopped before later qualification steps. The correction captures and rechecks actual sorted directory membership and entry identity. It also checks regular-file bytes and full modes. Only internal/app/studio_output.go and its owning test changed. The failed receipt and independent audit are retained.

The preceding integrity-correction qualification freeze is 683daca0e522193c7ff1b0de6ac2fee5d2fca080811bf184a8dfd5b90a33f224: 123 runtime inputs hash to d346ad15cd4239004e32e1b9f30d727eaf156be0187dc165ca874032a7cf962a, and 212 complete CLI inputs hash to 2abd1a044d8192b07f9bbc06b55dc8b4544d66ca17b8867971cec702ba3af088. The 0.5.0-r8-local Darwin arm64 candidate is 74ad94e73557f6538cd64edd1766d6df92c596d98411031159d94af072c186ec. The integrity-correction receipts below bind that source scope. The later one-test fixture amendment has a separate complete source identity and completed formal qualification boundary. Both are recorded below.

The current complete source freeze is now 196245a3ba09305e34b86539c8eb79f1473e4373ee47aa1f56f8933b04a42d43. The 123 runtime inputs remain d346ad15cd4239004e32e1b9f30d727eaf156be0187dc165ca874032a7cf962a. The 212 complete CLI inputs are 2c487bfb4c65ed40ff78356b2860de627e6ac1afa0da2df433b09345dab7f5b0. Only the owning published-cache test changed, to source 54c10ef89310256b5f4c165c7de5de9668e1d4d2991b71751076680141dbe779. The fixture amendment is separately guarded. Production bytes and all semantic assertions remain unchanged. Formal qualification of this complete source passed. It includes the current eight owning gates, reproduced archives, and full plain-Go AMD/ARM runs. Root A18 proof 2c018cb2afa3f26699a9e6b5a0971096246b12405fde5a27e43a9e213e46da60 binds all three declared supported targets and five reproduced archives.

Current proof and preserved earlier input boundary Observed result and bound receipt
New complete-source formal qualification Freeze 196245a3ba09305e34b86539c8eb79f1473e4373ee47aa1f56f8933b04a42d43, owning test 54c10ef89310256b5f4c165c7de5de9668e1d4d2991b71751076680141dbe779, unchanged runtime123. Current eight gates, host/archive and both full plain-Go Linux flows passed, bound by root A18 proof 2c018cb2afa3f26699a9e6b5a0971096246b12405fde5a27e43a9e213e46da60
Narrow integrity correction Owning receipt 6966d768025497b45958073d4c53a2a2981065c8a95857834dcf6a4faa4f0201. Independent audit 6cb5d2eabf57b41079026a38a674f46def9f56a15df17ad27e671e4f765798df
Prior-source six frozen owning gates Build, full offline Go unit/vet, whole actual Hugo/pinned tools, core race and public R8/output-helper race all 0. Elapsed 3.501/68.257/3.909/333.801/37.070/79.670 seconds. Summary b40b7787b3da8dc1e0763812b6dde529b4b5b69fe479d79940f1161223124e1d. Independent audit 20780662b7ff35019b2c8c84e6dc763f9351ae0816f6ef7a7789f7a15be99167
Eight current frozen owning gates Selected published-cache actual Hugo and race, build, full offline unit/vet, whole actual Hugo/pinned tools, core race and public R8/output-helper race all 0. Current summary d6272fcc4dfab114aecfcdf19a7e2b78f1e931817331b460f43ff2056bf754a4. Raw whole Hugo has 435 top-level passes, no failures, two optional top-level skips and the explicit long-path socket child skip. Runtime/binary bytes remain identical
Prior-source Darwin arm64 and archives Current extracted candidate runs outside the checkout with no consumer Node requirement. Seventeen commands and eight actual process tests, including child signals, passed without process-test skips. Two fresh release directories contain byte-identical five archives and checksums. Source/license/provenance/canonical tar checks pass. Summary bcb4d7599e965c1b3cfe7fe698ca14061ad53d45e7a194337aeebb8d37aa77c1. Independent audit 60a04771365d8be15ac91fbbd8d485b019aae081598e468018861ca5734e387c
Current Darwin arm64 and deterministic archives Seventeen extracted-archive/ordinary-Hugo/process commands passed expected exits, with missing Hugo explicitly 2. Eight actual signal/process cases ran without skips. Two independent fresh builds reproduced five byte-identical archives from current complete source. Summary 3890fd8468b6bce5271bb32ffa1a18bd5daf99c19c43becac0be8e3b908a5d57. Source, tools, module-cache and smoke-source guards remained equal
Prior-source Linux arm64 Actual nonroot Linux arm64 on ext4 with Go 1.27.1, Hugo Extended 0.166.0 and Git 2.47.3: full offline Go unit/vet, all 13 required pure top-level pass records and the membership case plus its four children without skips, 10 selected actual-Hugo cases without skips, native rebuilt archive identity, installed bilingual/offline/ordinary-Hugo/missing-Hugo 2 JSON and signal/source-mode checks passed. Guest summary 409990bc1425f4bf219f8911a71581af6e68729865580121dbeb6d85a06d2ea7. Outer receipt a022e40f068703cd59ce6d6a7fb6530cce6907681baa26eb1dfc77c09f0c8898. Exported-record audit 24afc50f6f860394d1ebfa7a8b754ddd9cb97f9e88a0dcfcbcb659193ecbfe5f
Current Linux arm64 Current nonroot Linux arm64 ran on ext4, with native ARM through QEMU HVF and Go1.27.1/HugoExtended0.166.0/Git2.47.3. Full offline unit/vet passed with 370 top-level passes. The following cases passed without skips: all13 decisive pure cases and4 membership children, and 10 selected actual-Hugo cases. Exact native/installed current archive identity and bilingual/offline/ordinary-Hugo/signal flows passed. 24 commands reached expected exits, including missingHugo2. Guest 268102f69c0950f9d2994d22cd2fd290fc11e24bd6d6f916fd70a93ca4946c74. Outer f3c066fdc9b97feff92160346185a1af978a5172eed5c81904ac7c0e5fc6c982. Source/SDK/borrowed/old-task guards were equal. The owned VM was reaped. Default optional unit skips retain their named gating reasons
Prior-source Linux amd64 failed trial Preserved current TCG trial failed. Outer receipt 3543664ba5590f2ba5a8f676b196bb636b72bc819913289f415d0a8a841c1bdb, guest summary 16075204d287713c7f7650c0a65dd289dd4bd83db07c9ba4b85b3f21244d5240. Full offline units (370 top-level passes), vet and the first three selected Hugo cases passed. The published-cache candidate request reached the test HTTP client’s 90-second deadline. Candidate parity, the remaining six selected Hugo cases, native rebuilt archive and installed archive smokes were not reached. Deadline review 12917b9eb89e3abc5893e08da3b6b6e20743dcb4c14e6f7ba8561628e3566934
Current Linux amd64 Current nonroot Linux amd64 ran on ext4 under QEMU TCG emulation, with Go1.27.1/HugoExtended0.166.0/Git2.47.3. Full offline unit/vet passed with 370 top-level passes. The following cases passed without skips: all13 decisive pure cases and4 membership children, and 10 selected actual-Hugo cases. Exact native/installed current archive identity and bilingual/offline/ordinary-Hugo/signal flows passed. 49 commands reached expected exits, including missingHugo2. Guest 3a1a32979efc843de8b95b7c13824026e17f71c06d4c458b738c0b9583fb4723. Outer 30cf4950cc83fa0732047d9a0f89bb59e68779ee2e8f5c755724c9679be265e3. Source/SDK/borrowed/old-task guards equal and owned VM reaped. Default optional unit skips retain their named gating reasons
Runtime-equivalent preceding four-consumer candidate corpus Source epoch 683daca0…33f224. Runtime123/binary74ad is byte-identical to current 196245a3…42d43. The corpus was not rerun after the test-only amendment. 853.249 seconds. Native/candidate-native 0/0/0/1, API/view 0/0/0/2. Diagnostics 28/144/120/11250, coverage 29/34/41/29. Summary a1e98ca3e10095a1134381666bacf256f8e8827cd3900c9e811b7120de4c2974, receipt 05c4562a50d9f83ba2c99879ec841870c5e753199e41792bd5bc718cf8046e7b, independent audit 3a1b0b6e3a8c6b1a0d82c5f82b46c84b1e44d6c30bab655610cb9e86e6a30b47
Final canonical lifecycle and rendered checks The promoted TEN bytes use independent canonical rendering and navigation/URL receipts

The preceding six-gate b40b7787b3da8dc1e0763812b6dde529b4b5b69fe479d79940f1161223124e1d, host/archive bcb4d7599e965c1b3cfe7fe698ca14061ad53d45e7a194337aeebb8d37aa77c1, and ARM outer a022e40f068703cd59ce6d6a7fb6530cce6907681baa26eb1dfc77c09f0c8898 / guest 409990bc1425f4bf219f8911a71581af6e68729865580121dbeb6d85a06d2ea7 / audit 24afc50f6f860394d1ebfa7a8b754ddd9cb97f9e88a0dcfcbcb659193ecbfe5f remain retained alongside the new exact-source proof. Historical 26 axe checks/screenshots and 22 codec cases are carried with unchanged UI/codec/runtime inputs.

The initial max-CPU AMD trial remains failed: receipt 3543664ba5590f2ba5a8f676b196bb636b72bc819913289f415d0a8a841c1bdb, guest summary 16075204d287713c7f7650c0a65dd289dd4bd83db07c9ba4b85b3f21244d5240. The test client timed out after 90 seconds while awaiting candidate headers. Candidate parity and the remaining six selected Hugo cases/native rebuild/installed smokes were not reached. Guest inputs stayed exact. The host guard recorded only a .git directory timestamp change. The separate qemu64 one-test preflight also failed at the unchanged 90-second HTTP client deadline: outer receipt fc68173ccdfd8ce263ecdf082a533d9da666a4cc2e1e5e29880ee827286132ac, guest summary e350ff65feeee166ffac1d337db9bbd70d3895b1fb6d93df0a30ca4de09019fc. The named case took 177.71 seconds. The first trial took 176.64 seconds. Its inputs remained exact and its VM was reaped.

A later Go-overlay diagnostic preserved the same production source and every original semantic assertion. Outer receipt 0bc6d563b7cd9ca862717c2123ee0836d83b6b927d00204a0b031049c38e93f0 and raw-bound classification 66a1422cdb79ab9f1cf683f441ade0ce4adb4a7a666d524c4b9ed98ebee28708 record a passed named case in 352.40 seconds. Original capture took 26.254 seconds, Studio capture 26.211, candidate HTTP 94.312, direct preview 94.318 and CLI preview 81.962. Both graphs retained 1,256 unique inputs, including 1,198 module inputs. The original-capture context had expired when the HTTP result arrived. Fresh independent direct/CLI contexts completed normally. All 20,564 host guards and five guest command guard pairs stayed exact. The owned VM was cleanly reaped. The scoped owning-fixture amendment now uses a 300-second budget for that candidate request and fresh direct/CLI operations, about 3.18 times the slowest observed operation. General/original-capture 90-second limits, restoration of the shared client, shutdown 15 seconds and Go’s default ten-minute cap remain unchanged.

The consumer driver compared complete typed diagnostics, coverage, native exit, PlanID, selected Base/After/full modes, unified diff and selected source between API and CLI. It separately verified the complete literal API review and its hash. For attachments and no-ops, the selected page retains reviewed Base. Page-file edits match reviewed After. Nonissue views are bounded samples. All issues and pages are paginated. The 53 protected operations have 212 all-four per-operation source proofs plus four overall proofs (864 raw inventory pairs across four categories) and 53 root pairs. Seventy-one retained artifacts are bound. No Apply, saved plan or consumer write occurred. The completed corpus’s file-only collector needed two preserved metadata corrections for absent historical trial/self-test files. No CLI/Hugo operation was rerun.

The repository retains its 10,462 pre-existing duplicate-ID findings and 788 review-info records. Its selected actual DFE HTML is available. Four oversized actual PRINT files remain unserved (413, zero response body): _print/pkg/index.html 73,976,221 bytes, _print/pkg/pgsql/index.html 69,903,999, zh/_print/pkg/index.html 73,086,240 and zh/_print/pkg/pgsql/index.html 69,052,754. The 64 MiB per-file preview bound is unchanged. Required partial-preview incompletion remains 2. Native findings remain 1. Apply is refused.

Linux prerequisites were prepared in exclusively owned guests from signed Debian metadata. Exactly ten new packages and three approved existing-package updates were verified before and after installation. SDK/Hugo/module caches were provisioned separately and reused offline. Qualification runs as an ordinary user on ext4. Cached inputs and all 212 source files’ bytes/full modes stay guarded. Default unit skips retain their actual gating/not-applicable reasons. All required pure top-level cases, no-skip membership children, selected Hugo cases, and signal cases must execute. Linux amd64 uses QEMU TCG on the ARM host and is explicitly emulated. Darwin amd64 remains an experimental archive. Actual execution returned Bad CPU type (errno 86). Windows is outside the declared scope.

The current Linux archive digests are ac883e54a1df0b820696279c63881ba75a00d279f507330128fe8d5aff59c52e (arm64, 4,552,687 bytes) and 2dde43bf94ef35aac2111b07dcb9b2766fbf9f883fe39ccd646d14a98b94d734 (amd64, 5,034,668 bytes). The preceding 683daca0…33f224 archive digests c191383af21913be6940ec41be11755b3d985344bbc0f65cc3f5de16424a96a4 and 6531b27d889260afe804c1f49f37541fbae46e57b5d17a20178c28cb51968794 remain historical. SDK/guest preparation failures, the first ext4 membership failure, and earlier private host metadata/resources trials remain immutable evidence. Uninvoked E1–E4 extensions are separate inactive scope.

7.8.13 - Document comparison research, 2026-10-29

Dated evidence for a generic OINK side-by-side reading mode across languages and revisions.
Historical research; see the 1.5.0 decision

Research date: 2026-10-29. This bounded source review of OINK v1.4.0 informed the translation reader in OINK 1.5.0. The accepted decision and the shell contract describe the released behavior and supersede the recommendations below.

Scope and evidence

The question was how OINK could support a generic comparison reading mode for roughly 30–40 local sites: translated documents, eight-language sets, proofreading books, arbitrary languages, and old/new revisions in one language. The review covered Docusaurus, VitePress, Material for MkDocs with mike, Read the Docs, Hugo, Sefaria, MediaWiki ContentTranslation, OmegaT, Weblate, Joplin, VS Code, and CodeMirror. Evidence came from official documentation and pinned source revisions; no upstream application was started and no browser behavior was claimed from source inspection alone. Unpinned official documentation records what was read on 2026-10-29; it is not an immutable source snapshot. OmegaT evidence is limited to the 6.1.0 manual, not an inspected implementation.

Project Fixed documentation/source revision Verified relevance
Hugo v0.168.0 source commit e27a6760c5547237f87d353aebe0580feadb20f4 Page relationships and native version rotation; no paragraph alignment.
Docusaurus, VitePress, Material/mike Docusaurus v3.10.2, VitePress v1.6.4, Material 9.8.0, mike v2.2.0 Stable identity, explicit anchors, version metadata, and existence-aware navigation.
Read the Docs and VS Code RTD platform commit 9b012b1df15fc153590b4db5f4da7bd3eb8ae74a; Addons d8f99069d9a8b9647f5bd68fdedb95ffa11e63ce; VS Code d7658021df8289f549bf2af8c8dec131f2580d69 Real rendered HTML/Markdown comparison and mapped previews; neither proves a generic OINK reader.
Sefaria and ContentTranslation Sefaria af96a1a1695a6307a3d616493455a6802455be1b; ContentTranslation 8d5dde493528330efcb373dc448f352e81815115 Independent panes and strict segment rows are separate products.
Joplin, OmegaT, Weblate, CodeMirror Joplin b48e75856d81152c32e71f21075e1c043d9ea922; OmegaT Standard Manual 6.1.0; Weblate 503129cadb056e2ea0db629fe8e37907b41ff054; CodeMirror 53c8f7b213332743a1f4f7d26bf40cfe2b512b53 Logical scroll maps, manual alignment, stable message context, and geometry spacers.
DDIA consumer sample Vonng/ddia a6b2e6f85e1249720c8a349273455190d2fc9515 Four Hugo members with repeated Chinese locales and distinct editions.

Identity and relationship lessons

For the original general-purpose scope, comparison would need to keep candidate-set identity, member identity, Hugo site key, actual locale and direction, edition/version, canonical URL, and anchor correspondence separate. A language code is not a document identity. The pinned DDIA configuration provides a concrete case: zh, tw, v1, and v1_tw are four configured members that include repeated Chinese locales and different editions. Deduplicating by locale would remove valid choices and mislabel editions as translations.

Hugo 0.168.0 provides a sound starting point. .AllTranslations includes the current page and .Translations excludes it; an explicit translationKey can relate pages whose paths differ. .Rotate "version" can adapt native version dimensions while holding other dimensions, including language and role, fixed. That adapter is optional for comparison and does not require a consumer to reorganize content. It must not be confused with an OINK params.versions menu, which is a navigation configuration rather than a semantic partner resolver. Custom version or role dimensions still need an explicit adapter so ambiguous native matches are not silently accepted.

Automatic translation candidates should therefore begin with the actual Hugo page relationship, then pass an eligibility filter: published, built, renderable HTML siblings; the selected page is excluded; home pages used as language-navigation fallbacks are excluded; foreign role or version members are excluded when the set requires that boundary. A navigation link proves that a destination exists, not that it is a translation or a comparable document. Missing translations remain missing; OINK must not copy source text behind the reader’s back.

The original generic design used two selectors for an eight-language set. It does not require 28 prebuilt pair pages or eight simultaneously loaded documents. Same-language v1/v2 comparison uses two member IDs and explicit labels. The same identity model also supports a proofreading book whose chapter structures differ, without pretending that equal filenames or title text establish correspondence.

What existing tools actually compare

Docusaurus document identity, localization, and versioning separate ID, slug, locale, and version tree; its heading guidance advises stable IDs for localized pages. VitePress constructs locale links from the current relative path in the inspected language helper and supports custom anchors, but that helper does not establish a semantic partner registry. Material uses mike version metadata and target sitemaps to find a corresponding URL; that confirms addressability, not semantic equivalence. These are useful identity and validation patterns, not evidence of native two-pane scrolling.

Read the Docs supplies direct fixed comparison evidence: its visual-diff documentation and Addons docdiff.js compare a rendered HTML body, mark changed DOM regions, and preserve a URL mode flag. This is a single-page rendered diff for review workflows. VS Code’s Markdown diff documentation and diffScrollSync.ts show two rendered previews with logical mappings and feedback suppression. They demonstrate that rendered comparison and mapped scrolling are feasible, while leaving OINK’s content identity and static-host constraints unresolved.

Sefaria explicitly distinguishes an independent Compare Text view from a shared-segment bilingual view: TextRange.jsx renders both texts inside a shared segment, while TextColumn.jsx keeps comparison columns independently scrollable. MediaWiki ContentTranslation’s alignSectionPair uses stable section IDs and height/margin compensation for paired translation regions in a single editing flow. OmegaT’s Align Files manual combines automatic matching with manual split, merge, move, and acceptance operations. Weblate’s Unit model keeps message IDs and context separate from text hashes. CodeMirror’s updateSpacers shows the geometry cost of aligned rows. Together they show why automatic similarity can suggest a relationship but cannot silently publish a paragraph map for arbitrary multilingual HTML.

Three reading semantics

The design must name three different behaviors. Independent scrolling gives each pane its own position and is the fallback for every valid pair. Anchor-linked scrolling keeps two independent panes and uses shared semantic anchors to estimate corresponding progress between anchors. It is a nearby section reference, not permanent paragraph or line alignment. Strict row alignment puts actual blocks into paired rows, uses the maximum row height and spacers for the shorter side, and needs one-to-many mapping, missing-block placeholders, and an explicit order policy. It is a separate later feature.

For OINK, the proposed default depends on mapping quality: enable anchor-linked scrolling only when the conservative conditions below pass, otherwise use independent scrolling. The reader can always turn synchronization off. A pair with zero or one valid internal anchor, duplicate explicit targets, missing explicit targets, or non-monotonic moved anchors stays independently readable. Ordinary unmatched sections are content gaps, not invalid targets. A direct “jump to counterpart” action can still work where continuous synchronization cannot.

Rendering and isolation finding

OINK’s v1.4.0 Docs shell, content rendering, Book ID handling, tabs, table reader, diagrams, and capability-selected scripts make arbitrary single-DOM body concatenation a large refactor. Two same-origin document frames preserve each page’s lang, dir, resource base, renderer context, IDs, and reader ownership. A frame is a document boundary, not an independent process or a security sandbox; same-origin content remains trusted content. See the iframe reference and same-origin policy.

The initially considered path used a prototype with ordinary eligible HTML pages in two same-origin frames and guards for navigation, keyboard shortcuts, analytics, comments, and recursive compare entry, then considered an optional content-only HTML view. The revised first phase keeps ordinary pages for the implementation too; a dedicated output is only a future optimization if measurements justify it. Hugo parameters alone cannot create an output format; any auxiliary output needs explicit output plumbing, Page Store/capability handling, relative-resource rules, and SEO policy.

Scroll model and bounded conclusions

The useful engine pattern is Joplin’s logical map and VS Code’s mapped rendered previews. Joplin’s scrollmap.js and VS Code’s diffScrollSync.ts support collecting layout-valid anchors, interpolating within intervals, and clamping positions. The revised translation MVP derives a strictly monotonic mapping from shared stable heading IDs; it needs no separately authored or reviewed pair map. Crossing or reordered anchors reject that map; the engine must not silently choose an LIS, drop moved anchors, or reorder the document. Geometry is invalidated when fonts, images, diagrams, tabs, details, or width changes alter layout. A state owner and sequence token prevent programmatic scroll events from echoing back. The user should keep focus and pane ownership; ordinary scrolling must not open hidden content or move focus.

An unmatched section is an ordinary content gap when the page and its surrounding anchors are valid. If valid anchors bracket it, the reader may label the interval approximate and interpolate through it. In a future manual-mapping feature, an explicit target that does not exist, is duplicated, or crosses another explicit target is an invalid mapping; it disables continuous synchronization for the pair rather than becoming an ordinal fallback. A direct explicit jump can report the invalid target.

The evidence supports a focused proposal, not an implementation claim. The upstream source investigation did not run the candidate comparison feature, a server, a browser, accessibility checks, or an eight-language consumer. It also did not prove that every framework lacks plugins or experimental comparison modes. Those limits are carried into the proposal’s acceptance matrix and keep the recommendation honest: first prove normal Docs/Book reading with two panes, then decide whether richer output or strict row comparison earns its maintenance cost.

Phase-one scope revision

Following the request to simplify on 2026-10-29, the companion proposal now assumes existing translation siblings with matching heading IDs. Its only site setting is params.ui.compare: true. Hugo supplies the existing translation links; the browser chooses one target and displays two ordinary pages in same-origin frames, with an on/off synchronization switch. The current language is fixed and only the target needs a selector.

The broader identity discussion above remains research for possible version comparison. It does not require phase-one compare sets, member IDs, manual anchor maps, a CLI, new layouts, new outputs, or prebuilt language pairs. Ordinary pages in frames are the recommended first implementation, not a necessary step towards a content-only output. The existing host remains behind the reader: two child documents plus that host means three documents, a cost to measure before adding another rendering path.

7.8.14 - CLI acceptance snapshot, 2026-09-29

Executed Starter, real-site, offline, upgrade, and reproducible-archive checks for the local CLI candidate.
Historical CLI acceptance snapshot (2026-09-29)

This page records the 2026-09-29 validation of the local 0.1.0-dev CLI candidate at commit e623d93. Current CLI behavior is defined in the CLI and result contract.

Inputs and method

The CLI belongs to the independent oink-cli Go repository. The CLI and result contract defines its accepted boundary. The CLI introduction gives reproducible user steps. Hugo remains an external renderer. Generated sites contain normal Hugo inputs.

Input Observed baseline
Host macOS, darwin/arm64
Go go1.27.1
Hugo 0.166.0+extended+withdeploy
CLI 0.1.0-dev, local commit e623d93d589c49e5c58b8fae1bd5db720fc904cb
Embedded Starter Commit 137843b25bacd76ddd1f7ce71330bf2e3155b954, complete licensed Git archive
Generated theme pin Public github.com/pgsty/oink v1.1.0, with recorded Go checksums
Documentation-site theme Local theme HEAD b0af631 plus uncommitted changes

The Starter archive hash is e55bde279715f6d8d19d3d88671a2cf7561b515be46915b0f12c640d0ce1d958. Its recorded projections select an existing language profile, pin OINK v1.1.0, and set enableGitInfo: false for a new directory. An observed failure required the last projection. The original enableGitInfo: true caused a warning-strict build to fail before the site’s first Git commit. No Git repository or commit was created to hide that failure.

Checks used disposable source copies, module/render caches, and output directories. Original Starter and consumer source trees were not written by the CLI checks. Existing unrelated theme and documentation edits were retained. The counts below describe those inputs and CLI revisions on 2026-09-29.

Starter and ordinary Hugo

All six ordinary-Hugo cases passed --environment production --panicOnWarning, with provisioned modules and isolated caches:

Language profile Root URL /manual/ subpath Reported Hugo pages
en Passed Passed EN 90
en,zh Passed Passed EN 91, ZH 89
all Passed Passed EN 91, ZH 89, FR 89

The tests checked the expected language roots and representative Docs, Blog, and Book outputs. They compared generated source bytes before and after Hugo. The public CLI’s init command also passed separately for all three profiles, returning zero diagnostics and 94 generated source files per profile. The profiles select different root configurations and disable untranslated sample files while retaining them in the snapshot.

Starter package unit, race, and vet checks passed. Failure cases cover nonempty and symlink targets, validation failure, and target replacement after planning. They also cover cancellation rollback, concurrent modification/deletion, and archive path rejection. The regeneration script reproduced the fixed archive, provenance, and license exactly.

Real-site inspection snapshot

Each run below returned CLI exit 0 with zero recorded diagnostics.

Site shape and theme source Files HTML files References Machine artifacts
Three-language Starter, public v1.1.0, release check at /manual/ 316 142 7,042 6
OINK documentation/regression site, local theme HEAD b0af631 plus dirty changes 1,127 506 72,562 8
PIG project site, root Docs/Blog route rewrites, public v1.1.0 1,392 424 64,440 4
Repository documentation with generated catalog, public v1.1.0 3,287 1,635 851,535 12

The last three are local consumer repositories: PIG and the catalog site validate published-pin resolution, while the OINK documentation run validates the local theme checkout.

The inspection covered HTML links, anchors, resources, and emitted machine artifacts. The completed Hugo manifest enumerated 261, 766, 662, and 3,192 output declarations respectively. Every supported enabled machine output was required by its actual language and URL. Before/after manifests compared all tracked and non-ignored untracked source bytes, modes, and Git status. All four sites were unchanged (94, 415, 858, and 2,294 source files respectively).

Two real regressions were fixed during this work. A NAVJSON template rendered only English. This previously hid the missing Chinese output. The check now returns policy exit 1 with the missing output location. The check initially treated PIG’s intentional build.render: link sidebar entries as missing pages. Hugo’s effective build parameters now exclude them. Direct and cascaded regression cases verify this behavior. A paired build of the bilingual Starter also proved all 223 ordinary artifacts byte-identical before and after adding the isolated probe.

Offline execution and upgrade

On macOS, a full check of an initialized bilingual Starter passed under sandbox-exec with (deny network*), after dependency provisioning. The result was exit 0, zero diagnostics, 223 files, 95 HTML files, 4,461 references, and four machine artifacts. A separate English init also passed under the same OS-level network denial, returning exit 0, zero diagnostics, and the expected 94 generated files.

A cold-cache fixture requiring example.invalid/[email protected] returned CLI exit 2 and retained Hugo’s original module lookup disabled by GOPROXY=off evidence. A missing dependency was therefore reported as incomplete work, without silently enabling resolution.

A separate temporary site exercised a real public-module upgrade from v1.0.0 to v1.1.0. The original consumer was not used as a write target:

Operation Observed result
Preview Exit 0; candidate validated; applied: false; plan named only go.mod and go.sum
--write --expect-plan Exit 0; the matching plan was validated and applied
Repeat the same target version Exit 0; candidate validated; no proposed changes and applied: false

All three operations preserved unrelated dirty README.md content and untracked user-note.txt. The preview and write used the same plan ID and before/after module-file hashes. Replacement, workspace, dirty-file, rollback, and failure-protection cases passed the final focused Go tests and race run. Only go.mod and go.sum changed on write; backup manifests retained original bytes. Preview and repeat executions preserved all source bytes.

Real thin-wrapper smoke tests also passed in a disposable initialized site. build --json returned 0 and produced index.html. dev --json served HTTP 200, forwarded SIGINT to Hugo, and closed the listener. Hugo exited 0. The canceled wrapper reported 2, as specified by the cancellation contract. These runs used provisioned local caches without --network.

Final make test (all packages plus vet), make test-hugo (ordinary Hugo, workspace/config precedence, output manifest, missing-language regressions), and go test -race ./... passed. All three public init profiles were rerun under OS-denied networking; the cold dependency fixture again returned 2.

Archive and installation preparation

One frozen CLI source snapshot produced four binary archives and one source archive, plus SHA256SUMS. Rebuilding independently from the source archive produced the same SHA-256 values for all five archives. The tested packaging input hash was:

b07c5b98ef787dfe9924ce7b50c57d018c6149ec493124bb0103551a01535547

This final snapshot supersedes the intermediate archive experiments. All five archive checksums were verified and reproduced from the extracted source archive. Both source and binary archives include the versioned JSON schema, licenses, dependency pins, and Starter provenance. Local make install into a temporary prefix and the installed binary’s --version succeeded.

Target Evidence
darwin/arm64 Compiled; host binary executed; local installation path exercised
darwin/amd64 Cross compiled only; not executed on that architecture
linux/amd64 Cross compiled only; not executed on Linux
linux/arm64 Cross compiled only; not executed on Linux

The archive builder records the toolchain, flags, source-input hash, and platform targets.

Reproduce the relevant checks

From a CLI checkout with dependencies already provisioned:

make build
make test
make test-hugo
go run scripts/snapshot-starter.go --source ../oink-starter

To repeat rendered-site checks in the sibling layout, keep JSON and logs outside each consumer’s source tree:

oink_acceptance_dir="$(mktemp -d)"
mkdir "$oink_acceptance_dir/reports"
./bin/oink init "$oink_acceptance_dir/my-docs" --languages all
./bin/oink check --site "$oink_acceptance_dir/my-docs" --release \
  --base-url https://example.org/manual/ --json \
  > "$oink_acceptance_dir/reports/starter.json" \
  2> "$oink_acceptance_dir/reports/starter.log"
HUGO_MODULE_REPLACEMENTS="github.com/pgsty/oink -> $(cd ../oink && pwd)" \
  ./bin/oink check --site ../oink.pgsty.com --json \
  > "$oink_acceptance_dir/reports/docs.json" \
  2> "$oink_acceptance_dir/reports/docs.log"
./bin/oink check --site ../pig.pgsty.com --release --json \
  > "$oink_acceptance_dir/reports/pig.json" \
  2> "$oink_acceptance_dir/reports/pig.log"
./bin/oink check --site ../repo.pgsty.com --release --json \
  > "$oink_acceptance_dir/reports/catalog.json" \
  2> "$oink_acceptance_dir/reports/catalog.log"

On a macOS host providing sandbox-exec, after initializing a bilingual site:

./bin/oink init "$oink_acceptance_dir/my-bilingual-docs" --languages en,zh
sandbox-exec -p '(version 1) (allow default) (deny network*)' \
  ./bin/oink check --site "$oink_acceptance_dir/my-bilingual-docs" --json \
  > "$oink_acceptance_dir/reports/offline.json" \
  2> "$oink_acceptance_dir/reports/offline.log"
sandbox-exec -p '(version 1) (allow default) (deny network*)' \
  ./bin/oink init "$oink_acceptance_dir/offline-en" --languages en --json \
  > "$oink_acceptance_dir/reports/offline-init.json" \
  2> "$oink_acceptance_dir/reports/offline-init.log"

The command reference describes preview, plan review, and explicit application. Use a separate review copy for write-path testing. For the archive experiment, keep the same Go toolchain and release version:

make release VERSION=0.1.0-dev DIST=dist/first
mkdir -p dist/rebuild
tar -xzf dist/first/oink_0.1.0-dev_source.tar.gz -C dist/rebuild
make -C dist/rebuild/oink_0.1.0-dev_source release \
  VERSION=0.1.0-dev DIST=dist
cmp dist/first/SHA256SUMS \
  dist/rebuild/oink_0.1.0-dev_source/dist/SHA256SUMS

Limits and delivery state

State At this snapshot
Local implementation Six first-stage commands and versioned result format exist
Executed validation The runs described above passed for their recorded inputs
Owning checks and documentation-site make check Passed after implementation and bilingual-document updates
Commit, tag, push CLI committed locally as e623d93; no tag, remote, or push. Documentation changes remain local alongside existing work
Public CLI release or distribution Not performed
Consumer source adoption or production deployment Not performed by these checks
Independent-user study or adoption No measured 4-of-5 / 15-minute study, retention, or independent-team adoption data

Raw JSON, logs, source-preservation manifests, upgrade recovery evidence, and archive-verification results are stored in the CLI checkout’s ignored tmp/acceptance/ directory, with release archives in dist/first/.

This first-stage candidate focuses on existing OINK sites; Docsy assessment and later migration, theme descriptor, version lifecycle, OpenAPI, MCP, and Studio remain tracked in the CLI roadmap.

7.8.15 - Component refinement, 2026-10-25

Local four-preset component validation, asset measurements, and separately reproduced gallery baseline failures.
Local acceptance record

Phases 0–3 and final acceptance in phase 5 are complete for this component change. The moving selection trial remains deferred.

Inputs and method

The final review used isolated sibling checkouts based on theme 7f9589f and documentation 855d890, with only this task’s source changes applied. Concurrent PlantUML, toolchain, and workflow work was excluded from the candidate and preserved in the shared worktrees. The accepted implementation is theme commit 15c39ef; guides, contracts, examples, and browser coverage are in documentation commit 81e2be9.

Validation used macOS ARM64, Hugo Extended 0.168.0 with built-in LibSass, Go 1.27.3, Node 26.12.0, and npm 11.20.0. Browser previews used the isolated sibling theme and hugo.yml,tests/fixtures/browser.yml, with both workspace mechanisms, watching, and live reload disabled. Screenshots came from actual documentation output. The bilingual comparison guide owns the reusable examples.

Executed validation

Check Result and scope
Theme component owners SCSS/compiler hooks, fonts, presets, content primitives, components, code blocks, and Landing passed. Invalid variants and inapplicable command strips warn and preserve content.
Theme shell/output owners Shell, actions, palette, keyboard, runtime isolation, and output checks passed. Output security passed on 501 fixture files with the fixture’s intentional third-party embeds enabled.
Real site make check passed all 64 non-browser tests, including bilingual headings, rendered Markdown, and internal links. Reviewed navigation/backlink additions account for the site’s Markdown golden changes.
Full browser suite make browser passed 409 Chromium tests. After the last correction to hide an empty command action column without JavaScript, all 20 presentation tests and the SCSS checker passed again against a fresh build.
Presentation coverage Four presets × light/dark × EN/ZH; 1440/768/390/320px layouts; literal clipboard text; independently operable native and full card links; native disclosures; no JavaScript; print; coarse pointers; reduced motion; forced colors; and Landing feedback width/contrast.
Additional engine review Chromium 151.0.7922.34, Firefox 153.0, and WebKit 26.5 passed 48 preset/mode/locale combinations covering Tabs, Fields, card links, and 1440/390/320px layouts. The final no-JavaScript command presentation also passed in all three engines.
Visual review Reviewed actual Paper, Slate, Ink, and Terminal cards, disclosures, Tabs, command strips, and field labels in light/dark and desktop/mobile output. Stable viewport captures preserve each preset’s geometry and reading hierarchy.
Production The final candidate passed a minified production build. Resource measurements below use the CSS linked by the generated page and the same documentation source for both builds.

The full suite’s A11Y_PATHS covered EN/ZH Presentation, Cards, Callouts, Tabs, Code, and Fields, plus English Badge and Kbd: 14 routes. The existing responsive accessibility matrix ran as well. Presentation axe scans included the article at 390px; the Landing check included its command box in all eight preset/mode combinations. Giscus requests were blocked as in the existing suites.

Asset cost

The exported theme base and the final candidate used the same documentation source and production build mode:

Asset Before After Change
Minified main CSS 528,665 bytes 539,450 bytes +10,785 bytes
Main CSS, gzip 79,541 bytes 80,887 bytes +1,346 bytes
Built JavaScript 39 files Identical hashes 0 bytes

These final measurements supersede the earlier 539,437-byte / 80,856-byte initial refinement measurement. No framework, font, remote asset, or JavaScript dependency was added.

Tab review follow-up

Maintainer review rejected the raised selected Tab. The accepted treatment keeps individual labels transparent at rest, supplies a subtle fill only on hover-capable devices, and uses link color plus a 2px bottom rule for selection. Labels retain the same medium weight and position. Keyboard focus has its own outline; coarse-pointer targets remain at least 44px high. Final engine review verified these states, arrow-key selection, and narrow layouts.

Field names and final corrections

Terminal field names now use the same unboxed, emphasized monospace treatment as the other presets. Both native Markdown tables and full Fields shortcodes were checked in EN/ZH and light/dark output. Ordinary prose code and keycap borders retain their separate role.

Final review reproduced and fixed two further edge cases:

  • A native card’s stretched title link intercepted a secondary link nested inside emphasis. Secondary controls now remain above that target; pointer checks verify both the nested link and the rest of the card navigate correctly. The title’s keyboard focus outline follows the card’s own shape.
  • A command strip retained a small empty action column when JavaScript was disabled. The wrapper now disappears until its copy control is available. In all three engines, the source column occupies the full 356px inner width of the 390px sample, while the normal enhanced copy control remains usable.

Baseline failures and limits

Two theme checks fail identically in the exported base and the component candidate:

  • check-namespace.py: committed blog gallery output uses unprefixed data-height, data-width, data-mode, and data-size, with corresponding unprefixed dataset reads.
  • check-goldens.py: the committed gallery’s prepaint condition, toolbar icon, and gallery markup are absent from the existing theme goldens. Comparing base and candidate with the checker’s normalizer found no additional difference across all 52 golden surfaces.

The component checker also includes a targeted correction: reject obsolete configuration keys rather than any occurrence of gallery, allowing the valid blog_gallery key.

Local acceptance covers this component change and its two final repairs. Ink and Terminal retain their experimental status. Runtime JavaScript is unchanged.

7.8.16 - Review repairs, 2026-11-06

Reproduced build, feed, navigation and interaction defects, bounded repairs, and remaining review work.
Development evidence

This record describes local changes after v1.5.0. It does not establish a new release, consumer upgrade, or deployment. The original review’s roughly 170 entries have not all been accepted or fixed.

Inputs and method

The review started from theme 84b93e4 and documentation a8eeb92. The candidate keeps Hugo Extended 0.168.0 and its built-in LibSass, with Go 1.27.3, Node 26.12.0, npm 11.20.0, and Python 3.14.8 on macOS ARM64. The Hugo 0.169.0 executable on the normal system path is outside this validation baseline. Its golden differences are not evidence of a defect on 0.168.0.

Build and output claims were checked with synthetic inputs in the owning theme checkers. Interaction claims were checked against the sibling documentation site through Playwright. Browser previews used the local theme replacement and hugo.yml,tests/fixtures/browser.yml, with both workspace mechanisms, watching, and live reload disabled. Existing passing snapshots were reviewed before recording intentional changes.

Confirmed defects and repairs

Area Confirmed behavior and repair Repeatable evidence
Invalid input Positional arguments reached map-only shortcode operations and failed the build. Named-parameter guards now warn and safely omit or fall back. Negative checks require exit status zero. check-content-primitives.py, check-book.py
Book numbering Invalid numbers produced empty targets such as fig-. Unnumbered content now retains readable output without target registration. check-book.py: HTML, print, Markdown, BookManifest and index rows, with valid-number controls
Nested components Independent Markdown documents and sibling shortcode containers reused local ordinals. Generated IDs and their references now include the containing scope. Authored code IDs are preserved. check-code-blocks.py: repeated nested code, fields, charts and FileTree in HTML and print
Feeds Weight-based order was limited before date sorting, undated items emitted year-one dates, and several components emitted raw Markdown. Both feeds now sort before limiting and omit unavailable dates; components use static HTML or their documented omission. check-content-primitives.py, check-book.py, RSS goldens
References and versions Missing manual references could fail Hugo; version links could repeat the deployment prefix. Shared resolvers now provide warning fallbacks and join version paths once. check-agent-indexes.py, check-actions.py
Navigation under subpaths Language-neutral navigation IDs and section-index keys retained the deployment prefix. A shared key resolver now removes deployment and language prefixes while published links keep both. check-agent-indexes.py: EN/ZH under root and subpath deployments, with and without a default-language directory
Landing Invalid record shapes and optional fields could break Markdown output. HTML and Markdown now share record and URL validation, preserve fragments and queries, and include standalone CTA links. Unsafe targets do not become empty links. check-landing.py
Blog navigation List pages inherited article-sequence navigation and English pagination. Localized semantic pagination now owns list navigation and head links. check-reading.py, check-shell.py, browser pagination regression
Focus and menus Native disabling removed focused controls; hidden self-links were unreachable; focus-opened menus reopened after Escape. Guarded aria-disabled controls retain focus, self-links remain reachable, and Escape restores focus before closing. JS units and browser code, shell, comparison and Expo suites
Mobile shell Navbar-only articles lacked mobile utilities, and closed sidebar drawers leaked a shadow. Site drawers now expose language, version and repository links; shadows appear only when open. check-shell.py, responsive browser regressions
Layout Book TOCs displayed two counters, prose margins affected component lists, Landing padding was overridden, and global link rules suppressed print underlines. The owning rules are now scoped correctly. Browser review regressions and SCSS checks
Locale and schema Pagination and breadcrumb labels were untranslated; several locale strings and the appearance legend were incorrect; the schema rejected language-code translation notices. Labels now cover all 32 locales and generated schemas match the resolver. check-i18n.py, generate-config-schema.py --check
Preset and alternate rendering Terminal’s current-row chevron selector was wrong; focus rings were translucent or clipped; forced colors hid current-state cues; the RTL artifact overwrote font roles. Targeted rules now retain these states. Repeated marquee rows are hidden from assistive technology. Preset/SCSS checks and browser accessibility coverage

Qualifications to the original review

The original report’s severity labels grouped build failures, accessibility defects, wording, and design preferences together. They should not all be treated as release-blocking failures. Reproducible build and output defects deserve the first repair batch; cosmetic consistency needs its own acceptance criteria.

“No TOC” does not mean “empty right rail.” Blog indexes can still contain author, category, series, and tag clouds. Removing the fallback control made that rail impossible to collapse and failed the existing RSS/browser regression. The final repair omits genuinely empty rails and labels populated non-TOC rail controls as sidebar actions. It preserves collapsing, focus restoration, and RSS access.

RSS fixes do not require every component to appear in feeds. Release assets keep their explicit omission. The Book checker now exercises the ordinary section feed instead of treating raw RenderShortcodes output as the RSS contract.

Different corner radii, duplicated template shells, and native/shortcode layout differences are maintenance or design concerns until a specific contract or user failure is demonstrated. Merging base templates solely because their bytes are equal can change Hugo lookup precedence. No such broad consolidation is included.

Validation

The theme’s focused checks, schema parity, 32-locale parity, 80 JavaScript tests, and 90 migration tests were run on the fixed toolchain. Output changes cover 52 HTML, print, Markdown, RSS, and LLMS golden surfaces. These checks are local unit and output evidence; the separate CI PDF/EPUB publication job is not claimed.

The documentation site’s final make check passed, including all 64 non-browser tests, with a larger Node heap for its rendered-link scan. A local production build also passed and generated 462 pages per language, with only the accepted upstream LibSass deprecation warning. The first browser pass completed all 30 accessibility tests; the responsive group passed 117 tests after retaining the taxonomy rail control. The other canonical browser groups also passed in their respective runs: keyboard 16, content components 70, code blocks and Landing 37, PRD5 8, theme color 10, community feedback 42, appearance 53, Expo 50, gallery 8, PlantUML 8, diagrams 19, table reader 15, standard comparison 19, and eight-language/RTL comparison 6. The canonical browser set totals 508 passed tests across these runs. A later focused accessibility run passed all 30 cases on the new research and blog pages. After the Escape correction, a fresh shell, blog and menu regression run passed 56/56. The final Landing checker passed after its metrics links were routed through the same URL policy.

The browser counts describe this repair candidate before separate comparison interface work appeared in the shared checkout. That concurrent work is outside this record’s browser acceptance. The completion audit additionally verified navigation keys under language/deployment prefixes and invalid Book numbers in HTML, print, Markdown, BookManifest, and the generated indexes.

Remaining work and order

This is a bounded repair batch. The following work remains outside its acceptance:

  1. Reader control over single-character shortcuts, command-palette handoff and localized error messages, and plural-aware contributor/search/code counts.
  2. Non-default-language Google search destinations and the documented meaning of relative content URLs.
  3. Remaining contrast cases and the previously recorded narrow, large-text footer overflow. These require their own rendered reproductions and browser checks.
  4. Geometry, interaction-state and overlay tokens, remaining manual-link owners, shared shell structure, and stylesheet size. These need bounded design decisions and measurements rather than a blanket replacement of literals.

The component, shell, architecture, and Landing contracts describe this batch’s development behavior. Historical release claims remain unchanged.

7.8.17 - Ink and Terminal need reading acceptance

Source review, bounded visual evidence, current failures and promotion limits for Ink and Terminal on 2026-10-26.
Pre-release research snapshot (2026-10-26)

This record preserves inputs and measurements from before the OINK 1.4.0 release preparation. At 320px with a 32px root/default font, footer links cause document overflow across all four presets. Ink and Terminal remain experimental; the stabilization proposal defines desired behavior and promotion gates.

Keep Ink neutral sans editorial and Terminal a readable engineering handbook. Both can become coherent official styles through their existing role system and shared shell. A new serif identity would weaken Ink’s distinction from a future Folio; a simulated terminal would weaken long-form engineering reading. The immediate work is shared text resilience, quieter hierarchy, and component consistency.

The evidence identifies a local snapshot

This 2026-10-26 review combines primary-source text/HTML/CSS inspection, an exported source snapshot, real documentation rendering, and a bounded injected CSS comparison. Theme HEAD was 7bc8658bc1be13ef0ad3050b9d9f6862f8f1f65f; documentation HEAD was 99751179eefada3fa7a91318c792ddac46d7e6bf. The snapshot also included uncommitted changes, including concurrent table work; the retained source manifest records that full state. Public tag lookup observed v1.3.2, peeled to bd2e856e6e2f7027a9189af009701f5b8cf562cf; documentation pinned v1.3.2. See the earlier experiment and component review for prior context.

The documentation server used the sibling snapshot with hugo.yml,tests/fixtures/browser.yml, both workspace mechanisms off, memory rendering, no watching/live reload, and enableGitInfo=false because the export lacked Git metadata. Tools were macOS ARM64, Hugo Extended 0.168.0, Go 1.27.3, Node 26.12.0, npm 11.20.0, and Playwright 1.62.1; engines were Chromium 151.0.7922.34, Firefox 153.0, and WebKit 26.5 (Playwright WebKit scope).

The reproducible method loads the same real route, language, mode, and preset at each width, compares baseline with the listed overlay, records full-document dimensions and computed styles, then captures relevant components. Source manifests, review-metrics.json, api-supplement.json, text-resize-findings.json, stress-metrics.json, reference-rendering.json, and screenshots are stored outside Git.

Whole-document overflow outweighs targeted passes

At 320px/32px root font, English Ink had scrollWidth=424 (+104px), Terminal 408 (+88px). Chinese Chromium overflow was +12px/+21px respectively; English Terminal also exceeded 768px by 11px. The overlay produced the same failures. Firefox’s font.size.variable.x-western=32 preference reproduced English overflow across all four presets; Paper reached 413px and Slate approximately 400px. Footer links including “Customization”, “Components”, and “Docsy discussions” exceed two narrow columns. This is a shared baseline blocker across all presets.

These probes exercise CSS root enlargement and native default-font preferences. Combining 320px width with enlarged text is a stronger stress condition than standalone reflow. Full manual 200% text enlargement and 400% browser zoom remain pending (Resize text, reflow).

Owning checks: check-presets.py passed text/link/accent checks over three surfaces, mode parity, frozen Slate, and configuration output; check-font-tokens.py passed.

Existing suites: 28 appearance-engine, text-reflow, and component tests passed. Their page-context/topline assertions missed the footer. The source-snapshot make check passed 64 non-browser tests before these documents were added. With the new documents, the site built and passed Markdown, bilingual heading, and internal-link checks; 63 of the 64 non-browser tests passed, with llms.txt golden differing only by the two new English document entries.

Main measurement matrix: Chromium: 11 valid routes × two presets × two modes × EN/ZH × 320/390/768/1440px × baseline/overlay. Firefox/WebKit: presentation/configuration/Book only at 320/1440px. Normal-size pages showed no document overflow.

Counting correction: The initial ledger’s 960 entries were computed-state observations. A mistaken API route produced 32 harness 404s; the corrected /docs/write/openapi/ supplement contributed 64 valid observations with HTTP 200 and no document overflow.

Additional stress: 64 simultaneous spacing observations used line-height 1.5, paragraph margin 2em, letter spacing .12em, and word spacing .16em; no page/heading overflow was observed. Sixteen 320px long-unbroken heading/Field-label probes preserved text without page overflow.

axe: 32 article scans at 390px covered presentation/configuration, both languages/modes/presets, and baseline/overlay. No violations were reported; all scans retained contrast incompletes on aria-hidden=true pager-arrow spans with non-text characters, which require manual graphic-contrast inspection.

Visual material: The archive contains 352 comparison captures, including Blog/Expo/Mermaid/ECharts scenes, plus eight external references, from which representative desktop/mobile and light/dark pages and controls were reviewed.

Long reading: The real EN/ZH /blog/oink/oink-implementation-diary/ article was checked in four selected combinations: Ink EN/1440/light, Ink ZH/390/dark, Terminal EN/390/light, and Terminal ZH/1440/dark. Baseline/overlay captures at top/middle/footer produced 24 additional images; normal-size document overflow was zero throughout. Eight representative images were inspected, including English before/after prose, Chinese prose, Terminal’s English mobile footer, and Ink’s English desktop opening.

The Chromium routes were home, Presentation, Configuration, Fields, Code, Table, Book structure, immersive-reading Blog, Expo studies, Mermaid, and ECharts, plus the corrected OpenAPI guide, with Chinese peers included. The capture pass also verified the first rendered Mermaid diagram and an ECharts canvas. The comparison gallery spans 11 scenes; seven control scenes cover both presets, modes, languages, and two widths. Offsite requests, including Giscus, were blocked in the main matrix.

The overlay tests a limited editorial change

Ink kept its palette/fonts while changing article h1 from 800/-.035em to 750/-.025em, h2/h3 from 750/-.025em to 700/-.012em, and its 2px emphasis top rule to a 1px existing strong-line rule. It removed enforced uppercase/tracking on minor/table/callout headings, used zero Chinese heading tracking, and compared native underlines at max(1px,.06em)/.22em, with Chinese .25em and skip-ink: none.

Terminal kept teal/mint and amber, used heading weight 600/zero tracking, removed generated heading prefixes and callout brackets, changed descriptive table headers to body 0.875rem/600 and search titles/excerpts to body, and reduced decorative code/tab/table/callout frames to the existing border role. Technical paths/controls stayed mono and Field names stayed unboxed. FileTree’s omitted literal 0.6rem radius became 0px/2px for Ink/Terminal. Flat Tabs, mobile targets, and vendor boundaries remained intact. The overlay tested CSS adjustments only, without adding new theme templates, fonts, runtime, or configuration.

Authored accent colors on actual pages were documentation blue #245f94/#5da2dd, Book orange #9a3412/#fb923c, and Blog violet #6d28d9/#a78bfa. Separately labeled palette probes reset only that authored accent to the preset default on both variants for controlled comparison. The architecture contract preserves overrides and the shared shell.

Palette calculations expose state risks

Opaque sRGB calculations and alpha compositing used source tokens and the WCAG luminance formula; displayed ratios are rounded while pass decisions use unrounded values (Contrast method, palette source).

Pair, light / dark Calculated contrast Implication
Ink accent #c8102e on white / #ff5c4d on #0b0b0b 5.88 / 6.46 Retain mode-specific red; keep red accents focused rather than repeating decorative red rules.
Terminal body on canvas 14.88 / 13.64 Strong neutral reading pair.
Terminal link on canvas / secondary surface 6.31 / 9.54; 5.75 / 8.73 Existing teal/mint supports these exact surfaces.
Terminal tertiary text on secondary surface 4.63 / 5.93 Light muted text has little margin for further tint/opacity.
Terminal amber on canvas 5.47 / 9.44 Amber remains a separate emphasis role.
Terminal 32%-alpha border on canvas 1.97 / 2.36 Suitable for decorative rules; insufficient if it alone identifies a required control or state.
Terminal 55%-alpha strong line on canvas 3.63 / 4.73 Candidate functional boundary on these canvases.
White on dark mint #4cc9bd 2.02 Avoid white labels on light mint fills.

Cross-mode reuse fails ordinary text contrast: mint on the light canvas is 1.85, dark teal on the dark canvas 2.79. Semantic tones, author accents, syntax comments, selected search excerpts, focus, and text-selection composites still require state-specific review. Non-text contrast applies to necessary identifying boundaries (WCAG 1.4.11).

Mature precedents support roles, not transplantation

All external references were accessed 2026-10-26. External Chromium rendering at 1440/390px returned HTTP 200 for Tufte, GOV.UK Headings, Effective Go, and A List Apart. Eight captures were saved, with visual review focused on Tufte desktop, GOV.UK mobile, Go mobile, and A List Apart desktop; the remaining precedents received source review.

Practical Typography. Puts body text first; prescribes Latin-oriented size, spacing, and measure. Its underlining advice opposes link underlines.

Review complete paragraphs before display titles, while keeping OINK’s bilingual rhythm and open-source font constraints.

Tufte CSS. Inspected desktop shows a narrow serif reading column and underlines. CSS declares ET Book, light #fffff8/#111, dark #151515/#ddd, and mobile note toggles.

Borrow proximity of explanation/figures and note hierarchy; leave serif/margin geometry to future Folio exploration.

GOV.UK Headings. Inspected mobile view has generous sans headings and underlined links; cookie banner was visible. Current type scale uses 19px/25px body sizing on small and large screens. Links distinguish prose from navigation; tables emphasize structure.

Strongest functional input for Ink’s sans hierarchy and persistent link cues, adapted to OINK’s bilingual scale and dark mode.

A List Apart: A Dao of Web Design. Inspected desktop distinguishes serif body from sans headings/controls. Current CSS uses Georgia 1.125rem/1.7, red #ba3925 h2, and blue links.

Separate reading/UI/code roles and keep adaptable content while keeping Ink’s sans-serif identity and restrained accent frequency.

Rust Book: Ownership. Source exposes numbered chapter structure, explanations, and code; CSS separates Open Sans prose and mono code.

Strong Terminal precedent for explanation/syntax separation and real chapter numbers, while keeping OINK’s mono headings and standard browser shortcuts.

Effective Go. Source/CSS separates platform sans and mono; inspected mobile TOC is dense and two-column.

Borrow straightforward technical hierarchy while keeping OINK’s responsive TOC.

man7 open(2). Stable reference sections, signatures, links, and preformatted text; (2) names a manual section.

Preserve predictable lookup and complete identifiers without forcing bilingual prose into fixed-width terminal cells.

CLI Guidelines. A navigable explanatory web guide recommends intentional, limited color.

Borrow restraint and clear labels while keeping standard web interaction semantics.

Paper’s warm general reading, Slate’s established technical presentation, Ink’s neutral editorial restraint, and Terminal’s technical role contrast remain distinct within the shared shell.

Language and accessibility need independent evidence

Actual Chromium per-glyph inspection on macOS found Inter for Ink Latin, IBM Plex Sans for Terminal prose, IBM Plex Mono for Terminal headings, and PingFang SC for Chinese in both. Windows and Android fallback rendering still require direct device verification (CSS font matching, Windows fonts, Android fallback).

Chinese underline clearance remains open. The CSS Text Decoration Working Draft explains Han collisions and fragmented ink skipping; CLReq’s Group Note Draft discusses interlinear marks without banning hyperlink underlines. Next steps should compare language-scoped text-underline-position: under with a small offset and initial skip-ink: auto, including Chinese inside English pages and mixed-script wrapped links (CSS draft, CLReq).

WCAG A/AA governs color-independent meaning, text/non-text contrast, keyboard operation, resize, reflow, spacing, language, and status announcements. OINK’s proposed 44px routine touch targets, 7:1 main text, and stronger 2px-area focus treatment are tracked as project targets above AA minima (WCAG 2.2, axe-core, focus appearance).

Still pending are actual Windows/Android loaded/blocked/system-font comparisons; branded Safari/iOS rendering; manual NVDA, VoiceOver, and TalkBack tasks; full manual 200%/400% zoom; and comprehensive keyboard, forced-colors, and state-contrast review covering navigation, Book numbers, table relationships, field errors, Chinese search input, status feedback, and dialog return focus.

Promotion should follow resilient reading

The footer overflow highlights the need for whole-document regression checks alongside article-top checks. Shared repair and whole-document regression should therefore precede preset promotion. FileTree’s radius leak and competing Terminal punctuation are bounded owner-level changes.

For 1.4, refine existing owners and promote Ink or Terminal independently once their acceptance gates close; otherwise retain experimental status. Folio (academic serif) and Canvas (creator geometry) remain deferred to avoid font/licensing and QA overhead in this cycle.

7.8.18 - OINK 1.5.1 readiness review, 2026-11-06

Three reproduced candidate regressions, complete browser acceptance, and remaining publication gates.
Local repair acceptance complete

The three reproduced regressions below have been repaired in the working trees and the targeted, theme, site, and browser checks pass. This dated review does not announce a release, consumer upgrade, or deployment.

Inputs and method

The review covered theme v1.5.0 (84b93e4) through c55849e: all six post-tag commits and 186 changed files. The documentation site was 43990ac. Theme main was three committed changes ahead of origin/main (d9ad654). Both working trees were clean before the initial review. Its original evidence is retained below; the later repair pass is recorded separately. No tag, push, consumer update, or deployment was performed.

The formal local toolchain was Hugo Extended 0.168.0 with built-in LibSass, Go 1.27.3, Node 26.12.0, npm 11.20.0, and Python 3.14.8 on macOS ARM64. CI uses Node 24. Supplemental Hugo 0.169.0 checks do not replace the pinned baseline. The accepted upstream LibSass deprecation remained visible.

Methods included implementation and contract inspection, the ordered theme CI commands, sibling-site checks, production builds, Chromium tests, and controlled minimal reproductions. Temporary fixtures and generated artifacts stayed outside Git. Local-theme and published-module checks used separate commands with both workspace mechanisms disabled.

Confirmed regressions

P2: RSS format restoration contaminates another render

The final assignment in layouts/_partials/content/rss-description.html restores a captured tdOutputFormat into the page Store, which shortcodes also read. In the reproduction, a summary render restores html while the page’s RSS shortcode render still uses that Store, producing mixed static and interactive output.

The formal bin/check-release-assets.py run failed because the RSS checksum table header disappeared and data-td-asset and <button appeared. A standalone rerun and eight single-page reproductions passed; rerunning until green does not resolve the original failure.

For a more reliable reproduction, expand that checker’s RSS fixture to 24 RSS-only pages, each with release-card and 20 release-assets blocks. Keep its custom layouts/docs/single.rss.xml, which sets tdOutputFormat to rss before .RenderShortcodes, and keep the ordinary home feed enabled. Build fresh output on Hugo 0.168.0 and inspect each page XML for interactive markers.

Three candidate builds all contaminated docs/rss-9/index.xml. One captured output had six static tables, fourteen interactive tables, and 28 buttons. The same fixture against published v1.5.0 passed three times. Overriding only the candidate summary partial to omit the final Store restoration also passed three times.

The confirmed scope is the custom RSS-page render exercised by the required checker, not every ordinary feed. Keep format state stable within its rendering context and add multi-page coverage while preserving HTML, Print, and normal RSS summaries. The temporary removal was a causal probe, not an accepted fix.

P2: Blog pagination duplicates Print aggregates

layouts/_partials/head.html calls shell/blog-index-state.html for every blog section, including Print. That partial calls .Paginate, so Hugo generates Print archive pages that each contain the entire section.

Use three dated blog posts, outputs.section: [HTML, print], and params.ui.blog_index_size: 1. The candidate generates _print/blog/index.html, _print/blog/page/2/index.html, and _print/blog/page/3/index.html; all three contain all three bodies. Overriding only head.html with its v1.5.0 version generates one aggregate. Both builds succeed on Hugo 0.168.0.

The documentation site’s production build also adds four duplicate aggregates across its English and Chinese blog roots and release indexes, totalling 1,120,141 bytes. Large archives amplify this waste. Limit blog-index pagination and its head links to HTML, then verify one Print aggregate and correct HTML pagination links.

P2: A stalled Duo View document never reaches Retry

assets/js/document-compare.js returns immediately for about:blank, including when its 15-second timeout calls ready(true). A pending main-document request therefore consumes the timeout without invoking failure handling. Loading and busy states remain without Retry or a direct-link fallback.

In the sibling site’s browser fixture, open /tests/compare-reader/ and hold /zh/tests/compare-reader/?oink_compare_pane=1 pending with a Playwright route. Open Duo View and wait 17 seconds. The source pane is ready, the target remains about:blank, and there is one loading note, zero failure panels, and zero Retry buttons.

This is not covered by the existing 404 and slow-image tests. Timeout expiry must fail a still-blank document while ignoring its initial blank load event and preserving parsed articles with slow images. Add the stalled-document case to the site’s comparison suite and retain the localized fallback promised by the shell contract.

Initial validation

Check Result and scope
Theme CI commands 48 of 49 runnable steps passed on Hugo 0.168.0. The release-assets RSS step failed; theme acceptance is not all green.
Theme tests 81 JavaScript tests, 90 migration tests, and 22 snapshot/consumer-tool tests passed. Locale parity, schemas, vendor inventory, output goldens, and Sass-boundary checks passed.
Site make check Passed, including 67 Node tests and source, translation, rendered-Markdown, and rendered-link checks.
Site make browser A single complete invocation passed 518 Chromium tests in approximately 23 minutes. A11Y_PATHS was unset: the sitemap scan was unrestricted.
Candidate production build Passed with the local theme and ordinary production configuration. Only the accepted LibSass warning appeared. Print duplication was inspected separately.
Publication isolation All three PDF safety tests passed with real Chromium, including remote CSS blocking and disabled scripts.
Publication smoke test Current-candidate EPUB/PDF generation and owning artifact checks passed: five chapters, 31 EPUB targets, and a 23-page PDF. Local Pandoc was 3.12.1. The CI-pinned Pandoc 3.10, Java 21, EPUBCheck, and subpath matrix were not rerun locally.
Published baseline Go proxy v1.5.0 resolved to 84b93e4 with the site’s recorded checksums. The release-pin check and production build passed with replacements and both workspaces disabled.

Browser groups passed 30 accessibility, 117 responsive-shell, 16 keyboard, 70 content-component, 37 code/Landing, 8 PRD5, 10 theme-color, 42 community-feedback, 53 appearance, 50 Expo, 8 blog-gallery, 8 PlantUML, 19 diagram, 15 table-reader, 26 standard comparison, and 9 multilingual comparison tests. The local sibling theme used hugo.yml,tests/fixtures/browser.yml and the separate eight-language comparison fixture. Firefox and WebKit were not rerun.

The suite at the initial review did not cover the additional stalled-request and Print cardinality cases above. The earlier review-repair record explicitly excludes later Duo View work from its 508-test result; this review supplies fresh evidence for the current candidate.

Repair verification

The repair pass changes the working trees based on theme c55849e and site 43990ac. The changes remain uncommitted and are not included in the remote CI runs linked below.

  • RSS summary rendering no longer restores the stale format value. The owning release-assets checker now renders 24 RSS pages with 20 asset tables each alongside the home feed, asserting every static table and checksum and the absence of interactive controls. A temporary override with the pre-fix partial fails this check; the repaired partial passes.
  • Blog pagination in the shared head is limited to HTML. The reading checker verifies one complete Print aggregate, three distinct HTML archive pages, correct HTML prev/next links, and no Print pagination links. A temporary override with the pre-fix head generates three Print aggregates and fails; the repaired head passes.
  • Duo View distinguishes document timeout from the initial blank load event. The new browser case holds the target document pending, checks the Retry label and direct link, and verifies that Retry restores the pane when the request can proceed. It passes with the repair. The earlier slow-image case continues to cover readiness without waiting for all resources.

The bilingual architecture and shell contracts and the theme’s Unreleased changelog accompany these fixes. The site’s LLMS golden gains only the new research-page entry; the output diff was reviewed before updating it.

Repair acceptance Result
Theme CI commands All 49 locally runnable steps pass on Hugo Extended 0.168.0, including 81 JavaScript, 90 migration, and 22 snapshot/consumer-tool tests.
Site make check Passes all source/rendered checks and 67 Node tests.
Site make browser Final complete invocation passes 519 Chromium tests: 510 in the main suite and 9 in the eight-language comparison suite. A11Y_PATHS was unset, so sitemap accessibility coverage was unrestricted.
Production build Passes with the local theme. Each EN/ZH blog and release index has one Print aggregate and no Print pagination links; all four duplicate aggregates are gone.

Browser acceptance uses the installed Playwright browsers without an executable path override, allowing the bfcache case to select full Chromium. That case and the new stalled-document Retry test also pass individually. These results use the same local tool versions recorded above; the publication-toolchain limits in the initial validation still apply.

All three reproduced regressions are locally resolved. The next release gates remain commit and push of the reviewed changes, green remote CI for that exact revision, then the separate tag, Go-proxy, consumer-pin, deployment, and production-identity states.

Publication and adoption

The latest observed public tag remains v1.5.0. The green theme CI run, including Book publication, belongs to d9ad654, not local c55849e. The green site checks and browser workflow also tested the remote theme available then. They do not certify the three later local commits.

A read-only bin/update-consumers.py v1.5.0 inventory found 50 primary checkouts: 42 current, three module consumers skipped for non-default branches, and five classic themes/oink consumers. All 45 module pins were v1.5.0. Four classic theme repositories were clean at the exact v1.5.0 tag. The remaining source copy matched sampled implementation files; that is not a full identity audit. No consumer was upgraded or deployed. The skipped branches and classic copies need individual handling during eventual adoption.

Release exit criteria

The three repairs, their owning checks, browser coverage, and bilingual contracts are present locally, and the working-tree acceptance recorded above is green. The next gate is green remote CI on the final release commit. Deferred design and accessibility work from the earlier review remains a separate backlog; this record does not claim every historical finding is fixed.

After code acceptance, prepare the 1.5.1 changelog and current version references, push reviewed commits, and verify CI for that revision. Tagging, push and Go-proxy verification, consumer pinning and appearance checks, deployment, and production identity are separate subsequent states. Those future steps are normal release preparation and remain distinct from local repair acceptance.

7.8.19 - Theme review, 2026-10-13

Reproduced image escaping and interaction defects, their repairs, and local theme and consumer validation.
Local review snapshot

This record describes local source and tests against the v1.2.0 consumer baseline.

Scope

The review began at theme commit f8f476e, with existing consumer-maintenance and compact-sidebar changes and an ongoing English documentation revision in the shared checkouts. Those edits were preserved. During the review, the existing sidebar repair was committed separately as 8f1f08d; its file contents remained unchanged.

Work covered the theme’s owning checkers, runtime unit tests, synthetic output fixtures, the real bilingual documentation site, and two different consumers: DDIA’s book and SILO’s large documentation tree. Source inspection focused on URL handling, image output, message boundaries, focus transitions, runtime isolation, and navigation. Visual review used real consumer pages and the documentation site.

Reproduced defects and repairs

Defect Reproduction and observed result Repair and regression
Image URL escapes its HTML attribute An ordinary image or Gallery URL containing a quote followed by an attribute-shaped query value creates an extra HTML attribute. An ampersand can also change the browser’s decoded URL. Escape URLs as HTML attributes after validation. check-media-primitives.py parses block and inline image attributes in HTML, Print, and RSS; check-gallery.py checks the quoted Gallery URL. The harmless extra-attribute probe failed before the repair.
Draw.io loses endpoint parameters and mishandles messages An endpoint with an existing query or fragment receives malformed embedding parameters. Null or malformed messages can throw; message handling does not require the configured origin. Build the URL with URL and searchParams; require the active iframe and configured origin; ignore malformed data; send to the exact origin and accept only PNG/SVG image export URLs. New Draw.io unit tests and the site’s editor integration tests exercise these paths.
Page Actions loses the keyboard interaction contract ArrowDown opens the menu before its CSS visibility transition permits focus. Tab can leave the disclosure while it remains open. Make visibility immediate, retain the opacity and movement transitions, and close on focus leaving the entire disclosure without reclaiming focus. Runtime and real-browser regressions cover forward and backward traversal.
Appearance clears a newly reopened dialog’s state Close and immediately reopen the dialog before its queued native close event arrives. The dialog stays visible while aria-expanded becomes false. Ignore an earlier close event while the dialog is open. Real-browser tests cover the race in Chromium, Firefox, and WebKit at desktop and phone widths.

The two taxonomy HTML goldens also needed the already accepted sidebar repair: 33 hidden-leaf markers in each snapshot were removed after reviewing the complete diff. The 52-surface golden gate then passed.

The behavior is recorded in the bilingual architecture and shell contracts. No configuration migration or new dependency is required.

Validation

Local tools were Hugo Extended 0.166.0, Go 1.27.1, Node 26.9.0, Pandoc 3.11, and Chromium 151.0.7922.34 for PDF generation.

Layer Evidence
Theme runtime All 52 JavaScript tests passed after the repairs. Python discovery passed 116 tests; the 90-test migration suite was also run separately.
Theme ownership Every bin/check-*.py checker was exercised, including explicit output-security arguments and packaged EPUB/PDF checks. Configuration schema generation was checked. Changed media, Gallery, output and golden owners were rerun after their repairs. Builds used warning-failure gates where the checker builds valid publishing output.
Documentation integration make check passed with the local theme replacement: source/rendered Markdown, bilingual heading coverage, internal links, alternate-site builds, favicons, output goldens and release-pin contracts.
Browser integration make browser passed 229 tests, including the full sitemap accessibility scan, six responsive widths, EN/ZH, navigation, search, components, code, community regressions and all four visual presets. The additional engine gate passed 6 tests across Chromium, Firefox and WebKit at 390 and 1440 px.
Publication fixtures EPUB: 5 chapters, 31 link targets, no internal-check errors; EPUBCheck 5.3.0 reported zero errors and warnings. PDF: 23 pages, all 5 expected titles, no internal-check errors; rendered pages were inspected.
DDIA sample Local-theme make check-local passed, including 14 translation chapters, 196 figure-layout checks and 38,170 internal references across 57 second-edition pages. Desktop, phone and dark reading views and the four-style menu were sampled.
SILO sample Local-theme make check passed, including 536,561 internal references across 1,552 HTML files. Desktop documentation and phone search results were sampled.

The initial broad run exposed the stale goldens. One standalone security invocation also lacked its required CLI arguments; its corrected invocation passed over 493 fixture output files. A new Print assertion initially omitted Print from the test page’s own output list; correcting that test setup produced a passing HTML/Print/RSS regression.

Performance observations

A strict, minified local production build of the documentation site was inspected. For /docs/customize/config/, the referenced local JavaScript totals 62,192 bytes gzip (about 61 KiB, including Lunr); the two stylesheets total 98,838 bytes gzip (about 97 KiB), excluding fonts, images, search indexes, and third-party service traffic.

The ordinary documentation page omits the large diagram bundles. The Mermaid sample adds a 976,006-byte gzip Mermaid dependency (about 953 KiB) plus its small page runtime. Runtime-isolation and output checkers passed.

Limits and delivery state

The accessibility suite excludes upstream Redoc and Swagger UI DOM under its existing policy. Consumer browser checks sampled key desktop and mobile routes across DDIA and SILO. The repairs were grouped into local theme and documentation commits.

7.8.20 - Give large tables room to read

Framework and component evidence, measured reading concepts, and a bounded direction for OINK technical tables.
Historical research; see the 1.4.0 decision

Research date: 2026-10-26. This study informed the table-reading design in OINK 1.4.0. The accepted decision and table guide describe the released behavior and supersede the entrance and scope recommendations below.

An explicit Read table action opening a large native dialog is the strongest candidate for further evaluation. It gives desktop tables more room while preserving text size, original table semantics, and a short return path. Inline widening remains sufficient for many tables; the measured package excerpt already fits that mode on desktop. On a 390px viewport, the dialog adds only 16px of width, so horizontal scrolling remains on narrow screens. Five documentation frameworks use native tables and overflow handling by default, while third-party table libraries add data operations alongside additional integration weight.

Evidence separates layout, content, and release state

The public tag snapshot was v1.3.2, with tag ref 68cfbbdc177064e2c28277c09c385d78e5363ffd. Research began at theme HEAD 7bc8658bc1be13ef0ad3050b9d9f6862f8f1f65f and site HEAD 99751179eefada3fa7a91318c792ddac46d7e6bf, with uncommitted changes in both working trees. The short .wide alias tested in this experiment was subsequently shipped in OINK 1.4.0 as documented in the component contract.

The local owners inspected were oink/layouts/_partials/content/table-render.html, table-body.html, assets/scss/td/_content.scss, _table.scss, the preset/font tokens, and the surface coordinator. .wide/.td-table--wide control intrinsic sizing and token wrapping; .full-width reaches the available article canvas. .matrix supplies first-column row headers and bounded two-axis scrolling, with existing sticky header/first-column CSS. That CSS gives each header top:0, without accumulated multirow offsets or a mobile first-column width guard. .fields emits a definition list. Ordinary captions belong inside the table; numbered Book captions belong to the outer figure (Table guide, output contract).

Selected consumer source Bounded sample and purpose
pigsty.cc/content/docs/infra/param.md, NGINX group Four columns × 14 body rows; linked parameter identifiers and descriptions
pgext/content/list/pkg.md, lines 44–69 Six columns × 24 body rows; package/version/repository/category/RPM/DEB comparison
pigsty.cc/content/docs/ref/linux.md, overview Eight columns × 14 body rows; OS/architecture and PG18–PG14 support cells
piglet.run/content/docs/reference/vscode/_index.md Two columns × seven body rows; small-table counterexample

Consumer snapshots were pigsty.cc at efaf7b19b442ddb284106418c78854552d949e9e, pgext at ab87fc7e5707d57f55129d3c45874abcc16fdef8, and piglet.run at 5a1572e6f9110cd5e8ef6e75cd50d9c4b6429f98; each pinned OINK v1.3.2. The separate 616-row extension catalog was left outside this prototype’s scope.

These examples distinguish many columns, many rows, long cells, and complex headers. Wider viewports primarily help the first; sticky orientation helps the second; wrapping helps the third. Multirow and merged headers need explicit associations and offset handling beyond wider layout. The selected prototype tables all have one header row and no merges. GFM pipe-table syntax itself defines a single header row and inline cell content (GFM tables, WAI multilevel headers).

Five frameworks make overflow the common baseline

The survey inspected official documentation and pinned default-theme source across five documentation frameworks. None of the five provides a built-in fullscreen table reader by default. Documentation links point to pages accessed on 2026-10-26; source links pin implementation commits.

Framework snapshot Default table behavior Optional/custom boundary
Material for MkDocs, 0f044dbe, 2026-10-24 Markdown tables; original table wrapped in local horizontal-scroll containers, with unwrapped fallback Sorting uses an opt-in Tablesort 5.3.0 recipe; no built-in table reader, sticky columns, or table copy (Guide, CSS).
Docusaurus Classic 3.10.2, f37f9035, 2026-07-10 MDX/GFM renders tables; Infima 0.2.0-alpha.45 applies display:block; overflow:auto Custom table views are built through MDX/remark/rehype (MDX mapping, Infima CSS).
VitePress 1.6.4, 1fc537b7, 2025-08-05; 2.0.0-alpha.21 snapshot d6d7a970, 2026-10-26 Both snapshots use block tables and horizontal overflow The 2.0 snapshot adds default tabindex="0"; Vue components handle custom behavior (1.6.4 CSS, 2.0 focus plugin).
Astro Starlight 0.42.7 snapshot af9557ea, 2026-10-24 Markdown table styling uses display:block; overflow:auto .not-content supplies a styling boundary; full-view and code-fullscreen features come from community plugins (CSS, directory).
Nextra 4.6.5 snapshot d6e80e1d, 2026-06-23 Official Table components wrap styled HTML with horizontal overflow Literal MDX <table> requires Table or the documented whitelist (Source, HTML boundary).

The inspected code licenses are MIT: Material, Docusaurus, VitePress, Starlight, and Nextra. Docusaurus documentation separately uses CC BY 4.0.

For static Hugo, the directly applicable patterns are semantic HTML, scoped scrolling, build-time focus attributes, and optional progressive enhancement. Material’s mounting code uses RxJS; Classic uses React/MDX, VitePress Vue/Vite, Starlight Astro, and Nextra Next/React.

Richer libraries add data operations and ownership

Current versions matter. DataTables 3.1.4 runs without jQuery, unlike 1.x releases. TanStack Table 9.3.1 uses constructTable, feature selection, and Store reactivity (DataTables v3 dependency boundary, TanStack v9 announcement).

Component Fit and available operations Integration and maintenance boundary
DataTables 3.1.4, released 2026-10-13; MIT, no required core runtime dependency Enhances HTML tables using an internal data model; core sorting/search/paging; optional Buttons, FixedColumns, FixedHeader, Scroller Closest candidate for later systematic table operations. Scrolling can duplicate or move headers; supports complex thead, not tbody spans (Release, package, span limit).
Tabulator 6.7.1, released 2026-10-22; MIT, no required runtime dependency Grid rendering, sorting/filtering, frozen columns, grouped columns, clipboard and virtual rendering HTML importer reads first-body td cells, dropping .matrix row-header th semantics, and does not preserve general caption/scope/headers (Release, package, importer).
TanStack Table core 9.3.1, released 2026-10-12; MIT, @tanstack/store ^0.11.2 Headless sorting/filtering/pinning/selection/spanning logic; vanilla integration is supported Maintainer owns HTML import, rendering, controls, styles, focus, export, and dialog; virtualizer is separate (Metadata, fixed package, vanilla guide).

DataTables’ FixedHeader/FixedColumns are optional extensions. Scroller requires equal-height rows, which conflicts with wrapped technical descriptions. Tabulator emits ARIA grid structures designed around cell navigation. TanStack’s spanning API requires explicit column definitions rather than importing arbitrary HTML merges. None of these packages includes a built-in fullscreen reading dialog (Tabulator’s printFullscreen targets print layout) (Scroller, Tabulator navigation, Print source, TanStack spanning guide).

Copy defaults also differ. Buttons 4.1.2 exports display data with current order/filter rules and span placeholders, without formula escaping by default. Tabulator 6.7.1 joins cell values with tabs/newlines without general quoting or formula neutralization. Tablesort 5.7.1 is a lightweight MIT, dependency-free sorting library that preserves table rows and handles Enter on headers without adding a reader, filter, or export layer (Buttons source, Tabulator Clipboard, Tablesort source).

Compare delivered bytes with their exclusions

Measurements used 14 fixed npm tarballs verified against registry integrity. Publisher-minified files were measured unchanged. Gzip used Python 3.14.8, zlib 1.2.12, gzip.compress(data, compresslevel=9, mtime=0); totals sum separately compressed files.

Measured delivery Minified bytes Gzip-9 bytes
DataTables 3.1.4 core JS + default CSS 153,523 46,181
Above + Buttons 4.1.2, FixedColumns 6.1.1, FixedHeader 5.1.2 JS/CSS 249,015 72,480
Scroller 3.1.1 JS/CSS, additional 13,300 4,389
Tabulator 6.7.1 full UMD JS + default CSS 483,872 107,512
TanStack 9.3.1 selected reader logic, local bundle 49,505 14,248
TanStack 9.3.1 all core exports plus bindings/render helper, local bundle 115,146 30,983
TanStack virtual-core 3.19.7, separate local bundle 32,872 9,745
Tablesort 5.7.1 core + number comparator 3,756 1,763

DataTables core alone is 123,516/42,338 JS bytes and 30,007/3,843 CSS bytes, minified/gzip respectively. Extension totals use the exact Buttons, FixedColumns, FixedHeader, and Scroller packages plus matching -dt CSS. Optional style-adapter JS adds 2,501/1,279 bytes to the four-package total; Scroller’s adapter adds 650/326. Initialization, dialog code, translations, JSZip/pdfmake, and source maps are excluded. Buttons copy resides in its main file, so the compatibility shim is not counted again. Tabulator’s full package includes all built-in modules; Tablesort supplies only sorting.

TanStack measurements come from local esbuild bundles. Node 26.12.0/npm 11.20.0 and esbuild 0.25.11 bundled core 9.3.1 with Store 0.11.2 using --bundle --minify --format=esm --platform=browser --target=es2020 --legal-comments=inline and production NODE_ENV. The selected entry exports constructTable, tableFeatures, sorting/filtering/pinning/spanning features, sorted/filtered row models, storeReactivityBindings, and FlexRender. The all-core entry exports the core namespace plus those two helpers; the virtual entry exports virtual-core separately. Metafiles found no external imports. The 14,248-byte gzip logic sample covers headless state management only, excluding UI, CSS, HTML import, accessibility controls, dialog, copier, and virtualizer (Core package, Store, Virtual).

The concept widens desktops much more than phones

The external prototype used a real sibling documentation-site Hugo build with an external content mount. PGEXT-specific shortcodes became explicit labels/links; parameter fragments resolved to their source URLs; support icons received “Supported” text alternatives. Added captions and .wide were experimental adaptations. Every reading mode reused the same generated table data. The build used the sibling replacement with both workspaces disabled and external destination/cache/resource directories.

The comparison shell used a 230px rail, 40px desktop main gutters, 68ch normal measure, and 1130px available canvas, styled with generated OINK CSS and fonts. A separate generated table-guide reference measured a 770px desktop / 358px mobile viewport.

Package-table scroll viewport Normal measure Inline widening Native dialog Separate page mode
Desktop Paper/Terminal 652.80px 1130px 1358px 1408px
Desktop Slate/Ink 686.38px 1130px 1358px 1408px
Mobile, all four presets 358px 358px 374px 374px

These are local qa.json measurements at 1440×1000 and 390×844, DPR 1; light/dark geometry matched within each preset. Text remained 16px and code 14px throughout. Mobile intrinsic table widths remained about 969–1009px. Desktop inline widening already fits the six-column excerpt; the dialog adds bounded height, persistent controls, and isolation from prose. Its desktop visible table height was 847.20px, versus 897.20px in page mode; mobile dialog heights ranged 711.02–741.20px as controls wrapped.

Inline widening has the lowest context-switching cost. The dialog gains desktop space while retaining a direct return to the article. The separate page gains only another 50px on desktop and no mobile width; its main advantage is reloadability and URL sharing, at the cost of losing article scroll/focus on return. None of these modes scales text or invokes the Fullscreen API.

Chromium 151.0.7922.34, through Playwright 1.62.1, checked 16 preset × color × device combinations, each with three reading concepts plus baseline. The package sample passed original-node identity, unique IDs, caption/header scopes, loaded styles/fonts, title initial focus, Enter open, Escape/Close return, both Tab boundaries, stable toolbar, separated control/table regions, and page-mode reload. No page overflow was recorded; three additional mobile samples and a 320px Terminal/dark overflow probe extended the scope. A separate inspection preserved link targets and selection and restored inline horizontal scroll to 180px. Screenshot review covered 48 concept captures and contact sheets, and a Paper/light/desktop dialog axe scan reported zero violations.

The parameter and version samples’ first columns occupied 59.3% and 58.3% of the 374px mobile reader, motivating an optional mobile width guard. Preset styling remained distinct: Paper’s Plex Sans and restrained framing, Slate’s Inter and rounded controls, Ink’s flat sharp geometry, and Terminal’s mono chrome with Plex Sans prose.

Reproduction uses the external builder, source manifest/hashes, generated reference output, QA/inspection JSON, and screenshot set with Hugo 0.168.0+extended+withdeploy, Go 1.27.3 on macOS ARM64, Node 26.12.0, npm 11.20.0, and Python 3.14.8.

This prototype scoped evaluation to Chromium desktop and touch emulation with single-row headers; Firefox/WebKit, screen-reader testing, forced-colors, 200/400% zoom, merged headers, coordinator integration, and print-while-open belong to implementation acceptance.

A small reader preserves more than a generic grid

The recommended first stage is an explicit Read table action, a native dialog, and one original-node move/restore lifecycle. Hover-only entry is undiscoverable on touch and keyboard; whole-table click competes with selection, links, and scrolling. A visible title/Close control, named scroll region, initial title focus, contained Tab navigation, Escape, and guarded focus/scroll return are core requirements. Reuse OINK’s surface coordination conventions. A near-full dialog is distinct from browser fullscreen, whose standard excludes <dialog> as the request target (APG dialog, Fullscreen Standard).

Retain caption/th/scope/headers and original IDs. Moving the original table avoids clone collisions and divergent state, while inherited styling, outer numbered captions, external ARIA references, and footnotes still need explicit handling. Native tables can contain links and sorting buttons without becoming ARIA grids. Start with single-row sticky headers; leave complex headers scrollable without stickiness until offsets are validated. Comfortable spacing is default; compact mode changes padding while keeping font size. Optional first-column pinning must leave useful mobile reading width (APG table, APG grid, header semantics).

Keep .wide/.td-table--wide layout-only, .full-width bounded by article canvas, .matrix semantics intact, and .fields ineligible. No-JS content remains readable; Print expands static tables, Markdown retains source, and RSS retains its static/summary boundary without leaking reading controls.

If TSV copy is added in the future, it should copy the whole eligible table, including offscreen rows/columns, in author order: links become labels, code text remains, hidden controls/decorative glyphs are excluded, and meaningful icons use explicit text equivalents. It should reject hidden data rows/columns, nested tables, unequal row lengths, multiple header rows, or any DOM rowSpan/colSpan other than 1 (including rowspan="0"), using TAB between cells, LF between rows, normalized in-cell whitespace, and a leading apostrophe for formula-like cells starting with = + - @ (HTML table model, DOM text, Clipboard 2026-06-24 Working Draft, OWASP spreadsheet-injection limits).

A focused native reader leaves data-grid adoption possible later while avoiding a second data model. See the accepted decision for the shipped implementation boundary.

Accepted direction after review

Maintainer review selected explicit {.wide .expand} composition and a direct corner action, revealed on hover/focus and visible on touch, omitting the above-table toolbar, ellipsis menu, and TSV copy. The accepted decision and current contract define the shipped behavior.

7.9 - Design proposals and PRDs

The canonical bilingual home for OINK PRDs and designs that remain under evaluation.
Non-normative material

A proposal describes behavior that may not exist. The contracts, accepted decisions, implementation, and owning checkers define current behavior. Never use a proposal as a configuration reference.

This section contains the authoritative 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
OINK CLI and the next product stage Independent Go repository and first-stage boundary accepted; local CLI candidate implemented, not publicly released; later theme, migration, adoption, versioning, OpenAPI, and platform stages remain proposals
Visual presets and appearance switching Paper/Slate ship in v1.2.0; Ink/Terminal ship as explicit opt-in experiments awaiting design acceptance; see the accepted decision and dated acceptance record

Component refinement, unified diagram reading and local search refinement ship in 1.4.0 and now live under accepted decisions. Translation comparison ships in 1.5.0 as an accepted decision; its draft is retired.

The table-reading draft is also retired: the accepted decision and component contract define the narrower, .expand behavior released in OINK 1.4.0.

The bulk agent-index proposal was retired after its outputs were released. Architecture defines the accepted behavior. Agent-ready output gives the user steps. The Book publication proposal was retired after BookManifest and the EPUB/PDF tools were released. The accepted behavior is documented in Architecture and Writing a book; dated downstream adoption evidence is recorded in Consumer evidence. Pending consumer adoption does not keep an upstream design proposal active. Git history retains both proposal drafts.

The generated-configuration-schema proposal completed its lifecycle and was retired: Configuration defines the required behavior, the generated configuration schema decision records the accepted rationale, and Git history retains the draft text.

CLI workspaces and adapters

Explicit workspaces and optional adapters remain in the current reduced CLI. The current contract defines the command boundary, and the R1–R8 and A18 record preserves the historical acceptance evidence. The finite maintenance roadmap remains retired from active navigation.

Where a new PRD goes

Create one English-primary page and its Simplified Chinese peer:

content/docs/design/proposals/<slug>.md
content/docs/design/proposals/<slug>.zh.md

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:

  1. status, owner, date, and affected contract surface;
  2. context and evidence;
  3. goals and explicit non-goals;
  4. proposed behaviour and output/accessibility/security boundaries;
  5. compatibility and migration impact;
  6. implementation and owning-checker plan;
  7. acceptance criteria and open decisions;
  8. a decision log for later changes to the proposal itself.

For a large experiment, you may add a dated page under ../research/. Keep temporary logs and generated artifacts outside Hugo content and Git.

Lifecycle

draft proposal
    ├── rejected/superseded → remove from the active tree; preserve Git history
    └── accepted
          ├── implementation + owning checker
          ├── affected EN/ZH contract
          ├── accepted Design decision when rationale is durable
          └── changelog, migration, and user docs when their audiences need them

Acceptance does not make the PRD a second contract. Move accepted behavior to the owning contract, stable rationale to Decisions, and user steps to the applicable guide, then retire the proposal from active navigation.

Review gate

Before implementation, confirm that the proposal does not duplicate an existing shell, resolver, component family, or data authority. If the design changes during implementation, update this bilingual proposal before changing the code. Acceptance requires the focused theme checker and the real documentation site. It also requires rendered EN/ZH, applicable outputs, accessibility checks, and responsive review.

Read-only Studio candidate

Studio is removed from the current CLI on 2026-10-04. Use oink dev, an ordinary editor, and structured inspect/check reports. The R7 record preserves historical acceptance of the earlier browser implementation.

Reviewed editing

General source editing is removed from the current CLI. Guarded new, move, review records, and baseline plans remain. Old editing plans are rejected. The R8 record remains historical evidence rather than the current command API.

7.9.1 - Backlinks and knowledge graph

A draft three-stage design to generate backlinks and local or global graph views from ordinary Hugo links.
G1 implemented; G2/G3 remain draft

On 2026-08-27, all G1 open decisions were resolved and G1 (static backlinks) was accepted. The theme’s main branch implements G1, which is included in OINK 0.8.0. The local and global graphs (G2/G3) remain drafts until G1 supplies real-world evidence. Their names and configuration are not public API until acceptance.

Premise

Reverse navigation and a view of connected pages come from the link graph, independently of [[wikilink]] syntax. Hugo already accepts ordinary Markdown links and ref / relref, so OINK can derive a graph from content authors already write, without another parser, Goldmark extension, or authoring syntax.

The first value is backlinks, not visualization. A static inbound-link list is useful without JavaScript and remains readable in print and Markdown, making an interactive graph an optional enhancement to an already 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 neighborhood;
  • 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 requires separate acceptance. G1 does not depend on G2. G2 does not require 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. After removing fenced code and inline code, it extracts ordinary Markdown links and ref / relref, resolves internal pages, and removes fragments for page identity. Self-links are excluded, and repeated references produce only one edge.

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;
  • ref and relref are included;
  • each language produces an independent graph;
  • an unresolved derived edge warns or is reported by the focused checker without making ordinary hugo server unusable.

Raw source scanning has known omissions: it may miss a URL in a custom shortcode parameter or raw <a href>. These omissions must be documented rather than hidden behind a claim of a complete semantic graph.

G1 renders an aside group in the right rail beside the table of contents and taxonomy clouds, placing what points to this page beside what is on this page. The group is expanded by default and shows the first eight entries, with the rest behind a native disclosure so a heavily referenced page cannot occupy the entire rail. The site key params.ui.backlinks is a bare boolean, off by default. A page overrides it with the prefix-free front matter key backlinks, which a section can cascade. Entries sort by stable page path: a language-independent order that naturally groups related paths without a second ordering authority. The group uses ordinary links and is omitted when there are no inbound pages.

G1 silently removes derived edges that it cannot resolve. This is a documented limitation of its role as a local navigation enhancement: site-wide broken-link reports would duplicate existing warnings.

Print and Markdown retain the readable list. RSS omits the list unless feed-level research shows that backlinks improve article feeds without adding unnecessary site navigation.

Interactive graph boundary

G2 uses the locally vendored ECharts graph series, with the current page at the center and its direct inbound and outbound neighbors forming the default depth. A hard node limit prevents unreadable or expensive views. Keyboard focus, text alternatives, reduced motion, forced colors, narrow screens, and print are acceptance requirements rather than later polish.

If JavaScript or ECharts is unavailable, G1 remains complete and visible. Only pages that render a graph load the runtime. The runtime must use the existing feature-bundle key to prevent asset-cache collisions between pages with different features.

Global output

G3 may add a dedicated graph page and an opt-in JSON output. The proposed JSON schema contains a version, language, nodes, and directed edges with stable URLs. It would not expose local file paths or unpublished pages. Generate this output from the same index as G1 and G2 so all three representations remain consistent.

Compatibility and migration

Ordinary Markdown remains unchanged and needs no content migration. Configuration names remain undecided until a prototype proves the minimum required surface. Every interactive or global output is off by default. A static backlink list may be considered separately because it supports local navigation without network access or browser state.

Acceptance criteria

Acceptance requires a focused graph checker, extraction fixtures, and HTML/Print/Markdown goldens. It also requires strict-build negative cases, browser accessibility and responsive tests, and a real bilingual-site build. Measure performance on a representative large site.

Open decisions

All G1 questions are resolved (see the decision log). G2/G3 must resolve these remaining questions:

  1. Does the local graph expose one depth or a tightly capped second depth?
  2. Which page metadata, if any, is useful enough to enter graph JSON?
  3. 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.backlinks is a bare boolean, off by default, and pages override it with backlinks. Shell types do not restrict this policy because it belongs to the site and page. A single stable-page-path sort replaces the section → weight → title chain: one deterministic authority is enough for reverse navigation, while a multi-level sort would introduce a second navigation authority. Unresolvable edges are silently removed, without warnings, as a documented limitation. G2/G3 and graph data output still require production evidence.
  • 2026-08-27: Design review moved the block from the page end to the right rail. Backlinks are page metadata and belong beside the table of contents, while the page end is the reader’s completion zone: share, feedback, provenance, pager, and comments. The rail group limits visible entries to eight, with the rest behind a native disclosure.

7.9.2 - Media convergence

A draft for the remaining convergence between content images, numbered figures, Landing media, and featured-image selection.
Partially implemented

The theme’s main branch implements M1 (the shared media-result contract) and M2 (Landing resource metadata). M3 is resolved as option 2. Only native Markdown images support processing. The full fig source form remains a container. Its parameter list deliberately excludes command/options. M4 (compatibility retirement) remains open until consumers are inventoried. The sections below retain 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 supply intrinsic dimensions and processed derivatives. data-td-image-zoom marks HTML Zoom eligibility, and the build-time detector checks only this theme-emitted marker.

Standalone Markdown images can combine a caption or Book numbering with processing and a link. Numbered image figures share td-figure and td-book-figure semantics. Landing media uses the shared URL trust policy, while featured images deliberately use a ranking resolver because they 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 does not obtain the same page-resource metadata and processing result as body images. Featured-image selection and explicit image resolution return different result shapes, some compatibility class names remain in markup, and Book’s full fig form cannot express every processing option that the native image hook supports.

The design question is therefore no longer “replace seven image entry points,” but 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 fig form 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 that the content and representative-image resolvers return, then extract their shared fields into one internal media-result contract. Source ranking stays in the featured resolver and source resolution in the content resolver. This internal refactor must preserve output bytes.

M2 — Landing resource metadata

Allow Landing items to resolve eligible local resources through the media contract, gaining intrinsic dimensions and the shared URL/security decision. Explicit width and height in Landing data retain precedence. Remote and static sources remain valid without claiming metadata for a processable resource.

M3 — Full figure capability decision

Choose one of two answers:

  1. add processing arguments to the full fig source form and normalize them through the same processing helper; or
  2. keep processing exclusively on native Markdown images and document full fig as the container for arbitrary numbered block content.

No implementation should leave both options half-supported. In both forms, Markdown/LLMS must consistently link to the documented source or derivative.

M4 — Compatibility retirement

Before removing old image-element classes or attributes, inventory downstream CSS and JavaScript. If a compatibility name is still used, retain it for a documented release window. Alternatively, migrate the owning site in the same release.

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

For each phase, collect byte-level HTML and Markdown evidence. Run content and Landing resolver tests, URL/security checks, image-processing tests, and Book target checks. Run gallery/Zoom browser tests and real-site EN/ZH narrow-screen review. Acceptance requires an explicit M3 capability choice.

Open decisions

  1. Is one shared result struct enough, or would a common lower-level URL/resource record keep resolver ownership clearer?
  2. Should Landing consume resource attribution, or only dimensions and URL?
  3. Does full fig processing solve a real consumer need now that native images support numbering, captions, links, and processing together?
  4. Which emitted compatibility names are still used by real consumers?

7.9.3 - OINK CLI and the next product stage

The accepted independent CLI boundary and local first-stage scope, with later adoption, theme, migration, and content-model work still proposed.
Local first-stage candidate; later roadmap remains draft

The independent Go repository pgsty/oink-cli and first-stage development were authorized on 2026-09-29. Its six commands now have a local 0.1.0-dev implementation, with final local acceptance recorded separately. Current behavior belongs to the CLI decision and result contract and CLI introduction. Theme 1.2, its tooling descriptor, Docsy migration, version lifecycle, OpenAPI, MCP, and Studio remain proposals.

Record Value
Status Repository choice and first-stage scope accepted. Local candidate implemented and validated. Later roadmap remains draft
Owner OINK maintainers
Date 2026-09-29
Scope OINK theme, independent CLI, existing Starter, and documentation site
Affected contracts Architecture, configuration/diagnostics, outputs, migration, and later version navigation/API content
Source snapshot Theme HEAD 3a18234, documentation HEAD 85f16bf, Starter HEAD 137843b, plus the explicitly identified local work below

Recommendation

Create a separate oink-cli repository, publish one executable named oink, and keep OINK as the single product identity. The theme renders content. The CLI helps users initialize, inspect, validate, upgrade, and eventually migrate their sites. The documentation site continues to own public guides, bilingual design records, and integration acceptance.

The CLI contract owns first-stage behavior, while the dated acceptance record lists executed checks and remaining limits. This roadmap stays active for its later stages and adoption targets.

The first release should improve the path from an existing repository to a reliable publication. Its four main workflows are doctor, check, init, and upgrade. dev and build may provide small, transparent Hugo shortcuts. A supported Docsy migration path follows evidence from actual input repositories. Version lifecycle and OpenAPI generation come after that first usable release, with one major content-model project active at a time.

Keep the theme usable without the CLI. Commit or otherwise deliver generated Hugo inputs. After you remove the CLI, ordinary Hugo must still be able to build the site. Regenerating those inputs remains a separate operation.

Product position and target user

Recommended public description:

OINK is a local-first documentation toolkit built on Hugo, publishing engineering knowledge for readers and agents.

Retain “Hugo theme” in installation and discovery pages because it describes what users install, while “knowledge compiler” describes the internal architecture. A new name should keep the current Markdown/Hugo workflow clear.

Prioritize Git-oriented maintainers of open-source infrastructure, developer tools, and multilingual technical documentation. They need to get a site working, diagnose failures, upgrade safely, and move content without losing URLs or meaning.

For the first stage, non-goals include a visual CMS, hosted accounts, a deployment control plane, a package marketplace, an LLM runtime, a semantic-search service, and a new rendering engine. Books, blogs, and landing pages remain supported, but their feature catalogs do not drive this roadmap.

Evidence and changes to the research recommendation

This proposal considers the supplied strategy report and checks it against the local implementation, bilingual Design section, Starter, and current primary documentation.

Observation Product consequence
OINK already has a public Starter, generated configuration schemas, migration scripts, publication tools, and focused theme checkers Provide selected workflows as product features. Do not create a second implementation of every tool
The front-matter schema deliberately omits type constraints Strict checks must follow the owning resolvers and actual Hugo output
Existing version support includes a cross-site menu, archive banners, and optional path concatenation Focus on lifecycle and reliable page correspondence
Current version documentation explicitly uses independent Hugo builds Preserve that model initially. Do not silently add a multi-version renderer to one build
OpenAPI widgets become specification links outside HTML and have documented accessibility exclusions Static, accessible endpoint content is a concrete future improvement
Backlinks are implemented. G2/G3 remain draft Graph visualization remains a draft proposal
bin/update-consumers.py exists in this working tree as local maintenance tooling Its release-resolution and preservation rules serve as design input for oink upgrade

The supplied report emphasizes adoption and an optional tools layer. Four changes make its recommendations implementable:

  1. Put safe upgrades alongside initialization and diagnosis. Existing users have an immediate, testable maintenance need.
  2. Separate maintainer regression checkers from consumer checks. A synthetic-fixture checker is not automatically a general-purpose site validator.
  3. Treat migrations as supported input profiles with explicit boundaries rather than complete Docsy or arbitrary MDX conversion.
  4. Replace the simultaneous versioning/OpenAPI/graph/platform program with sequential decisions.

Existing tools illustrate the workflow tradeoffs. Mintlify’s CLI exposes preview, validation, and link checking. Nimbus combines scaffolding and agent-readable output while remaining pre-1.0. Docusaurus makes version snapshots explicit and warns about their maintenance/build cost. Copying Nimbus’s full source-owned UI model would move upgrade work to OINK consumers. Use that pattern for small generated recipes. Keep the upgradable theme module.

Why a separate repository

Option Benefit Cost Decision
Extend Python scripts under theme bin/ Fastest small maintenance improvements. Tests accompany the change Difficult installation and distribution. No unified public command contract Retain for internal and historical tooling
Put cmd/oink in the theme’s root Go module One checkout and atomic source edits Mixes a Hugo asset module with application dependencies, binary releases, and consumer support Do not choose for the public CLI
Use an isolated Go submodule in the theme repository Atomic repository changes without sharing Go dependencies Separate module tags and releases still need management. Code can more easily depend on unpublished theme internals Viable fallback for a prototype with a fixed time limit. Not the preferred product repository
Create pgsty/oink-cli Clear executable boundary, independent releases, no need for users to clone theme internals Requires explicit compatibility and cross-repository acceptance Accepted. Local Go repository created

This decision assigns release and maintenance responsibilities rather than claiming that a monorepo is technically impossible. A nested module can isolate dependencies, and separate repositories still require coordination: a renderer change may need two pull requests, paired contracts, and a compatibility test. OINK already operates a theme/site/Starter split, so that cost is acceptable if the public boundary stays small.

Theme and CLI releases must not share a forced version number. A proposed oink CLI 0.1.x should work with a tested OINK 1.1.0 baseline and the next supported theme release. Declare compatibility per capability. Report unsupported functionality. Do not assume that every schema from the newest theme is valid for every older site.

Do not create separate repositories for the linter, migration engine, OpenAPI generator, or a shared SDK now. They can begin as internal CLI packages. The binary can be built in Go without importing Hugo’s internal Go packages or making the theme module depend on the CLI.

Responsibility map

Surface Owner Boundary
Layouts, components, style, navigation, search, accessibility, output semantics pgsty/oink Executes inside Hugo and the static site
Theme defaults, owning resolvers, generated schemas, output schemas pgsty/oink Authoritative theme behavior and its projections
Theme implementation checks and narrow invalid-input fixtures pgsty/oink Remain maintainer tools, even if implemented in Python or JavaScript
Environment diagnosis, consumer checks, initialization, upgrade; later migration transformations pgsty/oink-cli Initial commands implemented locally; migration remains proposed
OpenAPI parsing and generated source, later version snapshot orchestration Proposed later pgsty/oink-cli capabilities Produces ordinary Hugo inputs; does not own final rendering
Small official site skeleton and language profiles pgsty/oink-starter Single source for CLI initialization. Pinned snapshots can be embedded in CLI releases
Guides, examples, PRDs, accepted rationale, EN/ZH integration/browser review pgsty/oink.pgsty.com Continues as the canonical public documentation and regression site
Hosting credentials, account setup, deployment authorization Consumer workflow Existing CI/provider tools. The first CLI release does not deploy
                          optional oink CLI
                  init / doctor / check / upgrade
                      later migrate / generate
                               |
                               v
              user-owned Markdown + Hugo config + data
                               |
                     Hugo Extended + OINK theme
                               |
               HTML / Print / Markdown / search / indexes
                               |
                  readers / agents / optional adapters

The CLI reads Hugo’s effective configuration, the resolved theme’s published contract artifacts, and rendered outputs. It must not guess the final page tree from filenames or maintain a second navigation resolver. Hugo config exposes effective configuration. Module inspection must also account for replacements, workspaces, and vendoring.

Next theme release: proposed OINK 1.2

This section remains draft. The local CLI candidate works against published OINK v1.1.0. The first-stage CLI decision neither accepts nor requires a 1.2 release or a new tooling descriptor.

Set an adoption objective for this release: a site can explain its configuration and output capabilities to tools, and upgrade without adopting a new authoring model. The release should be small enough to ship independently of the broader roadmap.

Priority Requirement Acceptance
P0 Package a small, versioned tooling descriptor beside existing schemas, describing available schema/output contracts and supported toolchain boundaries Check the descriptor against its owning implementation. The CLI can inspect it from the resolved module. No extra per-page output or runtime request
P0 Make selected high-value configuration diagnostics actionable: parameter, invalid value, expected form, fallback, and owning guide Cover actual onboarding failures such as Goldmark/output/language configuration. Retain ordinary preview warnings and strict publication failure
P0 Preserve one navigation and Markdown authority across reader and machine outputs Existing output and navigation checks continue to cover language, ordering, subpath, and opt-in behavior. No duplicate CLI renderer
P0 Release with a tested Starter snapshot and a reviewed downstream adoption record Validate published module resolution separately from sibling replacement builds. Record consumer pins and deployments independently
P1 Add stable identifiers to the small set of diagnostics consumed by tooling, if the prototype shows they are necessary A focused owning checker verifies each identifier. The CLI never depends on parsing all human warning prose

The descriptor contains release metadata without becoming a configuration authority. Configuration schemas remain generated from current authorities, whose resolver/checker continues to own optional shape validation. A generic renamed-key registry would conflict with the current diagnostic decision. Migration transformations therefore belong to explicit CLI profiles rather than a permanent compatibility path in templates.

No new visual component family is required for 1.2. Correctness, accessibility, and already-demonstrated regressions can still justify changes. Existing media work keeps its own acceptance scope. This roadmap does not require every draft to be complete before release.

First CLI release: proposed 0.1

The commands below are implemented in the local 0.1.0-dev candidate. Their current flags, result semantics, and limits are defined by the CLI contract and CLI introduction.

Command User outcome First-release boundary
oink doctor Understand why the site cannot run or why its environment differs from CI Inspect Hugo Extended/version, module pin and effective source, Starter/toolchain requirements, essential configuration, and enabled outputs. No repair by default
oink check Know whether a publication build and its local references are valid One strict build into isolated output, then check local links/anchors/assets and enabled machine outputs. Report coverage and unsupported checks
oink init my-docs Start a small, neutral site that can be maintained without the CLI Generate from a pinned Starter snapshot into a new/empty target. Select the supported language profile and explicit theme pin
oink upgrade --to <tag> See the exact changes needed for a theme upgrade Preview first. --write applies a reviewed scope after validation. Protect unrelated module dependencies, user changes, and vendored output
oink dev / oink build Use a memorable entry point without learning a second build system Thin Hugo invocations with visible effective arguments. build uses publication strictness. Direct Hugo remains fully supported

check is the single quality entry point. Avoid separate overlapping lint, validate, audit, and check products in 0.1. Later --scope options can separate source hints from rendered-output validation when users need the distinction.

Diagnosis and quality scope

Start with failures that users can act on and that checks can identify reliably: wrong toolchain, unresolved theme, malformed required configuration, missing local link targets/anchors/assets, and inconsistent enabled output references. Resolve routing and anchor truth from Hugo’s output, including language and base-path handling. Do not label a valid custom front-matter key as invalid merely because an editor schema does not list it.

Disabled optional outputs are not missing-output errors. The local candidate does not check translation completeness. Any future completeness rule must use the site’s declared languages and coverage policies. Duplicate titles, orphan pages, missing descriptions, prose style, and freshness remain later optional observations after real false-positive review. Browser accessibility and interaction checks remain in the site Playwright/axe suite.

The local candidate freezes oink.result/v1. Structured diagnostics have stable rule IDs, severity, known locations, explanations, actions, and explicit coverage. JSON stdout contains only the result. Logs go to stderr. No command waits for input. Exit meanings are 0 for completed work with no blocking findings, 1 for policy findings, and 2 for required incomplete work. Required unsupported checks cannot succeed. The result contract owns the detailed fields. Build-derived findings never receive invented line numbers.

Keep raw Hugo errors available as subprocess evidence. A future SARIF export can project from the same result without changing rule semantics.

Upgrade and file preservation

The existing consumer-upgrade script provides local precedents: distinguish declared pin from resolved version, disable both workspace mechanisms for release verification, recognize module replacements, and inspect _vendor. Port those behaviors with focused tests inside the standalone Go binary.

For 0.1, upgrade one explicitly selected site. Multi-site fleet discovery stays with the maintainer script until a consumer need is demonstrated. A normal check may examine an intentional local theme replacement. check --release must verify the declared published release without those replacements. It should report a conflicting go.mod replacement rather than edit it away.

Preview the proposed touched files and verify the prospective upgrade before applying it. Back up only those files, refuse changes to files that changed since the preview, and preserve unrelated dirty work. A failed operation must describe what was and was not applied, with a recovery path that does not overwrite subsequent edits. A dirty repository is not a reason to block read-only diagnosis. Refreshing vendor content requires a separate explicit action.

Do not commit, push, deploy, alter global agent settings, or install system packages as side effects of initialization or repair. Initialization creates the requested named directory. Changes to existing files default to a preview.

Distribution and offline behavior

The local candidate currently provides a tested source/Make installation path and archive preparation on macOS arm64, with other archive targets cross-compiled. A published Homebrew formula and public download/tag installation remain future distribution work.

Use a Go executable with release archives/checksums and a Homebrew installation path. Initially qualify macOS and Linux on tested architectures. Mark other targets experimental until you validate their filesystem and process behavior. The installed CLI itself needs no installed Go toolchain, Python, Node, or account. Hugo remains an external renderer. Initial module resolution still needs the site’s documented Git/Go/Hugo toolchain.

Embed or ship an exact, licensed Starter snapshot for deterministic initialization. Do not fetch a moving main branch on every invocation, and do not maintain handwritten CLI copies of Starter configuration. Check embedded/template drift during CLI release.

Distinguish a cold installation from offline operation. Downloading Hugo, the theme, or an uncached template requires connectivity unless supplied locally. Once dependencies are present, local diagnosis/check/build paths must work without external services. On a cache miss, an offline request must fail clearly without fetching silently. External URL checking, remote specifications, and other network actions are separate opt-ins. No default telemetry or background update check is required.

Migration: the first expansion

Start with a documented Docsy input profile selected from actual candidate sites, reusing the current migration fixtures and report model as evidence. Validate configuration, navigation, assets, languages, and routes as well as Markdown syntax.

Proposed workflow: oink migrate --from docsy --source <site> --output <new-site>. Assess the migration before writing. Apply it only with an explicit flag and a separate destination. Every source item receives one primary status: unchanged-compatible, transformed, manual-review, or unsupported. Counts must reconcile, with reasons and source locations. Custom templates and dynamic behavior remain visible manual work.

Acceptance means source preservation, idempotent supported transforms, no edits inside literal code examples, valid local references, and an explicit old-to-new route report. Prefer unchanged URLs. URL changes require a redirect plan for the hosting target.

Arbitrary JSX, imports, and embedded React/Vue are programs. Do not execute untrusted source to infer their meaning or silently drop unsupported constructs. Start a second framework only after the first profile is reused successfully without maintainer rescue. Full MDX migration is a later product investment.

Next content capability: version lifecycle

After the first CLI is useful, version lifecycle is the default next candidate because it extends OINK’s existing independent-build model. Move OpenAPI ahead only if real API users provide the stronger repeated need. Do not implement both foundations simultaneously with one primary maintainer.

Theme responsibilities: consistent version identity in the reader surface, reliable page switching, archive status, and correctly scoped search/machine outputs. CLI responsibilities: inspect/list versions, prepare a snapshot, validate page correspondence, and change declared lifecycle state. Use oink --version for the executable. A future oink docs version ... namespace would distinguish documentation versions.

Prefer a small version manifest with version label, source reference, base URL, status, and default selection. Keep independent per-version builds and existing external archives. CLI-managed manifests may produce checked-in Hugo configuration; in that mode, authors edit the manifest and configuration becomes a checked projection. Existing manually managed params.versions remains supported. The initial prototype must settle this projection before freezing its format.

Page correspondence needs a logical page key scoped by documentation family, language, and version. Reuse a suitable existing translationKey or explicit stable key before inventing universal UUIDs. You should disclose missing peers and link them to a defined version/section landing page. You should not fabricate an equivalent page or concatenate an unchecked URL. Route aliases handle moves separately from page identity.

Distinct historical content should ordinarily keep its own canonical URL. Do not point every old page at the newest version. Language alternates must refer to genuine translated peers in the same version. Default search and agent bundles stay inside the selected language/version. A cross-version collection, if later needed, is explicit. Archiving preserves the source and records how the built artifact is retained. It does not delete the source or silently redeploy an immutable archive.

NAVJSON v1 currently has closed object schemas. Adding version or identity fields therefore requires an explicit new schema/output contract or a separate artifact, not a supposedly harmless addition to v1. No change to current page identity is justified merely to reserve space for a future graph.

Following capability: static OpenAPI reference

The first OpenAPI product should be a read-only static reference generator. The CLI parses a local specification and supported local references, emits ordinary Markdown/Hugo data, and records source provenance. The theme supplies accessible semantic presentation and the existing output pipeline. Ordinary Hugo then builds HTML, Print, Markdown, search, and agent indexes from those generated pages.

Start with operations, parameters, request/response bodies, and linked schema descriptions. After a parser prototype, explicitly declare supported OpenAPI versions and constructs. Unsupported constructs cannot disappear silently. Scope operation identity to the API/specification. If operationId is missing, the generator can derive a method/path key and warn about identity changes. Reused operationId values across different APIs must not collide.

Keep human-authored guides separate from generated facts. Generation must be deterministic, record source hashes and generator version, detect stale output, and refuse to overwrite unexpected human changes. Check generated source into the site, or supply it as a versioned build input, so rendering itself remains CLI-independent. Resolve remote references in an explicit preparation step. Normal generation must not follow arbitrary external URLs.

Acceptance requires a real user specification in addition to a toy example, complete accounting of supported operations, cyclic-reference handling, stable routes, semantic content in all selected outputs, and accessibility checks with no inherited Swagger/Redoc exclusion for the new static renderer. Measure a representative large specification before promising a throughput target.

Keep existing Swagger/Redoc integrations compatible. Interactive requests, credential handling, SDK generation, mock servers, and an API testing platform are outside this first compiler increment.

Architecture and compatibility rules

Keep CLI internals small: command handling, Hugo/process integration, diagnostics, template loading, and bounded file changes. Add migration and OpenAPI packages when their stages begin. This is a suggested decomposition, not a plugin ABI or public SDK.

Three boundaries need versioning: the CLI’s machine result format, the theme’s public schema/output contracts, and each supported migration/generation input profile. Prefer capability checks over a single “requires newest OINK” rule. A newer unsupported schema must produce a useful compatibility diagnosis.

The CLI cannot import a sibling theme’s private Python modules, depend on a local checkout layout, or download executable checks at runtime. Port selected consumer operations and test their behavior. Keep template-internal checkers in the theme, and make future changes to exposed consumer rules update their owning contract. Keep existing scripts available during the transition. Remove duplication only after the replacement covers supported cases.

No CLI configuration file is required initially. Hugo retains rendering configuration. If repeated usage later justifies a tool-policy file, it may hold ignored paths, rule severity, or a reviewed baseline, but must not mirror params.ui, navigation, languages, or module pins. A reviewed lint baseline cannot suppress a failed Hugo build, an unreadable input, or an unsupported required check.

Roadmap and staffing assumption

Stages 0 and 1 have a local first-stage candidate, with validation details in the acceptance record. The 8–12 week first-stage planning envelope below assumes roughly one full-time implementation owner plus part-time documentation/review help. Toolchain qualification, recruitment, and bilingual review take time; reduce scope before adding parallel work.

Stage Timing from approval Deliverable Exit evidence
0: establish the boundary Weeks 1–2 Accept repository choice; collect failure examples; define result format and supported baseline; prototype read-only doctor/check against current 1.1.0 Starter plus at least three varied real repositories; failures and coverage omissions recorded
1: complete the daily workflow Weeks 3–6 Doctor/check, pinned init, thin dev/build; prospective single-site upgrade and file-preservation tests New users can diagnose a seeded failure; ordinary Hugo still builds generated sites; no unexplained source changes
2: release a bounded product Weeks 7–12 Proposed theme 1.2 + CLI 0.1; compatibility record; docs; qualified installation; limited Docsy migration assessment/pilot First-run study and repeat upgrade use; migration limitations are explicit; published pins and consumer adoption checked separately
3: validate migration and one content model Months 4–6 Harden the first migrator; choose version lifecycle or OpenAPI based on users; propose theme 1.3 / CLI 0.2 as needed At least two real repositories use the chosen workflow; accepted contract precedes compatibility promises
4: earn expansion After month 6 The other content capability, then optional recipes/provenance or agent transport where justified Repeated use and maintenance capacity

If stage 2 overruns, remove migration writing from that release and keep its assessment report. Do not cut upgrade preservation, truthful diagnostics, or independence from the CLI. If no independent team wants the migration profile, stop expanding framework coverage and investigate onboarding/positioning instead.

Freshness/ownership is a later optional quality feature, initially a report whose findings users actually act on. A modification date must never be presented as verification. Ship a handful of useful official page recipes before a registry. Graph G2/G3, MCP, analytics adapters, executable examples, Studio, and managed services each need a specific user problem and capacity decision; this roadmap assigns them no delivery dates. Existing static agent outputs make MCP less urgent than adoption.

Acceptance and product measures

Area Initial target or required property
First successful use With prerequisites already installed, at least 4 of 5 unfamiliar target users reach local preview and a passing strict check within 15 minutes without maintainer intervention; Record cold installation separately
Maintenance value At least three real sites use diagnosis/checks and repeat a supported upgrade; Every failure has an actionable report
Diagnostic precision Triage all blocking findings in the pilot; aim for less than 5% false positives in an explicitly counted labeled sample
Integrity Zero silent content loss; every migration input is accounted for; repeat transforms have no diff; user edits and unrelated dependencies survive
Independence Initialized/generated sites build through ordinary Hugo with provisioned dependencies; optional CLI and output features remain optional
Offline behavior Run the qualified local workflow with outbound access denied after provisioning; record cache misses and explicitly networked features separately
Compatibility Current tested theme baseline and candidate release, pinned site regression toolchain, root/subpath, and EN/ZH cases
Adoption Seek five independent pilot teams within the first stage, and track which reach production and continue using the result at 30/90 days

Use independently maintained production sites as the main adoption measure, verified through public references or voluntary user confirmation. A stable documentation site should not stop counting merely because it has no commit in 60 days. Track theme upgrade recency separately from retention. Stars, download counts, internal consumer count, and agent-generated volume serve as supporting signals.

Track time to first local success, time to production, upgrade effort, and manual migration effort separately. Do not add default telemetry to obtain these measures.

Implementation ownership and validation

Change Owning validation
Tooling descriptor and schema compatibility A focused theme descriptor check plus generate-config-schema.py --check and relevant parameter checks
Diagnostics exposed to consumers Owning resolver/checker cases; CLI diagnostic result/exit-code tests
Existing output behavior check-agent-indexes.py, output/security/navigation checks appropriate to the changed surface
Init and upgrade CLI tests against pinned Starter snapshots and repositories with replacements, vendor content, unrelated dependencies, and dirty target files
Migration Ported/extended transform cases, source-preservation and repeat-run checks; reviewed real-site route/content evidence
Future version/API presentation Theme output checks plus bilingual documentation-site integration, browser, accessibility, responsive, and visual review

Run the smallest owning check first. Public behavior changes still require implementation, checker, and both language contracts in one coordinated delivery. Use the sibling site’s make check, make browser, and make dev workflow for actual integration and visual acceptance.

For a release, separately record local checks, commits, tags, published module/binary resolution, consumer pins, and deployment. Theme release adoption continues through the maintained consumer inventory procedure. Use a coordinated issue/checklist to connect the repositories.

Open decisions and stop conditions

Repository selection and first-stage implementation are settled locally. Remaining release decisions include qualified platforms, the public distribution channel, compatibility claims justified by executed evidence, and independent pilot recruitment.

Result and exit semantics are frozen for the local candidate in the CLI contract. The minimal theme descriptor and stable theme warning identifiers remain separate proposals. Before a versioning beta, settle manifest projections, archive retention, and page correspondence. Before an OpenAPI beta, settle the supported spec subset and generated-source ownership.

Reconsider the separate CLI investment if pilots only need a tiny maintenance script, if rules must repeatedly duplicate template semantics, or if maintaining distribution consumes more effort than the measured user benefit. Keep successful standalone scripts in that case. If evidence changes, reorder versioning and OpenAPI. Do not expand the total concurrent scope.

Decision log and sources

Date Record
2026-09-29 Draft created from the supplied strategy research and local source review. Recommends a separate optional CLI, a small adoption release, bounded migrations, and sequential content capabilities.
2026-09-29 Subsequent user authorization accepted the independent Go repository and first-stage development. A local 0.1.0-dev candidate implements doctor/check/init/upgrade/dev/build. Stable behavior moved to the CLI decision and usage guide. Local acceptance passed for CLI commit e623d93.

Local authorities consulted: Architecture, generated schema decision, migration boundary, version behavior, OpenAPI limits, and graph proposal status. The source inspection also covered theme bin/, schema/nav.v1.schema.json, the existing Starter, and documentation-site build/check commands.

External primary sources were checked on 2026-09-29: the linked Mintlify command reference, Nimbus repository, Docusaurus versioning guide, and Hugo config/module documentation.

7.9.4 - OINK CLI maintenance roadmap

Historical R1–R8 requirements for documentation maintenance and Oink Studio, distinct from acceptance evidence and the current reduced CLI contract.
Implemented requirements record

This dated requirements record preserves the original R1–R8 planning scope and anchors. Current CLI behavior belongs to the CLI contract and CLI introduction, and historical acceptance evidence belongs to the 2026-10-04 supplement. Conditional extensions E1–E4 remain inactive.

The proposed product should complete documentation maintenance before adding a local visual interface. It should help maintainers check changes, understand their effects, review safe modifications, and publish the exact artifact that passed checks. Studio should expose those same capabilities.

Record Value
Status Implemented locally for R1–R8 and A18
Owner OINK maintainers
Date 2026-10-03
Baseline Local CLI 0.1.0-dev, commit e623d93. Hugo Extended 0.166.0 and Go 1.27.1 on macOS arm64
Completion scope R1–R8 and the acceptance cases below. Conditional extensions have separate entry criteria
Affected surfaces CLI command/result contract, Starter projection, consumer CI, translation policy, maintenance operations, local Studio, EN/ZH guides
Schedule assumption One full-time developer with scheduled documentation and review support

Background and evidence

The original CLI roadmap accepted an independent Go executable and narrowed the first implementation to doctor, check, init, single-site upgrade, dev, and build. This proposal adds a maintenance program with a defined scope. Docsy migration, version lifecycle, OpenAPI generation, and theme 1.2 retain their own scopes.

The 2026-10-03 local audit reran the Go suite and actual Hugo integration tests. A bilingual initialized site passed checks over 223 files and 4,461 references. The PIG consumer site passed over 1,392 files and 64,440 references, with 858 source files and its Git state unchanged.

The audit also reproduced four limits. A missing rendered link failed check while build succeeded. Ordinary HTML references outside the configured base path were marked untested. doctor --release accepted a Starter still using https://example.org/. Both embedded deployment workflows called Hugo without the CLI’s additional checks. Translation completeness and readable upgrade diffs were absent. These findings define the first changes.

Product goal and users

Prioritize maintainers of multilingual engineering documentation and small teams maintaining several Hugo sites. Their recurring jobs are reviewing translations, preventing broken publications, updating dependencies, and reorganizing content without losing references or public URLs.

The product succeeds when an ordinary consumer repository can use one quality entry point locally and in CI. Maintainers must be able to inspect affected pages and apply reviewed changes while preserving unrelated work. CLI, Studio, and Agent callers must receive the same findings and change plans.

Feature selection

Capability from the supplied design Decision Delivery
Links, anchors, attachments and machine outputs Strengthen existing checks and explain uncovered cases R1–R3
Environment diagnosis, preview and strict builds Complete release diagnosis and add opt-in verified builds R1, R3
Translation completeness and protected structure Build as a primary product capability R2
Initialization and CI configuration Extend the fixed Starter and manage reviewed CI changes R3–R4
Native content rules and project style Implement a small deterministic core. Optional general tools R2, R6
New content, snippets and editor setup Implement ordinary Hugo inputs with overwrite protection R4
Safe upgrades and migration preflight Add diffs and candidate comparisons. Framework migration remains separate R4
Page moves, renaming and impact analysis Implement after page relationships and change plans are dependable R5
Issue panels and translation comparison Build a read-only local Studio first R7
Multiple sites Add an explicit site registry over the same single-site engine R6
EPUB, PDF and offline packaging Conditional adapter to distributed publication tools E1
Executable documentation examples Conditional, explicit execution profiles E2
Agent inspection, impact and context Implement deterministic local operations R5
AI translation and semantic review Conditional proposals after deterministic maintenance works E4
Sources, evidence and knowledge dependencies Limit this program to build/review provenance and observed page relationships Wider knowledge management deferred
Rich editing, live collaboration and native desktop apps Deliver safe Markdown editing only. Defer the broader platform R8. Remainder deferred

Scope and non-goals

R1–R8 define this PRD’s finite completion scope. Each stage can deliver value and be accepted separately. Suggested CLI versions 0.2, 0.3, and 0.4 identify release candidates. They do not require public tags or theme versions.

The program does not include a renderer, universal migration engine, hosting account manager, deployment API, built-in LLM, vector database, remote editor, real-time collaboration, native desktop shell, or full WYSIWYG editor. Existing provider workflows perform deployment. Publication permissions and credentials remain consumer-owned.

Shared project facts and check policy

This scope is accepted locally. The original requirements below are retained as proposal history. Current behavior and flags belong to the CLI contract.

Extend the existing isolated Hugo analysis. Do not introduce a second configuration parser or navigation authority. Proposed internal facts include page identity, language, publication state, actual output URLs, known source files, translation relationships, and observed rendered references.

Use Hugo’s public Page.Translations and Page.OutputFormats for relationships and outputs. Page.File can provide provenance, but some pages have no backing file. Such findings must retain an output location and unknown source state. Any temporary probe must leave ordinary published outputs unchanged after its removal.

Use oink.yaml only for check selection, severity, translation policy, reviewed exclusions, and tool/workflow options. Hugo continues to own languages, titles, menus, URLs, and site configuration. Module files own theme versions. Initially provide check links, check translations, and check style over one shared analysis. Keep --json. --format json may be an additive alias.

Report blocking errors, warnings, and suggestions through the existing error, warning, and info severities. Unsupported required tools or input shapes remain exit 2. Policy cannot turn failed builds, unreadable inputs, or incomplete required checks into success. Source locations need reliable mapping. Otherwise report the actual output and pointer.

Translation maintenance

This scope is accepted locally. The original requirements below are retained as proposal history. Current behavior and flags belong to the CLI contract.

Support filename languages, language-specific content directories, and translationKey relationships as resolved by Hugo. Coverage policies select required languages for an explicit content scope. Disabled languages and intentional localizations must not become missing-translation errors. Check duplicate identities and configured draft/publication requirements. If a production build omits a source needed to assess policy, use an explicit analysis view. Do not confuse that view with publishable output.

Provide two policies: strict correspondence for manuals, and localized content for blogs or product pages. Strict policy can require explicit IDs, declared placeholders, selected code blocks, and necessary fields to agree. Localized policy checks only declared shared constraints. Heading counts and all code blocks must not become universal requirements.

Propose translations status, translations diff <page>, and an explicit review-record operation. A versioned review record binds the translation to a source content hash or Git revision, plus the translation hash and declared source language. A missing record means unknown. A changed hash means changed since review, not automatically a bad translation. Recording review requires a user-requested write and must never happen just because a checker ran.

Native content rules and baselines

This scope is accepted locally. The original requirements below are retained as proposal history. Current behavior and flags belong to the CLI contract.

Start with a small catalog of high-confidence rules drawn from actual consumer failures: malformed supported component/attribute usage, conflicting explicit IDs, known deprecated forms, and configured protected content. Respect the syntax boundaries of code, inline code, shortcode bodies, raw HTML, and attributes. Do not apply regular expressions without regard to these boundaries.

Use the effective theme version’s contracts. An editor schema that omits types is not a complete strict validator. Missing compatible metadata must produce explicit limited coverage rather than validate against the latest theme. Do not require a future theme release to finish basic checks.

A visible versioned baseline can acknowledge existing findings with stable fingerprints, reasons, and review metadata. Reports show acknowledged and new findings separately. Baseline updates are explicit and reviewable. They cannot hide required incomplete work. Formatting and prose suggestions are optional. Automatic fixes first produce a diff, then validate a candidate before applying a narrow set of files.

Verified publication and CI

This scope is accepted locally. The original requirements below are retained as proposal history. Current behavior and flags belong to the CLI contract.

Preserve the current transparent build default. Add an explicit managed build --check workflow. Run one strict Hugo build. Run selected checks on the same output. Export only that verified artifact to a new or empty destination. Do not delete arbitrary directories or mix stale files into a verified tree. Default build must continue to report when extra checks did not run.

A local versioned manifest records the source revision and dirty state when known. It also records the source-input hash, effective theme identity, Hugo/CLI versions, build settings, base URL, check coverage, and file digests. Secrets and machine paths must not be copied into public metadata. If a public build marker is enabled, it contains only the minimum identity needed for verification. Artifact changes after validation invalidate the recorded result.

Propose ci init github-pages and ci init cloudflare-pages --mode direct-upload. Generate local configuration only, explain variables and permissions, and record template provenance. Detect existing workflows, preview diffs, preserve unknown modifications, and require explicit application. Both use the same quality engine and upload the verified output without another Hugo build. Before a public CLI release, templates must accept a documented immutable source/archival input rather than assume a nonexistent download tag.

Extend release diagnosis with an example-address warning, a release-policy error when publication checks require a real address, effective local source commit/dirty state when available, and comparisons with supported generated CI settings. Unknown custom CI is reported as unknown. A local checkout’s commit does not attest to a published module. Vendor byte identity remains separate.

Propose verify --site URL --manifest FILE with explicit network permission. Check representative pages, languages, resources, search/Markdown outputs, canonical addresses, and artifact identity. A generic fallback with HTTP 200 must fail identity checks. Timeout, authentication, rate limiting, or an absent required identity produce unknown/incomplete results, not invented success. Local HTTP fixtures test this without deploying to a provider.

Authoring and upgrade assistance

Add new, a small snippet catalog, and explicit editor-schema setup. Create page bundles, selected translation drafts, and ordinary front matter while refusing existing files. Translation drafts are not completed translations. Editor hints follow the effective theme and preserve existing editor settings.

Extend init with docs, blog, book, and project profiles by composing one licensed Starter source. Do not maintain four copied template trees. For existing projects, provide diagnosis and reviewed proposals. Do not replace their configuration.

Keep the explicit-tag, single-site upgrade protections. Add readable unified diffs and baseline/candidate route and capability comparisons. Report removed URLs, changed aliases and missing previously enabled outputs. A clean candidate build alone does not prove compatibility. Proposed configuration migrations need a documented transform and tests. Otherwise return a manual action. Conflicting replacements and vendor refresh remain explicit owner operations.

Impact analysis and safe content changes

This supported scope is accepted locally. The original proposal requirements below remain as history. Current behavior, limits and flags belong to the captured-facts contract and move contract.

Provide inspect <page>, impact --since <ref>, and context <task> over the shared facts. Inspect shows provenance, publication state, references, translations, and outputs. Context packages relevant local material with versions, paths, selection reasons, and size limits. No vector service or LLM is required. Document content is data. It cannot authorize command execution.

Initially check --since may still perform a full check and say so. Later optimization must include changed targets, their inbound references, translations, and derived outputs. Deleting B must still inspect unchanged A that links to B. Configuration, templates, navigation, or uncertain dependency changes expand the scope to a full check. Cache data is disposable evidence. It is not an authority.

Propose move <source> <target> as preview by default. A plan contains touched files, readable diffs, base hashes, translations, attachments, route changes, and an alias recommendation. Rewrite only links whose meaning and ownership are known. Ambiguous template/shortcode references require review. Apply verifies bases, validates an isolated candidate, protects concurrent edits, and retains recovery information. A failed or stale plan must not partially overwrite user work.

Workspaces and optional tools

An explicit workspace registry names selected site directories. It reuses the single-site engine, reports per-site results and aggregate completion, and allows writes only to explicitly selected sites. It must not discover and upgrade every sibling repository automatically or duplicate Hugo settings.

Optional markdownlint, Vale, and lychee adapters use explicitly configured, already provisioned tools and normalize their findings. Required missing tools return 2. Optional omissions remain visible. If the adapter cannot understand syntax, exclude it. Do not rewrite it. Ambiguous external-link failures require a network-status distinction. Tool installation and network access are separate actions, and generic formatters never overwrite content by default.

R6 accepted local boundary

The explicit registry and optional adapters are accepted locally in supported R6 scope. Stable fields and limits are documented in the registry contract and tool contract. The accepted R7/R8 boundaries below retain their own evidence and limits.

oink.workspace/v1 names 1–64 literal directories in one regular YAML file bounded to 256 KiB, without duplicating Hugo settings or discovering siblings. Exact names, canonical root identity, registry-order selection, per-site 0/1/2 parity and explicit-name saved-plan application are the supported workspace boundary. Already provisioned markdownlint-cli 0.49.1, Vale 3.24.0 and lychee 0.24.2 extend each site’s policy, with captured configuration and typed protocol/source/network coverage. They do not install or format tools/content. Lychee needs explicit network consent. Ambiguous external failures remain unknown rather than definite broken links.

Frozen Go/vet, actual Hugo/pinned-tool, and owning race gates have passed. The exact binary also passed four-site direct/aggregate diagnostic/coverage/exit parity and all source byte, full-mode, Git, ignored-input, and directory guards. Initial preparation failures remain excluded, and the receipt-driver-only command metadata correction is recorded without a CLI runtime correction or rerun. Guarded canonical EN/ZH source/rendered checks passed, and supported R6/A07/A15 scope is accepted locally in the R6 record. Current A18 runtime/archive qualification passed. Darwin amd64 remains experimental/unverified.

Read-only Oink Studio

Build a local web interface with project overview, issue panel, translation comparison, page relationships, and publication panel. These views use the same core results as CLI/CI. Support filters, known-source navigation, actual Hugo preview, change comparisons, and copying proposed actions. A large graph or embedded editor is not needed for this acceptance.

Default to loopback and an explicit site allowlist. Separate untrusted rendered content from the management origin. Handle Host/Origin checks and session authorization before adding write APIs. Ship prebuilt UI assets with the CLI. Node is a contributor build dependency, not a consumer runtime requirement. Cover keyboard operation, screen-reader labels, mobile layouts, light/dark themes, and readable long diagnostic lists.

R7 candidate boundary

The read-only Studio candidate now serves an embedded five-view browser and an authenticated literal-loopback API over the same native checks and captured Hugo facts. Stable candidate boundaries are in the contract. Explicit existing-site/registry selection, typed paginated findings, source/diff/hash states, actual production preview and separate optional analysis coverage preserve the CLI authority.

Frozen native/browser/core-case and exact-binary four-consumer receipts now qualify the supported read-only scope, including explicit partial-preview incompletion. They exercise native 0/1/2 parity, keyboard/mobile/light/dark flows, literal source data and separate-origin preview attacks. R7/A16 passed guarded canonical promotion/rendered gates and is accepted locally. R1–R8 supported scope and current A18 runtime/archive qualification passed.

Safe Markdown editing

After the read-only interface is accepted, add Markdown editing, front matter forms, selected component insertion, and attachments. Reuse the CLI change-plan engine and actual Hugo preview. There is no second save/validation mechanism.

An unchanged open/save cycle must preserve bytes. Updating one field preserves unknown fields, comments, order, encoding, and unrelated whitespace. Detect external-editor changes and refuse stale saves. If a front matter form cannot preserve a construct, keep it editable as text and explain the form limitation. Do not round-trip the whole document through a generic serializer.

Writes need an authorized local session, an allowed directory, base verification, and a visible diff. Reject path traversal, symlink escapes, and requests from untrusted preview content. Attachments must not overwrite existing files. Publishing a static site never adds these management APIs to it.

R8 accepted editing boundary

R8 now has one source-preserving proposal engine for CLI edit text, field, snippet and attachment, and the explicit studio --edit flow. Default Studio remains read-only. Known site-owned Markdown, exact source hashes, supported top-level YAML scalars with text fallback, original catalog byte-boundary insertion and new-only leaf-bundle attachments share the same saved plans and guarded writer. The complete visible review binds plan, file, and full-mode identities. Candidate HTML comes from actual selected nonpublishable Hugo analysis, with native findings and required view incompletion kept distinct.

The accepted local interface is documented in the contract. Corrected frozen public/Go/race/vet, actual/ordinary Hugo, Editor browser accessibility/mobile and exact-binary four-consumer preservation gates passed. R8/A17 supported local scope also passed guarded canonical source/render gates and is accepted in the R8 record. R1–R8 supported scope and current A18 runtime/archive qualification passed. Darwin amd64 stays experimental/unverified.

Delivery sequence and schedule

The following estimate assumes one developer. T0 is the implementation start after scope approval. Dependencies are sequential acceptance gates, which additional staff can support through independent tests and UI work.

Stage Effective weeks Delivery Acceptance gate
R1 1–2 Shared facts, check policy, focused scopes, trustworthy locations Hugo owns routes/relationships. Required incomplete checks cannot pass
R2 3–5 Translation policy/review state, native checks, visible baseline Three language layouts. Strict/localized cases. Reviewed fixes preserve files
R3 6–8 Verified build artifacts, CI init, release diagnosis, deployed-site verification One checked artifact is uploaded. Stale bytes and HTTP 200 fallback are detected
R4 9–11 New content, profiles, snippets/editor setup, upgrade diffs/comparisons Ordinary Hugo build. Dirty/replaced/vendor and route-regression cases remain safe
R5 12–15 Inspect, impact, context, move and shared change plans Unchanged inbound links and translations are included. Stale plans cannot write
R6 16–17 Explicit workspaces and optional check adapters Per-site parity. Required unavailable tools are incomplete. No implicit installs
R7 18–20 Read-only Studio and its security boundary Five useful views. CLI/UI findings agree. Accessibility and preview isolation pass
R8 21–24 Safe Markdown/forms/attachments with conflict review No-op save has zero diff. Comments/unknown fields survive. Concurrent saves fail safely

Allow another 4–6 weeks for integration, false-positive review, cross-platform execution, and repairs, distributed across the gates. The total planning range is 28–30 effective weeks (or roughly twice that elapsed calendar time at half-time availability).

R1–R3 yield a proposed 0.2 quality/publication candidate around weeks 9–10 including early reserve. R4–R6 yield a proposed 0.3 maintenance candidate around weeks 19–20 cumulatively. R7–R8 yield a proposed 0.4 local Studio candidate around weeks 28–30 cumulatively.

Conditional extensions

Extension Entry criterion Proposed boundary Separate estimate
E1 Publication exports At least two maintained books need a recurring export workflow Reuse distributable EPUB/PDF tools and package local artifacts. Declare external dependencies 1–2 weeks after R3/R4
E2 Executable examples Explicit owners identify runnable examples and disposable test environments Reviewed execution profiles, time/resource limits, offline default. Never execute discovered prose automatically 3–5 weeks after R5
E3 MCP An existing Agent integration needs capabilities beyond invoking JSON CLI results Thin adapter over inspect/check/impact/context/plans. Same permissions and diagnostics 1–2 weeks after R5
E4 AI review and translation Deterministic translation maintenance works and a reviewed evaluation corpus exists User-selected provider, explicit network/cost settings, proposals bound to source hashes. No automatic source writes 3–6 weeks for a limited experiment after R5

The R1–R8 total excludes these estimates. Activate an extension only for its stated use case. A future need is not an unfinished core milestone. Remote Studio, live collaboration, native shells, general knowledge provenance, vector retrieval, and universal framework migration require separate PRDs and evidence.

Architecture and compatibility

Use Go for core operations. Use subprocesses for Hugo and optional tools. Extend existing packages when they own the behavior. Add a package only with its capability. Do not create a generic plugin platform, public SDK, or shared service layer before an actual consumer needs it.

Preserve oink.result/v1, exit meanings, and the default thin wrappers. New diagnostic details and command data may be additive. Changed field semantics need a new result version. Version review records, baselines, plans, build manifests, and workspace registries independently. Detect supported capabilities from the actual theme. Do not require all users to install the latest release.

Read/check/preview, local file application, networking, example execution, and deployment have separate side effects. No telemetry, background updater, credential discovery, arbitrary directory cleanup, global configuration change, commit, push, or deployment happens as a maintenance side effect. Read-only consumer trials preserve sources, replacements, workspaces, and vendor bytes.

Acceptance cases and owning checks

Case Required outcome Primary owner
A01 JSON and completion One JSON result on stdout. Clean stderr separation. Findings 1, required incompletion 2 internal/report, internal/app, Schema
A02 Hugo truth Slug/url/permalinks/aliases, custom mounts, unlisted pages and language roots follow actual Hugo results internal/site, internal/outputcheck, actual Hugo fixtures
A03 Subpaths A definite project-local missing route fails. Outside-origin/path references remain classified honestly. Declared external scopes avoid false positives Output checker and policy tests
A04 Translations Filename, directory and translationKey layouts. Duplicate/missing/draft cases. Strict/localized policy Translation engine and public-command tests
A05 Review state No record is unknown. Changed source hash is visible. Mtime never determines review state Translation/review-record tests
A06 Content syntax Fences, inline code, shortcodes, HTML, attributes, custom fields and configured protected text do not generate invented findings Native-rule tests and real content corpus
A07 Baselines and adapters Acknowledged findings remain visible. New findings fail policy. Unavailable required tools cannot pass Policy/adapter tests
A08 Artifact identity Modify a file after check and manifest verification fails. Provider upload uses the same exported tree without rebuilding Managed-build and workflow tests
A09 CI preservation Both templates, existing customized workflows, permissions/variables, preview/apply conflict and provenance Starter/CI tests and local workflow rehearsal
A10 Public verification HTTP 200 fallback, wrong language/build, missing resource, canonical mismatch, timeout/auth/rate limit Local HTTP fixtures, no required cloud account
A11 Initialization and authoring Supported profiles/languages. Empty-target protection. Generated sites build with ordinary Hugo. Editor config preserves unknown settings internal/starter, authoring and Hugo tests
A12 Upgrade Readable diff, old/new routes, dirty files, both workspaces, replacement/vendor, failure recovery and concurrent edits internal/upgrade, public-command/Hugo tests
A13 Impact Deleted B finds unchanged A. Translations/attachments/derived outputs included. Global changes expand scope Impact and Git-baseline fixtures
A14 Change application Candidate validation before apply. Hash conflicts and failed writes preserve subsequent edits. Ambiguous references are not rewritten Shared plan/apply and move tests
A15 Workspace and context Per-site results match direct invocation. Selected writes only. Bounded context gives paths/versions/reasons without executing content Workspace/context tests
A16 Studio parity Five views show the same results as CLI. Usable keyboard/mobile/light/dark flows and source/preview separation Studio browser/accessibility tests
A17 Editor preservation No-op save is byte-identical. YAML comments/unknown values/order survive. Stale saves and attachment collisions are rejected Editor/browser and shared apply tests
A18 Runtime and recovery Test actual declared macOS/Linux targets. Signals stop child processes. Cached operations work offline. Unsupported inputs remain explicit Process/integration/installation tests

Run the smallest owning tests before broader integration. Keep Go unit fixtures offline. Repeat actual Hugo tests after parser, snapshot, probe, initialization, or upgrade changes. Keep focused checks. Do not require consumers to run theme internals or a browser suite for every document change.

For each candidate, record tool versions and source identities. Then test the Starter and three distinct maintained sites without writes. Compare source bytes, modes, and Git state before and after. Tests for new native rules need a reviewed valid/invalid corpus. Fix false positives before enabling a blocking default. Before promising incremental speed, measure full-build time against the same current-site baseline. Functional correctness takes priority over check counts.

Completion and release evidence

For each stage, record implemented behavior, known limits, focused tests, and actual integration results. Provide updated EN/ZH contracts/guides and a reviewable diff. Track individual requirement/case statuses. This PRD is complete only when R1–R8 and their required acceptance cases are satisfied.

After supported behavior is accepted, move it into the owning CLI contract, usage guides, and durable decisions. Retire the corresponding proposal sections through the existing lifecycle.

Decisions and stop conditions

Confirm staffing and start date before turning relative weeks into calendar dates. Decide supported runtime targets, review-record storage details, initial native-rule catalog, and precise additive command flags in R1. These are bounded implementation choices within this scope, not reasons to reopen the product boundary or wait for an entire theme release.

If preservation or correctness work exceeds a stage estimate, move its optional convenience work later. Never remove stale-write protection, truthful completion, or ordinary-Hugo compatibility. If repeated corpus review shows that a rule is unreliable, keep it advisory or remove it. If a form cannot preserve source bytes, keep that syntax in text mode. Interest in implementing a conditional extension does not add it to the critical path.

Decision log

Date Record
2026-10-03 Created from the current CLI audit and the supplied feature goals. Proposed R1–R8, optional extension gates, resource assumptions and executable acceptance cases.
2026-10-03 R1 shared Hugo facts and check policy passed local owning/actual-Hugo checks. Stable behavior moved into the CLI contract. The acceptance record tracks final refreshed reports and rendered EN/ZH evidence separately. R2–R8 and conditional extensions remain open.
2026-10-03 R2 translation scopes/hash reviews, syntax-bounded native rules and visible baselines now use shared guarded file plans. Local owning, actual-Hugo and focused race gates passed. Final refreshed consumer and EN/ZH documentation acceptance remains pending in the record. Implemented behavior is in the contract. R3–R8 remain open.
2026-10-03 R2 final corpus and bilingual documentation gates passed. R3 one-render checked export, exact file identity, guarded CI plans for both providers, release diagnosis and explicit-network HTTP verification passed their scoped local gates, including custom-workflow discovery. Stable behavior moved to the contract. Exact evidence and A08–A10 outcomes belong to the maintenance record. R4–R8, final A18 runtime/archive refresh and Darwin amd64 remain open.
2026-10-03 R4 supported local scope passed frozen Go/vet, actual Hugo/race and exact-binary read-only Starter/docs/PIG/repository gates. One unchanged licensed Starter composes all profiles/languages. Ordinary new/editor/snippet flows and source/external-input guards passed, as did readable bounded upgrade views and alias/output regression protection. Stable behavior belongs to the contract. Exact A11/A12 evidence and remaining limits belong to the record. R5–R8, final A18 runtime/archive refresh and Darwin amd64 stay open.
2026-10-03 R5 corrected frozen Go/vet, actual Hugo/race and exact-binary four-consumer read-only gates completed. Inspect/context complete for all sites. Historical impact and move blockers remain explicit. Guarded canonical source/rendered gates and stage acceptance are pending. Stable behavior belongs to the contract. The record identifies A13/A14/context evidence, the cached-module correction and exact preservation receipt. R6–R8, workspace A15 and final A18 remain open.
2026-10-03 R5 supported inspection/impact/bounded-context and guarded move scope is accepted locally after corrected frozen owning gates, exact-binary four-consumer preservation and first-promotion canonical source/rendered gates. The production translation owner retains only its known draft-release omission. Separate nonpublishable analysis passes all owners. Stable behavior belongs to the contract and command reference. The record retains exact outcomes and the separate post-render evidence boundary. A13/A14 supported CLI scope passed. A15 context passed while workspace/direct parity remains R6. R6–R8 and final A18 remain open.
2026-10-03 R6 explicit registry and bounded optional-tool candidate implemented. Focused workspace and corrected actual-protocol trials passed, with preparation failures kept separate. Final runtime/corpus/canonical gates and A07/A15 stage acceptance remain pending in the R6 record. R7/R8 and final A18 remain open.
2026-10-03 R6 frozen Go/vet, actual Hugo/pinned tools, owning race and exact-binary four-consumer direct/aggregate parity/preservation qualified. The completed receipt records six original operations, existing repository duplicate-ID findings and a driver-only command-summary correction over unchanged raw outputs. No CLI/Hugo rerun or runtime fix was needed. Canonical source/render checks and explicit R6/A07/A15 stage acceptance remain pending in the record. R7/R8/final A18 remain open.
2026-10-03 R6 supported explicit-registry and optional-tool scope is accepted locally after frozen Go/vet, actual Hugo/pinned tools, race, exact-binary four-consumer parity/preservation and guarded canonical source/render gates. A07 adapters and A15 workspace/direct/context supported scope passed. The R6 record separates first-promotion rendered bytes from this post-render status/evidence amendment. R7/R8 and final A18 remain open.
2026-10-03 R7 read-only embedded Studio candidate and authenticated loopback views implemented. Frozen core/browser and exact-binary four-consumer qualification completed within the declared scope. Guarded canonical promotion/render and explicit R7/A16 acceptance remain pending. R1–R6 remain accepted. R8/final A18 open.
2026-10-03 R7/A16 supported read-only Studio is accepted locally after frozen cumulative executed-case/browser proof, exact-binary four-consumer parity/preservation and guarded canonical source/render gates. First-promotion rendered bytes remain distinct from this post-render status amendment. Whole invocation failure and explicit partial-preview incompletion stay visible. R1–R7 accepted. R8/final A18 open.
2026-10-03 R8 CLI/opt-in Editor source-editing candidate implemented. Default Studio remains read-only. Pure-core/streaming-output focused evidence is recorded and failed browser trials retained. Final frozen public/browser/consumer/canonical gates and A17 stage acceptance remain pending in the R8 record. R1–R7 remain accepted. R8/final A18 open.
2026-10-03 R8/A17 reviewed CLI/opt-in Editor supported local scope is accepted after corrected frozen whole owning/browser gates, exact-binary four-consumer proposal parity/source preservation and guarded first canonical promotion/actual rendering. R1–R8 are accepted locally. The R8 record separately binds first-rendered and current status bytes, retaining every failed trial, native finding and required partial-preview incompletion. Final A18 current Linux/archive qualification remains open.
2026-10-04 Current backend integrity correction, refreshed owning/Hugo/race, carried unchanged-runtime four-consumer preservation and three declared runtime/archive qualifications passed. See the dated completion supplement. Finite R1–R8 implementation is complete locally. Retain this requirements record and anchors, retire it from active navigation, and keep stable behavior in the contract/guide. E1–E4 have not been invoked.

7.9.5 - Visual presets and appearance switching

Paper and Slate ship in 1.2.0; Ink and Terminal are explicitly enabled experiments awaiting design acceptance.
Phase 1 released in 1.2.0; Ink/Terminal opt-ins available

Paper, Slate and the Appearance menu ship with OINK 1.2.0. The architecture contract, accepted decision and acceptance record own phase-one behavior and evidence. A subsequent Ink/Terminal experiment provides actual selectable output; this proposal remains active for their design acceptance. The October 4 injected screenshots are research prototypes; they are distinct from the October 5 screenshots of actual theme output.

Status and surface

Field Value
Status Phase 1 released in 1.2.0; Ink/Terminal explicit opt-ins
Owner OINK maintainers
Date 2026-10-04
Baseline OINK v1.4.0 theme and documentation site; this row preserves the proposal’s October 4 research starting point
Affected contracts Architecture: Trust, CSS, and accessibility (font roles, accent roles, inline-code colour), Shell (theme control), Landing, Configuration decision, Brand guide
Phase 1 Paper preset, Slate preset, default changed to Paper, reader switching between Paper and Slate
Later phases Ink/Terminal design acceptance; Folio and Canvas names reserved only

Context and evidence

The baseline and limitations below record the October 4 research input, before phase 1.

OINK ships one visual identity, referred to here as Slate: a cool blue-grey canvas (#f1f4f8 / #0b1119), navy text, steel-blue links (#245f94), copper accents, Inter for interface and prose, Chakra Petch for display and wordmark, IBM Plex Mono for code and technical labels, a blueprint grid and glow on the Landing hero, and a crimson inline-code pair. It is defined by Bootstrap custom properties in assets/scss/td/_brand.scss, shell tokens in assets/scss/td/shell/_tokens.scss, and font roles in assets/scss/td/_tokens-typography.scss.

PG.CENTER, an independent site, has a warm editorial reading style that maintainers want as the future OINK default. Its presentation tokens live in media/css/pgsql.css of that project. Measured on its local preview (2026-10-04, light and dark, home, Docs index, long manual page, component manual):

Role Light Dark Note
Canvas #f7f6f3 #161513 warm white / warm black
Raised surface #ffffff #1d1c19 cards, code blocks
Secondary surface #efede8 #262420 table header, hover
Ink #21201c #ece9e3 body text
Secondary text #56534c #b6b1a7
Lines and washes ink at 4.5–22 % alpha light ink at similar alpha no tinted greys
Radius 12 px / 8 px same
Shadow 0 2px 10px rgba(33,32,28,.07) black-based warm, soft
Motion 160 ms cubic-bezier(.2,.7,.2,1) same

Typography is IBM Plex Sans (variable, 400–600) for interface and prose, IBM Plex Mono for code, dates and versions, and Chakra Petch for the wordmark only. The component-manual pages are the best long-form model: lead paragraph 17 px capped at 70ch, h2 followed by a hairline that runs to the edge, framed tables with a header band and no zebra, monochrome callouts with a 3 px rule.

The following PG.CENTER elements are site identity, not reusable reading rules: the PostgreSQL brand blue #336791 family, wine content links, version-state colours, release strips, search-kind badges, wiki tones, the duotone hero, and the 144-character measure of the imported PostgreSQL manual. Two values fail WCAG AA (muted text 3.67:1, link hover 4.22:1) and are corrected below rather than copied.

Measured OINK docs typography for comparison: 16 px / 1.7 body, ≈ 76ch measure, h1 36 px / 700, h2 24 px / 600, code 14 px. PG.CENTER Docs index: 15.5 px / 1.7, ≈ 120ch.

Current limitations

  • Colours are tied to data-bs-theme only. No attribute selects a second palette, and several surfaces bypass tokens: the Landing primary button (#2f6793 with navy glows), the grid, scrims (rgba(4,10,18,.45)), print colours, asciinema surfaces, and the giscus stylesheets.
  • About 85 literal border radii and several literal shadows make a flat preset impossible without a radius and shadow scale.
  • The light/dark control expands on hover or focus. Touch readers cannot reach “follow system”; the trigger mixes aria-pressed and aria-expanded; Esc does not close it. The Landing mobile drawer has no theme control.
  • contrast-on-canvas.html hard-codes Slate canvas luminance for the theme_color warning.
  • dark_mode is opt-in (false by default), so the palette and its menu are absent unless a site enables them.

Goals and non-goals

Goals:

  • one site key selects the default visual preset; Paper becomes the default;
  • Slate remains available and reproduces current output for sites that choose it;
  • readers can switch Paper and Slate instantly, without reload, independent of light/dark/system mode;
  • the configured default renders without JavaScript and with storage unavailable;
  • presets share templates, components, and layout geometry; they change paint and type;
  • local fonts only, ordinary Hugo build, no new runtime framework or required build tool.

Non-goals:

  • implementing Ink, Terminal, Folio, or Canvas in phase 1;
  • copying PG.CENTER brand colours, version UI, or page structures;
  • per-page or per-section presets (section colour remains theme_color);
  • changing layout geometry, density, or navigation structure per preset in phase 1;
  • theming Swagger UI, ReDoc, or third-party embeds beyond their existing light/dark handling.

Preset model

This table and the phase-one configuration below retain the original scope. The later experiment adds explicit ink/terminal configuration and menu-list entries; true still offers the stable set plus the site default. The architecture contract owns current behavior.

Preset Direction Phase 1 Reader menu
paper Warm editorial minimalism Implemented, default Yes
slate Technical minimalism (current OINK) Implemented Yes
ink Typographic minimalism, Swiss-inspired Spec + research prototype No
terminal Terminal-inspired utilitarian Spec + research prototype No
folio Academic / book publishing Name reserved No
canvas Playful geometric / creator Name reserved No

Reserved names are rejected by validation until implemented, with a warning that names the stable presets.

Configuration

params:
  ui:
    preset: paper        # paper | slate        (theme default: paper)
    preset_menu: false   # false | true | [paper, slate]
  • preset selects the site default. Invalid or reserved values warn through the existing validation path and fall back to paper. Publishing gates turn the warning into a failure.
  • preset_menu controls the reader choice. false renders no style group and emits no preset-init script; true offers every stable preset; a list offers a subset that must contain preset. Following the dark_mode precedent, the default is false; the documentation site enables it; starter adoption is outside this change.
  • preset is site-level only. Page and section overrides are not supported: switching identity per page would break reader expectation and the stored choice.
  • The Appearance menu exists when either dark_mode.show_menu or a style choice is enabled. A site with dark_mode: false and preset_menu: true shows only the Style group.

Relationship to existing keys

Precedence, lowest to highest:

  1. Slate base tokens on :root / [data-bs-theme] (unchanged selectors).
  2. Preset tokens on [data-td-preset=X].
  3. params.ui.typography: system — collapses font roles to system faces after the preset blocks, so it still requests no brand font in any preset.
  4. params.ui.fonts — emitted inline after the stylesheet at :root; equal specificity and later source order beat preset font roles. Explicit fonts always win.
  5. theme_color / theme_color_dark — page and section accent backgrounds only. They override the preset accent; they never touch links or inline code.
  6. Site _styles_project.scss — last in the bundle.

typography: technical remains the name for “use the preset’s bundled faces”. The preset decides which bundled faces those are (Paper: Plex Sans; Slate: Inter + Chakra Petch).

Reader state

Two independent dimensions:

Dimension Attribute Storage Values
Style data-td-preset on <html> localStorage['td-preset'] stable preset names
Mode data-bs-theme (+ .dark-mode, vendor data-theme mirror) localStorage['td-color-theme'] light, dark, auto
Situation Result
First visit Server renders data-td-preset="<site preset>" and data-td-site-preset; no script needed
Reader chooses a preset Applied at once, stored, td-preset-change dispatched
Reader chooses the preset marked “Default” Storage key removed; future site default changes reach this reader
Next page, refresh, other language Inline head script applies the stored value before first paint
Stored value no longer offered Removed; site default used
Storage unavailable Choice applies to the current page; the menu states that it will not persist
JavaScript disabled Site default preset renders in its light palette, as the current theme does without script; no style or mode control is usable
Style change Never writes td-color-theme; mode change never writes td-preset
Other tab changes the value storage event applies it

The inline script runs before the stylesheet, beside the existing dark-mode script. It validates the stored value against the allowed list embedded at build time, sets the attribute, and updates the theme-color meta and the pre-paint canvas colour for the preset and mode. It is emitted only when the menu offers more than one preset. Independently of the menu, the static pre-paint <style> and the resolved theme-color meta in head.html are rendered from the site default preset’s canvases instead of the current hard-coded #0b0d12, #ffffff, and #000000.

On switch the runtime sets data-td-preset-switching for one frame to suppress colour transitions, records the first visible heading or block as a scroll anchor, applies the attribute, and restores the anchor offset, again after document.fonts.ready because Plex Sans and Inter have different metrics. Focus, open menus, and form state stay untouched. Phase 1 uses no cross-fade or View Transition.

Appearance menu

Three options were compared:

Option Assessment
Keep the hover menu, add a style row Keeps the touch and keyboard gaps; hover-only discovery
Separate style and mode buttons Two icons in a crowded navbar; mobile drawer gets longer
One Appearance disclosure with two radio groups Chosen: one entry point, works with touch and keyboard, scales to more presets

Behaviour:

  • Trigger: one icon button (aria-expanded, aria-controls, label “Appearance”). It replaces the current theme button in the navbar and in the shell footer line. Sun means the current light state; moon means dark. The t shortcut keeps toggling light/dark.
  • Panel: a non-modal popover containing two native fieldset radio groups. The October 5 revision uses Style: icon-and-name buttons in two columns, with a preset-colored icon and no preview letters or experiment badges. The site default is identified by its tooltip and accessible name. Light: a segmented Light / Dark / System control. Selection applies immediately and the panel stays open so readers can compare.
  • Keyboard: Enter/Space or ArrowDown opens and focuses the checked radio; arrow keys move within a group (native radio behaviour); Tab moves between groups; Esc closes and returns focus to the trigger; focus leaving the panel or an outside click closes it.
  • Feedback: the selected option has a tinted background and accent border; keyboard focus has a separate outline. Changes are announced through native radio semantics; no extra live region.
  • Restore default: selecting the site’s default preset clears the stored choice. No separate reset button is needed.
  • Mobile (< 768 px): the trigger stays in the compact header and is also offered in the docs drawer footer and in a new row of the Landing mobile drawer. The panel opens as a bottom sheet with 44 px targets, the same two groups, and a close button. The sheet is a modal <dialog> opened with showModal(), so it lives in the top layer: the prototype showed that the sticky header’s backdrop-filter otherwise becomes the containing block of a position: fixed sheet and the drawer’s stacking context hides it.
  • Command palette: a switch_preset action next to switch_theme.

dark-mode.js keeps its storage key and attributes. It must sync the checked state of the Light radios and listen to their change events instead of the current aria-pressed buttons.

Token architecture

All presets compile into the single existing main.css. Fonts are declared with @font-face and are downloaded only when a rule uses them, so offering a preset costs CSS bytes but no font bytes until it is selected.

// Slate: existing selectors and values, unchanged
:root, [data-bs-theme='light'] { … }
[data-bs-theme='dark'] { … }

// Every other preset
[data-td-preset='paper'] { /* light tokens + font roles */ }            // (0,1,0)
[data-td-preset='paper'][data-bs-theme='dark'],
[data-td-preset='paper'] [data-bs-theme='dark'] { /* dark tokens */ }   // (0,2,0)

// Then: [data-td-typography='system'] font block (moved after presets)

Rules:

  1. Token parity. Each dark block redeclares every token of its light block, so Slate dark never leaks into another preset. A checker enforces it.
  2. Dark islands. The descendant form covers nested data-bs-theme="dark" islands (Landing code plate, previews).
  3. Font roles only at (0,1,0), so params.ui.fonts keeps winning.
  4. Accent indirection. Presets set --td-preset-accent (and -rgb, -hover); --td-accent defaults to it. theme_color keeps writing --td-accent and therefore overrides the preset in both modes.
  5. Slate stays attribute-free. data-td-preset="slate" matches no override block, so current site overrides of brand tokens behave exactly as today.
  6. Geometry is shared. Presets do not change grid columns, sidebar width, or breakpoints in phase 1.
  7. Preset-specific rules are few and scoped to [data-td-preset=X] in one partial per preset. Anything two presets need becomes a token.

New shared tokens required before Paper (phase 1): --td-shell-scrim, Landing --td-grid / --td-glow / primary-button tokens, --td-callout-tint, --td-code-inline-bg, --td-hairline, and a brand font role (--td-brand-font-family, default var(--td-display-font-family)) so the wordmark can keep Chakra Petch while Paper’s display headings use Plex Sans.

The original phase-two plan proposed global radius, shadow and density scales. The October 5 experiment instead scopes those changes to owned components; a wider token refactor is not a prerequisite for trying the designs.

Contract change: the architecture contract currently fixes inline code to a crimson pair. This proposal makes --bs-code-color a preset token (Slate keeps crimson, Paper uses an ink chip). theme_color still never touches it.

Fonts

Preset UI / body / heading Display Brand (wordmark) Meta Code New bytes
Paper IBM Plex Sans IBM Plex Sans Chakra Petch IBM Plex Sans IBM Plex Mono Plex Sans
Slate Inter Chakra Petch Chakra Petch IBM Plex Mono IBM Plex Mono none
Ink Inter Inter Inter Inter (tabular) IBM Plex Mono none
Terminal Plex Mono chrome, Plex Sans prose IBM Plex Mono IBM Plex Mono IBM Plex Mono IBM Plex Mono none after Paper

Paper vendors @fontsource-variable/ibm-plex-sans (OFL-1.1) into third_party/ with a VENDOR.json entry: Latin, Latin Extended, Cyrillic, Cyrillic Extended, Greek and Vietnamese subsets, normal and italic, weights 100–700. PG.CENTER’s normal-only 400–600 subset is 40,240 B (latin) + 25,868 B (latin-ext); exact sizes are recorded at vendor time. Italic is required because OINK prose uses emphasis and PG.CENTER’s synthesized italic is not acceptable. The full small subsets preserve the existing locale coverage; the browser loads only the ranges actually used. All 12 font files are recorded in VENDOR.json.

Chinese, Japanese and Korean use system stacks placed after the Latin face: -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Noto Sans SC', sans-serif. IBM Plex Sans SC was rejected because its files are megabyte-scale. Monospace stacks insert CJK sans families before the generic monospace keyword so mixed code keeps a predictable CJK face.

typography: system continues to request no brand font: the system block follows every preset block and resets all roles, including brand.

Serif. Phase 1 uses no serif. Latin serif headings beside CJK sans headings look inconsistent, Windows’ default CJK serif renders poorly at heading sizes, and a serif costs another font. A later opt-in display-only serif can be reviewed with the side-by-side mockup produced for this proposal.

Preset specifications

Shared foundation

Belongs to every preset, not to Slate:

  • layout geometry, breakpoints, sidebar/TOC widths, ≈ 76ch prose measure;
  • body 1rem / 1.7 for prose, 0.875rem for chrome, 0.8125rem for meta;
  • type scale ratios (h1 2.25rem, h2 1.5rem, h3 1.25rem, h4 1rem) — presets tune weight and tracking, not size, in phase 1;
  • focus ring: 2 px accent outline with 2 px offset, never removed; forced-colors fallbacks unchanged;
  • semantic status colours (note, tip, important, warning, caution) keep their hue; presets change tint strength and frame;
  • syntax highlighting keeps the existing light/dark Chroma palettes in phase 1;
  • motion tokens 100/150/250 ms; prefers-reduced-motion disables transitions;
  • WCAG AA: 4.5:1 body, 3:1 large text and UI boundaries, in both modes.

Paper

Warm editorial minimalism. Warm paper, ink text, quiet hairlines, soft shadows, and generous but not loose reading rhythm. It serves long-form reading: lower blue light on large canvases, less chrome contrast, and a typeface (Plex Sans) with open counters that reads well at 16 px.

Token Light Dark
Canvas --bs-body-bg #f7f6f3 #161513
Raised --td-brand-elev, --td-pre-bg #ffffff #1f1e1a / #121110
Secondary surface #efede8 #1f1e1a
Body #21201c (15.09:1) #ece9e3
Secondary text #56534c (7.10:1) #b6b1a7
Tertiary text #6b665d (5.27:1) #958f84 (5.68:1)
Border ink 12 % light ink 13 %
Link / hover #2b5f8c (6.23:1) / #1d68a5 (5.43:1) #7db5e6 (8.36:1) / #a3cdf3
Accent (copper) #9c5530 (5.17:1) #d99a6c
Inline code ink on ink-6 % chip light ink on 8 % chip
Shadow sm / md 0 2px 10px / 0 14px 38px ink 7 % / 13 % black 35 % / 50 %
Radius code 12 px, cards 12 px, controls 8 px same

Rules specific to Paper: headings Plex Sans 600 with −0.006em (h1 −0.012em); h2 followed by an edge hairline; framed tables (radius 10, header band, no zebra); callouts with a 4 % (dark 6 %) semantic wash and a single 3 px rule; hairline blockquote; Landing without grid or glow, primary button from tokens with a warm shadow, hero title 600 / −0.025em; selected navigation rows on a warm neutral ground with 9 % (dark 12 %) accent mixed in. Links remain blue: a reading convention, not decoration. Motion 160 ms ease-out for hover and popovers; no movement on scroll.

Slate

Technical minimalism. The current OINK appearance, unchanged: cool blue-grey canvas, navy ink, steel blue and copper, Inter body, Chakra Petch display, Plex Mono labels and metadata, blueprint grid and hero glow, crimson inline code, 8–12 px radii. Selecting preset: slate must reproduce v1.1 token values; a checker compares them. Grid, glow, Chakra display headings, mono metadata and crimson inline code are Slate identity. Layout, focus, status colours, and the shell structure are shared foundation.

Ink

Typographic minimalism, Swiss-inspired information design. Black, white and neutral grey; one red accent; hierarchy carried by size, weight and alignment rather than colour, shadow or rounded surfaces.

Token Light Dark
Canvas #ffffff #0b0b0b
Body #141414 #ededed
Secondary / tertiary #474747 / #636363 #b5b5b5 / #8f8f8f
Surface #f4f4f4 #161616
Link ink, underlined; hover red light ink, underlined; hover red
Accent #c8102e (5.88:1) #ff5c4d
Radius / shadow 0 / none 0 / none

Differences from Slate: no tinted canvas, no blue, no grid texture, no shadows, no rounded corners; links are identified by underline, not hue; headings use Inter 700–800 with tight tracking instead of Chakra Petch. Differences from Paper: neutral not warm, flat not soft, ruled not hairline, underline links not blue. Distinctive rules: 2 px black rule above h2; h1 800 / −0.035em; uppercase tracked h4, table headers and callout titles; selected navigation row marked by a 3 px red bar, not a fill; tabular numerals.

Terminal

Terminal-inspired utilitarian design. Structure and information expression, not CRT effects: monospaced chrome, command and path notation, compact controls, strong panel borders, amber or teal accents.

Token Light Dark
Canvas #f4f5f2 #0c0f0e
Body #1d211f #d3dbd6
Secondary #4a514d #9aa59f
Surface #e9ebe6 #141a18
Link (teal) #0a6560 (6.31:1) #4cc9bd
Accent (amber) #935400 (5.47:1) #f0a73a
Radius 2 px 2 px

Mono scope: navigation, headings, labels, metadata, breadcrumbs, buttons and code use IBM Plex Mono. Prose paragraphs, lists and table bodies use Plex Sans with platform CJK fallbacks, because long monospaced paragraphs and mixed CJK/Latin mono lines read poorly. Distinctive rules: ## prefix before headings rendered with content: '## ' / '' so assistive technology ignores it; bracketed callout labels ([NOTE]); the selected navigation row is inverted with a ▸ marker; 1 px strong panel borders; static ▍ caret in the hero. No scanlines, glow, blinking, or typing animation.

Difference matrix

Paper Slate Ink Terminal
Temperature warm cool neutral neutral-green
Canvas light #f7f6f3 #f1f4f8 #ffffff #f4f5f2
Canvas dark #161513 #0b1119 #0b0b0b #0c0f0e
Prose face Plex Sans Inter Inter Plex Sans
Heading face Plex Sans 600 Inter 600–700 Inter 700–800 Plex Mono
Display / wordmark Plex Sans / Chakra Chakra / Chakra Inter / Inter Plex Mono
Link signal blue steel blue underline + red hover teal
Accent copper copper red amber
Radius 8–12 8–12 0 2
Shadow soft warm navy-tinted none none
Section rule h2 trailing hairline none 2 px top rule ## marker
Selected row warm tint accent tint red bar inverted + ▸
Inline code ink chip crimson ink chip ink chip, bordered
Landing texture none grid + glow none none
Chrome density standard standard standard compact

Page density

Density follows the task, not the preset: the Landing hero allows the largest display type and brand expression; Docs prose keeps 1rem / 1.7 and ≈ 76ch; sidebar, TOC, parameter tables, search results and the command palette keep compact rows (0.875rem, 1.4–1.5 line height). Presets may change paint inside these zones but not their spacing in phase 1. Terminal’s compact chrome is a phase 2 density token.

Runtime surfaces

Surface Phase 1 impact
Blog, Book, taxonomy Tokens only; Book captions keep the prose face
Search dialog and command palette Scrim tokenized; selected row uses --td-shell-primary-dim
Mermaid, ECharts Colours baked at init on data-bs-theme. Add a data-td-preset observer only if charts take preset colours; phase 1 keeps mode-only chart palettes
asciinema Surface tokens; re-mount only if the code face changes (not in phase 1)
giscus Needs one stylesheet per preset and mode, re-posted on td-preset-change
Swagger UI, ReDoc Keep vendor styling and current light/dark handling
Print Tokenize navy and cool greys; print always uses a light palette from the active preset
404 Its own <html> must carry the new attributes

Accessibility, security, and output

  • Every preset palette passes WCAG AA for body, secondary and tertiary text, links, and accents in both modes (values above). theme_color contrast warnings compute against the active site default preset’s canvases.
  • The menu uses native radios; no role="menu". Focus is never trapped except in the mobile bottom sheet, which is modal and restores focus.
  • prefers-reduced-motion and forced colors keep current behaviour.
  • The init script is inline, static, and derived from validated configuration; the stored value is matched against a build-time allowlist before use.
  • No external font or script request is added. Output adds two <html> attributes, one inline script, and CSS.

Compatibility and migration

Changing the default to Paper changes every site that does not set preset.

  • Sites that want the current look add params.ui.preset: slate; the upgrade note leads with this one line. Slate output must equal v1.1 tokens.
  • Sites with custom brand overrides in _styles_project.scss: light overrides on :root keep working under Paper by source order; dark overrides on [data-bs-theme='dark'] are outranked by Paper’s dark block. Such sites should choose Slate or move overrides to [data-td-preset='paper'][data-bs-theme='dark']. The upgrade note and brand guide document this.
  • theme_color, typography, and fonts keep their meaning and precedence.
  • Sites with dark_mode: false still get one light palette, now Paper.
  • The release that changes the default must state it as a visible change. Whether that release is a minor (1.x) or major version is an open decision.
  • A consumer inventory should report sites with brand overrides before the default change is published.

Implementation plan

Phase 1, in dependency order. Each step names its owning checker.

  1. Tokenize Slate leaks. Landing primary button, grid, glow, scrims, print colours, asciinema surfaces; add --td-preset-accent, brand font role, and per-preset canvas luminance in contrast-on-canvas.html. Slate output must stay byte-for-byte equivalent in computed colour. Checkers: check-landing.py, check-output.py, check-font-tokens.py.
  2. Vendor IBM Plex Sans. third_party/, VENDOR.json, licence file. Checker: check-vendor.py.
  3. Preset tokens. New assets/scss/td/_presets.scss (imported after _brand.scss); the implementation keeps Paper in that file instead of a separate presets/_paper.scss. Place preset font roles before the system typography reset. Checkers: extend check-font-tokens.py (Plex Sans family, system block order, token parity between light and dark blocks).
  4. Configuration. hugo.yaml defaults (preset: paper, preset_menu: false); a resolver partial used by validate.html, document-attrs.html, layouts/404.html, and head.html (init script, theme-color, pre-paint canvas). Regenerate the schema. Checkers: check-params.py (accepted, invalid, reserved), generate-config-schema.py --check, check-namespace.py.
  5. Appearance menu. Shared partial used by navbar.html, shell/footer-line.html, and the Landing mobile drawer; preset.js runtime (or a section of dark-mode.js); dark-mode.js radio sync; palette action switch_preset; i18n strings in all 32 catalogs. Checkers: check-shell.py, check-actions.py, i18n checker, tests/js/preset.test.js, tests/js/dark-mode.test.js.
  6. Third-party surfaces. Per-preset giscus stylesheets and re-post.
  7. Documentation. EN/ZH architecture, shell and landing contracts; brand guide (presets, migration, fonts); configuration reference; changelog and upgrade note.
  8. Site validation. make -C ../oink.pgsty.com check, browser (add preset switching, persistence, storage failure, no-JS, EN/ZH, desktop/mobile, light/dark cases), and dev for visual review.

Acceptance criteria

The following are the original acceptance targets. Executed checks and remaining limits are recorded separately in the October 5 acceptance record:

  • With no preset key, output carries data-td-preset="paper" and renders Paper with JavaScript disabled.
  • preset: slate produces computed colours and font roles equal to v1.1 across the checker fixtures.
  • Switching style never changes td-color-theme; switching mode never changes td-preset; both survive navigation, reload, and language switch.
  • Invalid stored values are removed; storage failure leaves the page usable and shows the non-persistence note.
  • No first-paint flash between presets in Chromium, Firefox and WebKit at normal and throttled CPU.
  • Scroll position after a switch stays within one line of the anchor.
  • typography: system triggers no font request in any preset; params.ui.fonts overrides preset faces.
  • theme_color overrides the accent in both modes under Paper and Slate.
  • The menu is fully operable with keyboard, touch, and screen readers; axe reports no new violations.
  • All palettes meet the contrast table in both modes.
  • Presets add no external font or script dependency; explicitly configured services such as Giscus remain separate.

Open decisions

  1. Resolved for phase 1: preset_menu: false; the docs site enables it.
  2. Target resolved for release preparation: 1.2.0, with a prominent Paper-default notice and the preset: slate compatibility setting. Published in 1.2.0.
  3. Resolved for phase 1: the wordmark role is brand.
  4. Whether a display-only serif becomes a Paper option after phase 1.
  5. Whether charts (Mermaid, ECharts) should take preset colours in phase 2.

Ink and Terminal backlog

Implemented experimentally: both palettes, existing font roles, prose link and selection signals, heading treatments, scoped geometry, compact Terminal navigation, Giscus palettes, print and the existing switching mechanism. No new font file, animation or runtime is added. See the experiment record for actual output and verification.

Before stable promotion, review long-page red accent density and CJK underline weight in Ink; numbered headings, mono wrapping and dense parameter tables in Terminal; Windows/Android fallback faces and manual screen-reader speech. Mermaid/ECharts and API vendors remain mode-only for this experiment. Wider geometry/density tokens and preset-colored charts require a separate decision.

Decision log

Date Change
2026-10-04 Draft created with Paper/Slate phase-1 scope, Ink/Terminal research specs, Appearance menu choice, and token architecture
2026-10-05 Phase 1 implemented locally; defaults, brand role and mode-only chart scope accepted
2026-10-05 Subsequent explicit Ink/Terminal experiments implemented; stable menu policy retained; design acceptance remains open
2026-10-05 Release preparation targets 1.2.0; simplified Style/Light controls and current-state icons replace the earlier swatch proposal

7.9.6 - Stabilizing Ink and Terminal for reading

A conditional stabilization plan for two distinct reading styles, with a shared footer blocker and explicit bilingual accessibility gates.
Draft — promotion blocked

Enlarged text overflows the shared footer. At 320px with a 32px root/default font, English Ink reaches 424px document width and Terminal 408px. The comparison overlay does not repair this shared failure. Neither prototype is stable or accepted for promotion.

Status, ownership and scope

Status and date: Draft, 2026-10-26; proposed design direction, not an implementation contract

Accountable owner: OINK maintainers; separate recorded acceptance for each preset

Delivery owners: Theme maintainers own CSS, resolvers and focused checkers; documentation maintainers own bilingual contracts, real-site fixtures and browser review; accessibility reviewers own manual task evidence

Baseline: OINK v1.4.0; local source identity and dirty-snapshot limits are recorded in the research

Affected surfaces: Architecture, Components, Shell, Landing and the visual preset decision

Release intent: A later release, subject to refinement and independent acceptance; no new preset recommended

Context and evidence

Ink and Terminal already exist as explicit experiments. Their shared shell is an advantage: readers retain navigation, search, appearance controls and content semantics when changing style. Their remaining problems are repeated decoration, typography outside ordinary prose, inconsistent owned geometry, and incomplete acceptance. Ink is already neutral sans editorial; making it serif would consume Folio’s separate territory. Terminal should become a readable engineering handbook, retaining mono technical roles alongside proportional explanations.

The dated research distinguishes source facts, measurements, design judgments and a limited CSS overlay. Normal-size measurements, spacing probes and focused checks are encouraging, but the whole-document footer failure survives them. Manual 200% text-resize and 400% browser-zoom checks remain separate from the 32px font-preference probe.

Goals and explicit exclusions

Preset Audience and primary task Distinct identity within the same shell
Paper Broad documentation readers; guides and sustained reading Warm Plex Sans reading surfaces and restrained rounded components; remains the default
Slate Existing technical/product sites; reference navigation and product presentation Established cool palette, Inter/Chakra/Plex roles and richer Landing presentation; compatibility choice
Ink Editors, authors and readers of essays, design rationale and long documentation Neutral Inter/CJK sans, square flat surfaces, visible prose underlines and sparse red or authored accents
Terminal Engineers and operators; procedures, configuration lookup and code explanation Plex Sans explanations, mono headings/controls/identifiers, teal/mint links, amber emphasis and flat 2px geometry

Both candidates must work across Docs, Book, Blog, galleries, Expo and Landing in EN/ZH, light/dark and narrow/wide views. Neither is a separate shell, accessibility mode or table-density mode. Exclude new renderers, global spacing changes, font downloads, runtime dependencies, terminal effects, moving selection tracks, raised Tabs, boxed Field names and Paper heading tails.

Two optional concepts remain outside 1.4 scope:

Concept Distinct future purpose Cost and disposition
Folio Academic/book publishing: serif reading roles and scholarly captions/notes, sans navigation/search, mono code Requires a matched EN/ZH serif strategy, licensing, local font payload and fallback/print/device acceptance. Keep the shared shell; no imported margin-note column. Credible future study, not an Ink replacement.
Canvas Playful geometric presentation for creators: existing sans faces, controlled lilac/ochre accents and a few geometric markers No inherent font cost, but added palette/state/shape rules, asset guidance, menu and full regression coverage. Evidence is weaker and overlap with Slate is substantial. Defer until creator tasks justify it.

Proposed behavior and boundaries

Preserve the palette, assign each role

Retain these source colors initially. Values below are light/dark pairs, not a new configuration authority; preset SCSS remains the implementation owner. Refine repetition before changing hue.

Role Ink light / dark Terminal light / dark
Canvas #ffffff / #0b0b0b #f4f5f2 / #0c0f0e
Body #141414 / #ededed #1d211f / #d3dbd6
Emphasis #000000 / #ffffff #111713 / #edf4ef
Secondary text #474747 / #b5b5b5 #4a514d / #9aa59f
Tertiary text #636363 / #8f8f8f #606b63 / #8b9990
Secondary surface / code header #f4f4f4 / #161616 #e9ebe6 / #141a18
Tertiary surface #ffffff / #161616 #f8f9f6 / #18201c
Code block #fafafa / #111111 #ffffff / #0a0d0b
Link #141414 / #ededed #0a6560 / #4cc9bd
Hover link #c8102e / #ff5c4d #084f4b / #7de0d7
Default accent #c8102e / #ff5c4d #935400 / #f0a73a
On-link foreground #ffffff / #0b0b0b #ffffff / #0c0f0e
Ordinary border Body RGB at 24% / 24% Body RGB at 32% / 32%
Strong line Body RGB at 55% / 50% Body RGB at 55% / 55%
Inline code Body foreground; body RGB fill 5% / 6% Body foreground; body RGB fill 5% / 7%

Success, warning, error and information retain their semantic tones, icons and labels. Red is not every Ink status; mint is not every Terminal status. Keep neutral badges distinguishable from success. Decorative separators may remain subtle; required control boundaries need independent contrast measurements.

Authored theme_color remains authoritative for accent surfaces. Actual documentation uses blue #245f94/#5da2dd, Book orange #9a3412/#fb923c, and Blog violet #6d28d9/#a78bfa. These do not replace the link/code text roles. Review both authored accents and preset defaults.

Keep reading roles and verify actual fallback

Role Ink default Terminal default
body Inter + platform sans IBM Plex Sans + platform sans
ui Inter + platform sans Code role: IBM Plex Mono + mono fallbacks
heading Body role UI role
code IBM Plex Mono + mono fallbacks IBM Plex Mono + mono fallbacks
display Heading role Heading role
meta UI role UI role
brand UI role UI role
print Body role Body role

Book captions, table descriptions and human-readable search titles/excerpts use the body role. Authored chapter numbers retain their meaning and follow their heading’s role. Technical paths, command controls and identifiers remain mono in Terminal. Field names stay unboxed, at weight 600. Descriptive Terminal table headers move to body 0.875rem/600; ordinary prose retains 1rem/1.7 and the shared reading measure. Display titles retain page-specific scale and wrap; do not shrink mobile prose to accommodate mono chrome.

The existing sans fallback order is -apple-system, BlinkMacSystemFont, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans SC, generic sans-serif and emoji. The mono list follows IBM Plex Mono with SFMono-Regular, Cascadia Code, Roboto Mono, Menlo, Consolas, Liberation Mono, Sarasa Mono SC, Noto Sans Mono CJK SC and generic monospace. Listed names do not install fonts. YaHei precedes Segoe and can supply Latin too. Chinese glyphs need not occupy Latin mono cells. Preserve language metadata, typography: system, explicit roles and legacy Sass precedence; inspect loaded, blocked, system and overridden font conditions on actual Windows/Android. (Font matching)

Compare Ink article h1 at weight 750/tracking -.025em, h2/h3 at 700/-.012em, and a 1px strong-token top rule instead of the current heavier rule. Chinese headings use zero tracking. Remove enforced uppercase/tracking from minor headings, table headings and callout titles. Terminal headings use 600/zero tracking; remove generated ##/### and callout brackets while preserving authored Book numbers, semantic labels and heading anchors. No fake prompts, blinking cursor effects or decorative heading tails.

Ink prose links remain natively underlined at rest, including lists, notes and citations. Terminal retains a persistent non-color link distinction; verify its existing semibold cue in real prose and use native underlines where that cue is insufficient. Navigation relies on its location and persistent current-state marks. Hover must not change layout, weight or hit area. (GOV.UK links)

Underline geometry remains a review decision. The overlay uses max(1px, .06em) thickness, .22em offset, with a Chinese .25em/skip-ink: none comparison. A blanket .25em rule is not accepted. Compare English .12–.18em and Chinese text-underline-position: under with only 0–.06em extra offset, starting with skip-ink: auto. Scope by actual language, including Chinese blocks inside English pages. Test mixed glyphs, punctuation, descenders, inline code and wrapped links; no line may collide with glyphs or adjacent lines. (CSS Text Decoration draft)

Make component states coherent

Owned component corner radii: Ink 0px, Terminal 2px, no elevation shadows; include FileTree’s currently hard-coded 0.6rem corner radius. Preserve image, avatar, status-dot and vendor geometry.

Current/selected: Ink keeps a static accent rail; Terminal keeps neutral inverse rows/marker. Programmatic state remains. Hover, selection and keyboard focus stay distinct; no traveling pill.

Focus: Immediate static indicator, independently measured on every adjacent surface, visible in forced colors; no global outline reset.

Text selection: Start from current authored-accent fill at 22% light/35% dark; measure composited text/inline-code pairs. Change through the owning role if needed; do not assume the opacity passes.

Tabs: Flat labels, static bottom selection rule, separate focus; no raised active tab. Preserve keyboard, hash/storage behavior, long labels and touch hit areas.

Fields and inputs: Unboxed technical Field names; descriptions remain prose. Actual input controls retain persistent labels, identifiable boundaries and associated error text; placeholder alone is insufficient.

Code and tables: Quiet single frame; preserve syntax semantics, comments, copying, captions, header associations and numeric alignment. Descriptive headers can wrap; no whole-table uppercase. Preserve authored width/overflow policy and local scroll access.

Notes, quotes, footnotes: Keep essential text visible; restrained semantic marker/rule, close attribution and usable reference/backlinks. No oversized quote ornaments, blanket Chinese italics or added margin column.

Cards, steps, badges, disclosures: Stable labels and geometry; distinguish clickable from static content, keep real step numbers, native disclosure semantics and labeled states.

Search and appearance: Body-role result titles/excerpts, mono technical paths; readable matching/selected/muted text. Preserve dialog focus/return, IME editing, loading/error/no-results feedback and reachability with a mobile keyboard.

Landing, Blog/gallery, Book, Expo: Preserve existing composition and media behavior; fit display titles, captions, bylines and utilities into each reading task. Do not turn image surfaces into squared global resets.

Preserve output, vendor and security boundaries

Mermaid/ECharts remain mode-only; API widgets retain vendor palettes and their existing mode behavior. Do not add preset recoloring inside vendor DOM. Giscus automatic palettes follow preset and mode; explicit overrides remain authoritative, but blocked remote frames are not accepted by this review. Print keeps the light preset on white paper; no-JavaScript output keeps the site-default light palette and readable content. Markdown/RSS preserve semantic content without decorative prefixes or control leakage. Keep existing escaping, validated font/color inputs, local assets and appearance storage behavior; no new network permission or script dependency.

Compatibility and migration

Keep preset names, independent mode/style state, shell columns, breakpoints, content syntax and author overrides. Retain Paper as default and maintained consumers’ explicit [paper, slate, ink, terminal] menu. A refinement can ship while a preset remains experimental.

Promotion changes the meaning of preset_menu: true: it offers stable presets plus the site default, so promoting either experiment expands that automatic set. Make that change explicit in layouts/_partials/preset-config.html, its checker and bilingual configuration/release guidance. Explicit lists retain their chosen membership. Promotion requires per-preset maintainer acceptance, not a status-label edit. Preserve reserved Folio/Canvas warning/fallback behavior.

Implementation and owning-checker plan

Stage / owner Work and evidence
Shared repair / shell maintainer Fix footer shrink/wrap behavior at its owner; add whole-document scroll-width and visible-content assertions through the footer for all four presets. Avoid clipping or hiding overflow as a remedy.
Refinement / theme maintainer Implement reviewed changes in existing preset, typography and component owners. Start with bin/check-presets.py, bin/check-font-tokens.py and affected component/output checks; preserve frozen Slate.
Integration / documentation maintainer Extend appearance, font, component and text-reflow suites on real EN/ZH pages; include full-page checks and annotated visual review. Run sibling-source make check, make browser, and make dev review.
Promotion / maintainers and reviewers Close the gates below; update owning implementation/checker and affected EN/ZH contracts together, then record durable rationale and user-facing migration guidance.

Runtime edits additionally require node --test 'tests/js/**/*.test.js'; schema edits require python3 bin/generate-config-schema.py --check.

Acceptance gates and open decisions

Distinguish accessibility requirements from project targets

WCAG 2.2 A/AA is the baseline (references accessed on 2026-10-26).

Gate Required assessment
Text and controls Normal text 4.5:1; qualifying large text 3:1 (24px regular or 18⅔px bold, equivalent CJK sizing). Required control/state cues 3:1 against adjacent colors; decorative dividers are different. Include code comments, placeholders and every selected/hover state. (1.4.3, 1.4.11)
Reflow and resizing Independently test 320 CSS px reflow, manual 200% text enlargement and 400% browser zoom from a 1280px viewport. Necessary two-dimensional regions require individual justification; code/tables are not blanket exemptions. Also close the separate 320px/32px-font stress blocker. (1.4.10, 1.4.4)
Spacing Tolerate simultaneous line-height 1.5, paragraph-after spacing 2em, letter spacing .12em and word spacing .16em without loss. These are user overrides, not default typography; assess applicable Chinese/mixed-script properties. (1.4.12)
Keyboard and meaning Visible focus; focused controls not entirely obscured by authored content; non-color state cues; correct language, headings, labels and table relationships. Character shortcuts need disable/remap/focus-only handling, not merely suppression in text inputs. Announce applicable search statuses without moving focus. (2.4.11, 2.1.4, 4.1.3)
Target minimum AA uses 24×24 CSS px or a qualifying exception. For its spacing exception, a 24px-diameter circle centered on each undersized target must intersect neither another target nor another undersized target’s circle; inline, equivalent, user-agent and essential exceptions also exist. (2.5.8)
Proposed stronger targets Routine touch controls 44×44px; main text 7:1; whole focused control/ring visible; static indicator at least a 2px-perimeter area with 3:1 change at the same pixels, plus adjacent-color contrast. These go beyond AA and need explicit adoption; they do not imply whole-page AAA. (2.5.5, 1.4.6, 2.4.13)

Record complete tasks before promotion

Require both languages and modes at 320/390/768/1440px, with normal, enlarged and overridden text. Review long pages at top/middle/footer, heading/Book numbering, URLs, Fields, tables, code, search, appearance, media and Landing. Inspect rest/hover/current/selected/focus/text-selection, forced colors, reduced motion and no-JavaScript/print/Markdown/RSS. Record actual color pairs and human review of axe incompletes.

Actual Windows Chrome/Edge and Android Chrome font/rendering work remains pending, as do branded macOS/iOS Safari checks. Record device/OS/browser/font conditions. Pending manual tasks use NVDA on Windows, VoiceOver with actual Safari, and TalkBack on Android: traverse headings and meaningful numbers, read tables/field errors, search with Chinese input, hear concise status feedback, open/close dialogs and regain focus.

Open decisions are the Chinese underline treatment, stronger project targets, any state-specific color repair, and the promotion of each preset. If gates remain incomplete, retain experimental status or defer refinement; unrelated 1.4 work need not wait for new styles.

Decision log

Date Proposed decision and status
2026-10-26 Keep Ink neutral sans editorial and Terminal a readable engineering handbook; preserve current palettes and author accents. Direction recommended; no stability acceptance.
2026-10-26 Prioritize shared enlarged-font footer repair; keep flat Tabs, unboxed Fields and no Paper tails. Remove decorative Terminal punctuation through existing owners.
2026-10-26 Chinese underline alternatives and stronger accessibility targets remain review decisions; exploratory probes do not close promotion gates.
2026-10-26 No new preset for 1.4; Folio/Canvas remain distinct future concepts. Per-preset promotion and automatic-menu expansion require explicit acceptance.

8 - OINK CLI

An introduction to the optional OINK command-line companion for Hugo sites. The CLI is still a draft.

oink is an optional command-line companion for OINK sites that helps create sites, inspect Hugo output, and review content or theme changes. Hugo still renders the site, and the project remains a standard Hugo repository.

Optional draft · not needed for ordinary sites

Everyday OINK sites build and preview with standard hugo and hugo server commands—you do not need to install or build oink-cli. OINK CLI is developed in the independent oink-cli repository as a local 0.1.0-dev candidate with no public release or installer yet; commands may still change.

What it does

Command Purpose
oink init Create a site from a fixed Starter and select its profile and languages.
oink doctor Inspect tools, configuration, and the resolved theme source.
oink check Check rendered links, translation relationships, and source/style rules.
oink dev / oink build Run Hugo’s preview server or production build.
oink new / oink move Preview page creation or moves before applying a saved plan.
oink translations Inspect translation status and diffs, and record human reviews.
oink upgrade Compare a theme upgrade before writing module changes.

The CLI also supports JSON/YAML output, explicit multi-site workspaces, and checked build artifacts. Studio and general source-editing commands have been retired: use oink dev for live preview, a regular editor for authoring, and inspect or check for structured diagnostics.

Try the local candidate

From an oink-cli source checkout, build the binary with Go 1.27.3+ and Make:

cd oink-cli
make deps
make build
./bin/oink --help
./bin/oink doctor --site ../my-docs
./bin/oink check --site ../my-docs

Replace ../my-docs with your site path. Site commands require Hugo Extended 0.168.0+ and Go 1.27+ for module operations. oink init uses the CLI’s bundled Starter snapshot; follow the upgrade guide to update a generated site to OINK v1.5.1.

make deps downloads Go build dependencies. CLI commands run offline by default; pass --network when a command needs uncached remote inputs. Inspection and plan previews (new, move, upgrade) leave site sources untouched until you run their explicit apply step, whereas dev and build write normal Hugo output and caches.

Further reading