Skip to content

1 - OINK 1.0.0 — Stable contracts, a supported starter, and a complete public release

OINK 1.0.0 makes the current knowledge-publishing contracts stable and rolls up every theme change since 0.8.0: Print and Book correctness, pinned Go 1.27 and Hugo 0.165.0 release tooling, a supported Starter, and the public metadata and media needed to enter the wider Hugo ecosystem.

OINK 1.0.0 is a stability milestone, not a last-minute API reset. It promotes the component, configuration, content, output, and maintainer contracts built through the 0.x releases to the first major version. This note rolls up every theme change in v0.8.0..v1.0.0; the 0.8.0 agent outputs and backlinks are the starting baseline rather than being counted again as new 1.0 work.

An existing 0.8.0, 0.8.1, or 0.8.2 site needs no content or configuration migration. Pin the new module, run the warning-strict build, and review the rendered site as you would for any theme upgrade.

At a glance

  • The current authoring, shell, landing, Book, release, Print, Markdown, and agent-output contracts are now the OINK 1.0 stable surface.
  • Single-page Print preserves ordinary page heading and footnote IDs; only multi-page section and whole-Book aggregates namespace page-local targets.
  • Book sidebar numbers remain atomic beside long, wrapping titles.
  • Theme CI, the documentation site, and OINK Starter use Go 1.27 and Hugo Extended 0.165.0. The declared Hugo compatibility floor remains Extended 0.160.1.
  • OINK Starter is the supported first route into the framework, with neutral Docs, Blog, and Book content and strict GitHub Pages and Cloudflare Pages workflows.
  • The README, theme metadata, case-study links, badges, and optimized 3:2 Hugo Themes gallery images now describe and present the same product that the code ships.

What 1.0 makes stable

The version number covers contracts, not a promise that the interface will stop evolving. OINK can keep adding components and optional outputs in 1.x; the point of 1.0 is that an ordinary site should not have to relearn or rewrite the current foundations on every minor release.

Surface 1.0 contract
Content Native Markdown remains the source; components preserve explicit static degradation for non-interactive outputs
Configuration params.ui.* owns theme policy, page overrides drop the prefix, and invalid author input warns with a safe fallback
Shells Docs, Blog, Book, Swagger/Redoc, and Landing keep distinct, documented responsibilities
Outputs HTML, RSS, Print, Markdown, LLMS, LLMSFULL, NAVJSON, and BookManifest keep explicit opt-in and degradation boundaries
Runtime Vendored assets stay local and capability code loads only where rendered content needs it
Maintenance Implementation, owning checker, bilingual contract, release state, consumer pin, and deployment remain separate evidence

The normative English and Chinese records live under Design. Their status now reads released-v1.0.0; dated research and active proposals remain evidence or future work, not hidden 1.0 features.

Everything changed after 0.8.0

The complete source comparison is v0.8.0...v1.0.0. It contains a deliberately small stabilization set:

Area Change User-visible effect
Book sidebar Fixed the number cell and added a compiled-CSS regression assertion A long title wraps without compressing, clipping, or splitting its chapter number
Print anchors Distinguished a page Print from section and Book aggregates, then refreshed the output golden A fragment valid on an ordinary page remains valid on that page’s Print view; aggregate IDs remain collision-free
Theme CI Replaced the historical Hugo matrix with one pinned Extended 0.165.0 toolchain and explicitly pinned Go 1.27 for module-mode coverage Release evidence now matches the current upstream toolchain while the 0.160.1 Hugo floor stays documented separately
Public README Rebuilt the first-run path around OINK Starter, capability and compatibility tables, production cases, docs links, and the Docsy boundary A visitor can evaluate the project without reverse-engineering its regression site
Hugo Themes media Replaced the old captures with optimized 3:2 landing-page images The gallery receives the required aspect ratio at 166,526-byte and 68,488-byte PNG sizes without browser chrome
Theme metadata Expanded the description, tags, and features; normalized the OINK wordmark; recorded Docsy as the original theme Catalogue attribution and discovery now match the repository’s actual scope
Module directive 0.8.2 temporarily admitted the module to the former Go 1.26 upstream builder; 1.0 follows the newly updated upstream workflow on Go 1.27 This changes module admission only; OINK still contains no Go source and the rendered theme is unchanged by the directive

There is no component rename, configuration-key removal, default flip, or content-syntax migration in this range.

Correct Print identities

Page-local IDs and aggregate-document IDs solve different problems. An ordinary page and that page’s own Print representation are two views of the same document, so their authored or Goldmark-generated heading and footnote IDs should match. A section Print or whole-Book Print combines many source pages, so two chapters may both contain #overview or fn:1; those targets must gain a source-page namespace.

Output Heading and footnote IDs
Ordinary HTML page Authored or Goldmark-generated page-local IDs
Single-page Print The same page-local IDs as ordinary HTML
Multi-page section Print Source-page namespace added
Whole-Book Print Source-page namespace added

Book figures, tables, equations, examples, and rewritten cross-page links keep their existing explicit target rules. The fix narrows namespacing to the two outputs that actually aggregate documents.

A supported first mile

OINK Starter is now part of the supported release surface rather than an informal demo. It starts as a small, neutral project site: three language profiles, Docs, Blog, Book, local assets, and two warning-strict deployment workflows. It deliberately excludes OINK’s own analytics account, comments, brand, documentation corpus, browser suite, and maintainer fixtures.

The Starter tutorial moves from shallow changes to deep ones: establish an unchanged baseline, set identity, choose languages, replace home data, rewrite content and navigation, add brand, enable complete integrations, run the strict build, and only then deploy. Existing Hugo sites can still take the smaller module-from-scratch path.

Toolchain and compatibility

The official Hugo Themes update workflow moved to Go 1.27 and Hugo 0.165.0 on the day of this release. OINK 1.0 follows that current publication baseline:

Dependency OINK 1.0 policy
Hugo Extended 0.160.1 or newer; release, site, and browser validation pin 0.165.0
Go 1.27 or newer for Hugo Module resolution
Node.js Not required to build or run a consumer site

The short-lived 0.8.2 patch lowered only the module’s go directive so the previous official updater, fixed on Go 1.26 with local toolchain selection, could admit the theme. Once upstream moved to 1.27, keeping that exception no longer described the release environment. OINK itself is still templates, styles, assets, and checkers rather than Go source. An offline archive or Git submodule installation does not ask Go to resolve the module.

Upgrade

hugo mod get github.com/pgsty/oink@v1.0.0
hugo mod tidy
hugo --cleanDestinationDir --gc --minify --environment production \
  --printPathWarnings --panicOnWarning

Commit both go.mod and go.sum. Then inspect representative Docs, Blog, Book, Print, language, light/dark, and narrow-screen routes. A successful local build, the public tag, a resolved module checksum, a consumer pin, deployment, and live rendering remain separate release states.

The repository-level ledger remains in CHANGELOG.md.

2 - OINK 0.8.2 — Go 1.26 module compatibility for the Hugo Themes builder

OINK 0.8.2 lowers the Hugo Module’s Go directive from 1.27 to 1.26 so GOTOOLCHAIN=local builders, including the official Hugo Themes pipeline, can import the theme without changing its Hugo floor or rendered behavior.

OINK 0.8.2 is a module-metadata compatibility release. It changes no template, asset, component API, configuration key, content syntax, or rendered output. Sites already using 0.8.1 need no content migration.

At a glance

  • The theme module now declares Go 1.26 instead of Go 1.27.
  • The official Hugo Themes builder can import OINK under its pinned Go 1.26 toolchain with GOTOOLCHAIN=local.
  • Hugo Extended 0.160.1 remains the declared compatibility floor; OINK Starter continues to build with Hugo Extended 0.165.0.
  • OINK contains no Go source and uses no Go 1.27 language or module feature, so the lower directive changes module admission only.

Why this patch exists

The OINK 0.8.1 release was built and tested with Go 1.27. Its go.mod therefore declared go 1.27.0, even though the repository is a Hugo Module made of templates and assets rather than Go packages. The official Hugo Themes pipeline runs Go 1.26 with automatic toolchain switching disabled. It correctly rejected the higher directive before reading the theme metadata.

OINK 0.8.2 declares the oldest Go toolchain the current release workflow needs: Go 1.26. The module remains byte-for-byte identical at the rendered surface; only the compatibility gate that admits the module has changed.

Compatibility

Dependency OINK 0.8.2 requirement
Hugo Extended 0.160.1 or newer
Go for Hugo Module resolution 1.26 or newer
Node.js Not required

An offline archive or Git submodule installation does not require Go because it does not ask Hugo to resolve a module.

Upgrade

hugo mod get github.com/pgsty/oink@v0.8.2
hugo mod tidy

Commit go.mod and go.sum, then run the site’s warning-strict production build. The complete change list is in CHANGELOG.md.

3 - OINK 0.8.1 — Stable Print anchors, resilient Book numbers, and release-ready packaging

OINK 0.8.1 is a focused maintenance release: single-page Print keeps the same heading and footnote anchors as ordinary HTML, long Book titles no longer squeeze their sidebar numbers, and the project presentation now leads new sites through OINK Starter.

OINK 0.8.1 closes two narrow rendering defects and brings the public project surface into line with the framework that shipped in 0.8.0. It changes no component API, configuration key, content syntax, or compatibility floor. An existing 0.8.0 site can update its module pin without migrating content.

At a glance

  • A page rendered by itself in Print now keeps exactly the ordinary page’s heading and footnote IDs. Section and whole-Book Print still namespace those page-local IDs because several source pages share one aggregate document.
  • A Book sidebar number is now an atomic fixed-width cell. A long title may wrap, but it cannot shrink, clip, or wrap the number beside it.
  • Continuous integration uses one pinned Hugo Extended 0.165.0 toolchain for theme, publication, site, and browser checks. Hugo Extended 0.160.1 remains the declared consumer compatibility floor.
  • The project README now makes OINK Starter the primary route, documents the capability and compatibility boundaries, shows representative production sites, and explains why OINK is a distinct theme rather than a Docsy skin.
  • New 3:2 Hugo Themes gallery media, richer theme metadata, and explicit Docsy attribution complete the submission package without adding page assets to a consumer site.

Print IDs match the surface being rendered

Heading and footnote IDs are page-local facts in ordinary HTML. Before 0.8.1, the same page rendered alone as Print received the prefix reserved for an aggregate, so a URL fragment that worked on the regular page did not identify the corresponding element in that page’s Print output.

The rule is now explicit:

Output Heading and footnote IDs
Ordinary HTML page Authored or Goldmark-generated page-local IDs
Single-page Print The same page-local IDs as ordinary HTML
Multi-page section Print Source-page namespace added to page-local IDs
Whole-Book Print Source-page namespace added to page-local IDs

Explicit Book targets such as figures, tables, equations, and examples remain stable. Aggregate output still rewrites its cross-page links to the namespaced heading and footnote targets, preventing two chapters with #overview or fn:1 from producing duplicate IDs in one document.

Book numbers stay readable beside long titles

The Book sidebar uses a number cell followed by a title cell. The number cell could previously inherit shrinking and overflow behaviour from a long title, which made chapter numbers clip or break across lines at narrow widths. The number is now non-shrinking and atomic; only the title wraps. This is a CSS-only correction and does not change Book numbering or navigation order.

A clearer first route into OINK

The recommended starting point is now the small pgsty/oink-starter template rather than a clone of the documentation regression site. It includes neutral Docs, Blog, and Book content, three language profiles, and warning-strict GitHub Pages and Cloudflare Pages workflows without OINK’s own analytics, comments, test harness, or brand.

The new Starter tutorial takes that baseline through identity, languages, home data, content and navigation, brand, integrations, strict builds, and deployment in that order. Existing Hugo sites can still use the smaller from-scratch module path.

Upgrade

hugo mod get github.com/pgsty/oink@v0.8.1
hugo mod tidy

