# 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.

---

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

---

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](#at-a-glance)
first, then the [migration guide](#migration); the reference sections in
between list every change with its old and new form.

**v0\.5\.0 · 2026-08-18**
- [View release](https://github.com/pgsty/oink/releases/tag/v0.5.0)
- [Source · tar\.gz](https://github.com/pgsty/oink/archive/refs/tags/v0.5.0.tar.gz)
- [Source · zip](https://github.com/pgsty/oink/archive/refs/tags/v0.5.0.zip)
- [pgsty\/oink](https://github.com/pgsty/oink)

## At a glance {#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 {#components}

### Native forms first {#native-forms}

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 {#removed-shortcodes}

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 {#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 {#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-session` → `command`), 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 {#configuration}

### The three rules {#config-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](#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 {#config-renames}

| 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_mode` — `true`, 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](#styles-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 {#fail-loud}

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 {#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 {#templates}

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 {#styles-assets}

### One namespace {#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 {#sass}

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 {#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 {#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}

- `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 {#behavior}

- 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](#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 {#release-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 {#migration}

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:

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

### 1. Inventory {#migration-inventory}

```sh
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 {#migration-content}

```sh
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: `swaggerui` → `swagger`, `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 {#migration-config}

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

```text
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:

```yaml
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 {#migration-cascades}

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

```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 {#migration-site-code}

- `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 {#migration-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 {#migration-verify}

```sh
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:

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

## Compatibility {#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](#config-renames)); 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 {#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 {#full-change-set}

See the complete source diff from
[v0.4.2 to v0.5.0](https://github.com/pgsty/oink/compare/v0.4.2...v0.5.0) and
the theme's `CHANGELOG.md`.
