Oink 0.5.0 — Component API v5 and the converged configuration
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/```checksumsdata 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.pyrewrites 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’shugo.yaml. - Front matter: no
ui:block any more; page overrides are bare keys (section_index: cards),page_context_menumirrors the site map,manualLink*aremanual_link*,hide_*/exclude_searchare gone. - Namespace: theme classes are
td-*, data attributesdata-td-*, custom properties--td-*, JS globalsOink*; theoink-*set and Docsy leftovers (leaf,has-child,nav-*, …) are gone. Callout labels arecallout_*i18n keys. - Removed: the
home/**adapter partials,outputformat.html,td/render-heading.html, the Docsy community page andparams.links, thetd/code-dark/td/color-adjustments-dark/td/gcs-search-dark/td/extraSass files,.td-box*and-bg-*palette classes, Prism, Open Sans,click-to-copy.js,swaggerui(nowswagger). - 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 → checkfor 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,> [!NOTE] Titleblockquote;[!TYPE]-folded /[!TYPE]+open; optional{icon="fa-solid fa-x"}; typesnote tip important warning caution success danger question example quote details. No shortcode.Tabs,Native: adjacent fences (or tables) with
{tab= group= value=}.Shortcode:
tabs group= default= label=tab label= value=…/tab/tabs.Steps,Native:
1.list +{.steps}.Shortcode:
stepswith headings — the only shortcode written with the%delimiters (its body is page-level Markdown); headings inside steps enter the TOC.Cards,Native: link list +
{.cards}.Shortcode:
cardscard title= link= icon= badge= image= image_alt=|decorative=body/card/cards.Fields,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,```filetree {title=}fence, one- name[/] # comment {icon= tone= open= type=}line per entry; 2/4-space, tab, ortreeindentation. CSS + native<details>; the comment column is aligned at build time. No shortcode.Gallery,```galleryfence, one # description {link= class=}line per image; alt is required, items are Zoom-eligible. No shortcode.Image,plus an attribute line{#id num= caption= width= height= link= command= options=}for figures, numbering, links, and Hugo image processing.imgprocis retired; there is no image shortcode.Table family,{.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: image / table /
$$block / fence +{#id num= caption=}(default idsfig-,tbl-,eq-,eg-<num>).Shortcode:
figtbleqeg(egcaption required).Xref,Native: plain Markdown links (kind-less).
Shortcode:
xref fig|tbl|eq|eg="…" [page=] [anchor=].Code fences,Fence attributes
{title copy wrap collapse label id tab group value num caption lineNos hl_lines lineNoStart anchorLineNos tabWidth}; Chroma only.Data fences,mermaid plantuml markmap math chem echarts infographic checksums filetree gallery;echartsis declarative,$fn:<name>callbacks come fromwindow.OinkEchartsFunctions.
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,0.4:
alert color=… title=…,details,td-page-notice(all%shortcodes), raw<details><summary>.0.5.0:
> [!TYPE] titlecallouts,> [!DETAILS]-for a folded block.tabpane · tab · code-group · code-tab,0.4:
tabpanewithtab header=…(both%shortcodes),code-groupwithcode-tab.0.5.0: adjacent fences with
{tab= group= value=}(code-only panes), ortabswithtabfor mixed content.filetree · filetree/folder · filetree/file,0.4:
filetreewithfiletree/folderandfiletree/file; the interim{.filetree}list marker.0.5.0: the
```filetreefence —labelbecomestitle;open,icon,color,comment,linkare kept.gallery · gallery/image,0.4:
gallerywithgallery/image; an image list +{.gallery}.0.5.0: the
```galleryfence.echarts · infographic,0.4:
echarts,infographicshortcodes.0.5.0: same-named data fences;
$fn:callbacks are unchanged,jssub-fences move towindow.OinkEchartsFunctions.imgproc,0.4:
imgproc …(and the pre-releaseimage …).0.5.0:
+{command= options= caption=}on the attribute line.readfile,0.4:
readfile file=….0.5.0:
include file=… [code=true lang=…]— page resources, then assets, then content-relative paths.example · book-figures kind=,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),0.4:
fields/fieldwritten with%delimiters (never shipped).0.5.0:
fields/field._param · iframe · conditional-text · netlify · kind-less xref,Reported with
file:linefor manual review;_paramplaceholders are handled by theparam_placeholderstransform.
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"}.titleandfilenameare mutually exclusive on the same fence.- The Prism path is gone.
params.prism_syntax_highlighting,static/js/prism.js, andstatic/css/prism.cssno longer exist; Chroma withparams.highlight_classes(defaulttrue) is the only highlighter. Prism could not coexist withtab,group,value,num, andcaption, so any 0.4 site using tabs or numbered examples already failed with it enabled. - The Copy control follows
copy=all|command|true|falseon the fence, then the session-lexer default (console,shell-session→command), thenall.params.ui.code_copy: falsechanges the site-wide default only; a fence that namescopystill gets what it asks for. The olddisable_click2copy_chromasilently overrode an explicit author value. - Docsy’s
click-to-copy.js(never loaded since 0.3) and its.td-click-to-copystyles are removed.
Configuration
The three rules
- A boolean switch is the bare feature name:
ui.annotation: true, notui.annotation.enableand notui.annotation_enabled. The only_enabledsuffixes left areui.navbar_enabled,ui.sidebar_enabled, andui.sidebar_root_enabled, whose bare names would collide with sibling families. - 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). - 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_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) |
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 throughshell/sidebar-node.html.shell/config.htmlremains the single resolver for brand, logo, and section configuration. - Every content-rendering layout calls
content/render.htmlinstead of.Content(that is where the Image Zoom candidate scan runs). - Print:
print/page-content.htmlrenders each page’s print content exactly once per build throughpartialCached;print/render.html,print/content.html,book/print.html, and thesingle.print.htmllayouts 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, labelui_heading_self_link); print and RSS strip the link. - The DocSearch container is one
#td-docsearchelement; the two hard-coded#docsearch-0/1ids 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.jsandjs/core.jsare byte-identical on every page and stay cached; only a smalljs/page-<hash>.jsvaries. ECharts is its own<script>. Print output loads 7.9 KB instead of 100 KB. static/css/giscus-oink-{light,dark}.cssare gone. The palettes ship asassets/css/giscus-{light,dark}.css, are published only on pages that render comments, and are the defaultcomments.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.jsonand 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 asnote,example, orquote; a site that overrode those in its owni18n/renames them. - Removed:
community_join,community_introduce,community_learn,community_using,community_develop,community_contribute,community_how_to,community_guideline. - Added:
ui_heading_self_linkandui_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(ordata/home.yaml) must listsections; the implicithero → metrics → capabilities → principles → ctaorder is gone and its absence fails the build.- The fat footer reads
data/footer/<lang>.yaml(ordata/footer.yaml) only. Afooterkey insidedata/homefails the build naming the new location. data/landing/<key>/<lang>.yaml,data/docs_nav.json,data/download/<key>.yaml, anddata/brand.yamlare 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 carriesrole="main", and the sidebar<aside>no longer duplicates the inner<nav>’s “Section navigation” label.ui.dark_mode: trueturns on both the dark palette and the System / Light / Dark menu;show_menu: truealone impliesenable.ui.code_copy: falsesets 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;
fieldsfrom either form produce one rendering, and every entry gets a#field-<name>anchor. llms.txtreadsparams.ui.docs_sectionand lists documentation pages with their descriptions.- Image resolver errors are labelled by the caller (
image:for a Markdown image, the shortcode name forfig), 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, andcheckreject 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.onloador publishingwindow.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:
1. Inventory
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
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
Build the site. Each old key fails with its replacement:
A typical 0.4 hugo.yaml becomes:
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:
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/: replacepartial "home-data.html"/"home/section.html"withlanding/…; replacepartial "outputformat.html"with.Store.Get "tdOutputFormat"; delete a_markup/render-heading.htmlthat calledtd/render-heading.html; renametaxonomy_terms_*calls; compare every other copied partial or hook with 0.5.0 before keeping it.- Site JS and tests:
oink-*ids anddata-oink-*attributes aretd-*/data-td-*; the action manifest is#td-action-manifest; the per-page bundle isjs/page-<hash>.js, withjs/actions.jsandjs/core.jsbeside it. - Site
i18n/overrides: renamenote,tip, … tocallout_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
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:
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_linksare 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.