Commit both go.mod and go.sum, then run the site’s warning-strict production build. No content or configuration migration is required. The complete change list is in CHANGELOG.md.

4 - Oink 0.8.0 — A whole section in one fetch, the sidebar as data, and who links here

Oink 0.8.0 adds two opt-in output formats for readers that arrive as programs — a full-text bundle holding an entire section in one file, and a navigation tree published as JSON — plus static backlinks, which list the pages that link to a page in its right rail. All three stay off until you ask.

Oink 0.8.0 changes no component API and needs no content edits. Two of its three additions serve the reader that arrives as a program. Every page already publishes a .md twin, which serves an agent that knows the page it wants; an agent that wants the manual still had to crawl it a page at a time, discovering links as it went. Two new output formats answer the other half: give me the whole section, and tell me what is in the site before I fetch anything. The third addition is for the reader who is a person — a page’s right rail can now list the pages that link to it.

At a glance

  • LLMSFULL publishes llms-full.txt per top-level section: every page of the section, in sidebar reading order, in one file.
  • NAVJSON publishes navigation.json per language: the sidebar tree as data, versioned by a JSON Schema.
  • params.ui.backlinks lists the pages that link to a page in its right rail, derived at build time from the links already in your Markdown.
  • All three are opt-in and none is ever turned on for you. A site that asks for none of them builds byte-for-byte what it built before.
  • llms.txt lists whichever ones you enabled, so discovery stays in the file an agent already fetches.
  • A data/docs_nav.json node without a children key no longer crashes the build.

The full-text bundle

LLMSFULL collapses a whole section into one file: llms-full.txt at the section root, holding every page of that section concatenated in the order the sidebar and pager present them, each page introduced by a separator carrying its source URL. For an agent, /docs/llms-full.txt is one fetch where the alternative was one fetch per page plus a link graph to follow — and the result is ordered, so the section reads as a manual rather than as a bag of pages.

A section asks for it in its own front matter; the theme never adds it to a site’s output set:

content/docs/_index.md
---
title: Docs
outputs: [HTML, print, RSS, markdown, LLMSFULL]
---

Front matter outputs replaces the site-level list wholesale, so write back the formats the section already had. It is per language, so _index.zh.md repeats the line to get the Chinese bundle.

What a page contributes is the same semantic Markdown its own .md carries — not a second rendering of it. The per-page Markdown body moved into a shared partial that both outputs call, so a bundle entry is byte-identical to that page’s .md and the two cannot drift apart. Order comes from the same authority the sidebar reads: the explicit data/docs_nav.json tree where a docs or book section declares one, the weighted content tree otherwise. Pages held out of the sidebar stay out of the bundle.

A bundle belongs to a top-level section, and there is no whole-site variant: an agent that wants everything reads one bundle per section. Listing LLMSFULL further down the tree warns and emits nothing, so hugo server keeps working while a publishing build with --panicOnWarning stops there.

This site’s docs section has it enabled: https://oink.pgsty.com/docs/llms-full.txt is the entire documentation in one fetch. Details, including the file’s exact shape: Full-text bundle.

Navigation JSON

The sidebar is the site’s table of contents, and an agent that can read it plans a route before spending a single fetch on content. NAVJSON publishes it as data — navigation.json, one file per language at the language root. The site turns it on for the home page:

hugo.yml
outputs:
  home: [HTML, LLMS, NAVJSON]

The tree is not a second description of the site’s structure. It serializes the same authority the sidebar and the pager already read, through the same partial: the explicit data/docs_nav.json tree where one is declared, the weighted content tree everywhere else. A check asserts that the docs subtree flattens to exactly the page sequence the full-text bundle emits — two template paths, one authority.

Each node carries an id (the path with the language prefix removed, so the same page has the same id in every language), the absolute url, the markdown URL where the page publishes one, title, description, a kind, and its ordered children. Two properties are worth reading as promises rather than as implementation details:

  • Array order is the contract. The ordering has already been applied, and weight is never serialized — a consumer re-sorting the array would disagree with the sidebar the array came from.
  • The format is versioned. schemaVersion is 1, and the contract ships in the theme repository as schema/nav.v1.schema.json. Validate against it if you consume the file.

This site’s https://oink.pgsty.com/navigation.json is a live instance. Details, including the placeholder rows and the full key table: Navigation JSON.

A reader who lands on a page from search can see where it points and nothing about where it sits. Backlinks close that half: the pages that link to this one appear as a “Backlinks” group in the right rail below the table of contents, expanded by default; more than eight entries fold behind a disclosure. One key turns it on:

hugo.yml
params:
  ui:
    backlinks: true

A page overrides it with the front matter key backlinks, and a section cascades it to everything below.

The index is derived at build time from what you already wrote: ordinary Markdown links and ref / relref in the page source. There is no [[wikilink]] syntax to adopt, nothing to migrate, and no JavaScript — the links are in the HTML, they are in the page’s Markdown output, and they are there for a reader with scripts off. Code fences and inline code are stripped before scanning; repeated links to one target merge into one entry; self links, external links and same-page anchors never count; each language has its own graph. Order is the stable page path, so the same content always builds the same list, and when nothing links in there is no block at all.

One honest limit: reading the source misses a URL buried in a custom shortcode’s parameters or in a raw <a href>, and a destination that will not resolve is dropped quietly. This is navigation, not link checking — a link checker is still the tool for finding broken links.

This site enables it site-wide: look at the right rail of any docs page, and the most-referenced page — Configuration — lists more than forty inbound links. Details: Backlinks.

Discovery stays in llms.txt

Neither file is an alternate representation of a page, so neither appears in <head> or gains a page action. Instead llms.txt — the file an agent fetches first anyway — grows a ## Full-text bundles list of this language’s bundles, and lists this language’s navigation.json in its site index. Both entries appear only where the site actually publishes the file: the theme never points at something it did not emit.

A childless nav node no longer crashes the build

A node in data/docs_nav.json with no children key ended the build with a reflection error from inside the sidebar walker. The walker assumed every node had the key, which holds for generated JSON and does not hold for JSON someone writes by hand — where a leaf is naturally written as a node without children. Authored data now degrades instead of erroring: the childless node renders as the leaf it is.

Upgrading

hugo mod get github.com/pgsty/oink@v0.8.0
hugo mod tidy

Nothing changes until you ask for it. No component API changed and no content edits are required — the two formats are declared in outputs, backlinks are one boolean under params.ui, and a site that asks for none of the three publishes what it published under 0.7.1. The two output formats, and the shape of what they produce, are on AI-agent support; the backlink switch is on Navigation and menus.

The complete list is in CHANGELOG.md.

5 - Oink 0.7.1 — Nothing leaves the page, and bad input stops breaking builds

Oink 0.7.1 is a security and validation patch. Swagger UI stops sending your spec URL to a third party, misconfigured values warn instead of killing an ordinary build, and the OpenAPI and terminal components finally behave in print, Markdown and RSS the way every other component already did.

Oink 0.7.1 changes no component API and needs no content edits. It closes the code findings from an external review of the 0.7.0 line: one real privacy leak, a class of configuration values that could stop a build outright, and three components that had never been taught what a non-HTML output is.

At a glance

  • Swagger UI no longer contacts the online validator. A published API page made a third-party request on every view; it no longer makes any.
  • A URL written into site configuration now passes the same safety check as one written by an author.
  • A mistyped number or boolean in params warns and falls back instead of ending an ordinary hugo server session.
  • swagger, redoc and asciinema emit a plain link in print, Markdown and RSS, and load their runtime only in interactive HTML.

Swagger stopped phoning home

Swagger UI ships with an online validator enabled by default, pointed at validator.swagger.io. It skips that request for localhost, which is why no local preview and no browser test ever showed it — and why every deployed API page was quietly handing your spec URL to a third party. On an intranet that URL is an internal hostname.

The initializer now pins validatorUrl: null, and it moved out of an inline <script> into a cacheable js/chunks/swagger-init.js. A normal build still downloads nothing, and now a normal view uploads nothing.

Configured URLs go through the same gate as authored ones

Two settings reached an href unchecked: the custom links in params.ui.page_context_menu.links and the archived-site banner’s params.url_latest_version. A javascript: URL in either one rendered as a working, clickable script.

Both now run through the theme’s shared URL policy: an unsupported scheme warns and the link is dropped rather than repaired. The archived-version banner additionally escapes its URL where it is written into the page, because clearing the scheme is not the same as being safe inside an HTML attribute.

Custom links also skip entries with a missing or non-text name, and the separator above them only appears when a link actually survives.

Bad configuration warns; it no longer kills the preview

The theme’s rule has always been that invalid author or config input warns, falls back to a documented default, and keeps hugo server usable — with --panicOnWarning turning that warning into a failure where you publish. A group of numeric and boolean keys had never been wired into it.

Before 0.7.1, a value like blog_index_size: nope ended the build with a Go template error. Others were worse for being quiet: sidebar_width_min: -50 emitted a negative pixel width without a word, and blog_index_columns: 2.5 fed a fraction into a CSS grid.

Every numeric and boolean key now resolves through the shared validator:

Input Before Now
blog_index_size: nope build fails warns, uses 12
blog_index_size: 0 silently becomes 12 warns, uses 12
sidebar_width_min: -50 emits -50px warns, uses 220
sidebar_width_min: 300 with max: 200 inverted layout warns, uses 220/480
blog_index_columns: 2.5 fraction into CSS warns, uses 3
sidebar_item_overflow: clip silently ellipsis warns, uses ellipsis
print.toc: nope silently true warns, uses true

The same treatment reaches the Landing sections: the hero’s media.ratio and media.max_width, the capabilities board’s columns and rules, and a marquee’s rows. The hero’s two style inputs were the notable ones — they were written straight into a style attribute, so a page’s own front matter could put arbitrary CSS on the page. ratio now takes exactly two track sizes ('1fr 240px'), max_width a plain CSS length.

If you have been running with a value the theme silently corrected, you will see a new warning. That is the point — build once with --panicOnWarning after upgrading to find them.

OpenAPI and terminal recordings respect the other outputs

Every Oink component renders once and then adapts to the output it lands in: interactive HTML, static print, plain Markdown for agents, and RSS. Sixteen components already did this. swagger, redoc and asciinema did not — they rendered their interactive markup into all four.

The result was a Markdown output carrying <div class="td-asciinema"> and a JSON config block, a printed page holding an empty container where a player should be, and a single-page print that actually downloaded the player runtime to show one frozen frame.

All three now read the output format:

Output What you get
HTML the full interactive component
Print a labelled static link showing the URL
Markdown / LLMS a plain Markdown link, nothing else
RSS the same plain link

Only interactive HTML registers the runtime, so print and machine outputs load no player, no Swagger bundle and no ReDoc bundle. A cast or spec URL now also goes through the shared URL policy, and a mistyped speed, cols, rows or marker time warns and is ignored instead of ending the build.

Smaller repairs

  • The capabilities board’s rule bars render at their authored widths. The template had always emitted them; the stylesheet never read them.
  • The generated configuration schemas match what Hugo actually parses. Trailing comments in hugo.yaml were leaking into eleven default values — print.toc shipped as the string "true # section print views…" — and four comment blocks documented the wrong key. Keys that exist only to warn about a rename no longer appear in editor completion.
  • A hero whose media is not a map warns and drops the media instead of ending the build.

Upgrading

hugo mod get github.com/pgsty/oink@v0.7.1
hugo mod tidy

No content, configuration or template changes are required. One thing to do after upgrading: run a build with --panicOnWarning once. Configuration that used to be corrected silently now says so, and that build is where you will see it.

The complete list is in CHANGELOG.md.

6 - Oink 0.7.0 — A theme color, one typographic voice, and diagrams you can read

Oink 0.7.0 gives a section its own accent through the shell’s grounds, hands the site control of all seven typography roles, and turns a Mermaid fence into a figure that is centred, redrawn in place on a colour-scheme change, and openable at its own size.

