Skip to content

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.

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.

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.

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.

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/[email protected]
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.