Oink 0.7.0 changes no component API. It works on the two surfaces a reader actually spends time in — the shell around the page and the type on it — and finishes a fence that had never been designed, only inherited.

At a glance

  • params.ui.theme_color gives a section its own accent, applied to the shell’s grounds rather than to the prose.
  • params.ui.fonts reaches all seven typography roles; a Book no longer carries a face of its own.
  • A mermaid fence is a figure: centred, unframed, redrawn in place when the colour scheme changes, and openable at full size in a pan/zoom dialog.
  • Inline code is crimson ink on a hairline wash instead of a grey pill.
  • First-party browser behaviour publishes as stable capability chunks under js/chunks/, so a page selects scripts instead of minting its own bundle.
  • Configuration schemas are generated from the resolvers, not maintained by hand.

A theme color

params.ui.theme_color takes a #rgb or #rrggbb hex and tints the shell’s accent grounds: the selected sidebar row and the ground its neighbours take under the pointer, hover washes, the outline pill with its travelling rail and dot, tag and chip hovers, a card’s hovered edge, a share button’s hover fill, text selection, and focus rings.

hugo.yaml
params:
  ui:
    theme_color: "#2f6f4f"

A section can set its own, and a page opts out of an inherited one with theme_color: false. What it deliberately leaves alone is the reading surface — prose links, external URLs and inline code keep the brand palette in every section — so a colored section is a quiet signal of place rather than a recolor of the page.

One typographic voice

params.ui.fonts reaches the theme’s seven typography roles from configuration, so a site can change its voice without shipping a stylesheet.

A Book no longer carries typography of its own. Its numbers and captions used to render in a bundled monospace with a Latin subset only, which split a Chinese caption across two faces mid-sentence — the digits in one face and the characters in whatever fallback the reader happened to have. They now inherit the surrounding face, with tabular-nums holding the sidebar column aligned.

Diagrams you can actually read

A mermaid fence used to be five lines of passthrough: it handed Mermaid the <pre> of a code block and let startOnLoad decide the rest. Three defects followed from that one decision, and all three needed the same fix — keeping the source readable after Mermaid has run.

The fence now emits a figure holding an empty stage and its source as JSON, the shape echarts and infographic already use, and a runtime owns when each diagram is drawn.

Centred, and unframed. Mermaid emits width="100%" with a max-width at the diagram’s own size, so anything narrower than the column sat against the start edge with up to 300px of empty bordered box beside it — the border being the code block’s. There is deliberately no alignment attribute: a diagram is a figure, and no reader wanted one flush right.

Openable at its own size. Mermaid does not overflow a narrow column, it shrinks to fit one, so overflow-x never offered a way back: on a 390px phone the sequence diagram on this site’s own Mermaid page rendered at 35% of its natural width, turning 14px labels into five. Hovering a diagram — or reaching it with the keyboard — now reveals a control in its corner that renders the diagram a second time into a dialog at full size. Drag to pan, zoom with the wheel, a two-finger pinch or +/-, reset with 0, close with Esc. A diagram that would have to shrink past half size to fit opens at 1:1 at its starting corner instead of as a thumbnail, and zooming back out always reaches the whole diagram however large it is.

No more page reload on a colour-scheme change. The old runtime reloaded the whole page on every theme change on any page holding a diagram, citing a Mermaid limitation from the 8.x era. Mermaid 11 re-initializes cleanly, so the diagrams are redrawn in place, and each stage holds its height across the swap so nothing moves under the reader.

A diagram inside a tab that is not the open one now renders at its proper size. Inside display: none every text measurement returns zero, and Mermaid wrote the resulting max-width: 16px into the SVG for good; revealing the tab never recovered it.

Markdown, RSS and Print carry the fence source. Print had been carrying a <pre class="mermaid"> that no runtime ever reached, at font-size: 0, so a printed diagram was a blank gap.

The reading surface

Inline code is crimson ink on a hairline wash rather than a grey pill. The old tint made every token a lozenge; the much fainter wash now marks token boundaries while the monospace face, weight and hue do the identifying, which keeps a token-dense paragraph readable instead of turning it into a field of grey controls.

The series strip is a panel the width of the bar rather than a stack of links, taxonomy chips are quiet at rest and light up under the pointer, the navbar’s dropdown panels breathe in and out instead of popping, and link hover leaves the muted navy for a vivid azure.

Build and infrastructure

  • First-party browser behaviour publishes as stable capability chunks under js/chunks/. A page selects script tags by capability instead of minting a bundle of its own, so the chunks cache across pages.
  • bin/generate-config-schema.py projects the schemas from the resolvers, and CI fails when a new params key lands without one.
  • An opt-in BookManifest output records the Book sequence with stable ids.
  • One media-result contract sits behind every resolved image.
  • Google Analytics is limited to interactive HTML output; Print and machine outputs no longer carry it.
  • The Book publication job renders its PDF. It never had: chrome-headless-shell needs unprivileged user namespaces, which Ubuntu 24.04 restricts through AppArmor, and the job had failed on every run it ever had.

Upgrading

hugo mod get github.com/pgsty/oink@v0.7.0
hugo mod tidy

Nothing in the component API changed, so no content edits are required. Two things are worth knowing:

  • A mermaid fence no longer renders <pre class="mermaid">. Site CSS targeting that selector matches nothing now; the diagram is a figure.td-diagram holding .td-diagram__stage.
  • If a site pins the theme version in a check of its own, that assertion moves with the pin.

The complete list is in CHANGELOG.md.

7 - Oink 0.6.0 — Immersive blogs, safer builds, smaller internals

Oink 0.6.0 adds an immersive presentation to the existing Blog shell, completes Blog publishing with images, authors, series, three index forms and sharing, and replaces build-stopping template errors with safe warnings.

Oink 0.6.0 keeps the component API established in 0.5 and improves the systems around it: long-form reading, Blog discovery, attribution, release publishing, build resilience, and maintainability.

There is no new article type or second shell. Immersive reading is a configuration of the existing Blog shell, so articles remain in the same lists, feeds, taxonomies, series, and pager sequence.

At a glance

  • The Blog shell gains a full-bleed hero image and an in-flow outline rail.
  • Blog publishing gains profiles and bylines, series order, list/cards/table indexes, and a local-first share bar.
  • Vendored and translated pages gain optional, validated attribution.
  • The theme no longer calls errorf: ordinary previews warn and safely degrade; publishing builds remain strict through --panicOnWarning.
  • Release metadata is now one release_url, not a duplicated fact map.
  • Repeated template work, page bundles, and checker builds have been reduced without pruning Font Awesome or other public authoring assets.

Immersive Blog presentation

An immersive page uses four independent front matter keys:

featured_image: hero
toc_style: flow
toc_taxonomies: false
sidebar_enabled: false

Put the same keys in a section cascade to apply them to its posts. Hugo resolves cascade values on the section index that declares them as well as on its descendants, so a section that should share the presentation writes the keys once.

hero paints the resolved featured image behind the opening and masks it out before the article text. The ordinary navbar remains available and uses a fading scrim over the artwork. toc_style: flow gives the outline a wider in-flow rail that begins with the article and becomes sticky on scroll. toc_taxonomies: false removes term clouds from that rail. The Blog shell omits breadcrumbs by default; breadcrumb: true restores one for a page or cascade that wants it.

Each switch is independent. A missing image produces the normal opening; an empty TOC and disabled clouds produce no empty rail. Nothing changes the page’s Blog membership or output formats.

The Blog, completed

params.ui.featured_image and the page key featured_image support:

Mode Presentation
none No article image; the default
banner A framed 16:9 image above the title
wash A low-opacity image behind the article header
hero A full-bleed Blog backdrop

All modes reuse the representative-image resolver also used by list thumbnails and social metadata. Missing images are valid, and non-HTML outputs keep their static source-shaped forms.

Authors

Declare taxonomies: {author: authors}. The author term page is the profile: its title is the name, its description and body are the biography, and its representative image is the portrait. Posts list authors with authors: [vonng, oink]; the order is preserved. The legacy author: string remains the fallback when the taxonomy is not used.

Series

Declare taxonomies: {series: series}. Posts name one or more terms with series and may set series_weight. Weighted members come first in ascending order; unweighted members follow by ascending date. The article and term page share that resolver, so the position strip and archive cannot drift.

Three index forms

Key Default Meaning
ui.blog_index list list, cards, or table
ui.blog_index_columns 3 Card columns
ui.blog_index_size 12 Posts per list/cards page
ui.blog_index_toggle false Reader-side cycle among all three forms

List and cards share year grouping and pagination. A standalone table is a complete unpaginated archive. With the reader toggle enabled, all three forms share the current paginator slice; the full archive is not repeated on every generated page. The configured form controls first paint, and a local preference may override it.

Sharing

params.ui.share is an ordered list drawn from x, bluesky, mastodon, facebook, linkedin, reddit, hackernews, telegram, whatsapp, line, pinterest, weibo, chatgpt, claude, email, and copy. Empty is the default; share: false opts out one page.

The bar uses plain intent links and the local copy action. It loads no platform SDK, iframe, counter, or third-party stylesheet.

Page annotation

upstream_link is the per-page source URL. Its companion facts are upstream_name, upstream_copyright, upstream_license, upstream_notice, upstream_ref, and upstream_modified. Constants may come from site parameters, a data/upstreams entry, or front matter.

Incomplete facts, unknown licences, unsafe URLs, and invalid types warn and omit the whole attribution line. Strict builds reject the warning. upstream_link: "" explicitly opts a page out of an inherited attribution.

params.ui.translation_notice optionally names the authoritative language. It is never imposed as page front matter; a page may opt out with translation_notice: false.

Warnings instead of preview outages

The theme contains no errorf calls. Simple scalar values share validate.html; components retain the checks that understand their own records and markup.

Invalid input follows one rule:

  1. warn with the bad value and the safe fallback or omission;
  2. do not emit unsafe or misleading output;
  3. let an ordinary hugo server continue;
  4. let --panicOnWarning stop CI and publication.

This preserves strict gates without letting one typo take every preview URL down.

Outline rail

The outline exposes a visible range and a current cursor on one SVG path. The cursor carries aria-current="location"; reduced-motion and unsupported registered-property engines fall back without detaching the cursor from the line.

Fixed and simplified

  • Mounted content no longer publishes build-machine paths in Edit, History, or Create Child URLs.
  • Generic data-* and aria-* values use one HTML-escaping emitter.
  • Incomplete Algolia credentials emit no container, CSS, or JavaScript.
  • Draw.io loads only on pages with PNG/SVG candidates and inspects each distinct URL once.
  • Page actions, pager state, language targets, and section-index children reuse page- or site-scoped results instead of repeating full-site work.
  • Language-neutral feature bundles are shared across translations.
  • Field anchors are derived from field names and remain unique within a page.
  • Print aggregates namespace headings and footnotes without changing regular page IDs.
  • The maintained invalid-input checker batches equivalent cases: the content primitive stage now starts Hugo six times instead of 160 while preserving every warning and fallback assertion.
  • Obsolete CSS, i18n keys, rejected Article-shell artifacts, duplicate checker blocks, and narrative code comments have been removed. The complete supported Font Awesome distribution remains intact.

Configuration

Key Default Notes
ui.featured_image none none / banner / wash / hero
ui.toc_style fixed fixed / flow
ui.toc_taxonomies true Show taxonomy clouds in the right rail
ui.blog_index list list / cards / table
ui.blog_index_columns 3 Card columns
ui.blog_index_size 12 List/cards page size
ui.blog_index_toggle false Reader-side three-form cycle
ui.share [] Ordered share targets
ui.translation_notice false Optional authoritative language
time_format_blog 2006-01-02 Changed default
time_format_default 2006-01-02 Changed default

The default shell and pager type lists remain docs, book, blog, and swagger where applicable. No article type is added.

Migration

From 0.5:

  1. Keep explicit prose date formats if ISO dates are not desired.
  2. Ensure publishing commands use --panicOnWarning.
  3. Replace the old release map with release_url: https://github.com/<owner>/<repo>/releases/tag/<tag>.
  4. Replace upstream_attribution with upstream_link, and downstream_modified with upstream_modified.
  5. Do not migrate content to type: article; use the Blog presentation keys shown above.

The migration tool operates on content Markdown and supported YAML front matter. Configuration-file mappings remain an explicit maintainer task. From 0.4, keep the established sequence: report, migrate --write, then check.

Verification

The 0.6.0 release is verified with:

  • Hugo Extended 0.160.1 and 0.164.0;
  • 40 HTML/print/Markdown/RSS/LLMS golden surfaces;
  • 85 migration tests and 38 browser-runtime tests;
  • strict example, Hugo Module, system-font, legacy-font, and invalid-config builds;
  • the bilingual project-site build and its non-browser regression suite;
  • representative large-site performance measurement and real EN/ZH browser checks.

Local validation, a commit, a tag, a push, a consumer pin, and deployment remain separate release states.

Full change set

v0.5.0 to v0.6.0

8 - Oink 0.5.0 — Component API v5 and the converged configuration

Oink 0.5.0 replaces most shortcodes with native Markdown forms, converges every configuration and front matter key on three rules, removes the 0.x compatibility layer, and ships the migration toolkit that rewrites a 0.4 site. Every old key, shape, and shortcode fails the build with its replacement instead of being silently ignored.

Oink 0.5.0 is the API-freeze release. It contains the change set that the 1.0 line will freeze: the component API v5 (native Markdown forms first, 29 shortcodes as full forms), configuration and front matter keys converged on three rules, one naming namespace for everything the theme emits, the removal of the 0.x compatibility layer and of the Docsy leftovers no site used, and a migration toolkit that rewrites a 0.4 site. Every retired key, shape, or shortcode fails the build with a message that names its replacement.

This is a breaking release for every 0.4 site. Read At a glance first, then the migration guide; the reference sections in between list every change with its old and new form.

At a glance

  • Content: most components are written as Markdown — > [!TYPE] callouts, {.steps} and {.cards} lists, {.fields} / {.matrix} / {caption=} / {#id num=} / {tab=} tables, ```filetree / ```gallery / ```echarts / ```infographic / ```checksums data fences, adjacent code fences as tabs, and the Markdown image with an attribute line. Of the 53 shortcodes in 0.4.2, 32 are gone or renamed and 8 are new; 29 remain as full forms. scripts/migrations/oink06.py rewrites content.
  • Configuration: three rules — a switch is the bare feature name, single-key maps are flattened, a front matter key is the site key without ui.. Some forty keys are renamed or reshaped; every old one fails the build with its replacement. Every theme default is declared in the theme’s hugo.yaml.
  • Front matter: no ui: block any more; page overrides are bare keys (section_index: cards), page_context_menu mirrors the site map, manualLink* are manual_link*, hide_*/exclude_search are gone.
  • Namespace: theme classes are td-*, data attributes data-td-*, custom properties --td-*, JS globals Oink*; the oink-* set and Docsy leftovers (leaf, has-child, nav-*, …) are gone. Callout labels are callout_* i18n keys.
  • Removed: the home/** adapter partials, outputformat.html, td/render-heading.html, the Docsy community page and params.links, the td/code-dark / td/color-adjustments-dark / td/gcs-search-dark / td/extra Sass files, .td-box* and -bg-* palette classes, Prism, Open Sans, click-to-copy.js, swaggerui (now swagger).
  • Behaviour: headings carry a self-link, print content is rendered once per build (a real race fixed), three cacheable JS bundles, print pages load 8 KB of JS instead of 100 KB, shell motion honours reduced motion by construction, giscus palettes ship with the theme and load only where comments render.
  • Migration: oink06.py report → migrate --write → check for content and front matter, then a build whose errors are the configuration checklist.
  • Release hardening: a two-round adversarial review repaired the client-side namespace migration, action-registry load order, fail-closed migration input, multi-instance OpenAPI embeds, shared attribute and image-URL policies, and consuming-site configuration preflight before the API freeze.

Component API v5

Native forms first

The v5 principle: a component that a Markdown block can express is written as Markdown; a shortcode exists only for what a block cannot carry. Render hooks recognise the native forms, and one attribute policy governs every hook.

Components: native form and full form

Callout , native

> [!NOTE] Title blockquote; [!TYPE]- folded / [!TYPE]+ open; optional {icon="fa-solid fa-x"}; types note tip important warning caution success danger question example quote details. No shortcode.

Tabs , native + shortcode

Native: adjacent fences (or tables) with {tab= group= value=}.

Shortcode: tabs group= default= label= tab label= value=/tab /tabs.

Steps , native + shortcode

Native: 1. list + {.steps}.

Shortcode: steps with headings — the only shortcode written with the % delimiters (its body is page-level Markdown); headings inside steps enter the TOC.

Cards , native + shortcode

Native: link list + {.cards}.

Shortcode: cards card title= link= icon= badge= image= image_alt=|decorative= body /card /cards.

Fields , native + shortcode

Native: table + {.fields [caption=] [id=] [meta="type required default -"]} — first column name, last column description, middle columns metadata chips.

Shortcode: fields label= id= class= field name= type= required= default= body /field /fields — for block-level descriptions (this list is one). Both forms render the same chips; every entry gets a #field-<name> anchor.

FileTree , native

```filetree {title=} fence, one - name[/] # comment {icon= tone= open= type=} line per entry; 2/4-space, tab, or tree indentation. CSS + native <details>; the comment column is aligned at build time. No shortcode.

Gallery , native

```gallery fence, one ![alt](src) # description {link= class=} line per image; alt is required, items are Zoom-eligible. No shortcode.

Image , native

![alt](src "title") plus an attribute line {#id num= caption= width= height= link= command= options=} for figures, numbering, links, and Hugo image processing. imgproc is retired; there is no image shortcode.

Table family , native

{.full-width} {.fields} {.matrix} {caption=} {#id} {#id num= caption=} {tab= group= value=}; site classes pass through. Exclusivity: fields ⟂ matrix / full-width / num; num ⟂ tab.

Fig / Tbl / Eq / Eg , native + shortcode

Native: image / table / $$ block / fence + {#id num= caption=} (default ids fig-, tbl-, eq-, eg-<num>).

Shortcode: fig tbl eq eg (eg caption required).

Xref , native + shortcode

Native: plain Markdown links (kind-less).

Shortcode: xref fig|tbl|eq|eg="…" [page=] [anchor=].

Book indexes , shortcode

book-toc book-figures book-tables book-equations book-examples — no kind= parameter.

Code fences , native

Fence attributes {title copy wrap collapse label id tab group value num caption lineNos hl_lines lineNoStart anchorLineNos tabWidth}; Chroma only.

Data fences , native

mermaid plantuml markmap math chem echarts infographic checksums filetree gallery; echarts is declarative, $fn:<name> callbacks come from window.OinkEchartsFunctions.

Leaves , shortcode

kbd badge param include comment contributors asciinema (raw <kbd> also works); badge has no outline, param is scalar only.

Release / OpenAPI , shortcode

release-card release-assets download / swagger redoc; the checksums fence is the native release form.

The 29 shortcodes: core 14 (tabs tab steps cards card fields field include kbd badge param comment contributors asciinema), Book 10 (fig tbl eq eg xref book-toc book-figures book-tables book-equations book-examples), Release 3, OpenAPI 2. Nested names (tab, card, field) are valid only inside their parent; every shortcode validates its parameters, and an unknown parameter fails the build (asciinema, redoc, swagger, param, comment, and steps accepted anything silently in 0.4).

Removed shortcodes and their replacements

The chip on each entry is the toolkit key (scripts/migrations/oink06.py migrate --only <key>); manual means the report lists it and a human edits.

Removed shortcodes and their replacements

alert · details · td-page-notice , callout

0.4: alert color=… title=…, details, td-page-notice (all % shortcodes), raw <details><summary>.

0.5.0: > [!TYPE] title callouts, > [!DETAILS]- for a folded block.

tabpane · tab · code-group · code-tab , tabs

0.4: tabpane with tab header=… (both % shortcodes), code-group with code-tab.

0.5.0: adjacent fences with {tab= group= value=} (code-only panes), or tabs with tab for mixed content.

filetree · filetree/folder · filetree/file , filetree

0.4: filetree with filetree/folder and filetree/file; the interim {.filetree} list marker.

0.5.0: the ```filetree fence — label becomes title; open, icon, color, comment, link are kept.

gallery · gallery/image , gallery

0.4: gallery with gallery/image; an image list + {.gallery}.

0.5.0: the ```gallery fence.

echarts · infographic , datafence

0.4: echarts, infographic shortcodes.

0.5.0: same-named data fences; $fn: callbacks are unchanged, js sub-fences move to window.OinkEchartsFunctions.

doc-cards · doc-card · nav-cards · nav-card · card · cardpane · doc-carousel , cards

0.4: the Docsy card family and the OINK doc-cards / nav-cards wrappers.

0.5.0: cards with card, or a link list + {.cards}. card keeps its name as the child of cards, with a different contract.

imgproc , image

0.4: imgproc … (and the pre-release image …).

0.5.0: ![alt](src) + {command= options= caption=} on the attribute line.

readfile , include

0.4: readfile file=….

0.5.0: include file=… [code=true lang=…] — page resources, then assets, then content-relative paths.

fence filename= , fencetitle

0.4: {filename="x"} on a fence.

0.5.0: {title="x"}.

badge outline= , badge

0.4: badge … outline=….

0.5.0: drop outline — there is one badge appearance.

example · book-figures kind= , eg

0.4: self-closing example … + fence; book-figures kind="tbl".

0.5.0: eg/eg; book-tables, book-equations, book-examples.

fields · field (percent form) , fieldsdelim

0.4: fields / field written with % delimiters (never shipped).

0.5.0: fields / field.

_param · iframe · conditional-text · netlify · kind-less xref , reportonly

Reported with file:line for manual review; _param placeholders are handled by the param_placeholders transform.

blocks/cover · blocks/feature · blocks/lead · blocks/link-down · blocks/section , reportonly

0.5.0: layout: landing with sections (data file or inline front matter). Reported, not rewritten.

swaggerui , manual

Renamed to swagger; change the call.

pageinfo , manual

Write a > [!NOTE] callout instead.

td/site-build-info/netlify.md , manual

Removed without replacement.

New in 0.5.0 relative to 0.4.2: tabs, cards, include, eg, book-tables, book-equations, book-examples, and swagger (renamed). card and tab keep their names but are now children of cards / tabs with a different contract.

There is no image shortcode: the render hook resolves page resources, section resources, global assets, and static or remote paths for Markdown images, fig, and configuration image sources alike, and carries captions, numbering, links, and Hugo image processing (command, options) on the attribute line — everything imgproc did.

The block-attribute policy

Every render hook (table, image, code block, passthrough, blockquote, heading) shares one policy: allowlisted keys are consumed by the hook, class is token-validated and passed through, data-* and aria-* pass through, and style, on*, and any unknown key fail the build. Site CSS classes on content are legitimate and keep working; inline styles and handlers never reach the output.

Code fences

  • {filename="x"} is {title="x"}. title and filename are mutually exclusive on the same fence.
  • The Prism path is gone. params.prism_syntax_highlighting, static/js/prism.js, and static/css/prism.css no longer exist; Chroma with params.highlight_classes (default true) is the only highlighter. Prism could not coexist with tab, group, value, num, and caption, so any 0.4 site using tabs or numbered examples already failed with it enabled.
  • The Copy control follows copy=all|command|true|false on the fence, then the session-lexer default (console, shell-sessioncommand), then all. params.ui.code_copy: false changes the site-wide default only; a fence that names copy still gets what it asks for. The old disable_click2copy_chroma silently overrode an explicit author value.
  • Docsy’s click-to-copy.js (never loaded since 0.3) and its .td-click-to-copy styles are removed.

Configuration

The three rules

  1. A boolean switch is the bare feature name: ui.annotation: true, not ui.annotation.enable and not ui.annotation_enabled. The only _enabled suffixes left are ui.navbar_enabled, ui.sidebar_enabled, and ui.sidebar_root_enabled, whose bare names would collide with sibling families.
  2. A single-key map is flattened to a scalar. A map survives only for a feature with several settings — comments, ui.feedback, ui.page_context_menu, ui.dark_mode, ui.command_palette, ui.alt_site, taxonomy, print, search, plantuml, drawio, mermaid, copyright, ui.taxonomy_icons — and the on/off ones among them also accept a bare boolean (comments: false, plantuml: false, dark_mode: true, feedback: true, page_context_menu: false).
  3. A front matter key is the site key with its ui. prefix dropped, without exception (see Front matter).

Keys are snake_case, positive, and named for what they do. camelCase survives only where a value is passed straight through to an external runtime (comments.giscus.* carries giscus’s own attribute names, mermaid.* is handed to mermaid.initialize()).

Every old key or shape fails the build with a message that names the replacement — layouts/_partials/config-legacy.html for site configuration, layouts/_partials/front-matter-legacy.html for pages — so an upgrade is a matter of following the errors one by one. Nothing is silently ignored.

Renamed and reshaped site keys

0.4 0.5.0 Note
offlineSearch, offlineSearchIndex, offlineSearchMaxResults, offlineSearchOnServe, offlineSearchSummaryLength offline_search, offline_search_index, offline_search_max_results, offline_search_on_serve, offline_search_summary_length environment override: HUGOxPARAMSxOFFLINE_SEARCH_ON_SERVE=true (Hugo’s alternate x delimiter; _ cannot address snake_case keys)
ui.showLightDarkModeMenu (true / false / "enable-only (experimental)") ui.dark_modetrue, or { enable, show_menu } show_menu: true implies enable
ui.scrollSpy.disable ui.scroll_spy inverted; default false
ui.no_left_sidebar ui.sidebar_enabled inverted
ui.breadcrumb_disable ui.breadcrumb inverted; default true
print.disable_toc print.toc inverted; default true
disable_click2copy_chroma ui.code_copy inverted; sets the default only
ui.readingtime.enable ui.reading_time bare boolean
ui.ul_show ui.sidebar_expand_levels default 2
Taxonomy.taxonomyCloud, .taxonomyCloudTitle, .taxonomyPageHeader taxonomy.cloud, .cloud_title, .page_header one lowercase map
ui.annotation.enable, ui.image_zoom.enable, ui.keyboard_nav.enable ui.annotation, ui.image_zoom, ui.keyboard_nav bare booleans
ui.typography.preset ui.typography technical | system; environment override HUGO_PARAMS_UI_TYPOGRAPHY=system
ui.pager.types ui.pager_types [docs, book, blog]
markmap.enable markmap bare boolean
content_width (slim | norm | wide) reading_width (slim | normal | wide) Book reading measure; body class td-book-content--normal, token --td-book-content-normal
ui.docs_root ui.docs_sidebar_root section | home
github_url github_repo edit, history, and issue links derive from the repo
algolia_docsearch search.algolia with appId, apiKey, indexName fails the build
rss_sections removed it was never read
params.links.user[] / .developer[] removed the Docsy community page is gone
plantuml.enable, drawio.enable unchanged, and the maps accept plantuml: false / drawio: false
comments.enable unchanged, and comments: false is accepted
comments.giscus.lightTheme / darkTheme unset by default the theme’s own palettes are the default (see Styles and assets)

Every theme default is now declared in the theme’s hugo.yaml with its value range in a comment. Previously template-only fallbacks that are now declared: offline_search: false, offline_search_summary_length: 70, ui.breadcrumb: true, ui.reading_time: false, ui.dark_mode: false, ui.docs_sidebar_root: section, ui.sidebar_icon_policy: all, ui.section_index_columns: 2, ui.code_copy: true, print.toc: true, print.section_break_wordcount: 50, markmap: false, plantuml.enable: false, drawio.enable: false, github_branch: main. Two defaults stay derived and are documented as such: ui.quick_links (from docs_section and blog_section) and ui.taxonomy_icons (built-in categories/tags glyphs). The template fallbacks for ui.sidebar_expand_levels (2) and ui.sidebar_menu_truncate (2000) match the declared values.

Unchanged Docsy keys that keep working as they are: github_repo, github_project_repo, github_branch, github_subdir, path_base_for_github_subdir, time_format_blog, time_format_default, version, versions, version_menu, version_menu_pagelinks, archived_version, url_latest_version, copyright, description, author, gcs_engine_id, search.algolia.*, mermaid, plantuml.*, drawio.*, ui.sidebar_menu_compact, ui.sidebar_menu_foldable, ui.sidebar_menu_truncate, ui.sidebar_cache_limit, ui.sidebar_root_enabled, ui.feedback.{enable,reasons}.

Fail-loud, not silent

Configuring more than one search backend (offline_search, gcs_engine_id, search.algolia) now fails the build (it warned before). PlantUML without plantuml.svg_image_url, Diagrams.net without drawio.drawio_server, and Algolia without all three credentials still fail the build, as in 0.4. Build messages follow one shape — <component>: <subject> <expectation>; got <value> at <position> — lower case, one preposition for the location, configuration errors naming the full params. path; they no longer point at documentation URLs.

Front matter

The page key is the site key without its ui. prefix, and front matter never carries a ui: block. A section cascade works the same way (cascade: { params: { section_index: cards } } or the bare key). One resolver (ui-param.html) reads the page value, else the site value, for every params.ui.* setting a page may override: sidebar_menu_compact, sidebar_menu_foldable, sidebar_expand_levels, sidebar_width_min, sidebar_width_max, sidebar_item_overflow, sidebar_headings, sidebar_enabled, section_index, section_index_columns, lastmod_commit, breadcrumb, scroll_spy, code_copy, keyboard_nav, book_draft_banner, plus the explicit page keys navbar_enabled, navbar_autohide, footer_style, annotation, feedback, image_zoom, reading_time, page_context_menu, comments, page_width, reading_width.

0.4 front matter 0.5.0
params: { ui: { <key>: … } } (any key) <key>: … at the top level (or under params:)
params.ui.image_zoom.enable image_zoom: true | false
params.ui.keyboard_nav.enable, params.ui.annotation.enable keyboard_nav, annotation (bare booleans)
annotation: { enable: … } annotation: true | false
context_menu page_context_menu (true | false, or { enable, assistant_links })
assistant_links (top level) page_context_menu: { assistant_links: false } — a page can only narrow the site policy
hide_readingtime: true reading_time: false
hide_feedback: true feedback: false
exclude_search, excludeSearch search_exclude
content_width: norm reading_width: normal
manualLink, manualLinkTitle, manualLinkTarget, manualLinkRelref manual_link, manual_link_title, manual_link_target, manual_link_relref
body_class: td-no-left-sidebar sidebar_enabled: false
contributingUrl removed with the community page
Icon icon (Hugo is case-insensitive; the theme reads the lowercase form)

Unchanged page keys: toc_hide, toc_root, notoc, no_print, no_list, simple_list, hide_summary, sidebar_root_for, sidebar_divider, sidebar_expanded, sidebar_root_menu, sidebar_root_link_self, search_keywords, search_boost, pager, landing, sections, book_number, book_status, release, release_products, release_group_by_product, upstream_attribution, downstream_modified, byline, author, body_class.

scripts/migrations/oink06.py migrate --only frontmatter rewrites all of the renamed page keys, including inside cascade: maps and lists.

Templates, partials, and layouts

Removed, with what a site that copied or called them should use instead:

0.4 0.5.0
_partials/home/** (18 adapters), _partials/home-data.html _partials/landing/**, landing/home-data.html
_partials/outputformat.html .Store.Get "tdOutputFormat" (html | print | markdown | rss, set by every base template)
_partials/td/render-heading.html and a site-side _markup/render-heading.html that calls it the theme’s own _markup/render-heading.html — delete the site override
layouts/community/list.html, layouts/docs/community.html, _partials/community_links.html none — the Docsy community page is gone
_partials/taxonomy_terms_article.html, taxonomy_terms_article_wrapper.html, taxonomy_terms_cloud.html taxonomy-terms-article.html, taxonomy-terms-article-wrapper.html, taxonomy-terms-cloud.html
_partials/taxonomy_terms_clouds.html, code/markdown-escape.html dead in 0.4 already; shell/taxonomy-terms-clouds.html, content/markdown-escape.html
_shortcodes/swaggerui.html _shortcodes/swagger.html
layouts/_default/_markup/render-* copied from 0.4 compare against 0.5.0 before keeping any override — every hook changed

Other template-level changes a site with overrides should know about:

  • Both sidebar sources — the content tree and an explicit data/docs_nav.json — render every row through shell/sidebar-node.html. shell/config.html remains the single resolver for brand, logo, and section configuration.
  • Every content-rendering layout calls content/render.html instead of .Content (that is where the Image Zoom candidate scan runs).
  • Print: print/page-content.html renders each page’s print content exactly once per build through partialCached; print/render.html, print/content.html, book/print.html, and the single.print.html layouts read that. Sites that copied a 0.4 print template should drop the copy — the 0.4 pipeline raced on the page store when a section that is itself a section was aggregated by its parent.
  • The theme owns the heading render hook. Every heading carries its id and a hover-revealed self-link (.td-heading-self-link, label ui_heading_self_link); print and RSS strip the link.
  • The DocSearch container is one #td-docsearch element; the two hard-coded #docsearch-0/1 ids are gone.

Styles and assets

One namespace

Everything the theme emits is namespaced, and scripts/check-namespace.py keeps it that way. Site CSS or JS that hooked the old names must move:

Kind 0.4 0.5.0
Classes oink-* (landing subsystem), leaf, has-child, active-path, is-open, is-active, is-hidden, is-disabled, landing-header, landing-nav, landing-container, article-meta, pageinfo, nav-*, taxonomy-*, ul-N td-* throughout; the site header and nav are td-site-header, td-site-nav, td-site-container
Data attributes data-oink-* data-td-*
Custom properties --oink-*, --term-* --td-*
JS globals oink* / echartsFunctions window.OinkActions, OinkEchartsFunctions, OinkLanding, OinkSearchEngine, OinkSurfaceCoordinator
Author markers (unprefixed, unchanged) {.steps} {.cards} {.fields} {.matrix} {.full-width}

Sass and tokens

Removed Sass files (a site’s _styles_project.scss that imports them fails to compile): td/code-dark, td/color-adjustments-dark, td/gcs-search-dark, td/extra, td/extra/bs-defaults, td/extra/buttons, td/extra/main-container, td/extra/navbar, td/boxes (.td-box, .td-box--<color>, .td-box--height-*), td/colors (.-bg-<name>, .-text-<name>). Removed variables: $td-box-colors, $td-print-font-name, $td-enable-webfonts.

Renamed or new tokens: --td-book-content-norm--td-book-content-normal (and .td-book-content--norm--normal); --td-print-font-family keeps its role but follows --td-body-font-family in both presets; new --td-motion-duration-fast (100 ms), --td-motion-duration (150 ms), --td-motion-duration-slow (250 ms), which every shell transition uses and which prefers-reduced-motion: reduce sets to 0.

Typography: Inter for UI and prose (variable weight, Latin/Latin-ext/Cyrillic/ Greek/Vietnamese subsets served by unicode-range; CJK and emoji fall through to the platform stack), borderless inline code, quiet code cards with a hover-revealed Copy control, Mintlify-style field rows, a page-end pager of two text links, and a rule above card section indexes. Open Sans (18 woff2 subsets, 652 KB, published to every site for a print-only face) is gone; a site that wants a different face on paper sets --td-print-font-family in its own stylesheet. The system preset still requests no brand fonts.

Shell chrome icons are Font Awesome class pairs dispensed by shell/icon.html (<i class="td-shell-icon td-shell-icon--<name> fa-solid fa-…">) instead of inline SVG; --td-shell-icon-size sets the box.

Published assets

  • Three JavaScript bundles instead of one per feature combination: js/actions.js and js/core.js are byte-identical on every page and stay cached; only a small js/page-<hash>.js varies. ECharts is its own <script>. Print output loads 7.9 KB instead of 100 KB.
  • static/css/giscus-oink-{light,dark}.css are gone. The palettes ship as assets/css/giscus-{light,dark}.css, are published only on pages that render comments, and are the default comments.giscus.lightTheme / darkTheme; a site that pointed at the old paths removes those two lines (or names a giscus built-in theme / its own stylesheet URL).
  • Gone: static/js/prism.js, static/css/prism.css, static/webfonts/open-sans/, assets/js/click-to-copy.js. VENDOR.json and the vendor tree hashes are regenerated.

i18n

  • Callout labels are namespaced keys: callout_note, callout_tip, callout_important, callout_warning, callout_caution, callout_success, callout_danger, callout_question, callout_example, callout_quote, callout_details. The theme no longer claims bare top-level keys such as note, example, or quote; a site that overrode those in its own i18n/ renames them.
  • Removed: community_join, community_introduce, community_learn, community_using, community_develop, community_contribute, community_how_to, community_guideline.
  • Added: ui_heading_self_link and ui_field_self_link (English fallback in every locale; reviewed Chinese variants).
  • All 32 locale files keep exact key parity (174 keys).

Data files

  • data/home/<lang>.yaml (or data/home.yaml) must list sections; the implicit hero → metrics → capabilities → principles → cta order is gone and its absence fails the build.
  • The fat footer reads data/footer/<lang>.yaml (or data/footer.yaml) only. A footer key inside data/home fails the build naming the new location.
  • data/landing/<key>/<lang>.yaml, data/docs_nav.json, data/download/<key>.yaml, and data/brand.yaml are unchanged.

Behaviour and output changes

  • Headings carry a self-link revealed on hover; the anchor is stripped from print and RSS output, and Markdown output (RenderShortcodes) is unaffected.
  • Print aggregates render each page’s content exactly once per build. In 0.4 a chapter that was itself a section was rendered by its own print output and by its parent’s, concurrently, and the two renders raced on the page store — the visible symptom was intermittent duplicate td-code-… ids in _print/.
  • <main> no longer carries role="main", and the sidebar <aside> no longer duplicates the inner <nav>’s “Section navigation” label.
  • ui.dark_mode: true turns on both the dark palette and the System / Light / Dark menu; show_menu: true alone implies enable.
  • ui.code_copy: false sets the default only (see Code fences).
  • The navbar renders on the home page; callout titles meet contrast; Gallery items are Zoom-eligible on the same terms as other images; the tabs runtime keeps its run boundaries, unique peer ids, and print titles; FileTree and the whole shell honour prefers-reduced-motion.
  • The table render hook runs in print and RSS output, so tables keep caption, number, and scroll container outside interactive HTML; fields from either form produce one rendering, and every entry gets a #field-<name> anchor.
  • llms.txt reads params.ui.docs_section and lists documentation pages with their descriptions.
  • Image resolver errors are labelled by the caller (image: for a Markdown image, the shortcode name for fig), and configuration image sources are held to the same URL policy as content.

Release-candidate hardening

The final review found one systemic migration gap: templates emitted the new data-td-* contract while several runtimes and test mocks still read the old dataset names. It also found that the action manifest followed the synchronous action-registry bundle, so the registry could initialize empty. Both are fixed, with structural checks that reject either regression. Page actions, Command Palette search, code copying and collapse, feedback identity, disclosure labels, Giscus themes, Image Zoom labels, and Asciinema timers now exercise the same attributes in tests and in the rendered DOM.

The same hardening pass also:

  • makes migration report, migrate, and check reject missing, empty, unreadable, or non-UTF-8 targets instead of reporting a misleading clean result, and parses JSON front matter with a JSON decoder;
  • runs legacy front-matter guards in Markdown, RSS, and aggregate print output, and validates boolean/map shapes for page comments and other overrides;
  • preserves accepted data-* / aria-* attributes through data fences and callouts, while keeping chart booleans strict;
  • gives every Swagger and ReDoc embed a unique instance without replacing window.onload or publishing window.ui;
  • applies the shared URL policy to shell logos, wordmarks, and configured featured images; and
  • ships scripts/check-site-markup.py, which checks the resolved consuming-site configuration for the three Goldmark settings required by native forms.

Migration guide

The order matters: content first (the toolkit is dry-run by default and idempotent), then the build errors drive the configuration and layout edits.

Before rewriting content, verify that the consuming site can render the native forms:

python3 path/to/oink/scripts/check-site-markup.py --site ~/pgsty/example.com

1. Inventory

python3 scripts/migrations/oink06.py report --sites ~/pgsty/example.com --md report.md --json report.json

The report lists, per site, every 0.4 construct the toolkit will rewrite, what it will not touch (with file:line and a reason), and what would still be flagged afterwards.

2. Content and front matter

python3 scripts/migrations/oink06.py migrate --site ~/pgsty/example.com          # dry run: diffs + counts
python3 scripts/migrations/oink06.py migrate --site ~/pgsty/example.com --write  # atomic rewrite
python3 scripts/migrations/oink06.py migrate --site ~/pgsty/example.com --write  # second run: changed 0
python3 scripts/migrations/oink06.py check   --site ~/pgsty/example.com          # residual legacy syntax → exit 1

Transforms, in apply order: frontmatter (page keys, including cascade:), callout, param_placeholders, tabs, filetree, gallery, datafence, cards, fieldsdelim, image, include, fencetitle, badge, eg, reportonly. --only <key> selects a subset. Text inside fences is never rewritten; TOML/JSON front matter is reported, not rewritten. Across the eleven in-house sites the front matter transform touched 628 files with zero findings.

Manual follow-ups the report lists: swaggeruiswagger, pageinfo → callout, _param placeholders, iframe/conditional-text/blocks/*, kind-less xref, and js sub-fences of echarts that must become window.OinkEchartsFunctions entries.

3. Configuration

Build the site. Each old key fails with its replacement:

ERROR params.offlineSearch was renamed: use params.offline_search
ERROR params.ui.typography.preset was flattened: use params.ui.typography: technical | system
ERROR params.ui.showLightDarkModeMenu was renamed: use params.ui.dark_mode.show_menu
ERROR params.print.disable_toc was renamed: use params.print.toc (inverted)
ERROR params.rss_sections was removed: the key was never read; delete it

A typical 0.4 hugo.yaml becomes:

params:
  offline_search: true
  offline_search_on_serve: true
  offline_search_index: summary
  offline_search_summary_length: 70
  offline_search_max_results: 10
  reading_width: normal            # was content_width: norm
  markmap: true                    # was markmap: { enable: true }
  print:
    toc: true                      # was disable_toc: false
  comments:
    enable: true
    type: giscus
    giscus:
      repo:  # lightTheme / darkTheme lines removed
  ui:
    typography: technical          # was typography: { preset: technical }
    dark_mode: true                # was showLightDarkModeMenu: true
    sidebar_expand_levels: 2       # was ul_show: 2
    scroll_spy: false              # was scrollSpy: { disable: true }
    reading_time: false            # was readingtime: { enable: false }
    image_zoom: true               # was image_zoom: { enable: true }
    keyboard_nav: true             # was keyboard_nav: { enable: true }
    annotation: true               # was annotation: { enable: true }
    pager_types: [docs, book, blog] # was pager: { types: [...] }
    docs_sidebar_root: section     # was docs_root
    breadcrumb: true               # was breadcrumb_disable: false
    sidebar_enabled: true          # was no_left_sidebar: false
    code_copy: true                # was disable_click2copy_chroma: false (top level)

Delete params.links, prism_syntax_highlighting, rss_sections, github_url (use github_repo), algolia_docsearch, and the giscus lightTheme / darkTheme URLs.

4. Cascades and section indexes

A cascade that set params.ui.* becomes bare keys — the transform handles _index.md files, but check hand-written cascades in hugo.yaml:

cascade:
  type: blog
  params:
    sidebar_menu_compact: false    # was params.ui.sidebar_menu_compact
    sidebar_expand_levels: 3       # was params.ui.ul_show

5. Sass, layouts, and site scripts

  • assets/scss/_styles_project.scss: remove @import 'td/color-adjustments-dark', 'td/code-dark', 'td/extra', 'td/extra/bs-defaults', 'td/gcs-search-dark'; drop rules that targeted .td-navbar-cover, .td-navbar-transparent, .td-box*, -bg-*, oink-*, --oink-*.
  • assets/scss/_variables_project.scss: drop $td-print-font-name, $td-enable-webfonts, $td-box-colors.
  • layouts/: replace partial "home-data.html" / "home/section.html" with landing/…; replace partial "outputformat.html" with .Store.Get "tdOutputFormat"; delete a _markup/render-heading.html that called td/render-heading.html; rename taxonomy_terms_* calls; compare every other copied partial or hook with 0.5.0 before keeping it.
  • Site JS and tests: oink-* ids and data-oink-* attributes are td-* / data-td-*; the action manifest is #td-action-manifest; the per-page bundle is js/page-<hash>.js, with js/actions.js and js/core.js beside it.
  • Site i18n/ overrides: rename note, tip, … to callout_note, callout_tip, ….

6. Data

Move the footer: map out of data/home/<lang>.yaml into data/footer/<lang>.yaml; make sure data/home/<lang>.yaml lists sections.

7. Verify

hugo --printPathWarnings --panicOnWarning
python3 scripts/check-output-security.py --public public --base-url https://example.com/

Then check the surfaces that changed most: a docs page with code tabs and a callout, a page with images (zoom on, zoom off), a Book chapter and its _print/ aggregate, index.md Markdown output, an RSS feed, the home landing page, and the dark palette. Once the v0.5.0 tag is pushed, pin it:

hugo mod get github.com/pgsty/oink@v0.5.0
hugo mod tidy

Compatibility

  • Hugo Extended 0.160.1 remains the minimum; CI runs 0.160.1 and 0.164.0, and now also builds a consumer site in Hugo Module mode.
  • The module path remains github.com/pgsty/oink; consumers still need no Node.js, no CDN, and no build-time download.
  • There is no compatibility layer for 0.4: renamed keys, shapes, shortcodes, partials, and classes fail the build or vanish, by design. The old-key errors are the migration guide; the Docsy-origin entries among them also serve sites coming from Docsy.
  • Docsy keys that were reasonable stay unchanged (see the list under Configuration); the sidebar_* family keeps its names.
  • Interactive features remain opt-in: offline_search, ui.image_zoom, comments, ui.feedback, ui.dark_mode, page_context_menu.assistant_links are off unless a site turns them on.

Verification

Theme CI: 34 check scripts (i18n parity, taxonomy, font tokens, the navigation / component / content-primitive / Book contracts, runtime isolation, sidebar icons, search, actions, palette, reading, release assets, downloads, landing, Book migrations, shared scenarios, keyboard, shell, namespace, parameters, vendor inventory, output structure and security, four-state goldens over 30 surfaces, code blocks, content and media primitives, Image Zoom, Gallery, components), the browser runtime unit tests, the migration toolkit tests (85), the warning-strict fixture site on Hugo 0.160.1 and 0.164.0, the system typography preset, legacy Sass overrides, an invalid preset failing the build, and the new Module-mode consumer build. scripts/check-params.py builds one site per retired key (32 site keys, 14 page keys) and asserts that each fails naming its replacement.

This project site builds warning-free on 0.5.0 after the migration described above. The final gate ran the complete matrix on Hugo 0.160.1 and 0.164.0; its media assertions accept each supported Hugo release’s opaque derivative cache hash while still checking the rendered URL shape, dimensions, alt semantics, and Zoom exclusion. Source validation, the local annotated tag, remote tag publication, consumer pins, and deployment remain separately auditable gates.

Full change set

See the complete source diff from v0.4.2 to v0.5.0 and the theme’s CHANGELOG.md.

9 - Oink 0.4.0 — Scenario components for complete publishing workflows

Oink 0.4.0 adds sequential reading and release surfaces, reusable landing pages, Book publishing with stable references, and a keyboard-first site shell in one consolidated Scenario Components release.

Oink 0.4.0 delivers the complete Scenario Components system. The original design separated Reading & Release, Landing, and Book work into 0.4, 0.5, and 0.6 milestones. The public release consolidates those tracks into one signed v0.4.0 tag so consumers can adopt one coherent contract instead of a chain of interdependent previews.

The release remains local-first: consumer sites still build with Hugo Extended and Go, without Node.js, a browser-time API, or a CDN. Interactive behavior is progressive enhancement; HTML, print, Markdown, and RSS output retain the content needed to understand each surface.

Release highlights

Reading and release

Documentation, Book, and blog pages now have a sequential pager derived from the same flattened navigation tree readers see in the sidebar. Previous and next links also appear as same-origin rel metadata in the document head. Explicit navigation data, link-only entries, sidebar dividers, and blog time order keep their own semantics instead of becoming accidental destinations.

Math can use Goldmark passthrough with the theme’s local KaTeX renderer. Sites that cannot enable passthrough yet have a strict, parameter-free eq escape hatch for display math. Numbered Book equations use the same shortcode only when an explicit num is supplied.

Release pages can render local front-matter facts, release cards, checksums, and asset lists without querying GitHub from the browser. A validated data/download/<key>.yaml model feeds both the download shortcode and Landing download sections, with separate rolling and pinned channels and an explicit pending-release state.

See Sequential reading and mathematics and Releases and downloads for the complete contracts.

Landing pages

The data-driven homepage renderer is now a reusable layout: landing shell for ordinary pages. A page may load inline data or language-aware records from data/landing/<key>/, then compose 21 built-in sections including pricing, comparison tables, command boxes, steps, timelines, code plates, case studies, downloads, and bar charts.

All facts are local at build time. Optional reveal, count-up, copy, theme-image, and compact-menu behavior loads only when a Landing page needs it. Without JavaScript, the same page remains complete; marquees pause on focus or request, respect reduced motion, and hide duplicate tracks from assistive technology.

See Landing pages for data resolution, all 21 section types, local-fact rules, and the output matrix.

Book publishing

Long-form manuals can declare Book metadata on the existing documentation shell. Chapters gain draft labels, active-page sidebar headings, and semantic fig, tbl, numbered eq, and language-aware xref targets. Whole-Book figure lists and tables of contents use those same registries.

An opt-in aggregate print document rewrites cross-chapter component links into document-local references and namespaces repeated heading IDs. The accompanying migration tool is dry-run first and idempotent, with reproducible TPME, DDIA, and pg-internal recipes, machine-readable reports, ambiguity skips, and a second-run zero-change check.

See Book publishing for the authoring and migration contract.

Keyboard and shell

The shell now supports single-key reading navigation. w and s move through the sidebar, a and d fold or unfold groups, j and k move through the page outline, and q and e follow the sequential pager. h toggles a session reading mode; l, t, f, and c switch language, theme, search, and command surfaces. Every binding yields to editable controls, composition, held modifiers, and dialogs.

The navbar now spans documentation, blog, taxonomy, and Swagger layouts with one compact state instead of a second mobile menu. Page actions move into the breadcrumb row as a Copy Markdown split button. Footers support validated fat, slim, and none styles, while readers can collapse the fat footer’s link grid and retain that preference.

See Keyboard navigation and Navigation and menus.

Compatibility and behavior changes

  • Hugo Extended 0.160.1 remains the minimum supported version.
  • The module path remains github.com/pgsty/oink; consumers still need no frontend toolchain.
  • Pagers default to the docs, book, and blog content types. Set an explicit type list or pager: false when a page should opt out.
  • / now opens full search; \ opens command-only mode. The > prefix still selects command mode inside the Palette.
  • params.footer_icp and params.footer_icp_url are replaced by one inline Markdown value, params.footer_center_info. An explicit empty string hides the center region.
  • params.ui.navbar_enabled defaults to true. Sites can override it globally, by section cascade, or on one page.
  • Legacy homepage data and Docsy block shortcodes remain compatible, but new Landing work should use the canonical section registry.

Upgrade to 0.4.0

  1. Pin the signed tag and tidy the module graph.
  2. Replace ICP-specific footer fields with footer_center_info if the site used them.
  3. Review pager defaults, / and \ shortcuts, and any local navbar or footer overrides.
  4. Remove copied theme partials only after comparing their local differences with the 0.4 implementation.
  5. Build representative docs, blog, Landing, Book, print, Markdown, mobile, and color-mode surfaces.
hugo mod get github.com/pgsty/oink@v0.4.0
hugo mod tidy
hugo --gc --minify

Follow the project-site 0.4.0 upgrade guide for the consumer checklist. The theme repository retains the frozen PRD 5 migration reference.

Verification

The signed tag resolves to the same commit as the released theme source. Theme CI exercises Hugo Extended 0.160.1 and 0.164.0, 32-locale parity, vendored assets, runtime unit tests, every PRD 4/5/6 contract, and the warning-strict example site. The project site pins the public tag and covers bilingual source, rendered Markdown, internal links, alternate builds, browser behavior, and the full multilingual WCAG AA matrix.

Representative documentation, portal, Book, and archive sites also build from the public v0.4.0 module with workspaces disabled.

Source validation, a public tag, a consumer pin, and hosted deployment are separate evidence gates. Publishing this note does not replace a smoke test of the deployed URL after the site pipeline finishes.

Full change set

See the complete source diff from v0.3.0 to v0.4.0.

10 - Oink 0.3.0 — Authoring, navigation, and a lighter page

Oink 0.3.0 adds enhanced code blocks and code groups, everyday content primitives, nested navigation with a Command Palette, semantic typography presets, and removes jQuery from every page.

Publication gate: the linked tag must resolve, the project site must pin that exact tag, and the hosted checks must pass. Until then, treat this source page as release-candidate material.

Oink 0.3.0 is the authoring and navigation release. Writing a page gets a modern code-block presentation and a set of small, everyday components; reading one gets nested navigation and a Command Palette; and every page gets measurably lighter because jQuery is gone.

The module path, the minimum Hugo version, and the Hugo-only consumer build are unchanged. Three changes can affect an existing site and are documented in Breaking changes.

Release highlights

Code blocks and code groups

Ordinary fenced code blocks now render a real code surface: an optional filename, a language label, a copy button rendered by the server rather than injected by script, opt-in wrapping, and collapsing for long listings. Hugo’s own highlighting options — line numbers, line anchors, hl_lines, tab width — keep working exactly as before.

Copy behaviour is deterministic rather than guessed. Session lexers such as console and shell-session default to copying commands without prompts and output; every other language copies the whole block. copy=command is rejected on lexers that cannot distinguish the two, because silently copying the wrong thing is worse than a build error.

The code-group shortcode groups alternatives — package managers, languages, platforms — into synchronized tabs with a stable URL hash, so a link can open the exact variant a reader needs. Legacy tabpane content keeps working and keeps its storage key.

See Code blocks for the full attribute contract.

Everyday content primitives

Alongside the existing large components, 0.3.0 adds the small ones authors actually reach for daily: badge, kbd, fields, filetree, gallery, and an opt-in image_zoom. All of them render semantic HTML, none of the non-interactive ones load JavaScript, and each has a defined presentation in print and Markdown output as well as HTML.

A standalone public icon shortcode is deliberately still deferred; components use a private, allowlisted registry for their own decoration until that API is designed properly.

See Components for each contract.

Navigation and Command Palette

Top-level menus support one level of dropdowns on desktop and matching accordions on mobile, with the parent link and the disclosure control operated independently so a parent stays navigable. Flat menus are untouched.

Local search becomes a Command Palette with three modes: an empty query offering quick links and page actions, a text query returning grouped page results, and a > prefix that searches commands only. Pages can contribute search_keywords, a positive search_boost, and canonical exclusion; ranking applies the same boost on both the Lunr and CJK substring paths.

Page actions and palette commands now run through one shared registry, so Copy text, Open in ChatGPT, Open in Claude, View source, View edit history, printing, and switching theme, language, or version behave identically wherever they are invoked. Assistant prompts resolve the browser URL at activation time, preserving the deployed host, query string, and fragment; history links derive from the same repository path as Edit this page. Assistant handoff links are disabled by default; sites must opt in with params.ui.page_context_menu.assistant_links: true. On activation, the full URL leaves the site, so do not place secrets in its query or fragment.

Press / outside an editable control to open the Palette directly in command mode. Cmd/Ctrl-K remains the general entry point, and the single-character shortcut yields to inputs, textareas, selects, and contenteditable regions.

Sidebars gain an icon-density policy — all, groups, or none. The compatibility default stays all; the starter example opts into groups.

See the migration reference for the complete configuration surface.

Typography presets

Font choices move behind seven semantic --td-*-font-family roles covering UI, body, headings, code, display text, metadata, and print. Two validated presets ship: technical, which preserves the current Oink appearance, and system, which uses the platform stack and requests no Oink brand fonts at all. Existing Docsy and Bootstrap Sass font variables seed the roles, so prior overrides keep working.

This is the typography slice of a larger design-token effort. Colour, surface, radius, density, and appearance presets are not part of this release.

See Typography tokens.

A lighter page

jQuery is gone. It was previously fetched render-blocking in <head> on every page — 87.5 KB before any content — while the theme’s own architecture loads feature runtimes only on pages that use them. Nothing in the shell needed it, and the superseded offline-search.js runtime it powered was already replaced by the Command Palette.

Two other costs were removed rather than accepted. The active output format is now read from the page store instead of being re-derived thousands of times per build, and the shell configuration is cached per language; on a 576-page build that is 357ms of template time reduced to 72ms, with byte-identical output. CJK search folds its index fields once instead of re-lowercasing the entire corpus on every keystroke, taking an 800-document query from 3.44ms to 0.34ms per character typed.

On the measured project-site snapshot, removing jQuery and the superseded search runtime saved about 88 KB from a typical documentation page’s combined CSS and JavaScript. Exact totals vary as later candidate assets change.

Correctness and localization

This release also closes several less visible correctness gaps. Markdown pages link to llms.txt only when the active language actually publishes one, and the index no longer treats off-site menu chrome as content. Internal configured commands stay under a subpath deployment, while shared content types resolve to the right product root. The archived-version banner and Giscus fallback are now localized, and print or Markdown output strips interaction-only Image Zoom attributes regardless of quoting style. Legacy search links also percent-encode their query text instead of truncating a query at &.

Browser runtime tests now run in theme CI instead of relying on Hugo bundling as their only signal. Terminal recordings also wait for their configured font before fitting the player, avoiding geometry based on a fallback font.

Breaking changes

jQuery is no longer loaded. The third-party inventory previously listed it as part of the UI foundation, so a consuming site’s own scripts may rely on the global $. No theme feature requires it. Sites that need it must now bundle it through project JavaScript:

<!-- layouts/_partials/hooks/head-end.html -->
<script src="{{ (resources.Get "js/jquery.min.js").RelPermalink }}"></script>

static/js/tabpane-persist.js is removed. assets/js/code-tabs.js took over the legacy persistence contract, keeping the td-tp-persist storage key and data attribute, so authored tab content is unaffected. Only a site that referenced the published file path directly needs to drop that reference.

Body and heading typography roles apply directly to content. A site that previously restyled raw body or heading selectors should move to the matching --td-*-font-family role or the established Sass variable:

// Before
body {
  font-family: 'My Sans', sans-serif;
}

// Oink 0.3.0
:root {
  --td-body-font-family: 'My Sans', sans-serif;
}

Upgrade to 0.3.0

  1. Check whether any project JavaScript depends on the global $, and bundle jQuery yourself if so.
  2. Remove any direct reference to static/js/tabpane-persist.js; authored tabpane content itself does not change.
  3. Move raw body or heading font overrides to the typography roles.
  4. Decide whether to opt into assistant handoff links. If enabled, review URLs for sensitive query or fragment data and disclose the third-party boundary.
  5. Update the Hugo Module and tidy the module graph.
  6. Build and inspect representative documentation, blog, mobile, print, and color-mode pages.
hugo mod get github.com/pgsty/oink@v0.3.0
hugo mod tidy
hugo --gc --minify

No Markdown content rewrite is required. Existing fenced code blocks, tabpane content, flat menus, shortcodes, and ordinary Docsy-compatible pages continue to work unchanged.

Compatibility

Contract Oink 0.3.0
Hugo Extended 0.160.1 or newer; unchanged
Module path github.com/pgsty/oink; unchanged
Consumer frontend toolchain None; unchanged
Required content migration None
Required configuration migration None; assistant links are opt-in
Required project-JS migration Only if it depends on global jQuery

Verification

The 0.3.0 candidate is exercised through the sibling Oink project site, so the site builds against the candidate theme rather than its last pinned release. Before publication, the theme gate must pass the complete contract suite, a warning-free example-site build on the minimum and current Hugo versions, both typography presets, and the browser runtime unit tests. The site gate must pass formatting, bilingual page pairs and stable heading IDs, rendered Markdown and internal links, Hugo Module fixtures, alternate-configuration builds, Markdown and favicon goldens, responsive and component browser behaviour, and axe accessibility checks. The tag, public-module resolution, site version pin, and hosted smoke tests remain separate post-approval gates.

Full change set

See the complete source diff from v0.2.1 to v0.3.0.

11 - Oink 0.2.0 — Richer content and sharper presentation

Oink 0.2.0 adds composable homepage sections, theme-aware media, wordmarks, navigable component boards, a steps shortcode, polished terminal recordings, and a clearer release-publishing experience.

Oink 0.2.0 focuses on the parts readers and authors touch most: the homepage, brand presentation, blog discovery, section indexes, and instructional content. It also turns the Oink project site into a clearer bilingual reference for the theme’s current contracts.

The module path, minimum Hugo version, and Hugo-only consumer build remain unchanged. The one configuration rename that can affect an existing site is documented in Breaking change.

Release highlights

Homepage and brand

The homepage now composes 12 built-in section types from an ordered sections list. A string selects data with the same name; a map can reuse a presentation through a different key, disable a block without deleting its data, or carry a small one-off block inline. Sites without sections retain the 0.1.x homepage order, so explicit composition is additive rather than a required migration.

The data-driven homepage can now place responsive artwork beside the Hero. Authors may configure one shared image or separate Light and Dark sources, plus meaningful alternative text when the artwork carries information. The layout adapts from a two-column desktop Hero to a compact mobile presentation without requiring a site-level template override.

Oink also adds params.wordmark. A configured wordmark is used consistently in the landing navigation, documentation header, drawer, and footer; sites that only configure params.logo keep the existing mark-plus-title presentation.

Component boards on the homepage can become real navigation. Items accept links, optional external-link behavior, compact styling, and one to four columns. Decorative boards remain non-interactive, preserving the 0.1.0 contract.

See Homepage and footer for the complete data shape.

Blog and release publishing

Blog rows now treat images and summaries as one responsive layout. Featured images no longer force the text outside tablet-width containers, summaries can break long machine-generated tokens, and posts without images use the full text width. The byline’s section name is now a link, and RSS moves into the same action rail used by the rest of the page.

Categories and tags use the same collapsible group grammar as the TOC and page actions. Terms render as scan-friendly rows with count badges in both the wide rail and the mobile drawer. Section indexes are quieter, descriptions have more room, and last-modified metadata follows the child-page index instead of interrupting the page introduction.

The Oink project site now separates upstream Docsy history, Oink engineering articles, and versioned Oink release notes into distinct bilingual sections. That makes release reports discoverable without presenting inherited Docsy posts as Oink releases.

Content components

0.2.0 adds a Markdown-first steps shortcode. Direct child headings become automatically numbered steps connected by a guide line; moving, adding, or removing a step updates the visible sequence without maintaining numbers by hand. Authors can mark a supporting heading with class="no-step-marker" so it does not consume a number.

Asciinema recordings gain a polished terminal frame, title bar, compact control bar, color-mode-aware styling, and a font contract passed directly into the player. This avoids the player falling back to a different terminal font while keeping recordings responsive and readable in both themes.

ECharts callback blocks keep the established trusted-author model: callback code is executable content that must be reviewed like inline HTML or another custom integration. The renderer no longer emits a redundant warning for every reviewed callback block.

See Shortcodes for the new steps contract and Oink components for the broader component model.

Documentation and tests

The independent project site receives a matching documentation pass:

  • Expands English and Chinese homepage and component examples.
  • Documents all 12 composable homepage sections and uses the relevant ones on the project landing page.
  • Adds a real Asciinema installation recording and a dedicated giscus guide.
  • Moves examples under the documentation tree and removes obsolete community and maintainer-only pages.
  • Consolidates Hugo configuration into the root hugo.yml and retires the old Netlify-specific tooling.
  • Isolates browser tests from live reload and keeps responsive, accessibility, translation, rendered-Markdown, and link checks in the release gate.

These are project-site changes, not new runtime dependencies for theme consumers.

Breaking change

0.2.0 renames the inherited featured-image setting from default_featured_image to default_featured. Update page, section cascade, and site-level configuration where the old key appears:

# Oink 0.1.x
default_featured_image: /images/blog-card.webp

# Oink 0.2.0
default_featured: /images/blog-card.webp

The implicit theme placeholder is also removed. If no post image, matching page resource, or explicit default_featured exists, Oink now renders a clean text-only list entry. Set default_featured to a site-owned image when a whole section should keep a visual identity; set it to false to make the opt-out explicit.

There is no compatibility alias for the old key. This is the only required configuration migration in 0.2.0.

Upgrade to 0.2.0

  1. Replace every default_featured_image setting with default_featured.
  2. Update the Hugo Module and tidy the module graph.
  3. Build the site and inspect representative homepage, blog, documentation, mobile, and color-mode pages.
hugo mod get github.com/pgsty/oink@v0.2.0
hugo mod tidy
hugo --gc --minify

No Markdown content rewrite is required. Existing homepage sections, logo-only branding, shortcodes, and ordinary Docsy-compatible pages continue to work.

Compatibility

Contract Oink 0.2.0
Hugo Extended 0.160.1 or newer; unchanged
Module path github.com/pgsty/oink; unchanged
Consumer frontend toolchain None; unchanged
Required content migration None
Required configuration migration Rename default_featured_image

Verification

The 0.2.0 candidate is exercised through the sibling Oink project site so the site builds against the candidate theme rather than only its last pinned release. The release gate covers formatting, bilingual page pairs and stable heading IDs, rendered Markdown and internal links, Hugo Module fixtures, responsive browser behavior, and axe accessibility checks.

Full change set

See the complete source diff from v0.1.0 to v0.2.0.

12 - Oink 0.1.0 — A stable local-first foundation

The first stable Oink release turns the implementation preview into a polished Hugo module with a responsive shell, multilingual infrastructure, local-first components, and a stronger accessibility baseline.

Oink 0.1.0 is the first stable release of the Oink theme. It incorporates the 0.0.1 implementation preview and the stabilization work that followed: one coherent documentation shell, a Hugo-only consumer build, local-first browser assets, multilingual behavior derived from Hugo, and reusable content components.

This release keeps the module path github.com/pgsty/oink and requires Hugo Extended 0.160.1 or newer. A consuming site does not need Node.js, npm, PostCSS, Autoprefixer, or a CDN to build and serve theme-owned functionality.

Release highlights

Local-first theme foundation

Oink ships the styles, fonts, icons, local search, diagrams, API documentation runtimes, and content-component runtimes that it owns. Optional assets load only on pages that use them, and the distributable repository is a root Hugo Module rather than an embedded project site or frontend workspace.

The release also establishes the core product contracts:

  • Hugo languages and translation objects drive language routes, switching, hreflang, writing direction, and locale metadata.
  • The theme supports single-language, multilingual, and RTL sites without PGSTY-specific domain assumptions.
  • Asciinema, ECharts, Infographic, diagrams, API references, tabs, cards, and other reusable components share local, page-scoped runtimes.
  • GitHub Discussions comments are available through an opt-in giscus integration. No external comments script loads unless a site enables it.
  • Docsy-compatible content organization, menus, taxonomies, print outputs, and extension hooks remain available.

Responsive shell

The documentation, blog, and API-reference layouts now use one responsive shell. Desktop navigation, the resizable sidebar, the table of contents (TOC), page actions, taxonomies, version selection, and the footer follow the same visual and interaction grammar.

On tablets and phones, Oink moves the TOC, page actions, categories, and tags into the navigation drawer instead of rendering a second copy. This preserves unique IDs and keeps scroll tracking, disclosures, and copy actions working across live viewport changes. Language and color controls remain reachable at every width, and the color selector exposes Auto, Light, and Dark as explicit preferences.

Navigation entries gain consistent icons, the mobile menu traps keyboard focus, footer columns fill the available width, and the compact page-action menu no longer duplicates the right rail. Copy Markdown, view Markdown, edit, issue, and print actions now come from one implementation.

Publishing and content

Syntax highlighting now uses class-based Chroma output with coordinated light and dark palettes. Code remains readable before JavaScript initializes a color mode, and sites can still opt out with params.highlight_classes: false.

Blog lists gain a deterministic featured-image resolver. In 0.1.0, it checks front matter images, a matching page resource, inherited default_featured_image, site parameters, and finally the theme placeholder. The same resolver serves both modern blog rows and the compatible legacy partial.

The new ink-mark logo and the placeholder artwork respond correctly to all four system-theme and selected-theme combinations. Oink now declares the used color-scheme in both directions, so an explicit site preference wins over the operating-system preference.

Accessibility and correctness

0.1.0 fixes a set of issues found during desktop, mobile, print, and assistive technology review:

  • Corrects heading order, landmark names, task-list labels, and printable list semantics.
  • Keeps blog rows inside the viewport at tablet widths and lets long URLs or identifiers wrap safely.
  • Builds GitHub issue links with correctly encoded titles and URLs.
  • Localizes the 404 page and removes hard-coded punctuation from translated accessible names.
  • Gives iframe embeds titles and lazy loading, registers their resize helper once, and safely handles cross-origin frames.
  • Emits one contentinfo landmark per page and keeps theme extension partials and opt-in SCSS entry points available to consumer sites.

The compatibility audit also removes genuinely unreachable legacy shell code while restoring files that downstream sites can import directly. Reachability is evaluated from consumer layouts and _styles_project.scss, not only from the theme’s own entry points.

Upgrade to 0.1.0

Update the Hugo Module and rebuild the site:

hugo mod get github.com/pgsty/oink@v0.1.0
hugo mod tidy
hugo --gc --minify

No content migration is required. If a site imports Oink partials or SCSS directly, build that site as part of the upgrade so its customization surface is checked alongside the theme.

Compatibility

Contract Oink 0.1.0
Hugo Extended 0.160.1 or newer
Module path github.com/pgsty/oink
Consumer frontend toolchain None
Default browser dependencies Local-first
Primary content model Docsy-compatible Markdown and front matter

Verification

The final 0.1.0 candidate was swept against the theme fixture and the Oink project site across seven viewport widths. The recorded run reported no console errors, failed requests, horizontal overflow, or axe violations. Separate fixtures cover minimum and current Hugo versions, LTR and RTL languages, subpaths, print output, repeated component instances, and network-isolated consumer builds.

Full change set

See the v0.1.0 source snapshot.