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

Return to the regular view of this page.

Welcome to OINK

Install, customize, deploy, and maintain Oink documentation sites.

v0.2.0

Welcome to the OINK user guide for version v0.2.0. This guide covers the theme’s Hugo-only build, local-first runtime, multilingual framework, content components, customization, and deployment.

What is OINK?

OINK is an independent theme for the Hugo static site generator, designed for medium and large technical documentation sets. It evolves Docsy directly: Docsy’s mature content model and documentation features remain available, while OINK provides a new canonical shell, local dependencies, and reusable components drawn from production PGSTY sites.

A consuming site can build with Hugo Extended alone. It does not need Node.js, npm, PostCSS, Autoprefixer, or a CDN. Bootstrap, Font Awesome, fonts, local search, diagrams, API documentation runtimes, and content components ship with the theme and are loaded only when a page needs them.

OINK includes:

  • a responsive documentation and blog shell with navigation, table of contents, search, print output, dark mode, and accessible interactions;
  • a general multilingual framework with translated-page routing, missing-page fallback, language weights, RTL support, and SEO alternate metadata;
  • local Mermaid, KaTeX, Markmap, Swagger UI, Redoc, Asciinema, ECharts, and Infographic runtimes;
  • reusable details, tabs, cards, navigation cards, and document carousels;
  • a bilingual project site, static-host deployment guides, local theme assets, and an auditable vendor manifest.

OINK itself does not provide source hosting or deploy your generated site. Keep your project in GitHub, GitLab, a private Git service, or a local repository, then publish Hugo’s static output with the platform of your choice.

Is OINK for me?

OINK is most useful when a documentation project has many pages, several content types, multiple languages, or strict reproducibility and network isolation requirements. It is also a good fit when several sites should share a single maintained shell instead of copying layouts, scripts, and shortcodes.

For a project with only one or two pages and no structured navigation, a README or a smaller Hugo theme may be simpler. For a heavily application-driven portal, use OINK for the documentation surface and keep business-specific components in the site rather than forcing them into the theme.

Ready to get started?

Read the Oink overview to understand the product boundaries, then install Oink or create a bilingual site. The remaining guide covers authoring, advanced features, deployment, and upgrades.

1 - About Oink

Understand Oink’s examples, design principles, architecture, and open-source model.

OINK turns Markdown, configuration, and local assets into a complete technical documentation site. With the theme source and Hugo Extended, a site can build documentation, a blog, multilingual navigation, local search, diagrams, API references, and reusable content components without maintaining a frontend toolchain.

What Oink provides

Capability Theme contract
Documentation and blog layouts Responsive navigation, breadcrumbs, table of contents, page metadata, feedback, print output, and content indexes
Multilingual behavior Translated-page routing, language metadata, stable anchors, and per-language local search
Local-first browser features Versioned styles, fonts, search, diagrams, API references, recordings, charts, and infographics
Content components Tabs, details, steps, cards, carousels, diagrams, terminal recordings, charts, and parameter substitution
Reproducible delivery A Hugo-only consumer build, pinned assets, an auditable vendor manifest, and regression fixtures

OINK evolves Docsy’s mature Hugo content model into one independent theme. It uses Hugo as its build platform and takes design inspiration from Fumadocs. The open-source licenses page records those relationships and keeps upstream lineage, inspiration, dependencies, and licensing distinct.

Explore the project

The About section follows the lifecycle from evaluation through maintenance:

  1. Example sites shows the full bilingual project site, the small theme example, and the guide for creating a minimal consumer.
  2. Local-first operation defines which build and browser capabilities work without hidden network access.
  3. Architecture explains the repository, build, page-shell, runtime, and extension boundaries.
  4. Contribution guidelines covers theme and bilingual documentation changes.
  5. Release process separates source completion, validation, public release, documentation updates, and hosted deployment.
  6. Open-source licenses explains provenance, dependencies, acknowledgements, and the licenses for the site and theme.

Repository boundaries

The published Hugo Module is github.com/pgsty/oink. Documentation, examples, and regression tests live in the independent github.com/pgsty/oink.pgsty.com repository. Production consumers should pin a released tag or an immutable commit.

Site-specific content, branding, configuration, and business components remain under site control. General-purpose layouts, reusable components, local browser runtimes, and translations belong in the theme.

Start here

A successful local build proves that one source tree renders in one local environment. It does not prove that a theme tag is public or that the hosted site contains the same commit.

1.1 - Example sites

Explore the full project site, the theme example, and a minimal bilingual consumer.

OINK provides examples at three different scales. Choose the smallest one that answers the question you are investigating; the complete project site is useful for regression coverage, but it is deliberately larger than a normal consumer.

Choose an example

Example Best for Repository or guide
Bilingual project site Production-scale configuration and QA pgsty/oink.pgsty.com
Theme exampleSite/ Landing-page composition and theme checkout pgsty/oink/exampleSite
Minimal bilingual consumer Starting a new documentation site Create a new site

Bilingual project site

The pgsty/oink.pgsty.com repository is the complete documentation and regression site you are reading. It keeps English and Simplified Chinese content side by side, pins the published Oink module in go.mod, and exercises documentation, blog, search, print, rich content, metadata, and responsive navigation.

Clone and preview the published module path with Hugo:

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

Create the production artifact with:

BASH
hugo --gc --minify

Node.js and npm are maintenance dependencies for this repository’s formatting, translation, link, browser, and regression checks. They are not Oink consumer build requirements.

Theme example site

The theme repository contains a deliberately small exampleSite/. It exercises the checked-out theme directly and demonstrates a composable landing page without importing the project site’s documentation or npm workspace.

BASH
git clone https://github.com/pgsty/oink.git
cd oink/exampleSite
hugo server

Use this example when changing landing-page data or checking a theme checkout. Use the bilingual project site when testing documentation navigation, translations, local search, rich components, or release behavior.

Build a minimal consumer

Follow Create a new site to assemble a small bilingual consumer from an empty directory. The guide creates the module pin, configuration, content tree, and first preview explicitly, so readers can see every required file instead of depending on a copied starter that may drift.

Related references include:

Verify the right layer

A local preview proves that one checkout renders. It does not prove that a theme tag is public, a deployment contains the same commit, or the hosted route works. Record source, build, release, and hosted verification separately when using an example as evidence.

1.2 - Local-first operation

Build and browse Oink without hidden network access.

OINK’s local-first rule is simple: a feature owned by the theme must not silently depend on a public CDN, a build-time download, or an unconfigured public service. A complete distribution can be built and its core pages browsed inside a network-isolated environment.

What local-first covers

The theme serves these dependencies from the generated site:

Capability Local delivery
Shell and responsive UI Bootstrap and OINK CSS/JavaScript
Icons and fonts Font Awesome, Open Sans, Chakra Petch, IBM Plex Mono
Search Lunr plus a CJK substring fallback and per-language indexes
Diagrams and formulae Mermaid, KaTeX, and Markmap
API documentation Swagger UI and Redoc
Rich content Asciinema, ECharts, Infographic, and carousel runtime

Assets are committed under assets/ or static/. Hugo publishes them under the site’s baseURL, including subpath deployments.

What local-first does not cover

OINK cannot make arbitrary authored content offline. The following remain explicit network choices:

  • external links, remote images, video, iframes, and API specifications;
  • hosted search such as Algolia or Google CSE;
  • analytics, comments, identity providers, and other SaaS integrations;
  • PlantUML or Diagrams.net when an author chooses a remote renderer.

A page using one of these features can still be valid, but the site should not claim that page is fully available offline.

Service-backed diagrams

PlantUML and Diagrams.net differ from browser libraries: their normal workflows depend on a rendering or editing service. OINK therefore has no implicit public endpoint.

Enabling PlantUML without params.plantuml.svg_image_url, or Diagrams.net without params.drawio.drawio_server, fails the build with an actionable message. Configure a controlled local endpoint, publish a pre-rendered image, or make a deliberate remote-service choice:

YAML
params:
  plantuml:
    enable: true
    svg: true
    svg_image_url: https://diagrams.internal.example/plantuml/svg/
  drawio:
    enable: true
    drawio_server: https://diagrams.internal.example/

The OINK documentation regression site explicitly configures public demo servers so inherited diagram examples continue to render. That sample-site choice is not the theme default and should not be copied into an air-gap site.

Set:

YAML
params:
  offlineSearch: true

Hugo generates a search index for each language. The browser uses local Lunr search for Latin-script queries and a local substring fallback for CJK text. No query leaves the site.

Keep search useful by writing descriptive titles and summaries, setting the correct page language, and excluding generated or sensitive pages that should not enter a public client-side index. A local index is downloadable by every visitor and is not an access-control mechanism.

Per-page assets

OINK does not place every runtime on every page. Mermaid, KaTeX, Markmap, Swagger UI, Redoc, Asciinema, ECharts, Infographic, and the carousel are selected from page feature markers. A page that does not use a component does not receive that component’s runtime.

When a page contains several instances of the same component, the runtime is still included once. Production resources are fingerprinted where the Hugo pipeline permits it, which supports integrity metadata and long-lived caching.

Third-party provenance

VENDOR.json is the machine-readable inventory for bundled dependencies. For each dependency it records:

  • name and pinned version;
  • original source;
  • applicable license files;
  • selected artifact paths and SHA-256 values;
  • the maintainer update procedure.

The theme retains the corresponding license files beside vendor assets. Updating a runtime means refreshing the artifact, its license and notice material, its checksum, and its tests as one reviewable change.

Prepare an offline archive

Prepare the archive on a connected, trusted machine from an immutable Oink tag. For example:

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

Transfer both files into the isolated environment, verify the checksum, and extract the conventional theme:

BASH
shasum -a 256 -c oink-vX.Y.Z.tar.gz.sha256
mkdir -p product-docs/themes
tar -xzf oink-vX.Y.Z.tar.gz -C product-docs/themes

Then configure the isolated site to use it:

YAML
theme: oink

The archive must include go.mod, hugo.yaml, layouts, assets, static files, translations, LICENSE, NOTICE, and VENDOR.json. Inspect it before relying on it in a disconnected build. If a future release publishes an archive and checksum, verify those published artifacts independently instead of assuming that every tag has attached files.

Verify an isolated site

A meaningful air-gap acceptance test covers both build time and browser time:

  1. start with the verified theme archive and an empty Hugo cache;
  2. block outbound HTTP, HTTPS, and Go module proxies;
  3. run the production Hugo command;
  4. browse English and Chinese pages from the generated output;
  5. exercise search, dark mode, diagrams, API docs, and content components;
  6. inspect every HTML and CSS subresource URL for unexpected remote origins.

The project-site regression suite performs these checks against a local theme candidate. A successful test proves only the tested commit and environment; repeat it for every release candidate and after bundled dependency updates.

Content security policy

Local assets make a strict Content Security Policy practical, but OINK does not invent one universal policy for every site. Inline author HTML, ECharts callback scripts, analytics, remote specifications, and custom integrations can all change the required directives.

Start with the smallest policy that supports the site’s reviewed features. Keep ECharts options structured when callbacks are unnecessary, review inline scripts, and add remote origins only for integrations that the site deliberately enables.

1.3 - Architecture

See how Oink turns content and local assets into a static site.

Oink is a direct Hugo theme, not an application server or a runtime wrapper around Docsy. Hugo resolves content, configuration, layouts, and assets at build time, then emits a static site for any ordinary file host.

System boundary

flowchart LR
  C[Site content] --> H[Hugo Extended]
  G[Hugo configuration] --> H
  T[Oink Hugo Module] --> H
  V[Committed third-party assets] --> T
  H --> P[Static public directory]
  P --> B[Browser]

The consumer boundary starts with a site plus the resolved theme module and ends with Hugo’s static output. No JavaScript package manager, CSS postprocessor executable, or remote asset download is required in that path.

JavaScript still runs in the browser for interactive features. “Hugo-only” describes the build dependency, not a JavaScript-free user interface.

Repository boundary

Theme repository

github.com/pgsty/oink is the published Hugo Module. Its root contains the canonical layouts, partials, shortcodes, SCSS, JavaScript, fonts, icons, browser runtimes, translations, go.mod, and hugo.yaml. VENDOR.json records the bundled third-party assets.

The repository contains no project website or npm workspace. Root metadata such as README.md, LICENSE, NOTICE, theme.toml, and the vendor manifest is part of distributing and attributing the theme.

Project site repository

github.com/pgsty/oink.pgsty.com contains the documentation, bilingual examples, regression pages, site-specific layouts and assets, npm-based site tests, and deployment configuration. It imports the public theme module in hugo.yaml and pins its version in go.mod.

For local cross-repository development, an ignored go.work substitutes a sibling theme checkout. No relative filesystem replacement is committed to the site module.

Build pipeline

Hugo combines four classes of input:

  1. page bundles and Markdown content from the consuming site;
  2. native Hugo configuration and supported theme parameters;
  3. theme templates, translations, SCSS, and JavaScript;
  4. committed static or Hugo Asset resources.

Hugo compiles SCSS with its embedded pipeline, bundles page JavaScript, minifies production resources, fingerprints eligible outputs, and rewrites relative URLs for the configured baseURL. Oink does not invoke Hugo’s postCSS pipe.

The final public/ directory contains HTML, CSS, JavaScript, fonts, search indexes, feeds, sitemaps, and copied static files. It can be deployed without the source tree.

Page shell

The canonical page shell is assembled from small partials:

  • a global navbar and responsive sub-navigation;
  • language and color-mode controls;
  • a resizable, foldable documentation sidebar;
  • breadcrumbs, table of contents, reading metadata, feedback, and repository links where configured;
  • a shared footer and print layouts.

Normal Hugo lookup remains available for site-specific extensions. Override the narrowest partial possible instead of copying baseof.html or the entire shell.

Conditional runtime loading

Content shortcodes record feature use in the page store. Asset partials inspect those markers and include the corresponding local runtime at most once:

flowchart TD
  S[Shortcode renders] --> M[Set page feature marker]
  M --> A[Asset assembly]
  A --> Q{Feature used?}
  Q -- Yes --> L[Bundle local runtime once]
  Q -- No --> O[Omit runtime]

This keeps a plain article free of ECharts, Asciinema, or Infographic code while allowing multiple component instances on a feature page.

Multilingual routing

Oink delegates language identity to Hugo. The selector uses each page’s .Translations and the site’s configured languages, ordered by weight. Missing translations fall back to the target-language home page. The same data drives canonical and alternate metadata.

Security boundaries

Oink keeps authored data and authored executable code explicit:

  • structured ECharts options are parsed as JSON or YAML and safely serialized;
  • optional ECharts JavaScript blocks register callbacks only on pages that declare them;
  • component identifiers and configuration are generated by templates rather than unescaped HTML strings;
  • hosted search, analytics, comments, remote media, and service endpoints remain explicit site decisions.

Goldmark’s unsafe setting permits trusted project authors to use inline HTML; it is not a sanitizer for untrusted submissions.

Upstream maintenance

Oink preserves Docsy’s source history and Apache-2.0 obligations. Upstream changes are classified as applicable, superseded by an intentional Oink difference, or unrelated. Applicable changes are ported into the canonical implementation without recreating an upstream-versus-brand runtime switch.

Extension boundary

Put an implementation in the theme when it is broadly reusable, has a stable content API, and can own its assets and accessibility behavior. Keep it in the site when it embeds product data, pricing, catalog assumptions, or a one-off landing-page structure.

1.4 - Contribution guidelines

Contribute code and bilingual documentation to Oink.

OINK is an independent theme derived from Docsy. Contributions must preserve the Apache-2.0 history and applicable third-party notices while improving the single canonical implementation.

Before opening a change

  • Search existing issues and pull requests in the OINK repository.
  • For a bug, record the Hugo version, installation mode, language, route, production command, and smallest reproducible input.
  • For a feature, explain why it belongs in the reusable theme rather than in a consuming site’s business layer.
  • Do not introduce an oink.enabled switch, an oink.* configuration tree, or a parallel visual shell. OINK’s standard layouts are the product.

Small fixes can go directly to implementation. Larger behavior changes should state their compatibility, offline, accessibility, security, and migration impact before code is written.

Development environment

Consumer sites need only Hugo Extended, Go, and Git. The theme repository is a direct Hugo Module. The project-site repository uses its pinned Node.js and npm versions for formatting, links, translations, and regression tests.

Install the maintainer dependencies from the repository root using the lockfile. Do not update dependencies as a side effect of an unrelated change.

The project is split across:

  • github.com/pgsty/oink: published theme source and VENDOR.json;
  • github.com/pgsty/oink.pgsty.com: documentation, examples, and tests.

Build the consumer contract

Always verify the path that users run from a consuming site:

BASH
hugo --gc --minify

This build must succeed without npm installation in the consumer site and without a network request for theme-owned browser assets.

For a local theme candidate, clone both repositories as siblings and activate an ignored Hugo workspace:

BASH
go work init .
go work edit -replace=github.com/pgsty/oink=../oink
HUGO_MODULE_WORKSPACE=go.work npm run build

Run focused tests

Choose the smallest relevant suite first:

BASH
npm run test:hugo-build
npm run test:alt-site
npm run test:md-output
npm run test:favicons

Run npm test for the complete site suite.

Multilingual changes should cover the one-, two-, three-, and four-or-more language states, missing-page fallback, RTL, canonical URLs, hreflang, and Open Graph locale metadata.

Content-component changes should cover single and multiple instances, no asset load on unused pages, invalid parameters, subpath builds, print, keyboard use, reduced motion, and offline behavior.

Write bilingual documentation

All user-facing pages added under content/docs/ or content/blog/ need a .zh.md peer. Follow TRANSLATION.md for terminology and Chinese typography.

Translated Markdown headings use explicit IDs copied from the English rendered HTML. Check source coverage and, after building, rendered heading IDs:

BASH
node scripts/check-doc-translations.mjs
node scripts/check-doc-translations.mjs --public public

Preserve code, configuration keys, URLs, release facts, authorship, and link definitions. Translate visible metadata, alternative text, callouts, UI labels, and shortcode strings. Do not submit placeholder or untranslated prose merely to satisfy the filename check.

Preview documentation

Run the project website with the pinned public module, or activate the local workspace described above:

BASH
npm run serve

Review English and Chinese versions of the changed pages at desktop and mobile widths. Check light and dark modes, table of contents, language switching, search, code blocks, tables, callouts, print output, and fragment links.

A local build proves only local rendering. CI, release packaging, hosted preview, and production publication are separate verification layers.

Keep changes compatible

  • Reuse existing partials, shortcodes, SCSS helpers, and asset loaders.
  • Load browser runtimes only on pages that use them, and at most once per page.
  • Keep default behavior local-first and same-origin.
  • Serialize structured data safely; arbitrary JavaScript requires an explicit unsafe boundary.
  • Use logical CSS properties and test LTR and RTL.
  • Preserve site-owned business components and documented compatibility aliases.
  • Keep legal attribution and vendor metadata with redistributed assets.

Open the pull request

Keep commits and messages lean and explain user-visible behavior and migration impact. Include the focused commands run and their results.

If a change intentionally diverges from Docsy, update the relevant migration or release documentation. Do not remove upstream copyright, license, or history.

1.5 - Release process

Release the Oink theme and update its independent project site.

Oink treats implementation, validation, publication, and deployment as different states. A green local build is useful evidence, but it is not a public tag, a downloadable module, or a deployed documentation update.

Release states

State Required evidence
Source complete Scope, docs, changelog, attribution, and review are complete
Validated Theme-module and project-site checks pass
Published An immutable root tag exists in pgsty/oink and resolves through Go
Documented pgsty/oink.pgsty.com pins and documents that tag
Deployed The hosted documentation and target consumers pass verification

Report the exact state and evidence; do not call a local build a release.

Versioning

Theme releases use root tags such as vX.Y.Z in github.com/pgsty/oink. The theme is now the repository’s root module, so nested theme/vX.Y.Z tags are no longer used.

The project site’s version parameter identifies a published site variant and is not automatically a Git ref. Installation instructions and go.mod must use the actual resolvable theme tag.

Validate the theme repository

From a clean pgsty/oink checkout:

  1. inspect the source diff and attribution changes;
  2. verify every VENDOR.json file and SHA-256 entry;
  3. confirm the repository has no generated public/, resource cache, node_modules/, or embedded example site;
  4. build a minimal consumer through the Hugo Module path with the minimum and current supported Hugo Extended versions;
  5. inspect the module zip and confirm layouts, assets, translations, static files, licenses, and notices are present.

The module zip test matters because Go excludes special directory names such as vendor from published modules. Oink stores bundled dependencies under assets/third_party/ so they survive module distribution.

Validate the project site

Clone pgsty/oink and pgsty/oink.pgsty.com as siblings, then connect them with an ignored workspace:

BASH
cd oink.pgsty.com
go work init .
go work edit -replace=github.com/pgsty/oink=../oink
export HUGO_MODULE_WORKSPACE=go.work
npm ci
npm test

Inspect representative English and Chinese pages, mobile navigation, both color modes, local search, print output, diagrams, API documentation, and 404 pages. This validates the candidate against the site; it does not publish either repository.

Tag and publish the theme

After review, create one immutable signed root tag in the theme repository:

BASH
git tag -s vX.Y.Z -m "Oink vX.Y.Z"
git push origin main vX.Y.Z

Pushing and creating a GitHub release require explicit authorization. After the tag is public, verify it from a clean environment:

BASH
hugo mod get github.com/pgsty/oink@vX.Y.Z
hugo mod graph

If an offline archive is attached to the release, publish and independently verify its SHA-256 checksum. Keep LICENSE, NOTICE, and VENDOR.json in the archive.

Update the project site

Once the theme tag resolves publicly, update the independent site repository:

BASH
hugo mod get github.com/pgsty/oink@vX.Y.Z
hugo mod tidy
npm test

Commit go.mod, go.sum, version parameters, changelog, and upgrade guidance together. Deploy previews first, then advance the production publishing branch only after review.

Post-release verification

After publication:

  1. fetch the tag from a clean clone and inspect its signature;
  2. resolve the module through the public Go proxy;
  3. build a minimal new site with the documented commands;
  4. open the production documentation and verify module instructions, canonical links, languages, search, and assets;
  5. verify any released archive and checksum;
  6. record the final tag, module version, hosted URLs, and artifact hashes.

Hotfix and rollback

A hotfix follows the same evidence chain with a smaller scope. Never move or replace a published tag. Roll back a site deployment to a known artifact, then publish a new patch version of the theme when necessary.

Definition of done

A release is complete only when the approved tag exists, the public module resolves, required checks pass, the project site pins the tag, and hosted smoke tests succeed. Anything less should be reported by its actual state.

1.6 - Open-source licenses and acknowledgements

Understand Oink’s upstream lineage, dependencies, acknowledgements, and licensing boundaries.

OINK combines an Apache-licensed theme, a separately licensed documentation site, and third-party components that retain their own licenses. These layers are intentionally not relicensed as one undifferentiated work.

This page is a practical provenance guide, not a substitute for the license texts. When the summary and a license file differ, the license file controls.

License map

Surface License Authoritative record
Oink theme source and Oink theme changes Apache License 2.0 Theme LICENSE and NOTICE
Site code, build tooling, and Docsy-derived material Apache License 2.0 Site LICENSE and NOTICE
Original Oink documentation, unless noted otherwise Creative Commons Attribution 4.0 International LICENSE-CC-BY-4.0 and the page or asset’s own attribution
Browser libraries, fonts, icons, and other bundled assets Each component’s own license Theme VENDOR.json and the license files distributed beside the assets

The Creative Commons license applies to original documentation content, not to theme code, copied source code, trademarks, screenshots, or third-party assets that carry a different notice. Likewise, Apache-2.0 on the theme does not replace a bundled dependency’s license.

Upstream lineage

OINK is an independent theme directly derived from Docsy. It preserves Docsy’s source history, Apache-2.0 license, copyright notices, content conventions, and compatible APIs that remain part of the product. The project site is also derived from the Docsy project website and records that origin in its own NOTICE.

OINK is not an optional skin over a second Docsy installation. The inherited codebase has evolved into one standard theme with a Hugo-only consumer build, local browser runtimes, multilingual behavior, and its own release process. Contributors must retain applicable upstream notices and mark modified files as required by Apache-2.0.

Projects Oink depends on

Hugo Extended is the build platform. Go resolves the Hugo Module when a site uses the module installation path, and Git provides the source and release workflow. These tools are prerequisites; the theme does not redistribute their executables.

The theme does redistribute versioned browser assets so a consumer does not need npm or a public CDN. The following table groups the current major dependencies; VENDOR.json is authoritative for exact versions, selected artifacts, sources, checksums, and license paths.

Capability Included projects License families recorded by the theme
UI foundation Bootstrap, Popper, jQuery MIT
Icons and fonts Font Awesome, Open Sans, Chakra Petch, IBM Plex Mono CC BY 4.0, SIL OFL 1.1, and MIT as applicable
Search Lunr, DocSearch MIT
Diagrams and formulae Mermaid, KaTeX, Markmap, D3, Highlight.js, Web Font Loader MIT, ISC, BSD-3-Clause, and Apache-2.0
API and terminal views Swagger UI, Redoc, Asciinema Player Apache-2.0 and MIT
Data visualization Apache ECharts, AntV Infographic Apache-2.0 and MIT
Supporting runtimes pako, external-svg-loader, idb-keyval, PrismJS MIT, Zlib, and Apache-2.0 as applicable

Redistributions must keep the license and notice material required by each component. Updating a vendored file means updating its manifest entry, checksum, source, license files, and any required notice in the same change.

Projects Oink acknowledges

Project Relationship to Oink Contribution to the project
Hugo Build platform Content model, templates, asset pipeline, multilingual routing, taxonomies, and static-site generation
Docsy Direct upstream Repository history, documentation conventions, layouts, Bootstrap foundation, and compatible APIs
Fumadocs Design inspiration Content-first presentation, information hierarchy, navigation geometry, and table-of-contents treatment

Fumadocs is a design reference, not Oink’s code upstream or a runtime dependency. OINK reinterprets those ideas for Hugo and its Docsy-derived codebase rather than making a pixel-for-pixel copy. We thank the Hugo, Docsy, Fumadocs, and dependency communities for publishing and maintaining their work.

These references identify lineage, dependencies, or inspiration. They do not imply endorsement, and the project and product names remain the property of their respective owners.

Reusing the documentation

CC BY 4.0 permits sharing and adaptation of covered documentation for any purpose, provided that you give appropriate credit, link to the license, and indicate whether you made changes. You must not imply that Oink, PGSTY, or an upstream project endorses the adaptation.

A concise attribution can read:

Adapted from the Oink documentation by PGSTY contributors, licensed under CC BY 4.0. Changes were made.

If a page includes separately attributed media or imported text, preserve that material’s attribution and license as well. Removing a footer does not remove the obligation to provide attribution elsewhere.

Reusing the theme

Apache-2.0 permits use, modification, and distribution of the covered theme source and compiled output, subject to its terms. In particular, preserve the license, applicable copyright and attribution notices, and the contents of NOTICE when the license requires them; mark modified files when distributing modified source.

Theme distributions should include LICENSE, NOTICE, VENDOR.json, and the third-party license files referenced by the manifest. Apache-2.0 does not grant permission to use project trademarks, and it does not turn third-party assets into Apache-licensed works.

For a source contribution or redistribution review, start with the contribution guidelines and the actual license and notice files linked below.

2 - Get Started

Build a bilingual Oink documentation site with Hugo Extended.

Oink is a Hugo theme whose complete browser runtime ships with the theme. A consumer site builds with Hugo Extended alone: no Node.js package installation, PostCSS step, CDN, or build-time remote asset download is part of the default path.

Choose a starting point

  • Hugo Module — recommended: import github.com/pgsty/oink in an existing or new Hugo site. See the Oink quick start.
  • Project site: use the independent pgsty/oink.pgsty.com repository as a complete bilingual configuration and regression reference.
  • Existing Docsy site: follow the migration guide to remove common overrides and the consumer npm asset pipeline without rewriting content.

Install the prerequisites

Install Git, Go, and Hugo Extended 0.160.1 or newer. See Before you begin for platform notes and verification commands.

Add Oink

From the site root:

BASH
hugo mod init github.com/example/product-docs
hugo mod get github.com/pgsty/oink@THEME_REF

Then import the theme in hugo.yaml:

YAML
module:
  imports:
    - path: github.com/pgsty/oink

Pin THEME_REF to a released tag or immutable commit and commit go.mod and go.sum.

Build contract

The same commands preview and build every supported module consumer:

BASH
hugo server --disableFastRender
hugo --gc --minify

Next steps

  1. Set the basic configuration.
  2. Add repository, copyright, logo, and menu values.
  3. Put translations side by side as page.md and page.zh.md.
  4. Add and customize content.
  5. Choose a deployment target.

2.1 - Before you begin

Prerequisites for building an OINK site.

The consumer prerequisite is Hugo Extended. Git and Go are conditional on how the theme source is obtained.

Install Hugo Extended

Install version 0.160.1 or newer. The current validation baseline is 0.164.0. A release’s support matrix takes precedence when these values change.

Verify the selected binary:

BASH
hugo version

The output must contain extended. Standard Hugo cannot compile the theme’s SCSS. Use Hugo’s official installation guides for the platform and pin the same version in local development and CI.

Install Git when needed

Git is required to clone the site, use submodules, preserve .GitInfo, or fetch a theme checkout. Verify it with:

BASH
git --version

A site built from an already extracted offline archive can run Hugo without network access, but keeping the source in version control remains recommended.

Install Go only for Hugo Modules

Hugo’s module commands use Go. Install Go when the site imports the theme as a Hugo Module, then verify:

BASH
go version
hugo mod graph

A versioned archive, adjacent theme directory, or Git submodule does not require Go at site build time.

Do not install a frontend toolchain

OINK ships Bootstrap, Font Awesome, LTR and RTL CSS, fonts, search, and browser runtimes as local versioned assets. Consumer sites do not install Node.js, npm, PostCSS, Autoprefixer, or RTLCSS for the theme.

Node-based commands in the project-site repository are maintainer-only tools. The production consumer command is:

BASH
hugo --gc --minify

Check the complete distribution

For offline or air-gapped use, confirm that the theme archive contains go.mod, hugo.yaml, assets/, layouts/, static/, i18n/, LICENSE, NOTICE, and VENDOR.json. Install Hugo Extended before entering the isolated environment, then run the same build command with network access disabled.

What’s next?

2.2 - Install Oink

Add the pinned Oink Hugo Module to a site.

Oink is published as the Hugo Module github.com/pgsty/oink. A consuming site builds with Hugo Extended alone; Node.js, npm, PostCSS, and CDN-hosted browser packages are not part of the build contract.

Prerequisites

Install Git, Go, and Hugo Extended 0.160.1 or newer. The project site currently validates with 0.164.0:

BASH
git --version
go version
hugo version

The Hugo version output must include extended.

Add the module

From your Hugo site root, initialize a module if the site does not already have one, then pin an Oink release:

BASH
hugo mod init github.com/example/product-docs
hugo mod get github.com/pgsty/oink@THEME_REF

Replace THEME_REF with a published tag such as v0.2.0 or an immutable commit. Add the import to hugo.yaml:

hugo.yaml
YAML
module:
  imports:
    - path: github.com/pgsty/oink

Commit the resulting go.mod and go.sum. Do not run production builds against an unpinned branch.

Preview the site

Start an editing server:

BASH
hugo server --disableFastRender

Create a production artifact with:

BASH
hugo --gc --minify

Oink ships Bootstrap, Font Awesome, fonts, search, diagrams, API documentation runtimes, and its content components. A consuming site does not need a node_modules directory.

Develop against a local checkout

Clone the theme and site as siblings, then use a local Go workspace:

Sibling checkout layout
TEXT
~/pgsty/
├── oink/
└── product-docs/
BASH
cd ~/pgsty/product-docs
go work init .
go work edit -replace=github.com/pgsty/oink=../oink
export HUGO_MODULE_WORKSPACE=go.work
hugo server

Keep go.work out of version control. The committed go.mod remains pinned to the public module; the workspace substitutes the sibling checkout only on your machine.

Add bilingual content

Create the English page first:

TEXT
content/docs/operations.md

Then add its translation beside it:

TEXT
content/docs/operations.zh.md

Keep front matter identifiers, code, commands, parameter names, and link targets semantically aligned. Translate reader-facing prose. For stable cross-language deep links, preserve the English heading ID explicitly in the Chinese heading:

MARKDOWN
## 故障恢复 {#failure-recovery}

Configure the minimum site

The essential configuration is small:

hugo.yaml
YAML
title: Product Docs
baseURL: https://docs.example.com/
defaultContentLanguage: en

languages:
  en:
    label: English
    locale: en-US
    weight: 1
  zh:
    label: 简体中文
    locale: zh-CN
    weight: 2

params:
  logo: icons/logo.svg
  offlineSearch: true

module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: 0.160.1

Add menus, outputs, markup extensions, repository links, and optional features as the site grows. See Configuration for the supported model.

Validate before publishing

At minimum:

  1. build from a clean checkout with the committed module files;
  2. run hugo --gc --minify with the pinned Hugo Extended version;
  3. browse representative English and Chinese pages;
  4. verify language switching, search, mobile navigation, dark mode, and print;
  5. inspect browser network requests if the site promises offline operation.

These checks establish a build artifact. Publishing that artifact and verifying the hosted URL are separate deployment steps.

2.3 - Create a new site

Create a minimal bilingual OINK site without a frontend toolchain.

The independent bilingual project site is a useful reference. Use this procedure to create a smaller site with its own content structure.

Create the site skeleton

Run:

BASH
hugo new site --format yaml my-new-site
cd my-new-site

Initialize the site module and pin Oink:

BASH
hugo mod init github.com/example/my-new-site
hugo mod get github.com/pgsty/oink@THEME_REF

Add minimum configuration

Use this as hugo.yaml:

YAML
title: Product Docs
baseURL: https://docs.example.com/
defaultContentLanguage: en

languages:
  en:
    label: English
    locale: en-US
    weight: 1
    menus:
      main:
        - { name: Docs, pageRef: /docs, weight: 10 }
        - { name: Blog, pageRef: /blog, weight: 20 }
  zh:
    label: 简体中文
    locale: zh-CN
    weight: 2
    menus:
      main:
        - { name: 文档, pageRef: /docs, weight: 10 }
        - { name: 博客, pageRef: /blog, weight: 20 }

markup:
  goldmark:
    renderer:
      unsafe: true
  highlight:
    noClasses: false

params:
  offlineSearch: true
  ui:
    showLightDarkModeMenu: true
    sidebar_menu_foldable: true

module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: 0.160.1

Commit go.mod and go.sum. Do not add npm mounts or a PostCSS pipeline.

Add bilingual content

Create these files:

TEXT
content/
├── _index.md
├── _index.zh.md
├── docs/
│   ├── _index.md
│   ├── _index.zh.md
│   ├── getting-started.md
│   └── getting-started.zh.md
└── blog/
    ├── _index.md
    └── _index.zh.md

Every page needs front matter. For example, content/docs/getting-started.md:

MARKDOWN
---
title: Getting started
weight: 10
---

## Install {#install}

Install the product.

Its getting-started.zh.md translation keeps the explicit heading ID:

MARKDOWN
---
title: 开始使用
weight: 10
---

## 安装 {#install}

安装产品。

Using the same explicit ID in both examples is harmless and makes the intended cross-language contract visible. In a translated existing page, copy the ID from the English rendered HTML.

Preview and build

Run the development server:

BASH
hugo server --disableFastRender

Then verify the production build separately:

BASH
hugo --gc --minify

Check /docs/, /zh/docs/, the language selector, local search indexes, and the browser console before adding custom layouts.

Add features incrementally

Copy logo and brand assets first, then add repository links and menus. Add diagrams, API documentation, and content components only on pages that need them; OINK will publish their local runtimes on demand.

If a site needs a business-specific shortcode, keep it under the site’s own layouts/_shortcodes/. Move it into the theme only after its interface is free of site assumptions and multiple sites can reuse it.

What’s next?

2.4 - Basic site configuration

Configure the site, languages, navigation, and local features.

Hugo reads site-wide settings from hugo.yaml, hugo.toml, or hugo.json. The The Oink project site uses YAML because multilingual menus and theme options remain easy to scan and review.

Minimum configuration

The following excerpt shows the important structure for the Hugo Module.

hugo.yaml
YAML
 1title: Product Documentation
 2baseURL: https://docs.example.com/
 3defaultContentLanguage: en
 4
 5languages:
 6  en:
 7    label: English
 8    locale: en-US
 9    weight: 1
10    title: Product Documentation
11    menus:
12      main:
13        - name: Docs
14          pageRef: /docs
15          weight: 10
16        - name: Blog
17          pageRef: /blog
18          weight: 20
19  zh:
20    label: 简体中文
21    locale: zh-CN
22    weight: 2
23    title: 产品文档
24    menus:
25      main:
26        - name: 文档
27          pageRef: /docs
28          weight: 10
29        - name: 博客
30          pageRef: /blog
31          weight: 20
32
33markup:
34  goldmark:
35    renderer:
36      unsafe: true
37  highlight:
38    noClasses: false
39
40params:
41  offlineSearch: true
42  github_repo: https://github.com/example/product-docs
43  github_branch: main
44  copyright:
45    authors: Example Authors
46    from_year: 2026
47  ui:
48    showLightDarkModeMenu: true
49    sidebar_menu_foldable: true
50    breadcrumb_disable: false
51
52module:
53  imports:
54    - path: github.com/pgsty/oink
55  hugoVersion:
56    extended: true
57    min: 0.160.1

English has weight 1 and is the default language; Simplified Chinese has weight 2; additional languages follow. The language selector uses this order when a click cycles to the next language and when the full hover menu is rendered.

Content translations

Put translations beside each other:

Content tree
TEXT
content/
├── _index.md
├── _index.zh.md
├── docs/
│   ├── _index.md
│   ├── _index.zh.md
│   ├── install.md
│   └── install.zh.md
└── blog/
    ├── release.md
    └── release.zh.md

Keep route-affecting metadata aligned. Translate titles, descriptions, menu labels, summaries, tags, image alternatives, and visible shortcode strings. Use the English rendered heading ID as an explicit ID on each translated heading so that fragments remain stable across languages.

Local search and browser resources

offlineSearch: true enables the theme’s same-origin Lunr index and CJK fallback. The index is generated per language. Do not configure a public search service unless the site intentionally accepts that network dependency.

Mermaid, KaTeX, Markmap, Swagger UI, Redoc, Asciinema, ECharts, and Infographic are provided locally and loaded per page. PlantUML and Draw.io are service-based exceptions: configure an approved endpoint explicitly or keep them disabled.

Set title, per-language titles, params.logo, repository URLs, copyright, and menus at the site layer. OINK does not add an oink.* configuration tree; it uses Hugo and compatible Docsy parameter locations.

Repository metadata enables edit, view, issue, and age information on content pages. Keep github_repo, github_project_repo, github_branch, and github_subdir consistent with the source layout.

Production defaults

  • Use a real production baseURL, including any subpath.
  • Keep online analytics, comments, Google CSE, Algolia, and remote embeds off unless they are an explicit product choice.
  • Pin Hugo Extended and the theme release in CI.
  • Run hugo --gc --minify as the production command.
  • Keep LICENSE, NOTICE, and the vendor manifest in redistributed archives.

See the project site’s complete hugo.yaml for a buildable reference.

2.5 - Inspect the bilingual project site

Use the independent Oink project site as a complete reference.

The independent pgsty/oink.pgsty.com repository is the complete bilingual example and regression site. It is intentionally more comprehensive than a minimal consumer: use it as a reference, then keep only the content and configuration your product needs.

Clone the project site

Clone the project site and build its pinned public Oink release directly:

BASH
git clone https://github.com/pgsty/oink.pgsty.com.git product-docs
cd product-docs
hugo --gc --minify

The committed go.mod pins github.com/pgsty/oink. For local theme development, clone the theme as a sibling and use the workspace commands documented in the Oink quick start.

Run the site checks

Building or previewing a site with Oink does not require Node.js or installing npm packages. Node.js and the development dependencies in this example repository are needed only when maintaining the project site and running its formatting, link, translation, and regression checks:

BASH
npm ci
npm test

Open the generated site and check both English and Chinese pages. Use the language switcher from a translated detail page, not only from the home page.

Replace the example identity

Edit hugo.yaml and the files under config/, then replace:

  • site and per-language titles and descriptions;
  • baseURL;
  • repository and branch URLs;
  • copyright holder and starting year;
  • logo and brand assets;
  • English and Chinese menu labels.

Do not create an oink.* parameter namespace. Use Hugo’s language, menu, module, output, and markup settings plus the documented theme parameters.

Replace the example content

Keep each translation pair together:

TEXT
content/docs/getting-started.md
content/docs/getting-started.zh.md

Delete historical and regression content that the product does not need. Remove an example asset only after no page references it.

For translated headings, use the English rendered ID explicitly:

MARKDOWN
## Configure search
MARKDOWN
## 配置搜索 {#configure-search}

Put the new site in version control

Change the module path, repository metadata, and remote before publishing a derived site. Keep the Oink version pinned in go.mod. Do not commit generated public/ output unless the hosting workflow explicitly requires it.

What’s next?

2.6 - Run OINK in a container

Build and preview an OINK site with a Hugo Extended container.

A container is optional: OINK itself only needs Hugo Extended. Use a container when the team wants a pinned tool image or does not install Hugo on developer workstations.

Create the Hugo image

The following Dockerfile installs the currently validated Hugo Extended version from its release package. Keep the version aligned with the theme’s support matrix.

Dockerfile
DOCKERFILE
FROM debian:bookworm-slim

ARG HUGO_VERSION=0.164.0
ARG TARGETARCH

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl git \
    && curl -L -o /tmp/hugo.deb \
      "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.deb" \
    && apt-get install -y /tmp/hugo.deb \
    && rm -rf /var/lib/apt/lists/* /tmp/hugo.deb

WORKDIR /src
EXPOSE 1313
ENTRYPOINT ["hugo"]
CMD ["server", "--bind", "0.0.0.0", "--disableFastRender"]

Build it from the site root:

BASH
docker build -t oink-hugo .

The image build downloads Hugo. For an air-gapped environment, mirror the base image and Hugo package in advance or use OINK’s complete offline distribution with an approved internal image.

Preview the site

Mount the complete site source, including its adjacent or vendored theme:

BASH
docker run --rm -it \
  -p 1313:1313 \
  -v "$PWD:/src" \
  oink-hugo

Open http://localhost:1313/. Changes on the host are visible to Hugo’s live reload process inside the container.

Run a production build

Override the default server command:

BASH
docker run --rm \
  -v "$PWD:/src" \
  oink-hugo --gc --minify

The generated site is written to public/ in the mounted source directory. Ensure the container user can write there; in a shared environment, run with a mapped user ID or fix ownership according to local policy.

No Node.js, npm, PostCSS, or remote browser asset step belongs in this image.

2.7 - Other setup options

Use an OINK archive, Git checkout, or Hugo Module.

The recommended installation uses the github.com/pgsty/oink Hugo Module. The following options change how Hugo obtains the same theme source; they do not change content or the Hugo-only build command.

Prerequisites

Every option requires Hugo Extended 0.160.1 or newer. Git options require Git, and Hugo Modules require Go. None of the options requires Node.js, npm, PostCSS, or Autoprefixer for the consuming site.

Option 1: complete release archive

The complete offline archive contains the theme, local browser runtimes, fonts, licenses, notices, vendor manifest, and checksums. It is the preferred input for air-gapped builds and the simplest way to preserve an exact distribution.

Extract the theme under the site’s themes/ directory:

Theme directory layout
TEXT
site/
├── hugo.yaml
└── themes/
    └── oink/

Configure:

hugo.yaml
YAML
theme: oink

Verify the archive checksum before extracting it. Use only an archive attached to an explicit release, not a locally assembled file presented as a published distribution.

Option 2: Git submodule

A submodule records the exact OINK repository commit in the site repository:

BASH
git submodule add https://github.com/pgsty/oink.git themes/oink
git -C themes/oink fetch --tags
git -C themes/oink checkout THEME_REF
git add .gitmodules themes/oink
git commit -m "Add OINK theme at THEME_REF"

Configure the nested theme path:

hugo.yaml
YAML
theme: oink

CI must initialize submodules before running Hugo. Pin THEME_REF to a release tag or immutable commit; do not leave production on main.

Option 3: pinned Git clone

A clone works when the hosting platform requires the complete theme tree in the build input or when the site vendors a reviewed copy:

BASH
git clone https://github.com/pgsty/oink.git themes/oink
git -C themes/oink checkout THEME_REF

Use the same theme: oink setting. Record the resolved commit and the process that restores the clone. If the files are committed into the site repository, preserve OINK’s LICENSE, NOTICE, and VENDOR.json.

OINK is not distributed as an npm package. Existing Docsy npm consumers should follow the npm migration guide.

Option 4: Hugo Module

Pin the public module to a release tag or immutable commit:

BASH
hugo mod get github.com/pgsty/oink@THEME_REF
hugo mod tidy

Import it in hugo.yaml:

hugo.yaml
YAML
module:
  imports:
    - path: github.com/pgsty/oink

For local theme development, use an ignored Go workspace that includes the site module and a sibling OINK checkout.

Preview and verify

All source options use the same commands:

BASH
hugo server --disableFastRender
hugo --gc --minify

Verify that a clean production build succeeds with no node_modules directory, that local assets resolve under the configured baseURL, and that both English and Chinese pages and search indexes are present.

See Upgrade Oink for version changes and override review.

2.8 - Troubleshooting and known issues

Diagnose Oink installation, build, language, and platform issues.

Start diagnosis from a clean production build:

BASH
hugo --gc --minify --logLevel info

The consumer command should not invoke npm, PostCSS, Autoprefixer, or download theme browser assets.

Build issues

Hugo is not Extended or is too old

Run hugo version. The output must include extended, and the version must be at least 0.160.1. If a shell, editor, CI runner, or container still selects an older binary, inspect its PATH and pinned tool configuration rather than installing another copy blindly.

The theme cannot be found

An error such as module "github.com/pgsty/oink" not found means that Hugo cannot resolve the configured theme. Check the selected installation mode:

  • for a Git checkout, the theme name and directory path must agree;
  • for a Hugo module, run hugo mod graph and inspect go.mod, go.sum, and any configured Hugo workspace or replacement;
  • for a CI checkout, initialize the pinned submodule or restore the complete release archive before running Hugo.

A local browser asset is missing

Do not fix a missing Bootstrap, Font Awesome, Lunr, Mermaid, or other OINK asset by adding a CDN URL. Confirm that the distribution is complete and contains assets/third_party/, assets/js/third_party/, static/webfonts/, and VENDOR.json. Re-extract or re-fetch the same pinned release if files are missing.

A translated page does not appear

Check all four conditions:

  1. languages.zh exists and has a weight in hugo.yaml.
  2. The file is named page.zh.md, including lowercase zh.
  3. The translated front matter does not set draft: true or a future date.
  4. Route-affecting metadata matches the source unless a different route is intentional.

The language selector links to a page translation when Hugo reports one; otherwise it deliberately falls back to the target-language home page.

Translated heading text normally generates a different automatic ID. Add the English rendered ID explicitly to the translated heading:

MARKDOWN
## 安装 {#installation}

Do not infer IDs for headings containing shortcodes or inline HTML. Inspect the English rendered HTML, then compare the English and Chinese heading ID lists.

Search issues

With offlineSearch: true, each language produces its own search index. Check that offline-search-index.en.json and offline-search-index.zh.json exist in the output and that the browser requests them from the site’s base URL. A wrong baseURL is a common cause of missing indexes on subpath deployments.

Chinese tokenization uses the theme’s CJK fallback. If results are empty, first verify that the Chinese page content is present in the Chinese index rather than changing the tokenizer.

Platform issues

macOS reports too many open files

Large live-preview trees can exceed the shell’s open-file limit. Inspect the current limit with ulimit -n and raise it temporarily for the current shell if local policy permits. Prefer excluding generated or unrelated directories from the watched tree before applying a machine-wide limit change.

Windows Subsystem for Linux is slow or misses changes

Run Hugo against a Linux filesystem path rather than a Windows-mounted path. Cross-filesystem notification and permission behavior can make live reload slow or unreliable.

Diagnostic checklist

  • Reproduce with the exact pinned Hugo Extended version.
  • Remove stale public/ and resources/ output through the project’s normal clean command, then rebuild.
  • Compare development and production configuration layers.
  • Check the first build error, not only the final cascading message.
  • Test a minimal page to separate theme behavior from site overrides.
  • Re-enable site overrides and content components in small groups.
  • Inspect the browser console and network log for the failing page.

3 - Authoring

Author, organize, and style content for an Oink documentation site.

Start with site-wide configuration, then shape the content tree, writing conventions, navigation, visual language, media, shortcodes, and reusable components. These pages focus on authoring choices that remain under the consuming site’s control.

3.1 - Configuration

Configure Oink with Hugo settings and focused theme parameters.

OINK follows a “native first” configuration model. Site identity, languages, menus, outputs, taxonomies, markup, and modules stay in their Hugo-defined locations. Existing Docsy parameters remain where their semantics are useful. OINK adds only focused choices for behavior that cannot be inferred.

Configuration rules

  1. Prefer Hugo configuration over a theme-specific duplicate.
  2. Prefer an established Docsy parameter over an OINK synonym.
  3. Put brand, content, repository, and UI choices in their semantic locations.
  4. Keep internal vendor paths and template composition out of the public API.
  5. Fail early for invalid values or a missing required endpoint.

There is no oink.enabled flag and no params.oink.* tree. Adding either would create a second theme mode and make every fix, test, and document ambiguous.

A complete baseline

This example makes English primary and Simplified Chinese secondary:

YAML
title: Product Documentation
baseURL: https://docs.example.com/
defaultContentLanguage: en
enableRobotsTXT: true

languages:
  en:
    label: English
    locale: en-US
    weight: 1
    title: Product Documentation
    menus:
      main:
        - { name: Docs, pageRef: /docs, weight: 10 }
        - { name: Blog, pageRef: /blog, weight: 20 }
  zh:
    label: 简体中文
    locale: zh-CN
    weight: 2
    title: 产品文档
    menus:
      main:
        - { name: 文档, pageRef: /docs, weight: 10 }
        - { name: 博客, pageRef: /blog, weight: 20 }

outputs:
  home: [HTML]
  section: [HTML, RSS, print]

markup:
  goldmark:
    renderer:
      unsafe: true
    extensions:
      passthrough:
        enable: true
        delimiters:
          block: [['\[', '\]'], ['$$', '$$']]
          inline: [['\(', '\)']]
  highlight:
    noClasses: false

params:
  logo: icons/logo.svg
  offlineSearch: true
  offlineSearchIndex: summary
  offlineSearchMaxResults: 10
  github_repo: https://github.com/example/product-docs
  github_branch: main
  footer_icp: ''
  footer_icp_url: https://beian.miit.gov.cn/
  copyright:
    authors: Example Authors
    from_year: 2026
  ui:
    showLightDarkModeMenu: true
    quick_links: [docs, blog]
    sidebar_menu_foldable: true
    sidebar_item_overflow: wrap
    breadcrumb_disable: false

module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: 0.160.1

The module version is pinned in the site’s go.mod. A conventional theme checkout can instead use theme: oink with the repository under themes/oink/.

Languages

defaultContentLanguage determines the unprefixed primary site. Language weight controls the visible order. label is the language’s self-name, and locale supplies the full HTML and SEO locale. Add languageDirection: rtl to an RTL language.

File naming

For the colocated model used by this site:

TEXT
content/docs/guide.md
content/docs/guide.zh.md

Files with the same base name are translations. Keep their logical page identity aligned. OINK reads Hugo’s translation relationships; it does not guess from arbitrary URL patterns.

Selector states

The selector needs no mode parameter. It is hidden for one configured language. With two or more, clicking the language icon advances to the next language by weight; hovering for half a second or focusing it opens the complete menu.

If the current page lacks a target translation, the target-language home page is used. Do not add dead page-shaped URLs merely to keep the selector on the same path.

Brand and repository

Set the site and per-language title and description. params.logo can point to a Hugo Asset or a path under static/. Keep favicons and social images in the documented asset locations.

Repository metadata drives “edit this page,” issue, and last-modified links:

YAML
params:
  github_repo: https://github.com/example/product-docs
  github_project_repo: https://github.com/example/product
  github_branch: main
  github_subdir: site

github_project_repo defaults to github_repo where supported. github_subdir is the content site’s path inside a monorepo. Keep github_branch resolvable; a display version is not necessarily a Git ref.

Use params.wordmark for a horizontal brand asset that should appear in the landing navigation, documentation header, mobile drawer, and footer. It accepts the same asset and static/ paths as params.logo. If wordmark is absent, OINK keeps the existing logo-and-title treatment:

YAML
params:
  logo: images/product-mark.svg
  wordmark: images/product-wordmark.svg

OINK retains Docsy menus and UI parameters and adds focused shell controls:

YAML
params:
  page_width: normal
  ui:
    quick_links: [docs, blog]
    sidebar_width_min: 220
    sidebar_width_max: 480
    sidebar_item_overflow: wrap
    sidebar_menu_compact: true
    sidebar_menu_foldable: true
    sidebar_root_enabled: true
    sidebar_root_menu: true
    sidebar_search_disable: false
    breadcrumb_disable: false
    showLightDarkModeMenu: true
    page_context_menu:
      enable: true
      links: []
    readingtime:
      enable: true

page_width accepts normal, wide, or full and can be overridden in page front matter. Sidebar minimum and maximum values are pixels used to clamp the desktop drag resizer. sidebar_item_overflow: wrap wraps long labels; other values retain the compact ellipsis behavior.

quick_links names top-level page references shown by the shell. Define their translated names in each language’s main menu.

The page context menu keeps Open in ChatGPT / Claude, Copy as Markdown, View Markdown, edit, issue, and print actions reachable at every viewport width. The built-in assistant links appear on documentation pages and send the current URL inside a localized prompt only when a reader activates one; they do not upload the page body. links is empty by default. Additional custom links accept URL-encoded {url}, {title}, and {markdown_url} placeholders:

YAML
params:
  ui:
    page_context_menu:
      enable: true
      links: []
      # - name: Ask an external assistant
      #   icon: fa-solid fa-wand-magic-sparkles
      #   url: https://assistant.example/new?source={markdown_url}&title={title}

Homepage content lives in data/home/<language>.yaml, with English used as the fallback. Each language file contains named data blocks and an optional sections list that composes those blocks into the exact landing-page order. The footer uses the same file but is rendered independently of sections.

Compose sections

A string entry uses the same value as its section type and data key. A map entry can select a built-in type, read a differently named key, set a stable id, or temporarily set enabled: false:

YAML
sections:
  - hero
  - metrics
  - capabilities
  - type: logo_wall
    key: ecosystem
  - gallery
  - faq
  - cta

ecosystem:
  title: Built with familiar tools
  columns: 4
  items:
    - {
        name: Hugo,
        icon: fa-solid fa-bolt,
        url: https://gohugo.io/,
        external: true,
      }

Map entries may also carry their content in data, which is useful for a short one-off block. Reuse a built-in type with different keys when two sections need the same presentation. A site-owned layout can name an explicit partial, but that is a custom template contract rather than portable homepage data.

If sections is absent, OINK preserves the 0.1.x order by rendering the blocks that exist among hero, metrics, capabilities, principles, and cta. Adding sections opts into explicit composition; omitted blocks then stay out of the page even if their data remains in the file.

Built-in sections

OINK 0.2.0 provides 12 section types:

Type Use it for
hero Primary message, actions, and theme-aware artwork
metrics Compact facts, numbers, links, and supporting text
capabilities Alternating feature narratives and specialist visual panels
principles Numbered product or operating principles
cards Generic feature, benefit, service, or path collections
logo_wall Tools, integrations, partners, or project lineage
gallery Screenshots or icon-led examples with badges and actions
testimonials Quotations with optional attribution and source links
contributors People, roles, avatars, and profile links
faq Native disclosure controls with Markdown answers
markdown Free-form prose when no collection layout is appropriate
cta One final action or a compact group of actions

Common collection blocks accept eyebrow, title, desc or text, columns, and items. Item fields vary by presentation but consistently use title or name, desc or text, icon, image, url, and external. Ordinary text fields render Markdown. Keep internal URLs relative to the language root; set external: true for links that should open as external navigation.

Every block is optional, so a site can keep a short landing page without copying the layout. For example:

YAML
hero:
  eyebrow: Local-first documentation
  title_lines:
    - words:
        - { mark: P, text: roduct, color: red }
        - { mark: D, text: ocs, color: blue }
  lead: Documentation built and served with Hugo.
  image:
    light: images/hero-light.webp
    dark: images/hero-dark.webp
    alt: Product documentation workflow
  actions:
    - {
        label: Read the docs,
        url: docs/,
        icon: fa-solid fa-book,
        style: primary,
      }

footer:
  brand:
    name: Product Docs
    tagline: A short **Markdown-enabled** description.
    slogan: Clear answers, close to the product.
  columns:
    - title: Product
      links:
        - { label: Overview, url: docs/ }

The optional hero.image block adds a theme-aware visual on the right. Set light and dark to files under the site’s static/ directory; the active image follows the color-theme selector. If only src, light, or dark is provided, OINK uses that image for both themes. A string value is also accepted as a shared image. Omit image to keep the text-only Hero.

The homepage renders the large brand-and-navigation footer above the common footline. The footline uses params.copyright on the left, optional params.footer_icp and params.footer_icp_url in the center, and every configured language on the right. Markdown in the copyright author and footer brand text is rendered as links and inline markup.

Linked capability boards

A capability row can turn its component board into a compact navigator. Add a url to each linked item, name the region with aria_label, and choose one to four columns. Items without a URL remain decorative, so existing boards keep their current behavior:

YAML
capabilities:
  items:
    - title: Content on demand
      visual:
        type: components
        aria_label: Browse content components
        columns: 3
        compact: true
        items:
          - {
              title: Asciinema,
              icon: fa-solid fa-terminal,
              url: docs/content/components/#asciinema,
            }
          - {
              title: Mermaid,
              icon: fa-solid fa-share-nodes,
              url: docs/content/diagrams-and-formulae/#diagrams-with-mermaid,
            }

The project site enables local search by default:

YAML
params:
  offlineSearch: true
  offlineSearchIndex: summary
  offlineSearchSummaryLength: 70
  offlineSearchMaxResults: 10

offlineSearchIndex controls how much text is downloadable in each language’s index. The scopes are cumulative: title indexes titles and taxonomy metadata; heading adds page headings; summary adds descriptions or summaries; and content also adds the complete body. content is the compatibility default, while summary is a smaller starting point for most documentation sites. offlineSearchMaxResults applies to both Lunr and the CJK substring fallback.

Each language receives a distinct index. Hosted alternatives remain supported through their established Docsy settings, but enabling them intentionally adds an external service boundary. Do not configure several competing search providers without also deciding which UI should be visible.

Content runtimes

Browser-only runtimes

Mermaid and KaTeX are detected from content. Enable Markmap at the site level:

YAML
params:
  markmap:
    enable: true
  mermaid:
    theme: default

Swagger UI, Redoc, Asciinema, ECharts, Infographic, and carousel assets load when their shortcodes appear. Their local runtime paths are internal and should not be configured.

Service endpoints

PlantUML and Diagrams.net require explicit endpoints:

YAML
params:
  plantuml:
    enable: true
    svg: true
    svg_image_url: https://diagrams.internal.example/plantuml/svg/
  drawio:
    enable: true
    drawio_server: https://diagrams.internal.example/

Leave the features disabled in an air-gap site unless those URLs are reachable inside the isolated network.

Page-level overrides

Hugo’s .Param lookup allows many site parameters to be overridden in front matter:

YAML
---
title: Wide reference
page_width: wide
hide_feedback: true
hide_readingtime: true
ui:
  no_left_sidebar: false
  scrollSpy:
    disable: false
---

Use overrides for real content differences, not to reconstruct a separate visual system page by page.

Avoid false configuration

Do not expose:

  • a switch between “Docsy” and “OINK” shells;
  • paths to vendored JavaScript, CSS, fonts, or internal partials;
  • duplicated language or repository values under a brand namespace;
  • toggles that merely select one of two copied implementations.

If a site needs a custom product matrix or portal, keep that component in the site and use a narrow hook or shortcode. A local business feature is clearer than a misleading global theme option.

Validate changes

After changing configuration:

  1. build with the minimum supported Hugo Extended version and the current validation version;
  2. test every configured language and one page without a translation;
  3. verify root and subpath baseURL output if both are supported;
  4. inspect local search and optional runtime requests;
  5. check the desktop and mobile shell, dark and light themes, and print output.

An accepted configuration is one that builds and behaves correctly, not merely one that parses as YAML.

3.2 - Adding content

Structure and author bilingual documentation and blog content.

OINK uses Hugo’s content model: Markdown carries the information, front matter carries page metadata, and layouts turn both into a static site. This guide describes the conventions used by the bundled English and Simplified Chinese sample site.

Content root directory

Site content lives below content/. A multilingual site can use separate roots such as content/en/ and content/zh/, or translated filename suffixes in one mounted tree. This repository uses the second form:

TEXT
content/docs/content/
├── adding-content.md
└── adding-content.zh.md

The English file is the source page and the .zh.md file is its Simplified Chinese translation. Both files share the same logical path after Hugo applies the language suffix.

Keep generated files and files that must be copied byte-for-byte outside the content tree. Put those in static/ as described in Adding static content.

Content sections and templates

Every top-level content directory is a Hugo section. OINK includes layouts for:

  • docs: documentation with a section tree, table of contents, breadcrumbs, previous/next navigation, and repository links;
  • blog: dated articles, taxonomy metadata, feeds, and chronological lists;
  • community: project and contributor links;
  • default pages: landing pages without the documentation sidebar.

Hugo chooses a layout from the content section. A page below content/docs/ therefore uses the docs layout. Set type in front matter only when a page must use another section’s layout.

Custom sections

Create a directory below the content root, then give its pages a type when the default layout is not sufficient:

YAML
---
title: Architecture decisions
description: Accepted design decisions for the project.
type: docs
weight: 30
---

For section-wide behavior, put shared values in the section’s _index.md cascade rather than repeating them on every page. Add a project layout under layouts/ only when no existing OINK layout or partial is suitable.

Doc-rooted sites

EXPERIMENTAL

A documentation-first site can publish the docs section at the URL root while keeping source files under content/.../docs/:

YAML
permalinks:
  page:
    docs: /:sections[1:]/:slug/
  section:
    docs: /:sections[1:]

The docs section landing page then becomes the home page. Add this front matter to the physical site-root index for each language so it can still act as a link without competing for the same output path:

YAML
build: { render: link }

Check for path conflicts

Docs now share the URL root with blog, community, and other sections. Build with --printPathWarnings and resolve every duplicate target before publishing:

BASH
hugo --printPathWarnings

Legacy docs-only setup

Older Docsy examples used a front matter cascade to force page types. Remove that workaround when moving to the permalink-based doc-rooted setup; otherwise the home page and section layouts can resolve inconsistently.

Page front matter

Front matter is page metadata written in YAML, TOML, or JSON. OINK’s sample site uses YAML:

YAML
---
title: Local-first architecture
linkTitle: Local-first
description: How OINK removes browser and build-time CDN dependencies.
weight: 20
date: 2026-08-08
tags: [architecture, offline]
---

title is the practical minimum. In maintained documentation, also provide a concise description for search and metadata, and a weight when order matters. Use linkTitle only when navigation needs a shorter label.

Translations should localize human-facing metadata while preserving structural values:

YAML
---
title: 本地优先架构
linkTitle: 本地优先
description: OINK 如何消除浏览器端与构建期的 CDN 依赖。
weight: 20
date: 2026-08-08
tags: [架构, 离线]
---

Do not translate keys, shortcode names, configuration keys, file paths, or stable identifiers.

Docs and blog pages render a compact metadata block above the site footer. The last-modified date comes from Hugo’s .Lastmod value. Two optional front matter fields add provenance notices:

YAML
lastmod: 2026-08-09
upstream_attribution: https://upstream.example/docs/page/
downstream_modified: true

upstream_attribution links to the upstream source and its attribution. downstream_modified: true states that the downstream project changed the page. Omit either field when its notice does not apply.

Page content

Write pages in Markdown unless a layout genuinely requires HTML. Hugo renders Markdown with Goldmark and supports attributes, footnotes, tables, task lists, render hooks, and fenced code blocks.

Markdown

Keep source readable without the rendered site:

  • use ATX headings (## Heading);
  • put blank lines around lists, blocks, and fenced code;
  • specify the language of every code fence when one exists;
  • use descriptive link text and image alternative text;
  • wrap prose at a review-friendly width, but never reflow code or URLs.

OINK adds render hooks for blockquote alerts and for Mermaid, math, chemistry, Markmap, and PlantUML code blocks. See Diagrams and Formulae.

Markup, shortcodes, and content features

Use standard Markdown for ordinary prose. Use a shortcode when it supplies meaningful behavior such as tabs, cards, a terminal recording, an API viewer, or a safe chart. Shortcodes are part of the content contract: verify their arguments in both languages and avoid copying rendered HTML into translations.

Alerts

OINK supports GitHub-style blockquote alerts and optional Obsidian-style titles:

MARKDOWN
> [!TIP]
>
> Run the translation audit before every release.

> [!WARNING] Stable anchors required
>
> A translated heading must keep the English page's rendered ID.

Supported semantic types include NOTE, TIP, IMPORTANT, WARNING, and CAUTION, plus the Bootstrap-compatible types and NB. Use alerts sparingly: important instructions must still make sense to screen readers and in print. See Alerts for appearance.

Use root-relative links for stable public routes and ordinary relative links for nearby pages or bundle resources. Hugo’s ref and relref shortcodes validate content references and account for language and permalink rules:

MARKDOWN
[Configuration]({{< ref "/docs/about/configuration" >}})

For bilingual pages:

  • link to the logical page, not directly to a .zh.md filename;
  • keep fragment IDs language-neutral;
  • verify that both language variants resolve the same fragment;
  • use relref when the destination must remain relative to the current host.

Run the internal-link check after changing routes or headings.

Content style

Write task-oriented documentation in direct language. Introduce a concept before its configuration, state defaults explicitly, and distinguish local build verification from deployment or publication. The Chinese edition follows the terminology and typography rules in oink.pgsty.com/TRANSLATION.md.

Page bundles

A standalone page is a single Markdown file. A leaf bundle is a directory with an index.md and page resources:

TEXT
content/docs/tutorial/
├── index.md
├── index.zh.md
├── architecture.svg
└── example.yaml

Both language pages can use the same image and downloadable file. Hugo normally shares page resources across language variants on a single host, so do not duplicate identical binary assets. Localize an image only when it contains meaningful text; give the localized resource a clear language suffix.

Use branch bundles (_index.md) for sections that contain child pages and leaf bundles (index.md) for terminal pages with resources.

Adding docs, blog posts, and release notes

Create every maintained English page and its Chinese peer in the same directory:

TEXT
guide.md
guide.zh.md

For bundle pages, pair index.md with index.zh.md. Keep routing metadata, dates, weights, aliases, and resource declarations aligned unless a language-specific difference is intentional.

Organizing your documentation

Use directories to reflect the reader’s information architecture, not the implementation’s package tree. Each documentation subsection needs an _index.md and an _index.zh.md. Child pages appear in the sidebar ordered by weight, then by the configured fallback ordering.

Prefer a shallow hierarchy. Split a page when it serves a distinct task or audience; do not split merely to shorten a file. See Organizing Your Content.

Docs section landing pages

A docs _index.md renders child-page summaries by default. Use:

YAML
simple_list: true

to render a compact list, or:

YAML
no_list: true

to suppress the generated list. Give each language variant a localized title and description, and keep the structural option identical.

Organizing blog posts and release notes

Separate posts by publisher and audience. Keep every upstream Docsy article, including Docsy release reports, flat under blog/docsy/. Keep OINK-specific articles flat under blog/oink/, and reserve blog/release/ for versioned OINK release notes. Do not add year subdirectories; pair each article in place:

TEXT
content/blog/
├── docsy/
│   ├── 0.16.0.md
│   ├── 0.16.0.zh.md
│   ├── hugo-upgrade.md
│   └── hugo-upgrade.zh.md
├── oink/
│   ├── implementation-diary.md
│   └── implementation-diary.zh.md
└── release/
    ├── 0.1.0.md
    └── 0.1.0.zh.md

A Docsy release note normally supplies a publisher-qualified linkTitle:

YAML
---
title: Release 0.16.0 report and upgrade guide
linkTitle: Docsy 0.16.0 release
date: 2026-07-29
tags: [release, upgrade]
---

Prefix link titles for other Docsy articles with Docsy as well, so mixed sidebar and list views make ownership clear.

Keep the publication date and author identity consistent across translations. Translate the title, description, taxonomy labels, caption text, and body. Do not translate commit IDs, release tags, commands, or URLs.

Working with top-level landing pages

Default-layout pages are suitable for the home page, product overview, and other destinations that do not need the docs sidebar.

Customizing the example site pages

The bundled home page is content/_index.md with content/_index.zh.md as its translation. It uses the same local assets and theme pipeline as the rest of OINK. Change content and project assets in the site; do not edit vendored runtime files merely to alter branding.

Building your own landing pages

Compose landing pages from standard Markdown and blocks/* shortcodes. Keep essential information in text, make call-to-action links meaningful, and test the page at mobile and desktop widths in both languages.

Adding a community page

Create community/_index.md and community/_index.zh.md. The community layout uses params.links.user and params.links.developer:

YAML
params:
  links:
    user:
      - name: User forum
        url: https://community.example.org/
        icon: fa-solid fa-comments
        desc: Ask questions and share solutions
    developer:
      - name: GitHub
        url: https://github.com/pgsty/oink
        icon: fa-brands fa-github
        desc: Source, issues, and pull requests

Entries may set rel; OINK also adds noopener to external HTTP links where appropriate. Set params.contributingUrl in the community page front matter if the contribution guide is not at the conventional docs route.

Adding static content

Files below static/ are copied to the published root without Markdown rendering or fingerprinting:

TEXT
static/reference/api/index.html

is published as /reference/api/index.html. Use this for externally generated reference sites, verification files, and downloads that require stable names. Prefer page resources or Hugo Pipes for assets that need resizing, fingerprinting, or bundle-relative lookup.

OINK’s browser runtime is intentionally shipped from the theme or site itself. When adding a library, vendor and pin it, record it in VENDOR.json, and do not introduce an implicit CDN fallback.

RSS feeds

Hugo creates feeds for the home page and list sections. Disable them globally only when the site has no feed consumers:

YAML
disableKinds: [RSS]

If a section declares custom outputs, retain RSS explicitly:

YAML
outputs:
  section: [HTML, RSS, print]

Check the generated language-specific feed URLs and ensure titles, summaries, dates, canonical URLs, and hreflang relationships are correct.

Sitemap

Hugo generates sitemap.xml by default. Site-wide settings are:

YAML
sitemap:
  changefreq: monthly
  filename: sitemap.xml
  priority: 0.5

A page can override these values:

YAML
---
title: Release notes
sitemap:
  priority: 0.8
---

Treat changefreq and priority as hints, not promises. Exclude drafts, private material, and noncanonical duplicates before deployment, then inspect the generated sitemap for every published language.

3.3 - Organize your content

Structure documentation around reader goals and content types.

Oink derives the documentation sidebar from Hugo’s content tree. The directory structure is therefore part of the reader experience, not just a source-code detail. Start with the questions readers need answered, then create the smallest hierarchy that makes those answers easy to find.

Start from reader goals

Give new readers a short path from product context to a successful first task. Give returning readers direct routes to procedures, reference material, and troubleshooting. A practical documentation set usually needs:

  • an overview that establishes scope and product boundaries;
  • a get-started path that produces a working result;
  • task-oriented guides for common jobs;
  • reference pages for parameters, APIs, and compatibility;
  • troubleshooting for predictable failure modes.

Examples are useful when readers can copy or compare them, but they should not replace the procedure or reference that explains the behavior.

Use predictable content types

Keep pages focused on one reader intent:

Content type Reader question
Overview What is this, and when should I use it?
Tutorial How do I reach a first working result?
How-to guide How do I complete one specific task?
Reference What fields, commands, or interfaces exist?
Explanation Why does the system behave this way?
Troubleshooting How do I diagnose and recover from a failure?

Do not create an empty top-level section merely to mirror an organization chart. Add a section when several pages share a stable reader purpose.

Keep the hierarchy shallow

Prefer a short, explicit route over a deep classification tree. Use page weights to establish a learning sequence, and keep related page weights spaced consistently so new pages can be inserted without renumbering the entire section. Add an icon and concise description to every navigable page so the sidebar and section indexes remain scannable.

See Adding content for Hugo’s bundle and section model, and Navigation and menus for sidebar behavior.

Plan languages together

Create the English source and Simplified Chinese peer in the same directory. Keep page order, intent, examples, and stable heading IDs aligned. If the two languages need different prose lengths, preserve the same information rather than forcing sentence-for-sentence symmetry.

Review the complete route

After moving or adding pages, review the documentation landing page, section index, sidebar, breadcrumbs, previous/next navigation, local search, and every homepage link. Build both languages and validate rendered fragment links before publishing.

3.4 - Hugo authoring tips

Avoid common pitfalls when writing content for an Oink site.

Oink is a Hugo theme, so ordinary Markdown and Hugo’s content model remain the authoring foundation. These conventions keep pages readable in source form and stable after translation, reorganization, or subpath deployment.

Link readers to the canonical published URL, not to a neighboring source-file path. Root-relative links such as /docs/content/ are easy to audit across the site. When a link should follow a page through source moves, Hugo’s ref and relref shortcodes can resolve the target page:

MARKDOWN
[Configuration]({{</* ref "/docs/content/configuration" */>}})

After moving a page, add an alias for the old public route and update every internal link to the new canonical route. Do not rely on the alias as the site’s permanent navigation path. See Adding content for link and image behavior.

Keep front matter useful

Every navigable page needs a clear title, concise description, intentional weight, and suitable Font Awesome icon. Keep descriptions to one sentence that fits on one line in a normal desktop content card. Add linkTitle only when the navigation label genuinely needs to differ from the page title.

English is the primary source language. Add the Simplified Chinese peer beside it as .zh.md, and translate reader-facing metadata as carefully as the body.

Preserve stable headings

Use explicit heading IDs when pages are translated or widely linked:

MARKDOWN
## Failure recovery {#failure-recovery}

Copy the same ID to the corresponding Chinese heading. When renaming a heading, preserve an established ID unless its meaning also changes.

Write procedures as tasks

State prerequisites before commands, use imperative steps, and show the expected result or verification command. Separate local preview, production build, hosted deployment, and public release evidence; success at one layer does not establish the next.

Make code examples actionable

Name a block when it represents a real file, use console for a transcript with prompts and output, and collapse long reference listings that readers do not need to scan before continuing. Use a Code Group only when panels are interchangeable ways to complete the same task.

hugo.yaml
YAML
params:
  offlineSearch: true
  print:
    disable_toc: false

Metadata should clarify an example, not decorate every fence. See Code blocks and Code Groups for filenames, Copy policies, wrapping, collapse, line links, and synchronized alternatives.

Review rendered states

Build both languages and inspect representative pages on desktop and mobile, in light and dark modes. Verify headings, fragments, code, tables, alerts, navigation, search, print output, and page descriptions in the rendered site.

3.5 - Navigation and menus

Configure navigation, language switching, sidebars, and outlines.

OINK combines Hugo’s content tree and menu model with a documentation workspace: a global navbar, a collapsible and resizable section sidebar, and a collapsible page outline. The same structure works for English, Chinese, and right-to-left languages.

The global navbar is built from Hugo’s main menu plus OINK-generated controls. Depending on configuration and page type, it can include version, language, color-mode, and search controls.

Adding main menu entries

Define a menu entry in page front matter:

YAML
---
title: Documentation
linkTitle: Docs
menu:
  main:
    weight: 20
    pre: <i class="fa-solid fa-book" aria-hidden="true"></i>
---

Lower weights appear first. A site-level external link is similar:

YAML
menus:
  main:
    - name: GitHub
      identifier: github
      weight: 50
      url: https://github.com/pgsty/oink
      pre: <i class="fa-brands fa-github" aria-hidden="true"></i>

Use an identifier for configuration that refers to a menu item. Localize name or linkTitle in language configuration, but keep identifiers stable.

Version menu

The selector appears when params.versions is configured. Each entry can be a heading, separator, release, development build, or site variant:

YAML
params:
  version: v1.0.0
  version_menu: v1.0.0
  version_menu_pagelinks: true
  versions:
    - version: v1.1.0-dev
      kind: next
      url: https://next.example.org/
    - version: v1.0.0
      kind: latest
      url: https://docs.example.org/

version identifies the published site variant and is not necessarily a Git ref. Commands that require a resolvable tag should use the project’s explicit release-ref parameter instead. With page links enabled, OINK first tries the equivalent path on the target version and otherwise uses its configured URL.

Language menu

OINK builds language targets from Hugo’s AllTranslations. When a translated peer is missing, the target language’s home page is used instead of a broken URL. One configured language hides the control. With two or more languages, a click advances to the next language by weight, while hovering for half a second or focusing the control opens the complete menu. The current site cycles from English to Simplified Chinese and back. Targets include lang, hreflang, locale, and text-direction attributes.

Light/dark theme menu

When color-mode support is enabled, the navbar and documentation workspace show a theme control. See Light/dark-mode menu.

The documentation workspace uses a local search dialog when offline search is enabled. The sidebar button advertises the platform shortcut (Command/Ctrl+K). Online search integrations remain available by explicit configuration. See Search.

Adding icons to the navbar

Use pre or post on a menu entry. OINK includes the free local Font Awesome assets:

YAML
menus:
  main:
    - name: Source
      identifier: source
      url: https://github.com/pgsty/oink
      weight: 50
      pre: <i class="fa-brands fa-github" aria-hidden="true"></i>
      post: <span class="visually-hidden"> (external)</span>

Decorative icons need aria-hidden="true"; the link itself must retain a useful text or accessible label. External links that open a new tab must use rel="noopener".

Side navigation

The left panel on docs and blog pages is generated from the content hierarchy. OINK orders entries by weight and uses linkTitle when present. Sections come from _index.md files; translated sections need a peer _index.zh.md so their navigation metadata is localized.

Hide a page from the sidebar with:

YAML
toc_hide: true

Hide it from a section landing-page summary with hide_summary: true. Set both only when the page should be absent from both discovery surfaces.

Side-nav options

The common controls are:

YAML
params:
  ui:
    sidebar_menu_compact: true
    sidebar_menu_foldable: true
    sidebar_menu_truncate: 128
    sidebar_cache_limit: 2000
    sidebar_search_disable: false
    sidebar_width_min: 220
    sidebar_width_max: 480
    sidebar_item_overflow: ellipsis
  • sidebar_menu_compact shows the active branch and nearby entries.
  • sidebar_menu_foldable lets readers expand or collapse sections.
  • sidebar_menu_truncate limits entries and emits a build warning when the limit is too small.
  • sidebar_cache_limit enables shared navigation markup above the configured site size.
  • sidebar_width_min and sidebar_width_max clamp the desktop drag-resizer.
  • sidebar_item_overflow is ellipsis by default; use wrap for long labels.

The reader’s collapse state, width, and scroll position are preserved locally. The mobile view becomes a dismissible drawer with a backdrop and focus-safe controls.

Adding icons to the side nav

Set icon in page front matter:

YAML
---
title: Operations
icon: fa-solid fa-screwdriver-wrench
---

Use icons consistently across siblings. They are secondary cues, not a replacement for text labels.

Create a placeholder page at the desired position:

YAML
---
title: API status
weight: 90
manualLink: https://status.example.org/
manualLinkTitle: Live service status
manualLinkTarget: _blank
---

Use manualLinkRelref instead of manualLink for an internal content reference; Hugo then fails the build if it cannot resolve the destination. OINK adds noopener for new-tab links. Include a short body explaining the destination because Hugo still generates a page for the placeholder.

Enable rooted sidebars:

YAML
params:
  ui:
    sidebar_root_enabled: true
    sidebar_root_menu: true

Then set a section’s _index.md:

YAML
---
title: API Reference v2
sidebar_root_for: self
sidebar_root_link_self: true
---

self applies the root to the section index and descendants; children keeps the index in the parent tree but roots its descendants. The optional root menu lets readers switch between roots. Rooted sections can nest, but redundant or invalid values produce build warnings.

Table of contents (TOC)

Hugo builds the right-side page outline from Markdown headings. OINK renders it as a fixed documentation panel with quick links, language and theme controls, repository metadata, and taxonomy terms. Readers can collapse the panel; its state is stored locally.

Headings emitted by Markdown shortcodes ({{%/* ... */%}}) participate in Hugo’s table of contents. Headings emitted only by standard shortcodes ({{</* ... */>}}) generally do not, so content structure should remain in Markdown whenever possible.

TOC customization

Hide the outline on one page:

YAML
notoc: true

Configure which heading levels Hugo includes:

YAML
markup:
  tableOfContents:
    startLevel: 2
    endLevel: 4
    ordered: false

Localize labels such as toc_on_this_page in the site’s i18n bundle. If custom CSS changes the outline rail or fixed-panel dimensions, test active tracking, zoom, keyboard focus, and pages with no headings.

Active TOC entry tracking with ScrollSpy

OINK uses a local Bootstrap ScrollSpy patch and IntersectionObserver to track the active heading. The workspace draws a continuous rail, active segment, and position marker. Disable tracking for a page with:

YAML
params:
  ui:
    scrollSpy:
      disable: true

The legacy ScrollSpy configuration also accepts a global rootMargin. Changing it affects when an entry becomes active and should be tested with short sections, long sections, and direct fragment navigation.

Advanced ScrollSpy customization

Prefer configuration and project CSS. Overriding the ScrollSpy attribute partial or docs-shell.js creates an implementation-level fork; add browser fixtures for hash updates, back/forward navigation, resizing, reduced motion, and pages that contain duplicate or missing IDs.

Breadcrumbs are shown above ordinary content pages and in taxonomy results. Disable them globally:

YAML
params:
  ui:
    breadcrumb_disable: true
    taxonomy_breadcrumb_disable: true

The same ui.breadcrumb_disable value can be set in a page or section cascade. Breadcrumb labels come from localized page titles and must follow the same logical hierarchy as the sidebar.

Enable OINK’s heading render hook in a consuming site:

GO-HTML-TEMPLATE
{{ partial "td/render-heading.html" . }}

The generated .td-heading-self-link control uses # by default. It remains visible on touch devices and appears on hover or focus for pointer devices. Keep the link keyboard reachable and preserve a scroll offset that clears fixed navigation.

Heading aliases and in-page targets

Changing a heading can break inbound fragment links. Treat its ID as a public route. To rename an ID, retain the old one as an empty anchor and set the new one explicitly:

HTML
## Quickstart <a id="get-started"></a> {#quickstart}

Use an empty <a id="..."></a> for an alias or other in-page target. Do not use a span solely as a fragment target. IDs must be unique, stable, ASCII where practical, and identical across language variants.

Quickstart

This live heading demonstrates that both #get-started and #quickstart reach the same location. Translated headings should write the English rendered ID explicitly rather than relying on language-specific automatic slug generation.

Implementation notes

  • The document sets a global scroll offset for fixed chrome.
  • Built-in block targets use td-anchor-no-extra-offset to avoid applying the additional offset twice.
  • The translation audit compares rendered heading IDs between English and Chinese pages.
  • Removing an old alias is a breaking documentation change and needs a redirect or an explicitly documented compatibility decision.

3.6 - Look and feel

Customize themes, typography, code styles, and page layouts.

OINK ships a complete visual system built on Bootstrap and Docsy, with local fonts, icons, styles, and browser code. A consuming site can change tokens and project styles without rebuilding a Node dependency tree.

Project styles

Hugo Extended compiles the theme’s SCSS through Hugo Pipes. Project overrides participate in the same bundle, so production builds can minify, fingerprint, and integrity-check one same-origin stylesheet.

Project style files

Override these files in the site’s assets/scss/ directory:

File Purpose
_variables_project.scss Variables set before Bootstrap and OINK defaults
_variables_project_after_bs.scss Variables or maps that require Bootstrap definitions
_styles_project.scss Project selectors loaded after the theme’s component styles

Start with the smallest override:

SCSS
// assets/scss/_variables_project.scss
$primary: #315f8f;
$secondary: #b4762e;
SCSS
// assets/scss/_styles_project.scss
body.td-blog {
  --td-body-font-family: 'Noto Serif', 'Noto Serif SC', serif;
}

Do not edit vendored Bootstrap, Font Awesome, or local font files for ordinary branding. A theme update would overwrite those changes and obscure the dependency boundary.

Advanced style customization

For the stable customization layers, typography presets, semantic font roles, and content-scoped patterns, read Advanced customization.

OINK’s SCSS import order is:

  1. Bootstrap functions;
  2. project variables;
  3. OINK defaults and Bootstrap;
  4. post-Bootstrap project variables;
  5. OINK components and local brand layer;
  6. project styles.

Use variables or CSS custom properties for stable design decisions. Override a selector only when no token exists, and scope it to the smallest component. Inspect both light and dark output because many colors are theme-dependent.

⚠️ Resetting internal styles

OINK’s internal partials are not a public Sass API. Importing or suppressing individual internal files couples a site to repository layout and import order. If a product needs a fundamentally different shell, override a Hugo layout or maintain a deliberate theme fork instead of resetting the entire stylesheet.

Extra styles

For isolated third-party CSS, publish a local asset from a hook:

GO-HTML-TEMPLATE
{{ $extra := resources.Get "css/extra.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $extra.RelPermalink }}"
  integrity="{{ $extra.Data.Integrity }}" crossorigin="anonymous">

Put the template in layouts/partials/hooks/head-end.html. Prefer the project SCSS files when the rules belong to the site’s design system. Never use a remote stylesheet as an implicit fallback.

Colors and color themes

Bootstrap semantic colors and OINK brand tokens are available throughout the theme. Semantic names communicate intent better than literal colors.

Site colors

Set Bootstrap variables before compilation:

SCSS
$primary: #315f8f;
$secondary: #b4762e;
$success: #2c7a4b;
$warning: #9a6700;
$danger: #b42318;

OINK’s canonical layer also exposes CSS properties such as --td-brand-elev, --td-brand-silk, --td-brand-copper, --td-brand-header-bg, and --td-brand-mark-gradient. Override them on :root and [data-bs-theme='dark'] as a pair:

SCSS
:root {
  --td-brand-copper: #a66722;
}

[data-bs-theme='dark'] {
  --td-brand-copper: #e0a35c;
}

Light/dark color theme and mode support

Color theme is the palette used by a component; color mode is the site-wide light or dark state. OINK uses Bootstrap’s data-bs-theme="light|dark" attribute and stores an explicit reader choice in local browser storage. With no choice, it follows prefers-color-scheme.

Every custom component must define legible states for both modes, including hover, focus, disabled, selected, and code colors. Do not encode meaning by color alone.

Light/dark color modes

The default sample site enables color-mode support and shows the selector:

YAML
params:
  ui:
    showLightDarkModeMenu: true

The selector updates the document before normal interaction to limit a flash of the wrong theme. OINK’s script is local and does not contact an external service.

Choosing themes or color modes for your site

Use the default automatic behavior for most sites. Choose a forced mode only when the complete visual identity has been tested in that mode and readers do not need an alternative. Screenshots are not sufficient: check real text, tables, alerts, forms, diagrams, code, and focus indicators.

How to disable dark mode

To disable dark mode and hide the menu:

YAML
params:
  ui:
    showLightDarkModeMenu: false

The experimental value enable-only (experimental) enables theme-aware styles without showing a selector. Treat it as transitional because the configuration surface can change.

How to pick colors with good color-contrast

Meet WCAG contrast requirements in every component state. Test actual computed colors, including translucent layers over images. As a working minimum, normal text needs 4.5:1 contrast and large text needs 3:1; focus and non-text UI indicators also need adequate contrast. Automated tools catch common failures, but keyboard and visual review remain necessary.

Fonts

OINK does not fetch Google Fonts. Open Sans, Chakra Petch, IBM Plex Mono, and Font Awesome files used by the theme are stored locally. The legacy Sass variable $td-enable-google-fonts controls the bundled Open Sans faces despite its historical name.

Set typography in _variables_project.scss:

SCSS
$td-enable-google-fonts: true;
$font-family-sans-serif: 'Noto Sans SC', 'Open Sans', system-ui, sans-serif;
$font-family-monospace: 'IBM Plex Mono', ui-monospace, monospace;

OINK also exposes build-time typography presets and runtime-independent semantic font roles. See Advanced customization for the complete public interface and examples that scope a font to blog, OpenAPI, or code-heavy pages.

If you add a font, subset and self-host it, include the required scripts, use font-display: swap, document its license in VENDOR.json, and test CJK fallback. Do not make page rendering depend on a font CDN.

CSS utilities

Bootstrap utility classes are available in Markdown with raw HTML and in layouts. Prefer semantic Markdown and OINK shortcodes for content; use utilities for small, presentational adjustments that remain understandable at different breakpoints. Project-wide patterns belong in _styles_project.scss.

Code blocks

OINK supports Hugo Chroma by default and a locally vendored Prism option. Choose one highlighter consistently; enabling both produces duplicate markup or styles. For filenames, Copy policies, wrapping, collapse, line anchors, and shareable Code Groups, see Code blocks and Code Groups.

Code highlighting with Chroma

Chroma runs during the Hugo build and requires no browser highlighter. Use a language identifier:

MARKDOWN
```go
fmt.Println("hello")
```

Basic Chroma style configuration

Configure markup in Hugo:

YAML
markup:
  highlight:
    guessSyntax: false
    noClasses: false
    lineNos: false

OINK expects class-based output so light and dark styles can differ. When regenerating a palette, keep the generated CSS local and review it against the brand background.

Light/dark code styles and more

The theme includes separate Chroma palettes under assets/scss/td/chroma/ and applies them by mode. Project overrides should target .chroma beneath the relevant theme attribute, not hard-code a global background.

Selecting console block content

Use console for terminal transcripts. OINK styles prompts and output for selection so readers can copy commands without decorative prompt text. Keep commands and their output on distinct lines, and never rely on color alone to distinguish them.

Code blocks without a specified language

An unlabelled fence renders as plain code. Use it only when no grammar applies, and label command sessions as console or bash instead of asking Chroma to guess.

Copy to clipboard

Copy buttons are enabled for Chroma unless params.disable_click2copy_chroma is true. Clipboard access requires a secure context in deployed browsers. The control must remain keyboard accessible and must not copy line numbers or prompts.

Code highlighting with Prism

Set:

YAML
params:
  prism_syntax_highlighting: true

to use OINK’s local prism.js and prism.css. This is a compatibility option for existing sites; Chroma is preferred for a browser-light build.

Code blocks with no language

Prism also treats unlabelled blocks as plain text. Add the correct language class rather than enabling heuristic detection.

Extending Prism for additional languages or plugins

Build and vendor the exact Prism bundle, replace the local files in a controlled theme change, record its version and license, and add a fixture that exercises the language or plugin. Do not pull Prism components from a CDN at runtime.

OINK’s navbar contains the project identity, main menu, version and language selectors when applicable, color-mode control, and search. On small screens, overflowing primary items remain horizontally reachable.

Default look and feel

The navbar uses the local brand palette and a fixed minimum height.

On mobile

The brand and actions stay visible while the primary menu can scroll. Test long Chinese labels, 200% zoom, touch targets, focus order, and both page directions.

On desktop

The main menu expands inline; version, language, mode, and search controls stay grouped. Avoid enough custom entries to push controls outside the viewport.

Translucent over cover images

The blocks/cover shortcode marks the navbar as cover-aware. It starts translucent and gains the normal background as the page scrolls.

Use configuration for behavior and project SCSS for presentation. Preserve the landmark, focus order, accessible labels, and responsive overflow behavior when overriding the navbar partial.

Override $td-navbar-min-height before theme styles compile. Re-test anchor offsets, sidebar height, mobile wrapping, and cover blocks because all depend on this value.

Set --td-navbar-bg-color or --td-brand-header-bg in both modes. If the background is translucent, validate contrast over every cover image and provide a solid scrolled state.

A page can set ui.navbar_theme: dark in front matter or cascade when its cover requires light foreground controls. This changes navbar component styling; it does not force the whole site’s color mode.

Translucent over cover images

Disable translucency site-wide with:

YAML
params:
  ui:
    navbar_translucent_over_cover_disable: true

Prefer this when cover imagery is unpredictable or accessibility review cannot guarantee contrast.

Styling your project logo and name

Place logo partial overrides under layouts/partials/ and source assets under assets/ or static/. Provide meaningful alternative text for informative marks and an empty alternative for a purely decorative mark. SVGs must use a view box and inherit or define colors for both modes.

The OINK sample uses a text wordmark with a local gradient. Change the site title in language configuration and the visual tokens in project SCSS; do not replace brand text with an image when selectable text works.

Light/dark-mode menu

The selector appears when params.ui.showLightDarkModeMenu is true. Keep it in the shared navigation so its state applies consistently across languages and page types.

Alerts

Markdown alert types map to semantic OINK/Bootstrap styles. Customize .alert-* and the alert render hook only as a pair, retain a visible label or icon, and test links and inline code inside every background. See Adding Content for syntax.

Tables

Markdown tables receive responsive and theme-aware styles. Keep cells concise, use real header cells, add a caption in custom HTML when context requires one, and test horizontal overflow on mobile. A table should not be used to position unrelated content.

Customizing templates

Hugo resolves site layouts before theme layouts. Copy only the smallest partial that needs changing and compare it during upstream syncs; a full baseof.html override can silently miss future accessibility and asset-pipeline fixes.

Add code to head or before body end

Use layouts/partials/hooks/head-end.html for head additions and layouts/partials/hooks/body-end.html for scripts or closing integrations. Self-host assets, load them only on pages that need them, and keep production CSP compatible.

Adding a banner before page content

Override the relevant hook or content partial with a condition based on page parameters. A banner must not hide the page heading, trap keyboard focus, or shift anchor targets beneath the fixed navigation.

Adding custom class to the body element

Set body_class in page front matter or a section cascade:

YAML
---
body_class: product-reference
---

OINK appends the value to its generated body classes. Use a project-specific, semantic class name and never insert untrusted content into this field.

3.7 - Code blocks and Code Groups

Add filenames, exact Copy behavior, wrapping, collapse, and shareable groups to Hugo code examples.

OINK enhances Hugo’s ordinary fenced code blocks without replacing Chroma or requiring a browser highlighter. The server emits the complete code and shell; small page-scoped scripts only enable Copy, visual collapse, and tab state.

Enhanced fences

Add metadata in Hugo’s fence attribute list. A fence without attributes still receives the same responsive shell and its normal Copy default. filename adds a visible header; title is its compatible alias, and setting both is a build error. With neither, OINK uses a compact overlay instead of an empty title row.

Authoring

content/docs/example.md
MARKDOWN
```yaml {filename="hugo.yml" copy="all" lineNos="table" hl_lines="4 7-9" wrap=false collapse=18}
params:
  offlineSearch: true
```

Live result

Rendered result

This block combines a filename, inline line numbers, a stable root ID, line links, and highlighted source lines. Line numbers begin at 12, while hl_lines still addresses the source lines inside the fence:

hugo.yaml
YAML
12markup:
13  highlight:
14    noClasses: false
15params:
16  offlineSearch: true
17  ui:
18    sidebar_menu_foldable: true

Shell parameters

Attribute Values Behavior
filename string Visible filename and accessible group name
title string Alias for filename on an ordinary fence
copy all, command, false, or true Copy policy; true is shorthand for all
wrap true or false Visually wrap long lines without changing text
collapse positive integer Initial maximum number of visible source lines
label string Accessible label when no filename is suitable
id string Stable public block ID and line-anchor prefix

Hugo generic class, safe data-*, aria-*, and global attributes remain on the .td-code root. Names beginning with data-td-code and data-language are reserved. OINK rejects event-handler and inline-style attributes. Use label to override a filename-derived accessible name; a generic aria-label together with label or filename is a build error.

Hugo options

The render hook continues to pass these options to Hugo:

  • lineNos, lineNoStart, and anchorLineNos;
  • hl_lines;
  • tabWidth and style.

Class-based Chroma markup remains inside .highlight and .chroma, so existing token-level overrides keep working. The new stable outer element is .td-code; sites using direct-child selectors such as .td-content > .highlight should update those selectors.

The visible language label normalizes the common bash, sh, and shell lexer aliases to BASH. The original lexer value is still passed to Chroma and retained in data-language.

Diffs deliberately use Chroma’s standard diff lexer rather than a custom transformer:

Authoring

content/docs/configuration.md
MARKDOWN
```diff {filename="hugo.yaml.diff"}
 params:
-  offlineSearch: false
+  offlineSearch: true
```

Rendered result

hugo.yaml.diff
DIFF
 params:
-  offlineSearch: false
+  offlineSearch: true

Copy semantics

Ordinary source defaults to copy="all". console and shell-session default to copy="command": only lines carrying Chroma prompt tokens are copied, and prompt/output tokens are excluded. Use copy="all" when a complete transcript is intentional. command on another language is a build error.

Copy preserves indentation, internal blank lines, and Unicode, removes line numbers, trims only trailing newline characters, and appends exactly one final newline. A session lexer that emits no prompt token reports a localized failure and copies nothing. Set params.disable_click2copy_chroma: true to hard-disable Copy for the entire site.

Copy is shown as a compact icon without adjacent text. Its localized label is still exposed to assistive technology and as a hover tooltip; success and failure also change the icon and update the live status message.

For a multi-line terminal command, include the continuation prompt (normally >) on every continued transcript line. Chroma classifies an unprompted line as output, so copy="command" deliberately excludes it.

The Copy action on this live session copies the two commands, not the prompts or output:

Authoring

content/docs/terminal.md
MARKDOWN
```console {title="Terminal session"}
$ hugo version
hugo v0.164.0+extended darwin/arm64
$ hugo --gc --minify
Total in 742 ms
```

Rendered result

Terminal session
CONSOLE
$ hugo version
hugo v0.164.0+extended darwin/arm64
$ hugo --gc --minify
Total in 742 ms

Wrapping and collapse

wrap=true changes presentation only; copied source is untouched. It is incompatible with Chroma’s table line-number layout because separately wrapped gutter and source cells would drift. Use inline line numbers or disable wrap. OINK fails the build instead of silently misaligning them.

collapse=N is progressive enhancement. The server always emits all source; the browser clips only after it can measure the Nth real Chroma line. Without JavaScript, in assistive technology, and in print, the listing remains complete. Reduced-motion preferences disable the height animation.

The first example wraps a long value without altering copied text:

Authoring

content/docs/downloads.md
MARKDOWN
```text {filename="config/artifacts.env" wrap=true}
ARTIFACT_URL=https://downloads.example.com/releases/2026/08/oink-complete-offline-distribution-arm64.tar.zst
CHECKSUM=sha256:6d3dce4f7acb18f586469adcb80ab35f3e859f9837786e151cfbc2b3c0f587b2
```

Rendered result

config/artifacts.env
TEXT
ARTIFACT_URL=https://downloads.example.com/releases/2026/08/oink-complete-offline-distribution-arm64.tar.zst
CHECKSUM=sha256:6d3dce4f7acb18f586469adcb80ab35f3e859f9837786e151cfbc2b3c0f587b2

The second emits all lines on the server but initially shows six in a browser:

Authoring

content/docs/configuration.md
MARKDOWN
```yaml {filename="hugo.yaml" collapse=6}
baseURL: https://docs.example.com/
title: Product Documentation
defaultContentLanguage: en
languages:
  en:
    label: English
    weight: 1
  zh:
    label: 简体中文
    weight: 2
params:
  offlineSearch: true
```

Rendered result

hugo.yaml
YAML
baseURL: https://docs.example.com/
title: Product Documentation
defaultContentLanguage: en
languages:
  en:
    label: English
    weight: 1
  zh:
    label: 简体中文
    weight: 2
params:
  offlineSearch: true

Set a page-unique explicit id when publishing line-number links. IDs cannot contain ASCII whitespace or control characters and cannot collide with another code component’s generated viewport, tab, panel, title, or line-anchor ID. OINK reports any such collision as a build error:

Authoring

content/docs/server.md
MARKDOWN
```go {id="server-start" lineNos="inline" anchorLineNos=true}
func start() {}
```

Rendered result

GO
1func start() {}

OINK derives unique line-anchor prefixes from that ID. Generated IDs are safe inside a page but depend on the block ordinal and are not a permalink contract; inserting an earlier fence can change them.

Code Groups

Use code-group when examples are alternatives rather than independent tabs:

Authoring

content/docs/install.md
GO-HTML-TEMPLATE
{{< code-group id="docs-install-client" sync="docs-package-manager" persist=false
    label="Choose a package manager" copy="all" >}}
  {{< code-tab title="npm" value="npm" lang="bash" >}}
npm install @example/client
  {{< /code-tab >}}
  {{< code-tab title="pnpm" value="pnpm" lang="bash" selected=true >}}
pnpm add @example/client
  {{< /code-tab >}}
  {{< code-tab title="yarn" value="yarn" lang="bash" >}}
yarn add @example/client
  {{< /code-tab >}}
{{< /code-group >}}

Rendered result

npm BASH
npm install @example/client
pnpm BASH
pnpm add @example/client
yarn BASH
yarn add @example/client

code-tab contains raw code, not Markdown. OINK removes the framing newline and closing-shortcode indentation while preserving all source whitespace inside. Because a Markdown formatter can otherwise reflow that raw body, put <!-- prettier-ignore --> immediately before each live code-group when using Prettier, as in the examples below.

Group and tab parameters

Every group requires a page-unique lower-case id. Optional sync, persist, label, copy, wrap, and collapse values apply to the group; the last three are inherited defaults. persist defaults to true.

Every child requires a plain-text title and stable lower-case value. lang defaults to text; selected, copy, wrap, collapse, and the Hugo highlight options can override group defaults. A group cannot be empty, repeat a value, or contain more than one selected=true child. Filenames are omitted inside groups because the tab itself identifies the example.

Selection, sync, and persistence

A selected panel has the public hash #<group-id>-<value>, for example #install-client-pnpm. Initial selection priority is URL hash, saved value, selected=true, then the first child.

Groups sharing sync select the same value when that value exists in each group; a peer missing it stays unchanged. A user selection updates the hash with replaceState and saves the value when persistence is enabled. Visiting a shared hash activates the requested examples without overwriting the reader’s saved preference. persist=false disables storage, not in-page synchronization.

Live synchronized groups

The rendered install group above and the run group below share the same sync key. Choose a package manager in either group and the other follows. The first group’s npm, pnpm, and yarn panels also have shareable hashes.

Authoring

content/docs/run.md
GO-HTML-TEMPLATE
{{< code-group id="docs-run-client" sync="docs-package-manager" persist=false >}}
  {{< code-tab title="npm" value="npm" lang="bash" >}}
npm run docs:dev
  {{< /code-tab >}}
  {{< code-tab title="pnpm" value="pnpm" lang="bash" selected=true >}}
pnpm docs:dev
  {{< /code-tab >}}
  {{< code-tab title="yarn" value="yarn" lang="bash" >}}
yarn docs:dev
  {{< /code-tab >}}
{{< /code-group >}}

Rendered result

npm BASH
npm run docs:dev
pnpm BASH
pnpm docs:dev
yarn BASH
yarn docs:dev

Output and compatibility

Print hides controls and tab rows, expands every listing, and places each group title before its code. Markdown output turns every grouped or legacy tab into a readable titled fence and chooses a longer delimiter when source contains backticks. Feeds and other non-interactive outputs use stacked examples. Pages without applicable code or tabs do not load their runtimes.

Existing tabpane source and its td-tp-persist:* browser keys remain compatible. Prism remains a legacy alternative and does not receive Enhanced Code Blocks or Code Groups. Specialized mermaid, math, chem, markmap, and plantuml hooks continue using their own renderers.

3.8 - Logos and images

Configure logos, page icons, favicons, and images.

Oink uses assets/icons/logo.svg as its default brand mark. Override it with params.logo, or set params.wordmark when the header should show a complete wordmark instead of an icon followed by the site title:

YAML
params:
  logo: icons/product.svg
  wordmark: images/product-wordmark.svg

Both parameters can name a Hugo asset or a public path. Prefer assets/ for theme-processed files and static/ when a file must be copied unchanged. Keep the source SVG tightly cropped so the header, sidebar, and footer can size it consistently.

For brand typography, dimensions, and project SCSS, see Look and feel.

Use icons

Oink bundles Font Awesome Free and serves its fonts locally. Set a page’s icon in front matter to give the page and its navigation entry a stable visual cue:

YAML
---
title: Deployment
icon: fa-solid fa-cloud-arrow-up
---

Use an icon available in the bundled free set. The exact vendored version is recorded in the theme’s VENDOR.json. Menu-specific icon behavior is covered in Navigation and menus.

Add favicons

Oink does not impose a product favicon. Instead, it discovers conventionally named files in the consumer site’s static/ directory and adds the matching <link> elements to every page.

File Generated link
favicon.ico rel="icon"
favicon.svg rel="icon" and type="image/svg+xml"
favicon-NxN.png rel="icon", PNG type, and sizes="NxN"
apple-touch-icon.png rel="apple-touch-icon"
apple-touch-icon-NxN.png rel="apple-touch-icon" and sizes="NxN"

Square numbered variants are emitted in ascending size order. A practical baseline is favicon.ico, favicon.svg, and apple-touch-icon.png.

For a web app manifest or other head metadata, add markup in layouts/_partials/hooks/head-end.html. To change discovery itself, override layouts/_partials/favicons.html and keep URLs subpath-safe with relURL.

Generate favicons

Generate the files with a reviewed graphics workflow such as ImageMagick, favicon.io, or RealFaviconGenerator. Oink’s production build does not depend on Node.js or a favicon generator; Hugo only publishes the files already present in static/.

Add images

Put images beside a page when they belong to that page bundle. This keeps the source and its media together and lets Hugo process the resource. Use regular Markdown for simple images or the imgproc shortcode when you need resize, crop, or display options.

Landing-page covers

The blocks/cover shortcode selects the first page resource whose filename contains background. For raster images, Oink creates responsive 1920x1080 and 960x540 variants. Use image_anchor to control the crop and height to choose auto, min, med, max, or full:

GO-HTML-TEMPLATE
{{%/* blocks/cover
  title="Welcome to Oink"
  image_anchor="center"
  height="min"
*/%}}
Documentation that gets out of the way.
{{%/* /blocks/cover */%}}

Static images

Put files in static/ when they must retain a fixed public path and do not need Hugo image processing. Reference them with a root-relative URL, and verify that the same URL works when the site is built with its production baseURL. See Adding static content for the trade-offs.

3.9 - Shortcodes

Use OINK’s local-first content components safely and accessibly.

Shortcodes add behavior that ordinary Markdown cannot express. OINK retains the core Docsy components and adds locally served charts, terminal recordings, infographics, carousels, cards, and disclosure widgets. Browser runtimes load only on pages that use them.

Prefer Markdown for headings, prose, lists, links, tables, and images. A shortcode becomes part of the content API: changing its name or parameters can break every page that calls it.

Shortcode delimiters

Hugo supports two forms:

  • {{< name >}} uses standard delimiters and passes inner content as-is;
  • {{% name %}} uses Markdown delimiters and renders inner Markdown in the surrounding content context.

Use the form documented for the component. Nesting, indentation, and blank lines matter, especially inside lists and blockquotes. In examples, the /* ... */ escape prevents Hugo from executing the displayed shortcode.

blocks/* shortcodes

Block shortcodes compose full-width landing pages. Their color argument uses OINK/Bootstrap semantic colors or a project-defined block style. Their height argument accepts the values documented for each block.

blocks/cover

Creates a hero from the page bundle image matching *background* and optional *logo*:

MARKDOWN
{{< blocks/cover title="OINK" subtitle="Local-first documentation"
    color="dark" height="max" >}} [Get started](/docs/tutorial/){ .btn .btn-lg
.btn-primary } {{< /blocks/cover >}}

image_anchor and logo_anchor control image cropping; byline attributes the image. Heights are auto, min, med, max, or full. Essential hero text must remain readable without the background.

blocks/lead

Creates a prominent introductory band:

MARKDOWN
{{% blocks/lead color="primary" height="min" %}} OINK builds the whole
documentation experience with Hugo Extended. {{% /blocks/lead %}}

The height accepts auto, min, med, max, or full.

blocks/section

Creates a general landing-page band:

MARKDOWN
{{% blocks/section color="light" type="row" height="auto" %}}

### One section

Use ordinary Markdown inside the block. {{% /blocks/section %}}

type selects the container treatment; height uses the block height values. Keep heading levels consistent with the page outline.

blocks/feature

Creates one feature cell, normally inside a section:

MARKDOWN
{{% blocks/feature icon="fa-solid fa-box-archive"
    title="Works offline" url="/docs/about/local-first/"
    url_text="Read the design" %}} All required browser assets are pinned and
served locally. {{% /blocks/feature %}}

The icon is decorative; title and link text must carry the meaning.

Adds a link from one block to the next. It must be nested inside a block. Set an explicit id when the generated target must remain stable.

Below-navbar layout correction

Blocks that begin directly below fixed navigation use td-below-navbar/td-anchor-no-extra-offset to compensate for navbar height. Reuse these classes rather than adding arbitrary top margins; verify direct fragment navigation after changing navbar dimensions.

Helper shortcodes

alert

The legacy alert shortcode remains available:

MARKDOWN
{{% alert title="Compatibility note" color="warning" %}} Prefer Markdown
blockquote alerts for new content. {{% /alert %}}

color maps to a Bootstrap alert suffix. New content should generally use the Markdown alert syntax described in Adding Content.

Alerts, indentation, and examples

Keep the opening and closing shortcode aligned with their surrounding list or blockquote. Leave a blank line around block Markdown. If an example must show a shortcode literally, escape its delimiters rather than wrapping an active call in another component.

pageinfo

Renders an informational panel around Markdown:

MARKDOWN
{{% pageinfo color="info" %}} This page describes a preview interface.
{{% /pageinfo %}}

Use a semantic alert for warnings; pageinfo is intended for contextual page information.

imgproc

Processes an image from the current page bundle:

MARKDOWN
{{% imgproc "architecture" Fit "960x540" %}} OINK runtime architecture.
{{% /imgproc %}}

Commands are Fit, Resize, Fill, and Crop. The third argument follows Hugo image-processing syntax. The inner text becomes a caption, and a resource params.byline is appended when present. Always provide useful alternative or adjacent text.

swaggerui

Embeds the locally vendored Swagger UI runtime:

MARKDOWN
{{< swaggerui src="/openapi.yaml" >}}

Use a same-origin specification for offline and CSP-safe deployments. A remote src is an explicit network dependency and can expose reader metadata to that host. Only one Swagger UI instance should be placed on a page with the current compatibility shortcode.

redoc

Embeds the locally vendored Redoc runtime:

MARKDOWN
{{< redoc "openapi.yaml" >}}

The first argument is a page-relative, site-relative, or explicit HTTP specification. The optional second argument contains Redoc element options. Treat specification content as reviewed input and test large schemas on mobile.

iframe

Embeds another page:

MARKDOWN
{{< iframe src="/demo/" name="demo" id="demo-frame"
    sandbox="allow-scripts allow-same-origin" >}}

Set a descriptive name, a unique id, a fallback sub message, and the narrowest viable sandbox. The defaults support width and automatic-height behavior, but cross-origin documents cannot always be measured. An iframe is a security and privacy boundary, not a general layout tool.

OINK content components

The following components are additions carried by OINK. Each runtime is pinned in VENDOR.json and loaded on demand from the same origin.

details

Creates an accessible disclosure:

MARKDOWN
{{% details title="Show migration notes" closed="false" %}} The body accepts
Markdown. {{% /details %}}

closed defaults to true. Use a concise summary and do not hide mandatory instructions inside a closed disclosure.

steps

steps presents a sequence with automatically generated numbers and a visual guide line. Write ordinary Markdown headings and content inside the shortcode; do not type the numbers yourself.

Create the content

Write one direct child heading for each step, followed by any Markdown content that belongs to it.

Check the sequence

Move, add, or remove whole steps. The displayed numbers update automatically.

Publish the result

Verify the sequence on narrow screens and in both color themes.

Use Markdown shortcode delimiters so Hugo renders the inner content:

MARKDOWN
{{% steps %}}

### Create the content

Add the first instruction.

### Check the sequence

Add the next instruction. The number is generated automatically.

#### Optional detail {class="no-step-marker"}

This heading belongs to the current step and does not consume a number.

### Publish the result

Add the final instruction.

{{% /steps %}}

Every direct child heading from h2 through h6 becomes a step. Add class="no-step-marker" when a direct child heading is a subsection of the current step. Keep the same heading level for peer steps, preserve a logical page outline, and avoid nesting one steps block inside another.

asciinema

Plays an asciinema .cast recording:

MARKDOWN
{{< asciinema file="casts/install.cast" speed="1.25"
    markers="0:Start,18:Verify" fit="width" >}}

The window title uses title when supplied and otherwise displays file. Other important parameters include theme, autoplay, loop, preload, speed, startAt, poster, cols, rows, idleTimeLimit, pauseOnMarkers, markers, and fit (width, height, both, or none). Local recordings can come from Hugo assets or a site-relative URL. Avoid autoplay, remove secrets from terminal history, and provide nearby text for essential steps.

echarts

Apache ECharts is a full visualization system rather than a one-paragraph shortcode. Its advanced guide documents the wrapper, structured options, themes, responsive behavior, accessibility, and trusted callback boundary:

The shortcode body accepts a JSON or YAML options object. Use height, theme, and full only as described in the dedicated guide.

infographic

AntV Infographic has its own advanced guide because template choice, DSL structure, themes, visual semantics, and accessibility need more than an inline example:

The shortcode body contains the Infographic DSL. Use height and full as documented there, and keep an equivalent textual explanation beside every essential visualization.

doc-cards and nav-cards

Both containers accept cols from 1 through 4. Their child cards accept title, link, image, alt, icon, desc, accent, and badge:

MARKDOWN
{{< nav-cards cols="2" >}}
{{< nav-card title="Get started" link="/docs/tutorial/"
      icon="fa-solid fa-rocket" desc="Build with Hugo {version}." >}} {{< nav-card title="Architecture" link="/docs/about/architecture/"
      badge="Design" >}}
{{< /nav-cards >}}

doc-card/doc-cards share the rendering contract and suit editorial content; nav-card/nav-cards signal navigation. Description tokens such as {version} resolve from site parameters. Card images are lazy-loaded; supply meaningful alt text unless the image is decorative.

Places doc-card elements in a keyboard-scrollable carousel:

MARKDOWN
{{< doc-carousel label="Release highlights" >}}
{{< doc-card title="Local assets" >}}No CDN required.{{< /doc-card >}}
{{< doc-card title="Bilingual" >}}Stable English and Chinese
routes.{{< /doc-card >}} {{< /doc-carousel >}}

label names the region for assistive technology. Previous/next buttons are localized. Do not place information only in an off-screen card; the track must remain usable without script.

param

Prints a page parameter, falling back through Hugo’s Page.Param rules to site configuration:

MARKDOWN
OINK version {{< param version >}}.

A missing parameter fails the build. Use param for scalar display values, not for injecting unreviewed HTML. The internal _param compatibility shortcode also performs numbered placeholder replacement for legacy content.

Tabbed panes

Tabs group equivalent representations, such as YAML/TOML/JSON configuration. They must not hide sequential steps or unrelated choices.

MARKDOWN
{{< tabpane text=true persist=lang >}}
{{< tab header="YAML" lang="yaml" >}} params: offlineSearch: true
{{< /tab >}} {{< tab header="TOML" lang="toml" >}} [params]
offlineSearch = true {{< /tab >}} {{< /tabpane >}}

Selection persistence is local to the browser. persist accepts header, lang, or disabled. The deprecated persistLang should not be used in new content.

Shortcode details

text=true renders inner content as prose rather than highlighted code. right=true aligns tabs to the end. langEqualsHeader=true derives language identifiers from headers. Pane defaults can be overridden per tab.

tabpane

The parent validates boolean and persistence parameters, builds unique IDs, and ensures a selected tab. Use one disabled header tab only when it adds a useful group label.

tab

tab must be inside tabpane. It accepts header, selected, lang, highlight, text, right, and disabled. Only one tab should be selected. Translate reader-facing headers, but keep language identifiers stable.

Code Groups

Use code-group/code-tab for code-only alternatives that need stable public hashes, synchronized values, and exact Copy behavior. Unlike legacy tabpane, each child has a required machine value, and non-interactive outputs expand every example. Read Code blocks and Code Groups for the complete parameter and persistence contract.

Card panes

The legacy cardpane/card pair lays out Bootstrap-style cards. New navigation surfaces should prefer OINK content cards, but existing Docsy content can keep the compatibility component.

Shortcode card: textual content

MARKDOWN
{{% cardpane %}}
{{% card header="Note" title="Local build" footer="Verified" %}} Markdown
**content**. {{% /card %}} {{% /cardpane %}}

header, title, subtitle, and footer accept rendered text. Keep equal cards concise and avoid using cards as a replacement for headings.

Shortcode card: programming code

Set code=true and optionally lang/highlight:

MARKDOWN
{{< cardpane >}} {{< card code=true header="Go" lang="go" >}}
fmt.Println("OINK") {{< /card >}} {{< /cardpane >}}

Card groups

Adjacent cards in cardpane form a responsive group. Test unequal text length, mobile stacking, code overflow, and both language variants.

Include external files

The readfile shortcode reads a repository file at build time and either renders it as Markdown or highlights it as code. The path is relative to the current content file unless it begins with /.

Reuse documentation

MARKDOWN
{{% readfile "includes/installation.md" %}}

Included Markdown is not an independent published page and is exempt from the page-pair audit. If shared prose is reader-facing, create and select language-specific include files deliberately; Hugo cannot translate an include.

Installation

Keep reusable fragments under an includes/ directory near their callers. Document ownership and avoid deep include chains: readers and reviewers should be able to locate the source quickly.

Include code files

MARKDOWN
{{< readfile file="includes/config.yaml" code="true" lang="yaml" >}}

code=true highlights the file with lang. Never include secrets, generated credentials, or untrusted paths.

Error reporting

A missing file fails the build. draft=true replaces that failure with a visible draft warning, which is suitable only during authoring and must not reach a release build.

Conditional text

conditional-text selects content using params.buildCondition:

MARKDOWN
{{% conditional-text include-if="enterprise,preview" %}} This paragraph
appears only in matching builds. {{% /conditional-text %}}

include-if and exclude-if accept condition lists. A condition cannot appear in both. Use the feature for genuinely different published variants, not for language selection; multilingual content belongs in translated page files.

3.10 - Content components

Use Oink’s local, reusable components for richer documentation.

OINK promotes the content components that proved reusable across PGSTY sites into the theme. Each component has a stable authoring API, unique instance IDs, local assets, and a defined safety boundary. Site-specific data widgets remain outside the theme.

Loading model

Interactive shortcodes mark the features used by a page. OINK then adds each required stylesheet or runtime once, even if the page has several component instances. A plain page does not download component code it never uses.

Relative asset and link parameters pass through Hugo’s URL handling, so they remain correct under a subpath baseURL. Component markup also has print, dark-mode, mobile, keyboard, and reduced-motion behavior where applicable.

Everyday content primitives

Everyday primitives cover small structures that recur throughout engineering documentation. Each guide below explains when to use the primitive, shows the rendered result beside its source, and records its complete version-one API.

Choose a primitive

Documentation need Reference JavaScript
Release state, lifecycle, or short status Badge None
Shortcut or key sequence Kbd None
Configuration, parameter, or response data Fields and Field None
Repository or directory structure FileTree None; folders use details
Inspect a screenshot or architecture image Image Zoom Optional, loaded on demand
Compare several related images Gallery Reuses optional Image Zoom JS

Shared authoring contract

All primitives except Kbd use named parameters and standard {{< ... >}} shortcode notation. Parameter names are case-sensitive. Unknown parameters, quoted booleans or integers, empty required strings, invalid enum values, and incorrect parent/child combinations stop the build with the source position.

The public APIs do not accept arbitrary class, style, colors, or event handlers. Visible labels come from the author or Oink’s translations. Static primitives add no JavaScript; interactive primitives mark their page so the required runtime is included once.

Validation and fallbacks

The output contract keeps the same information available without a browser runtime:

Primitive HTML Markdown Print and RSS JavaScript
Badge Semantic status span or link Emphasized text or link Static inline content None
Kbd Nested kbd sequence Ctrl + K Plain key notation None
Fields Responsive definition list Metadata bullet list Complete definitions None
FileTree Nested lists and native disclosure Nested list Fully expanded tree None
Shared image Figure, image, and caption Ordinary image and caption Static figure Reuses Zoom when enabled
Gallery Responsive figure grid Images and captions Sequential static figures Reuses Zoom when enabled

Missing required parameters and invalid values fail the Hugo build instead of silently changing meaning. Historical positional imgproc remains compatible, but new content should use the accessible named form.

Deliberate limits

Version one does not add a public Icon shortcode or an icon parameter to Badge. Oink’s private shell SVG registry remains separate from author-facing content icons. Automatic TypeScript parsing, API playgrounds, directory reads, remote image downloads, and complex pan or wheel-zoom controls also remain outside the Hugo-only theme boundary.

Asciinema

Use asciinema for a terminal recording stored as a local .cast file:

GO-HTML-TEMPLATE
{{< asciinema
  file="images/install.cast"
  speed="1.5"
  markers="0:Start,1:Done"
>}}
images/install.cast

file is required and can also be the first positional argument. The terminal window uses title when supplied and otherwise displays the file value. Supported options are title, theme, fit (width, height, both, or none), autoplay, loop, preload, speed, startAt, poster, cols, rows, idleTimeLimit, pauseOnMarkers, and comma-separated markers.

Keep cast files local for offline use. A remote URL is accepted only when the author explicitly supplies it.

Advanced visualizations

ECharts and Infographic remain Oink content components, but each now has a dedicated section under Advanced. This page keeps the reusable component overview concise and points to the richer examples.

Apache ECharts

Use Apache ECharts for quantitative charts based on structured JSON or YAML. The chart gallery contains several live patterns, and callbacks and trusted code documents the explicit executable-code boundary.

AntV Infographic

Use Infographics with AntV for declarative processes, timelines, cycles, grids, and funnels. The dedicated pages explain template semantics, themes, local-first constraints, and accessible textual fallbacks.

doc-card and nav-card share one card implementation. doc-cards and nav-cards create responsive groups of one to four columns. The aliases let an existing site’s content keep its most descriptive name without duplicating markup or styles.

GO-HTML-TEMPLATE
{{< nav-cards cols="3" >}}
  {{< nav-card
    title="Architecture"
    link="/docs/about/architecture/"
    icon="fa-solid fa-diagram-project"
    desc="Understand the build and runtime boundaries."
  >}}
  {{< nav-card
    title="Deployment"
    link="/docs/deploy/"
    badge="Hugo-only"
  >}}Publish the static output.{{< /nav-card >}}
{{< /nav-cards >}}

A card accepts title, link, image, alt, icon, desc, accent, and badge. Its body can contain Markdown links. Tokens such as {version} in desc resolve from site parameters when a matching value exists.

Wrap document cards in doc-carousel to create an accessible horizontal carousel:

GO-HTML-TEMPLATE
{{< doc-carousel label="OINK workflow" >}}
  {{< doc-card title="Write" >}}Create paired content.{{< /doc-card >}}
  {{< doc-card title="Build" >}}Run Hugo Extended.{{< /doc-card >}}
  {{< doc-card title="Verify" >}}Inspect the static site.{{< /doc-card >}}
{{< /doc-carousel >}}

label supplies the carousel’s accessible name. Arrow keys and visible previous/next controls navigate the track; reduced-motion preferences disable unnecessary animation.

Details

details emits native details and summary elements:

GO-HTML-TEMPLATE
{{% details title="Why Hugo-only?" closed="false" %}}
Committed browser assets keep the consuming build reproducible.
{{% /details %}}
Why Hugo-only?
Committed browser assets keep the consuming build reproducible.

title sets the summary. The block is closed by default; set closed=false to render it open.

Tabs

OINK keeps Docsy’s tabpane and tab authoring model while preserving selected=true and whitespace behavior used by imported sites:

GO-HTML-TEMPLATE
{{< tabpane text=true >}}
  {{< tab header="Local" selected=true >}}
  Build with the complete local theme.
  {{< /tab >}}
  {{< tab header="Cloudflare" >}}
  Run the same Hugo command from the source branch.
  {{< /tab >}}
{{< /tabpane >}}
Local
Build with the complete local theme.
Cloudflare
Run the same Hugo command from the source branch.

Use text=true for Markdown content; otherwise tabs are syntax-highlighted code. Tab panes also support language-aware persistence, disabled tabs, and right-aligned entries. Generated tab and panel IDs have matching ARIA relationships.

Parameters

param prints a page parameter, falling back to the site parameter of the same name:

GO-HTML-TEMPLATE
Current version: {{< param version >}}

Current version: v0.2.0

The shortcode fails the build when the named parameter does not exist. This is intentional: a missing release or repository value should not silently produce misleading documentation.

Existing rich content

OINK also ships local runtimes for inherited content features:

  • fenced mermaid, math, and markmap code blocks;
  • swaggerui and redoc API documentation shortcodes;
  • Docsy blocks, alerts, image, include, readfile, cards, and other established shortcodes.

See Shortcodes and Diagrams and formulae for the complete authoring reference.

Authoring rules

  • Prefer structured data over executable content.
  • Give images useful alt text and carousels a meaningful label.
  • Do not enable autoplay unless the content genuinely requires it.
  • Test several identical instances on one page when creating a new wrapper.
  • Verify keyboard navigation, focus visibility, dark and light themes, mobile layout, print output, and reduced-motion behavior.
  • Keep business-specific data components in the consuming site.

3.10.1 - Badge

Add compact, semantic status labels without custom colors or JavaScript.

Use Badge to place a short status beside a feature, option, or release name. The author chooses a semantic tone; Oink maps it to theme tokens that retain contrast in light and dark modes.

When to use

Badge works well for lifecycle states such as Beta, New, Experimental, and Deprecated. Keep the text explicit: color supplements the label and never replaces it. Use ordinary prose or an alert when the status needs explanation, instructions, or a deadline.

Quick start

Source

GO-HTML-TEMPLATE
{{< badge text="Beta" tone="warning" >}}
{{< badge text="Deprecated" tone="danger" outline=false >}}
{{< badge text="v0.3" tone="info" link="/blog/release/" >}}

Rendered result

Neutral Info Supported Beta Deprecated v0.3

The final badge is a link. The others are static inline labels.

Parameters

Badge parameters

text
string, required

A nonempty string shown to the reader.

tone
enum, default: neutral

One of neutral, info, success, warning, or danger.

link
URL

A validated internal, relative, HTTP(S), or mailto: destination. When set, the Badge becomes a link.

outline
boolean, default: true

Set to false to select the filled treatment.

Pass booleans without quotes. For example, use outline=false, not outline="false". Unknown parameters and invalid tone or link values stop the Hugo build and report the source position.

Semantics and fallback

A static badge renders as a span; a linked badge renders as an a. Oink does not make it a live status region, so adding a badge does not create unexpected screen-reader announcements. Its visible text remains present in every output: Markdown uses emphasized text (and preserves the link), while print and RSS use static inline content. Badge loads no JavaScript.

Deliberate limits

Badge does not accept arbitrary colors, CSS classes, styles, or event handlers. Version one also has no icon parameter. Use a concise textual label now; content icons can receive a separate public API after their naming, licensing, accessibility, and Markdown fallback contracts are settled.

3.10.2 - Kbd

Write keyboard shortcuts as accessible, static key sequences.

Use Kbd to distinguish literal keys and shortcuts from surrounding prose. It renders semantic HTML, remains readable in Markdown and print, and needs no JavaScript.

When to use

Use Kbd for keys the reader should press, including multi-key shortcuts. Use inline code for commands, option names, or text the reader should type; those are not physical or virtual keys.

Quick start

Source

GO-HTML-TEMPLATE
Press {{< kbd "Ctrl" "K" >}} to open search.
Use {{< kbd "⌘" "Shift" "P" >}} to open the command palette.

Rendered result

Press Ctrl with K to open search. Use with Shift with P to open the command palette, or press Alt with Enter to apply an action.

Interface

Kbd accepts one or more nonempty positional strings:

GO-HTML-TEMPLATE
{{< kbd "key" >}}
{{< kbd "first key" "second key" "third key" >}}

It has no named parameters. Quotes are required because every key must be a string. Missing keys, blank strings, named arguments, or non-string values stop the build with the source position.

Use the label printed on the relevant platform when the distinction matters. For cross-platform instructions, name the platform in prose instead of placing alternatives inside one key sequence.

Semantics and fallback

HTML contains one nested kbd element per key. Visual plus signs are hidden from assistive technology; a localized word separates the keys for screen readers. Markdown, print, and RSS use an unambiguous sequence such as Ctrl + K. The instruction remains complete when CSS or JavaScript is absent.

Deliberate limits

Kbd represents simultaneous key sequences only. It does not model menus, gesture input, key remapping, platform detection, or an interactive shortcut recorder. Explain sequential actions in prose: “press Escape, then Enter.”

3.10.3 - Fields and Field

Describe configuration, parameters, properties, and response fields with responsive semantic HTML.

Use fields with field children to document named values and their metadata. The component favors a responsive definition list over a wide fixed table, so long names and descriptions remain usable on narrow screens.

When to use

Fields works for configuration keys, command or API parameters, object properties, and response members. Use a regular Markdown table when readers must compare many rows across the same columns. Use prose when the entries are steps rather than definitions.

Quick start

Source

GO-HTML-TEMPLATE
{{< fields label="Search configuration" >}}
  {{< field name="offlineSearch" type="boolean" required=true default=true >}}
  Builds a **local** search index and command palette.
  {{< /field >}}

  {{< field name="offlineSearchMaxResults" type="integer" default=10 >}}
  Limits the number of visible results.
  {{< /field >}}
{{< /fields >}}

Rendered result

Search configuration

offlineSearch
boolean, required, default: true

Builds a local search index and command palette.

offlineSearchMaxResults
integer, default: 10

Limits the number of visible results while retaining keyboard navigation.

searchPlaceholder
string, default: ""

Sets optional placeholder text. The empty-string default remains visible.

theme.components.media.previewMaximumWidthInCharacters
string, default: auto

This deliberately long field name demonstrates wrapping without widening the page.

Descriptions accept Markdown, including links, emphasis, inline code, and lists. Keep each description self-contained because Markdown output presents each one beneath its metadata.

Fields parameters

fields parameters

label
string

A nonempty visible label associated with the complete definition list.

The container must have at least one direct field child. Text or another shortcode directly inside fields stops the build.

Field parameters

field parameters

name
string, required

A nonempty string identifying the field.

type
string

A nonempty type label such as boolean, string[], or duration.

required
boolean, default: false

When true, adds the localized required marker.

default
scalar

A string, boolean, integer, or floating-point value. false, 0, and "" are preserved.

Every field also requires a nonempty body. It must be a direct child of fields. Parameter names and types are validated at build time, and unknown parameters are errors.

Semantics and fallback

HTML uses dl, dt, and dd. Metadata is displayed as columns where space permits and stacks naturally on mobile. The optional label names the definition list for assistive technology. Markdown emits an indented bullet list with code-formatted names, types, and defaults; print and RSS retain every definition. No JavaScript is loaded.

Deliberate limits

Version one does not implement kind, deprecated, since, location, or per-field links. It also does not parse TypeScript or an API schema inside Hugo. An external generator may emit these shortcodes later, keeping compiler and schema runtimes outside the theme while preserving this output contract.

3.10.4 - FileTree

Present repository and directory structures as semantic, progressively disclosed lists.

Use FileTree to explain the part of a repository or directory layout that matters to the reader. Folders use native disclosure controls in interactive HTML; every output retains the complete nested structure.

When to use

FileTree works best for curated structures in setup guides, architecture overviews, and contribution instructions. Use a code block for literal command output that should be copied verbatim. Describe generated or highly dynamic trees in prose instead of committing a large snapshot that will quickly drift.

Quick start

Source

GO-HTML-TEMPLATE
{{< filetree label="Repository structure" >}}
  {{< filetree/folder name="content" open=true >}}
    {{< filetree/file name="_index.md" >}}
    {{< filetree/folder name="docs" open=true >}}
      {{< filetree/file name="getting-started.md" >}}
    {{< /filetree/folder >}}
  {{< /filetree/folder >}}
  {{< filetree/file name="hugo.yml" link="/docs/getting-started/" >}}
{{< /filetree >}}

Rendered result

Repository structure

The blog folder starts closed. Activate its summary with a pointer, Enter, or Space to reveal the child file; this behavior comes from the native details element rather than a custom script.

Root parameters

filetree parameters

label
string

A nonempty visible label associated with the root list.

The root accepts only direct filetree/folder and filetree/file children. Add at least one meaningful entry rather than publishing an empty tree.

Folder and file parameters

filetree/folder parameters

name
string, required

A nonempty visible directory name.

open
boolean, default: false

Controls the initial interactive HTML state.

filetree/file parameters

name
string, required

A nonempty visible file name.

link
URL

A validated internal, relative, HTTP(S), or mailto: destination.

A folder can contain folders and files recursively. A file cannot contain children. Unknown parameters, text between children, or a child outside an allowed parent stops the build with its source position.

Semantics and fallback

The structure is a nested ul. Interactive folders add native details and summary; Oink deliberately does not declare role="tree", because that ARIA widget would require a complete arrow-key navigation model. Print and RSS expand all folders. Markdown becomes a nested list with linked file names where applicable. No JavaScript is loaded.

Deliberate limits

FileTree is author-controlled and never reads a local directory during a Hugo build. This keeps builds safe and reproducible. Version one also has no public badge or icon parameters for entries; the built-in folder and file glyphs are presentational theme details, not content APIs.

3.10.5 - Image Zoom

Let readers inspect meaningful standalone images with an optional native dialog.

Image Zoom progressively enhances eligible content images with one native dialog. It is useful for screenshots and architecture diagrams whose details may be hard to read at the document width. The original image remains complete when JavaScript or dialog support is unavailable.

When to use

Enable zoom when a reader benefits from seeing the source image at a larger size. Prefer a purpose-built crop or a clearer diagram when enlargement does not solve the readability problem. Decorative icons, logos embedded in prose, and linked thumbnails should retain their existing behavior.

Enable the feature

Image Zoom is disabled by default. Enable it for the whole site in Hugo configuration:

YAML
params:
  ui:
    image_zoom:
      enable: true

A page can override the site value in its front matter with the same structure. Use a real boolean:

YAML
params:
  ui:
    image_zoom:
      enable: false

Oink only includes the JavaScript runtime and dialog on an enabled page that has an eligible image. Enabling the switch alone adds no runtime to a text-only page.

Quick start

Source

Ordinary standalone Markdown images are eligible. The named imgproc form is useful when Oink should generate a smaller preview but open the original:

GO-HTML-TEMPLATE
{{< imgproc
  src="images/content-primitives/oink.webp"
  command="Fit"
  options="640x320"
  alt="OINK local-first documentation preview"
>}}
A processed preview with a **Markdown caption**.
{{< /imgproc >}}

Rendered result

Activate the image with a pointer, Enter, or Space. Close the dialog with Escape, the visible close button, or the backdrop.

OINK local-first documentation preview

The document displays a processed preview. Image Zoom opens the original resource, and closing the dialog restores focus to this trigger.

An image inside a link is deliberately skipped and remains a link:

Linked OINK image remains a link

Eligible images

Oink enhances a meaningful image when all of these conditions hold:

  • The image is standalone in a paragraph or figure, or Gallery marks it explicitly.
  • It has a nonempty alt value and usable source.
  • It is not inside a link, button, or element marked data-no-zoom.
  • It is not marked aria-hidden="true", role="presentation", or role="none".

Inline images among text and empty-alt decorative images are skipped. Authors can add data-no-zoom to an image or ancestor in trusted HTML when an otherwise eligible image should not open.

Named imgproc parameters

Named imgproc parameters

src
resource path, required

An exact page or global image resource.

command
enum, required

One of Fit, Resize, Fill, or Crop.

options
string, required

Nonempty Hugo image-processing options, such as 640x320.

alt
string

Meaningful alternative text. It is required for content images and omitted only with decorative=true.

decorative
boolean, default: false

When true, alt must be absent and Image Zoom is suppressed.

The optional shortcode body is a Markdown caption. The historical three-value positional imgproc form remains compatible, but new content should use the named form so alternative text is enforced at build time.

Interaction and fallback

Progressive enhancement wraps an eligible image in a real button with aria-haspopup="dialog". The native dialog moves focus to its close button, supports Escape, copies the image’s alternative text and direct caption, and restores focus after closing. Without JavaScript or HTMLDialogElement, the image and caption remain ordinary static content. Markdown, print, and RSS do not include dialog controls.

Deliberate limits

Version one does not implement dragging, panning, wheel zoom, editing, or previous and next image navigation. It also never downloads a remote image at build time. Use Gallery to group related images while reusing this same dialog.

3.10.6 - Gallery

Arrange related images in a responsive static grid that can reuse Image Zoom.

Gallery groups related images in a responsive grid. It is static-first: images, alternative text, and captions remain available without JavaScript. When Image Zoom is enabled, Gallery reuses the same dialog instead of loading another lightbox.

When to use

Use Gallery to compare a small set of screenshots, states, or related visual examples. Use a single image when sequence and comparison do not matter. Use Carousel when the content intentionally needs slide navigation and hiding noncurrent items is acceptable.

Quick start

Source

GO-HTML-TEMPLATE
{{< gallery columns=3 label="OINK screenshots" >}}
  {{< gallery/image
    src="images/content-primitives/oink.webp"
    alt="OINK documentation overview"
    caption="Documentation overview"
  >}}
  {{< gallery/image
    src="/images/feedback.png"
    alt="OINK feedback interface"
    caption="Feedback controls"
  >}}
{{< /gallery >}}

Rendered result

This page enables Image Zoom. Activate any image to inspect it in the shared dialog. With JavaScript disabled, the same three figures remain visible in the same reading order.

gallery parameters

columns
integer, default: 2

An unquoted value from 1 through 4; this is the desktop maximum.

label
string

A nonempty visible label associated with the gallery list.

The container requires at least one direct gallery/image child and accepts no ordinary body text. Small viewports reduce the effective column count without changing the requested desktop maximum.

Image parameters

gallery/image parameters

src
image URL, required

A validated page, global, static, or remote image URL.

alt
string, required

Meaningful nonempty plain text describing the image.

caption
string

Nonempty plain text shown below the image.

Gallery records intrinsic width and height for local Hugo resources when available and adds lazy loading. It accepts a remote source URL but never downloads that image during the Hugo build, so remote dimensions remain unknown. Captions do not render Markdown; keep them concise and move rich explanation into nearby prose.

Semantics and fallback

HTML uses a labeled ul of figure, img, and optional figcaption elements. Each image retains its own alternative text; the gallery label names the collection. Markdown emits ordinary images followed by italic captions. Print and RSS render sequential static figures. Gallery has no private JavaScript runtime: it only marks its images for Image Zoom when that page-level feature is enabled.

Deliberate limits

Gallery does not crop images to a forced aspect ratio, reorder them by breakpoint, hide overflow, or provide slide navigation. It has no Gallery-specific lightbox. These constraints preserve document order and keep the fallback complete.

3.11 - Diagrams and formulae

Add local diagrams, mind maps, and scientific formulae to a page.

OINK supports KaTeX, Mermaid, Markmap, PlantUML, and Diagrams.net. KaTeX, Mermaid, and Markmap use build-time or same-origin resources shipped with the theme. PlantUML and the Diagrams.net editor require an explicitly configured service endpoint; they do not silently default to a public service.

LaTeX support with KaTeX

KaTeX renders TeX mathematics for the web. Hugo’s embedded KaTeX support can render formulae at build time, so readers do not need a remote math service.

Inline formulae

Inline formulae use the passthrough delimiter pairs configured in Goldmark. Keep surrounding spaces and punctuation outside the formula when possible.

Formulae in display mode

Use a math code block for a formula on its own line:

MARKDOWN
```math
E = mc^2
```
E=mc2E = mc^2

Activating KaTeX support

math and chem code blocks use theme render hooks automatically. For inline and delimiter-based formulae, enable Goldmark’s passthrough extension and set the delimiter pairs appropriate for the site. The included oink.pgsty.com config shows square-bracket, double-dollar, and parenthesis pairs.

Enable the passthrough extension

The relevant YAML structure is:

YAML
markup:
  goldmark:
    extensions:
      passthrough:
        enable: true
        delimiters:
          block: []
          inline: []

Fill the arrays with Hugo’s documented delimiter pairs. Choose pairs that do not conflict with the site’s prose or code and apply the setting consistently in every build environment.

Add the passthrough render hook

For delimiter-based math, create layouts/_markup/render-passthrough.html in the site:

GO-HTML-TEMPLATE
{{ partial "scripts/math.html" . }}

The hook can be scoped to a content type or section by placing it under the corresponding layout directory. A scoped hook avoids treating unrelated content as mathematical passthrough.

Chemical equations and physical units

Hugo’s embedded KaTeX supports the mhchem extension. Use chem code blocks for chemical equations. The same extension supports physical units. See the mhchem manual for its equation and unit syntax.

Diagrams with Mermaid

Mermaid turns a text definition into a diagram in the browser. Use a mermaid code block:

MARKDOWN
```mermaid
flowchart LR
  Source --> Hugo --> Static
```
flowchart LR
  Source --> Hugo --> Static

The theme detects the block, publishes its pinned local Mermaid runtime, and loads it once on that page. Pages without Mermaid do not load the runtime.

Site-wide Mermaid settings live under params.mermaid:

YAML
params:
  mermaid:
    theme: neutral
    flowchart:
      diagramPadding: 6

Per-diagram front matter can override supported Mermaid settings. Keep diagram text readable in source, test both color modes, and provide surrounding prose for information that must remain accessible when a diagram cannot render.

UML diagrams with PlantUML

PlantUML supports sequence, use-case, class, state, and other UML-oriented diagrams. A plantuml block contains the source:

MARKDOWN
```plantuml
actor Reader
participant Browser
participant "PlantUML endpoint" as Server
Reader -> Browser: Open page
Browser -> Server: Request encoded diagram
Server --> Browser: SVG
```

PlantUML requires a renderer endpoint. Enable it only with an approved local or explicit remote service:

YAML
params:
  plantuml:
    enable: true
    theme: default
    svg_image_url: https://plantuml.internal.example/plantuml/svg/
    svg: false

The endpoint receives encoded diagram source from the browser. Review its confidentiality, availability, CSP, and offline implications. For an air-gapped site, use an internal endpoint or commit pre-rendered images; do not point the default configuration at a public demo server.

Mind-map support with Markmap

Markmap converts a Markdown outline into an interactive mind map:

MARKDOWN
```markmap
# Local-first
## Build
- Hugo Extended
## Browser
- Local scripts
- Local fonts
```
# Local-first
## Build
- Hugo Extended
## Browser
- Local scripts
- Local fonts

Enable the feature globally when desired:

YAML
params:
  markmap:
    enable: true

The runtime is pinned and served locally. Keep the underlying outline useful and avoid relying on pointer-only interactions.

Diagrams with Diagrams.net

Diagrams.net (draw.io) can export SVG and PNG files that retain an embedded copy of their editable diagram. OINK can detect those images and show an Edit action when an editor endpoint is explicitly configured.

YAML
params:
  drawio:
    enable: true
    drawio_server: https://drawio.internal.example/

Export with Include a copy of my diagram enabled. The page can display the exported image offline, but opening the editor requires the configured service. Saving in the editor downloads an updated file to the browser; it does not write directly to the documentation repository.

Treat a public Diagrams.net endpoint as an online integration. If editing must stay inside an organization, deploy an approved self-hosted editor and set drawio_server to it.

Resource and authoring checklist

  • Use text-based diagrams when reviewable diffs are valuable.
  • Provide alt text or adjacent prose for essential meaning.
  • Test light, dark, mobile, print, and reduced-motion behavior.
  • Keep local runtimes pinned in VENDOR.json and load them only when used.
  • Never include secrets in diagram source sent to a service endpoint.
  • Use pre-rendered output when an online renderer is unacceptable.
  • Verify all asset and endpoint URLs under a subpath baseURL.

3.12 - Taxonomy support

Organize content with tags, categories, and custom taxonomies.

Oink supports Hugo taxonomies in its docs and blog sections. You can see the default layout and can test the behavior of the generated links on this page.

Terminology

To understand the usage of taxonomies you should understand the following terminology:

  • Taxonomy: a categorization that can be used to classify content - e.g.: Tags, Categories, Projects, People

  • Term: a key within the taxonomy - e.g. within projects: Project A, Project B

  • Value: a piece of content assigned to a term - e.g. a page of your site, that belongs to a specific project

A movie-website sample taxonomy is provided by the Hugo docs.

Parameters

There are various parameters to control the functionality of taxonomies in the project configuration file. Taxonomies are enabled by default for tags and categories in Hugo. To disable taxonomies, add the following to your project config:

Configuration file:
hugo.toml
TOML

disableKinds = ["taxonomy"]
hugo.yaml
YAML

disableKinds: [taxonomy]
hugo.json
JSON

{
  "disableKinds": [ "taxonomy" ]
}

With the default settings, Hugo generates taxonomy pages for tags and categories. If you want to use other taxonomies you have to define them in your configuration file. If you want to use beside your own taxonomies also the default taxonomies tags and categories, you also have to define them beside your own taxonomies. You need to provide both the plural and singular labels for each taxonomy.

With the following example you define a additional taxonomy projects beside the default taxonomies tags and categories:

Configuration file:
hugo.toml
TOML

[taxonomies]
tag = "tags"
category = "categories"
project = "projects"
hugo.yaml
YAML

taxonomies:
  tag: tags
  category: categories
  project: projects
hugo.json
JSON

{
  "taxonomies": {
    "tag": "tags",
    "category": "categories",
    "project": "projects"
  }
}

You can use the following parameters in your project’s config to control the output of the assigned taxonomy terms for each article resp. page of your docs and blog sections, plus a taxonomy cloud in Oink’s right sidebar:

Configuration file:
hugo.toml
TOML

[params.taxonomy]
taxonomyCloud = ["projects", "tags"] # set taxonomyCloud = [] to hide taxonomy clouds
taxonomyCloudTitle = ["Our Projects", "Tag Cloud"] # if used, must have same length as taxonomyCloud
taxonomyPageHeader = ["tags", "categories"] # set taxonomyPageHeader = [] to hide taxonomies on the page headers
hugo.yaml
YAML

params:
  taxonomy:
    taxonomyCloud:
      - projects    # remove all entries
      - tags        # to hide taxonomy clouds
    taxonomyCloudTitle:   # if used, must have the same
      - Our Projects      # number of entries as taxonomyCloud
      - Tag Cloud
    taxonomyPageHeader:
      - tags        # remove all entries
      - categories  # to hide taxonomy clouds
hugo.json
JSON

{
  "params": {
    "taxonomy": {
      "taxonomyCloud": [
        "projects",
        "tags"
      ],
      "taxonomyCloudTitle": [
        "Our Projects",
        "Tag Cloud"
      ],
      "taxonomyPageHeader": [
        "tags",
        "categories"
      ]
    }
  }
}

The settings above would only show a taxonomy cloud for projects and tags (with the headings “Our Projects” and “Tag Cloud”) in Oink’s right sidebar and the assigned terms for the taxonomies tags and categories for each page.

To disable any taxonomy cloud you have to set the Parameter taxonomyCloud = [] resp. if you don’t want to show the assigned terms you have to set taxonomyPageHeader = [].

By default, the plural label of a taxonomy is used as its cloud title. You can override the default cloud title with taxonomyCloudTitle. But if you do so, you have to define a manual title for each enabled taxonomy cloud (taxonomyCloud and taxonomyCloudTitle must have the same length!).

If you don’t set the parameters taxonomyCloud resp. taxonomyPageHeader the taxonomy clouds resp. assigned terms for all defined taxonomies will be generated.

Partials

The partials used by default for displaying taxonomies are defined so that you can easily use them in your own layouts.

taxonomy_terms_article

The partial taxonomy_terms_article shows all assigned terms of a given taxonomy (partial parameter taxo) of an article respectively page (partial parameter context, most of the time the current page or context .).

Example usage in layouts/docs/list.html for the header of each page in the docs section:

GO-HTML-TEMPLATE
{{ $context := . }}
{{ range $taxo, $taxo_map := .Site.Taxonomies }}
  {{ partial "taxonomy_terms_article.html" (dict "context" $context "taxo" $taxo ) }}
{{ end }}

This will give you for each in the current page (resp. context) defined taxonomy a list with all assigned terms:

HTML
<div class="taxonomy taxonomy-terms-article taxo-categories">
  <h5 class="taxonomy-title">Categories:</h5>
  <ul class="taxonomy-terms">
    <li>
      <a
        class="taxonomy-term"
        href="//localhost:1313/categories/taxonomies/"
        data-taxonomy-term="taxonomies"
        ><span class="taxonomy-label">Taxonomies</span></a
      >
    </li>
  </ul>
</div>
<div class="taxonomy taxonomy-terms-article taxo-tags">
  <h5 class="taxonomy-title">Tags:</h5>
  <ul class="taxonomy-terms">
    <li>
      <a
        class="taxonomy-term"
        href="//localhost:1313/tags/tagging/"
        data-taxonomy-term="tagging"
        ><span class="taxonomy-label">Tagging</span></a
      >
    </li>
    <li>
      <a
        class="taxonomy-term"
        href="//localhost:1313/tags/structuring-content/"
        data-taxonomy-term="structuring-content"
        ><span class="taxonomy-label">Structuring Content</span></a
      >
    </li>
    <li>
      <a
        class="taxonomy-term"
        href="//localhost:1313/tags/labelling/"
        data-taxonomy-term="labelling"
        ><span class="taxonomy-label">Labelling</span></a
      >
    </li>
  </ul>
</div>

taxonomy_terms_article_wrapper

The partial taxonomy_terms_article_wrapper is a wrapper for the partial taxonomy_terms_article with the only parameter context (most of the time the current page or context .) and checks the taxonomy parameters of your project’s hugo.toml/hugo.yaml/hugo.json to loop through all listed taxonomies in the parameter taxonomyPageHeader resp. all defined taxonomies of your page, if taxonomyPageHeader isn’t set.

taxonomy_terms_cloud

The partial taxonomy_terms_cloud shows all used terms of a given taxonomy (partial parameter taxo) for your site (partial parameter context, most of the time the current page or context .) and with the parameter title as headline.

Example usage in partial taxonomy_terms_clouds for showing all defined taxonomies and its terms:

GO-HTML-TEMPLATE
{{ $context := . }}
{{ range $taxo, $taxo_map := .Site.Taxonomies }}
  {{ partial "taxonomy_terms_cloud.html" (dict "context" $context "taxo" $taxo "title" ( humanize $taxo ) ) }}
{{ end }}

This will give you the following HTML markup for the taxonomy categories:

HTML
<div class="taxonomy taxonomy-terms-cloud taxo-categories">
  <h5 class="taxonomy-title">Cloud of Categories</h5>
  <ul class="taxonomy-terms">
    <li>
      <a
        class="taxonomy-term"
        href="//localhost:1313/categories/category-1/"
        data-taxonomy-term="category-1"
        ><span class="taxonomy-label">category 1</span
        ><span class="taxonomy-count">3</span></a
      >
    </li>
    <li>
      <a
        class="taxonomy-term"
        href="//localhost:1313/categories/category-2/"
        data-taxonomy-term="category-2"
        ><span class="taxonomy-label">category 2</span
        ><span class="taxonomy-count">1</span></a
      >
    </li>
    <li>
      <a
        class="taxonomy-term"
        href="//localhost:1313/categories/category-3/"
        data-taxonomy-term="category-3"
        ><span class="taxonomy-label">category 3</span
        ><span class="taxonomy-count">2</span></a
      >
    </li>
    <li>
      <a
        class="taxonomy-term"
        href="//localhost:1313/categories/category-4/"
        data-taxonomy-term="category-4"
        ><span class="taxonomy-label">category 4</span
        ><span class="taxonomy-count">6</span></a
      >
    </li>
  </ul>
</div>

taxonomy_terms_clouds

The partial taxonomy_terms_clouds is a wrapper for the partial taxonomy_terms_cloud with the only parameter context (most of the time the current page or context .) and checks the taxonomy parameters of your project’s config to loop through all listed taxonomies in the parameter taxonomyCloud resp. all defined taxonomies of your page, if taxonomyCloud isn’t set.

Multilingual taxonomy support

For multilingual sites, taxonomy terms get counted and linked within the language site only. Taxonomy config parameters can be adjusted per language.

4 - Advanced

Configure customization, visualizations, languages, search, versions, and integrations.

These capabilities extend the core authoring workflow: advanced visual customization, multilingual routing, search, version navigation, ECharts, infographics, comments, analytics, repository actions, AI-agent discovery, and print output. Enable only the features that match the site’s audience, accessibility needs, privacy boundary, and operating environment.

4.1 - Multi-language support

Configure languages, translations, stable links, and RTL layouts.

OINK uses Hugo’s multilingual page model rather than site-specific domain or template assumptions. The included site makes English the primary language, and Simplified Chinese (zh) the second language.

Configure languages

Define the default language and every enabled language in hugo.yaml:

YAML
defaultContentLanguage: en

languages:
  en:
    label: English
    locale: en-US
    weight: 1
    title: Product Documentation
    params:
      description: Product guides and reference
  zh:
    label: 简体中文
    locale: zh-CN
    weight: 2
    title: 产品文档
    params:
      description: 产品指南与参考资料
      time_format_default: 2006年1月2日
      time_format_blog: 2006年1月2日

weight controls both language ordering and the selector order. label is shown in that language’s own script. locale supplies standards-friendly language tags for HTML, alternate links, and Open Graph metadata.

Language-specific parameters override global values; other parameters inherit their global value. Put translated menus under each language when labels differ.

Organize translated content

The Oink project site colocates translations:

TEXT
content/docs/
├── install.md
└── install.zh.md

The base name makes the files translations of one page. Keep dates, weights, aliases, resources, and route-affecting metadata aligned unless a deliberate language-specific difference is required.

Translate all visible text, including front matter titles and descriptions, summaries, menu labels, tags, image alternatives, callouts, and shortcode arguments. Preserve commands, identifiers, configuration keys, filenames, URLs, and product names.

Sites with very large independently maintained language trees may use Hugo’s language-specific contentDir model instead. Do not mix layouts casually: pick one model, document it, and verify how Hugo associates translations.

Automatic heading IDs depend on the heading text, so translated headings would normally break shared fragment links. Use the English page’s actual rendered ID as an explicit ID in the translation:

MARKDOWN
## Configure local search
MARKDOWN
## 配置本地搜索 {#configure-local-search}

Inspect rendered HTML rather than guessing. Inline HTML, punctuation, badges, and shortcodes can affect Hugo’s generated ID. Corresponding pages should have the same heading order and rendered ID list.

Language selector behavior

The selector is generated from Hugo’s configured sites and page translations. It is hidden for a single language. With two or more languages it renders one consistent language button: a direct click advances to the next language by configured weight, while hovering for half a second or focusing the control reveals the complete language menu.

For each target language, the selector links to the current page’s translation when it exists. If it does not exist, it links to that language’s home page instead of producing a dead or falsely translated route. The current language has visible and aria-current state.

SEO and document metadata

Every page emits:

  • the correct HTML lang and dir values;
  • its canonical URL;
  • rel="alternate" links with hreflang for configured languages;
  • Open Graph locale and alternate-locale metadata.

Alternate targets follow the same translated-page-or-language-home fallback as the visible selector. Use a correct production baseURL; subpath deployments are supported and must not be replaced by hardcoded absolute paths in layouts.

Right-to-left languages

Set direction: rtl on an RTL language:

YAML
languages:
  ar:
    label: العربية
    locale: ar
    direction: rtl
    weight: 4

The theme loads its committed local Bootstrap RTL artifact and uses logical CSS properties in its own shell. LTR and RTL sites use the same command:

BASH
hugo --gc --minify

Consumer sites do not install RTLCSS, PostCSS, or npm. Test actual RTL content, navigation, code, tables, diagrams, and mixed-direction strings rather than assuming stylesheet selection is sufficient.

UI translation bundles

Theme UI strings live in i18n/. OINK includes English, Simplified Chinese, Traditional Chinese, and other inherited bundles. A site can override only the strings it needs by creating its own i18n/<language>.yaml; remaining values fall back to the theme bundle.

During translation work, run:

BASH
hugo server --printI18nWarnings

Contribute generally useful translations to the theme. Keep product-specific language in the site bundle.

Search by language

With offlineSearch: true, OINK generates a separate same-origin index for each language. The Simplified Chinese index uses the theme’s CJK fallback. Search results stay within the active language.

Verify that both offline-search-index.en.json and offline-search-index.zh.json are generated, contain the expected pages, and resolve under the deployed baseURL.

Translation checklist

  • Every source page in the supported scope has a .zh.md peer.
  • Front matter identity and route metadata match.
  • Visible prose, UI strings, alternative text, and metadata are translated.
  • Every translated Markdown heading has an explicit stable ID.
  • English and Chinese rendered heading ID lists match.
  • Internal links and fragments resolve in both languages.
  • Navigation, breadcrumbs, previous/next links, and search stay in language.
  • Dates, punctuation, spacing, and technical terminology follow the target language’s editorial conventions.
  • The production build emits correct canonical and alternate metadata.

For Hugo’s underlying model, see Multilingual mode.

4.2 - Search

Configure local multilingual search or an explicit online provider.

OINK’s default and recommended search is local. Hugo generates a per-language index; the theme serves Lunr and its CJK fallback from same-origin assets. The site can build and search without a public crawler, external account, CDN, or network connection.

Google Custom Search and Algolia DocSearch remain compatible online integrations. They are disabled by default and should be enabled only when the site accepts their external requests, indexing, availability, and privacy boundaries.

Only one search implementation can be active at a time.

Local search with Lunr

Enable local search in hugo.yaml:

YAML
params:
  offlineSearch: true

Do not configure gcs_engine_id or params.search.algolia at the same time. After a production build, the output contains one index per language, for example:

TEXT
offline-search-index.en.json
offline-search-index.zh.json

The browser loads the active language’s index and displays results without leaving the page. Chinese content uses OINK’s CJK fallback instead of depending on whitespace tokenization.

Build the index before testing

Run a normal build before starting a preview:

BASH
hugo --gc
hugo server --disableFastRender

If the server was already running when the index changed, restart it. On a subpath deployment, confirm that the browser requests the index under the configured baseURL rather than from the domain root.

Configure result summaries and limits

Set the summary length and maximum result count:

YAML
params:
  offlineSearch: true
  offlineSearchSummaryLength: 120
  offlineSearchMaxResults: 12

Choose limits that keep the search dialog responsive on mobile devices. The summary is a discovery aid, not a replacement for a well-written page description.

Exclude a page

Set exclude_search: true in page front matter:

YAML
---
title: Internal index
exclude_search: true
---

Use this for utility, duplicate, generated, or test pages. Do not exclude a page only because its current translation is incomplete; fix the translation instead.

Style the result panel

The result panel grows with its content. A site can constrain it in assets/scss/_styles_project.scss:

SCSS
.td-offline-search-results {
  max-width: 46rem;
}

Preserve keyboard focus, visible selection, mobile width, and dark-mode contrast when overriding search styles.

Search entry points

OINK exposes search from the branded shell and can also show a sidebar input. To hide the sidebar input while retaining the main search entry, configure:

YAML
params:
  ui:
    sidebar_search_disable: true

The shell’s open and close controls expose their dialog relationship and state to assistive technology. A custom implementation must preserve those semantics.

Search stays in the active language. Verify that:

  • every published language has its own index;
  • translated titles, descriptions, and body text appear in that index;
  • a result URL contains the correct language prefix;
  • English results do not replace Chinese results through content fallback;
  • the language selector on a result page reaches the corresponding translation or the documented language-home fallback.

For Chinese search failures, inspect the generated Chinese JSON before changing tokenization. A missing or English-only index is usually a content or build configuration problem.

Google Custom Search Engine (GCSE) searches a public site through Google’s index. It requires a deployed, crawlable production site and sends queries to a third-party service.

After creating an engine in Google Programmable Search, add a search result page:

YAML
---
title: Search results
layout: search
---

Then configure its engine ID:

YAML
params:
  gcs_engine_id: YOUR_ENGINE_ID
  offlineSearch: false

The Google search dark-mode compatibility layer is opt-in. Import it from the consuming site’s assets/scss/_styles_project.scss when GCSE is enabled:

SCSS
@import 'td/gcs-search-dark';

Create a translated result page for every supported language and use a language-appropriate engine configuration when needed. Removing gcs_engine_id disables GCSE.

Document the external request and privacy implications in the consuming site’s policy. GCSE is not available in an air-gapped deployment.

Algolia DocSearch (optional)

Algolia DocSearch provides a hosted crawler and interactive result panel for eligible public documentation sites. Obtain the project’s application ID, search API key, and index name, then configure:

YAML
params:
  offlineSearch: false
  search:
    algolia:
      appId: YOUR_APP_ID
      apiKey: YOUR_SEARCH_API_KEY
      indexName: YOUR_INDEX_NAME

Use a search-only public key, never an administrative key. Keep crawler rules, language facets, index updates, and external-service disclosure with the site configuration. This integration is intentionally separate from the local-first default.

The theme partials layouts/_partials/algolia/head.html and layouts/_partials/algolia/scripts.html can be overridden for a site-specific integration. An empty override disables that theme partial.

If none of the supported choices fits, a site can replace the search input, result behavior, and styles. Reuse the shell’s dialog and accessibility contracts where possible. Keep custom code at the site layer unless it is provider-neutral and reusable across multiple products.

A custom online provider must be opt-in and document its network, privacy, indexing, failure, and offline behavior. A custom local provider must publish all runtime assets from the site or theme and respect language and baseURL boundaries.

4.3 - Versioning

Link documentation versions and mark archived releases.

Depending on your project’s releases and versioning, you may want to let your users access previous versions of your documentation. How you deploy the previous versions is up to you. This page describes the Oink features that you can use to provide navigation between the various versions of your docs and to display an information banner on the archived sites.

Adding a version drop-down menu

If you add some [params.versions] in hugo.toml/hugo.yaml/hugo.json, the Oink adds a version selector drop-down to the navbar. You specify a URL and a name for each version you would like to add to the menu, as in the following example:

Configuration file:
hugo.toml
TOML

# Add your release versions here
[[params.versions]]
  version = "master"
  url = "https://master.kubeflow.org"

[[params.versions]]
  version = "v0.2"
  url = "https://v0-2.kubeflow.org"

[[params.versions]]
  version = "v0.3"
  url = "https://v0-3.kubeflow.org"
hugo.yaml
YAML

params:
  versions:
    - version: master
      url: 'https://master.kubeflow.org'
    - version: v0.2
      url: 'https://v0-2.kubeflow.org'
    - version: v0.3
      url: 'https://v0-3.kubeflow.org'
hugo.json
JSON

{
  "params": {
    "versions": [
      {
        "version": "master",
        "url": "https://master.kubeflow.org"
      },
      {
        "version": "v0.2",
        "url": "https://v0-2.kubeflow.org"
      },
      {
        "version": "v0.3",
        "url": "https://v0-3.kubeflow.org"
      }
    ]
  }
}

Remember to add your current version so that users can navigate back!

The default title for the version drop-down menu is Releases. To change the title, change the site parameter version_menu in hugo.toml/hugo.yaml/hugo.json:

Configuration file:
hugo.toml
TOML

[params]
version_menu = "Releases"
hugo.yaml
YAML

params:
  version_menu: Releases
hugo.json
JSON

{
  "params": {
    "version_menu": "Releases"
  }
}

If you set the version_menu_pagelinks parameter to true, then links in the version drop-down menu point to the current page in the other version, instead of the main page. This can be useful if the document doesn’t change much between the different versions. Note that if the current page doesn’t exist in the other version, the link will be broken.

You can also configure individual menu entries:

  • Use name instead of version when the menu label is not a version number.
  • Set name to --- to add a menu separator.
  • Omit url to render a disabled text item, such as a group heading.
  • Set kind to add a kind-specific class for styling. For details, see Navigation and menus.
  • Set pagelinks: false on an entry to link to that version’s main URL even when the global version_menu_pagelinks parameter is true.

For example:

YAML
params:
  version_menu: v1.2
  version_menu_pagelinks: true
  versions:
    - name: '**Versions**'
    - version: v1.3-dev
      kind: next
      url: https://next.example.com
    - version: v1.2
      kind: latest
      url: https://docs.example.com
    - name: ---
    - name: Preview variant
      kind: home
      pagelinks: false
      url: https://preview.example.com

To learn more about Oink menus, see Navigation and menus.

Displaying a banner on archived doc sites

If you create archived snapshots for older versions of your docs, you can add a note at the top of every page in the archived docs to let readers know that they’re seeing an unmaintained snapshot and give them a link to the latest version.

For example, see the archived docs for Kubeflow v0.6:

A text box explaining that this is an unmaintained snapshot of the docs.
Figure 1. The banner on the archived docs for Kubeflow v0.6

To add the banner to your doc site, make the following changes in your hugo.toml/hugo.yaml/hugo.json file:

  1. Set the site parameter archived_version to true:

    Configuration file:
    hugo.toml
    TOML
    
    [params]
    archived_version = true
    hugo.yaml
    YAML
    
    params:
      archived_version: true
    hugo.json
    JSON
    
    {
      "params": {
        "archived_version": true
      }
    }
  2. Set the site parameter version to the version of the archived doc set. For example, if the archived docs are for version 0.1:

    Configuration file:
    hugo.toml
    TOML
    
    [params]
    version = "0.1"
    hugo.yaml
    YAML
    
    params:
      version: 0.1
    hugo.json
    JSON
    
    {
      "params": {
        "version": "0.1"
      }
    }
  3. Make sure that site parameter url_latest_version contains the URL of the website that you want to point readers to. In most cases, this should be the URL of the latest version of your docs:

    Configuration file:
    hugo.toml
    TOML
    
    [params]
    url_latest_version = "https://your-latest-doc-site.com"
    hugo.yaml
    YAML
    
    params:
      url_latest_version: https://your-latest-doc-site.com
    hugo.json
    JSON
    
    {
      "params": {
        "url_latest_version": "https://your-latest-doc-site.com"
      }
    }

4.4 - Advanced customization

Customize typography and scoped visual styles through Docsy-compatible Sass inputs and OINK semantic tokens.

OINK provides a small, layered customization contract instead of requiring a site to copy component selectors or fork the theme. Everything still passes through Hugo Extended and Hugo Pipes: no Node.js, npm, PostCSS, remote font service, or client-side preset loader is required.

This page documents the supported extension points and the first public semantic-token slice: typography. For general color-mode, code-highlighting, navbar, and template examples, see Look and feel.

Choose the right layer

Use the highest-level interface that can express the change:

Layer Extension point Best for
Hugo configuration hugo.yaml or page front matter Supported choices such as typography preset and page width
Sass foundation assets/scss/_variables_project.scss Existing Docsy and Bootstrap variables that affect the whole bundle
Sass after Bootstrap assets/scss/_variables_project_after_bs.scss Rare overrides that depend on Bootstrap variables or maps
Semantic CSS assets/scss/_styles_project.scss Role tokens, a content type, one page family, or one component
Hugo templates layouts/ and partial hooks Structural or DOM changes that CSS cannot represent

Start with configuration or an established Sass variable. Use a semantic CSS property when a decision needs a narrower scope. Override a selector only when there is no suitable token, and override a template only when the structure itself must change.

Do not edit files inside the theme, Bootstrap, Font Awesome, or bundled font directories for site branding. Those edits are difficult to audit and will be lost or conflict during an upgrade.

How the CSS contract is layered

OINK keeps the dependency direction one-way:

TEXT
Docsy / Bootstrap Sass variables
Bootstrap --bs-* properties
OINK semantic --td-* roles
component aliases and selectors

Existing Docsy and Bootstrap variables remain the foundation. OINK adds a semantic role only where components need a shared meaning, such as “article body” or “technical metadata”. Bootstrap properties never point back to OINK roles, which avoids custom-property cycles.

The following API status is intentional:

  • Established Docsy and Bootstrap variables remain compatible inputs where practical.
  • The typography roles documented below are public site-customization APIs.
  • A documented component alias, such as --td-asciinema-font-family, has a narrower component-specific contract.
  • Undocumented --td-shell-* and selector-local properties are implementation details. Do not assume every property with a --td-* prefix is public.

Typography presets

Choose a built-in site-wide preset in hugo.yaml:

YAML
params:
  ui:
    typography:
      preset: technical # technical | system
Preset Result Font requests
technical Default OINK treatment, including Chakra Petch display text and IBM Plex Mono technical text Uses locally bundled files only
system Platform sans and monospace stacks, suitable for a neutral base or the smallest text-font footprint Does not request OINK’s bundled text faces unless project CSS explicitly references one

OINK writes the resolved value to data-td-typography on the <html> element. An unsupported value stops the Hugo build instead of silently falling back. This is a build-time site choice, not a JavaScript-powered reader preference.

The preset selects defaults; project Sass and CSS remain authoritative. For example, a site that explicitly references IBM Plex Mono can still request it while using the system preset.

Public font roles

Components consume roles rather than naming a brand font directly:

CSS property Controls Default source
--td-ui-font-family Navigation, controls, search, and general chrome Bootstrap body font
--td-body-font-family Documentation and blog prose UI role
--td-heading-font-family Article headings $headings-font-family, or body role
--td-code-font-family code, pre, kbd, samp, and terminal content $font-family-code
--td-display-font-family Wordmarks and display titles Chakra Petch, then UI role
--td-meta-font-family Technical labels and metadata IBM Plex Mono, then code role
--td-print-font-family Printed body copy and default print headings $td-google-font-name, then Bootstrap body font

Change the broad semantic role in most cases. For example, Asciinema consumes --td-asciinema-font-family, which defaults to --td-code-font-family. Override the component alias only when terminal playback should deliberately differ from all other code.

Reuse Docsy and Bootstrap Sass variables

OINK interprets established names instead of adding parallel Sass knobs:

Existing variable OINK interpretation
$td-fonts-serif, $font-family-sans-serif, $font-family-base Bootstrap body font, then UI and body roles
$headings-font-family Heading role when explicitly set
$td-font-family-monospace, $font-family-monospace Bootstrap monospace foundation
$font-family-code Code role and ordinary code elements
$td-google-font-name Default print face

Put these compile-time overrides in _variables_project.scss, just as in Docsy:

SCSS
// assets/scss/_variables_project.scss
$font-family-sans-serif: 'Noto Sans SC', 'PingFang SC', system-ui, sans-serif;
$headings-font-family: $font-family-sans-serif;
$font-family-monospace:
  'Sarasa Mono SC', 'Cascadia Code', ui-monospace, monospace;
$font-family-code: $font-family-monospace;

This changes the compiled defaults. Use semantic CSS properties in _styles_project.scss when different areas of one site need different treatments.

Add a site-owned font

Store reviewed .woff2 files under the consuming site’s static/webfonts/ directory, then declare and assign the face in _styles_project.scss:

SCSS
// assets/scss/_styles_project.scss
@font-face {
  font-family: 'My Sans';
  font-display: swap;
  font-style: normal;
  font-weight: 400 800;
  src: url('../webfonts/my-sans-variable.woff2') format('woff2');
}

:root {
  --td-ui-font-family:
    'My Sans', 'Noto Sans SC', 'PingFang SC', system-ui, sans-serif;
  --td-body-font-family: var(--td-ui-font-family);
  --td-heading-font-family: var(--td-ui-font-family);
  --td-display-font-family: var(--td-heading-font-family);
}

For code, include an explicit CJK fallback when content can contain Chinese:

SCSS
:root {
  --td-code-font-family:
    'My Mono', 'Sarasa Mono SC', 'Noto Sans Mono CJK SC', monospace;
}

Subset and self-host fonts, include every script the site needs, use font-display: swap, and record the license. OINK intentionally does not accept arbitrary font URLs or CSS strings through YAML.

Scope styles by content type

Semantic properties inherit, so a content-specific treatment does not require a second stylesheet or copied component rules. Blog pages already carry the td-blog body class:

SCSS
// assets/scss/_styles_project.scss
body.td-blog {
  --td-body-font-family: 'My Serif', 'Noto Serif SC', serif;
  --td-heading-font-family: var(--td-body-font-family);
}

OINK also adds td-swagger to Swagger/OpenAPI pages. For a project-defined page family, set body_class in front matter or a section cascade:

YAML
---
body_class: code-reference
page_width: wide
---
SCSS
body.code-reference {
  --td-meta-font-family: var(--td-code-font-family);
}

Use a semantic, site-owned class. Never put untrusted content in body_class.

Keep layout and typography independent

Typography, article width, and component structure are separate axes. OINK’s existing page_width parameter supports normal, wide, and full, globally or in page front matter:

YAML
params:
  page_width: normal

This provides a clean foundation for distinct experiences without one all-purpose preset:

Experience Recommended composition
Standard documentation page_width: normal plus the site typography preset
Full-width canvas page_width: full plus a page-specific body_class if needed
Blog or editorial reading td-blog scoped body and heading roles, usually with normal width
Code-heavy reference A project body class that adjusts code and metadata roles
OpenAPI reference The Swagger layout and td-swagger; keep renderer-specific structure in its layout

Keeping these concerns independent prevents preset multiplication and lets a future editorial, API, or code treatment reuse the same roles.

Colors and component surfaces

Use Bootstrap variables such as $primary, $secondary, and $danger for the compiled palette. At runtime, prefer Bootstrap semantic properties such as --bs-body-bg, --bs-body-color, --bs-link-color, and --bs-border-color.

OINK also documents a small brand layer, including --td-brand-elev, --td-brand-silk, --td-brand-copper, --td-brand-header-bg, and --td-brand-mark-gradient. Override light and dark values as a pair. The Look and feel page shows the complete pattern.

Do not globally override a shell or component token merely because its current name looks convenient. First change its Bootstrap or documented semantic source; use a component alias only when that component must diverge.

Review checklist

Before shipping a customization:

  1. Build with the oldest and newest supported Hugo Extended versions.
  2. Check light, dark, print, forced-colors, and reduced-motion behavior.
  3. Review documentation, blog, code, search, and any OpenAPI pages in scope.
  4. Test narrow and wide viewports, including long CJK text and code lines.
  5. Confirm font requests are local, intentional, licensed, and no larger than necessary.
  6. Prefer one semantic override over repeated selector patches.

These rules preserve OINK’s central constraint: a customized site remains a single-Hugo-binary project with no additional build or runtime dependency.

4.5 - Apache ECharts

Build responsive, local-first charts from structured JSON or YAML.

The echarts shortcode renders an Apache ECharts options object with the versioned runtime bundled by Oink. Hugo parses JSON or YAML at build time, serializes the result into the page, and loads ECharts only on pages that use the component.

Use ECharts for quantitative charts whose axes, encodings, tooltips, or series need more control than a diagram or table provides. Keep a nearby textual summary so the conclusion does not depend on color, pointer interaction, or JavaScript.

Quick start

GO-HTML-TEMPLATE
{{< echarts height="300px" >}}
xAxis:
  type: category
  data: [Draft, Review, Publish]
yAxis:
  type: value
series:
  - type: bar
    data: [12, 9, 4]
{{< /echarts >}}

The example shows 12 draft pages, nine pages in review, and four pages ready to publish.

How Oink loads a chart

The shortcode creates a unique chart container and stores the parsed options in an application/json element. The page includes the local ECharts runtime and Oink initializer once, even when it contains several charts.

If theme is not set, Oink initializes the chart for the current site color mode and redraws it when the reader changes modes. A ResizeObserver resizes the chart with its container. Setting an explicit ECharts theme opts out of automatic site-theme switching for that chart.

Shortcode parameters

Parameter Default Behavior
height 400px Accepts a nonnegative number with px, rem, em, vh, vw, or %
theme unset Uses a named ECharts theme; when unset, follows the site’s light or dark color mode
full false Set to true to remove Oink’s normal content-width clamp

Invalid height values fail the Hugo build. The shortcode body must decode to an ECharts options object; malformed JSON or YAML also fails at build time instead of creating a blank chart silently.

Choose a guide

  • Chart gallery demonstrates datasets, bars, lines, areas, pies, scatter plots, legends, and visual encodings.
  • Callbacks and trusted code explains formatter functions, data-dependent styles, the $fn:name bridge, and its security boundary.

Start with declarative JSON or YAML. Add JavaScript callbacks only when the ECharts option cannot be expressed as data.

Authoring checklist

  • State the chart’s conclusion and data scope in prose.
  • Label axes, units, series, and time ranges explicitly.
  • Do not use color as the only way to distinguish important values.
  • Keep legends and tooltips readable in both site color modes.
  • Test the chart at narrow widths and with long translated labels.
  • Prefer a shared dataset when several series use the same records.
  • Record the data source and observation date for nonillustrative data.
  • Avoid animation when it does not help comprehension, and respect reduced motion for custom effects.

Further reference

OINK documents its wrapper and delivery behavior; the full options schema belongs to Apache ECharts. Use the ECharts concepts handbook, dataset guide, and option reference for chart-specific settings. The theme’s VENDOR.json records the exact runtime version and license shipped by a release.

4.5.1 - ECharts gallery

Copy practical declarative ECharts patterns for documentation pages.

These examples use only structured YAML. They require no callback code and therefore stay within the simplest ECharts authoring and review boundary. The numbers are illustrative.

Reuse a dataset

ECharts dataset keeps records separate from their visual encoding. Series can refer to dimensions by name, which is easier to review than repeating parallel arrays.

Bar chart from a dataset

GO-HTML-TEMPLATE
{{< echarts height="320px" >}}
dataset:
  source:
    - [stage, minutes]
    - [Draft, 18]
    - [Review, 11]
    - [Publish, 4]
xAxis: { type: category }
yAxis: { type: value, name: Minutes }
series:
  - type: bar
    encode: { x: stage, y: minutes }
{{< /echarts >}}

The example shows the median duration falling from 18 minutes for drafting to four minutes for publication.

Line and area comparison

Use a shared category axis when several series describe the same intervals. The area fill emphasizes volume; the lines preserve the individual trends.

GO-HTML-TEMPLATE
{{< echarts height="340px" >}}
tooltip: { trigger: axis }
legend: { data: [English, Chinese] }
xAxis:
  type: category
  data: [Mon, Tue, Wed, Thu, Fri]
yAxis: { type: value, name: Pages }
series:
  - name: English
    type: line
    smooth: true
    areaStyle: { opacity: 0.12 }
    data: [5, 8, 7, 11, 13]
  - name: Chinese
    type: line
    smooth: true
    areaStyle: { opacity: 0.12 }
    data: [4, 6, 8, 9, 13]
{{< /echarts >}}

Both language queues reach 13 reviewed pages on Friday; the Chinese queue catches up after starting one page lower.

Donut breakdown

A donut works for a small part-to-whole comparison. Keep the categories few, show labels directly, and provide the totals in text.

GO-HTML-TEMPLATE
{{< echarts height="340px" >}}
tooltip: { trigger: item }
legend: { bottom: 0 }
series:
  - name: Documentation pages
    type: pie
    radius: [42%, 68%]
    avoidLabelOverlap: true
    label: { formatter: "{b}: {c}" }
    data:
      - { name: Guides, value: 28 }
      - { name: Reference, value: 17 }
      - { name: Tutorials, value: 11 }
      - { name: Concepts, value: 8 }
{{< /echarts >}}

The 64-page set contains 28 guides, 17 reference pages, 11 tutorials, and eight concept pages.

Scatter plot with visual encoding

visualMap can encode a third dimension without callback code. The following plot maps build size to point size and build status to color.

GO-HTML-TEMPLATE
{{< echarts height="360px" >}}
tooltip: { trigger: item }
xAxis: { type: value, name: Build seconds }
yAxis: { type: value, name: Pages }
visualMap:
  - type: continuous
    dimension: 2
    min: 10
    max: 50
    inRange: { symbolSize: [10, 32], color: ["#60a5fa", "#f97316"] }
    right: 0
    top: middle
series:
  - type: scatter
    encode: { x: 0, y: 1, tooltip: [0, 1, 2] }
    data:
      - [1.8, 24, 12]
      - [2.6, 41, 22]
      - [3.9, 67, 35]
      - [5.1, 92, 48]
{{< /echarts >}}

Larger sites take longer to build in this illustrative sample; point size and color both encode the third value so color is not the only cue.

Production notes

Keep example data close to the chart only when it is small and editorial. For larger or generated datasets, produce the options during the site’s content pipeline and review the resulting page source. Oink does not fetch chart data from a remote endpoint automatically; adding a network request is an explicit site integration and changes the local-first and privacy boundary.

4.5.2 - ECharts callbacks and trusted code

Use reviewed formatter and styling functions when structured options are not enough.

Most ECharts options should remain declarative JSON or YAML. Some valid options, including custom formatters and data-dependent styles, require functions. Oink supports those cases through fenced JavaScript blocks and $fn:name references.

Trusted-author boundary

Callback code runs in every visitor’s browser with the page’s origin and normal JavaScript privileges. Oink safely serializes structured chart options, but it does not sandbox author-supplied callbacks. Only trusted project authors should add or review them.

Callbacks can also change a site’s Content Security Policy requirements because the shortcode emits an inline registration script. Prefer declarative options when they can express the same behavior.

Register and reference functions

Place one or more js or javascript fences inside the shortcode. Declare each function with a named var, let, const, or function declaration, then refer to it from YAML or JSON as $fn:name.

GO-HTML-TEMPLATE
{{< echarts height="320px" >}}
```js
var formatMinutes = function (value) {
  return value + ' min';
};
```

```yaml
yAxis:
  type: value
  axisLabel: { formatter: $fn:formatMinutes }
```
{{< /echarts >}}

Oink removes the JavaScript fences before parsing the remaining options, registers the named functions, and replaces $fn:name values before calling chart.setOption().

Example: labels and colors

The following chart formats duration labels and highlights the slowest stage. Its data says writing takes 18 minutes, review takes 11, and publication takes four.

Callback checklist

  • Keep functions deterministic and limited to chart presentation.
  • Do not read cookies, credentials, storage, or unrelated page content.
  • Do not fetch remote data from a formatter or style callback.
  • Use a unique, descriptive function name on pages with several charts.
  • Treat code copied from an external example as source code that requires review and license checking.
  • Exercise callbacks with missing, null, string, and numeric values as appropriate.
  • Test both site color modes, narrow layouts, printing, and reduced motion.

Troubleshooting

If a $fn:name value remains unresolved, verify that the spelling matches a named declaration inside the same page and that the fence language is js or javascript. Anonymous expressions that are not assigned to a name cannot be registered.

If Hugo fails before rendering, reduce the body to valid JSON or YAML first, then add one callback. A browser console error means the structured options parsed successfully but callback execution or an ECharts option still needs inspection.

4.6 - Infographics with AntV

Turn concise declarative data into local SVG infographics.

The infographic shortcode renders the AntV Infographic DSL with the versioned runtime bundled by Oink. Use it for processes, timelines, cycles, funnels, roadmaps, and compact visual summaries where a statistical chart would be too literal.

The DSL is serialized as data, not inserted as arbitrary HTML or executable code. The browser runtime turns it into SVG and loads only on pages that use the shortcode.

Quick start

GO-HTML-TEMPLATE
{{< infographic >}}
infographic list-row-simple-horizontal-arrow
data
  title Documentation workflow
  items
    - label Draft
      desc Write the first version
    - label Review
      desc Check facts and language
    - label Publish
      desc Build and verify the site
{{< /infographic >}}

The same three steps appear below. Drafting creates the first version, review checks facts and language, and publication builds and verifies the site.

Syntax anatomy

An infographic normally contains:

  1. infographic TEMPLATE, which selects a built-in AntV template;
  2. a data block with an optional title and desc;
  3. an items list with label, desc, optional value, and optional nested children fields;
  4. an optional theme block for a built-in theme or explicit colors.

Indentation defines structure. Keep labels short, use descriptions for context, and choose a template whose visual relationship matches the prose. A decorative sequence is not a substitute for an actual hierarchy or comparison.

Shortcode parameters

Parameter Default Behavior
height auto Accepts auto or a nonnegative number with px, rem, em, vh, vw, or %
full false Set to true to remove Oink’s normal content-width clamp

Invalid height values and an empty DSL body fail the Hugo build. DSL schema or template errors are reported by the browser runtime in the infographic container.

AntV themes belong to the DSL rather than the shortcode parameters. They do not automatically follow Oink’s site color mode, so verify foreground, background, and surrounding-page contrast in both modes.

Choose a guide

The AntV package contains many templates. Start with the smallest visual form that clarifies the relationship, not the most decorative form available.

Authoring and accessibility

  • Summarize the same conclusion in ordinary text before or after the graphic.
  • Keep the reading order meaningful and labels concise.
  • Do not use color or shape as the only carrier of status.
  • Check long translated labels, narrow screens, printing, and both site color modes.
  • Avoid remote image or icon identifiers in a local-first page unless their network and license boundary has been reviewed explicitly.
  • Record the source and date when values are not illustrative.

SVG improves visual fidelity, but it does not guarantee that every template exposes the same semantic structure as native headings, lists, and tables. Essential instructions must remain available in adjacent prose.

Further reference

OINK documents its shortcode and delivery boundary. For the full DSL, template gallery, and theme model, use the AntV Infographic documentation, gallery, and source repository. The Oink theme’s VENDOR.json records the exact bundled version, checksum, and MIT license file.

4.6.1 - Processes, timelines, and cycles

Match sequential information to horizontal, chronological, and circular templates.

Sequence templates answer different questions. A horizontal process emphasizes ordered handoffs, a timeline emphasizes chronology, and a cycle emphasizes that the last stage feeds the first again. The surrounding prose must state which relationship matters.

Horizontal process

Use list-row-simple-horizontal-arrow for a short left-to-right sequence. On narrow screens, keep labels brief and verify that the rendered order remains clear.

GO-HTML-TEMPLATE
{{< infographic >}}
infographic list-row-simple-horizontal-arrow
data
  title Documentation delivery
  items
    - label Plan
      desc Define the reader and outcome
    - label Write
      desc Draft the smallest complete page
    - label Review
      desc Check facts, language, and links
    - label Ship
      desc Build and verify the hosted route
{{< /infographic >}}

The process moves from planning through writing and review to a separately verified hosted result.

Chronological timeline

Use sequence-timeline-simple when time or release order is the primary relationship.

GO-HTML-TEMPLATE
{{< infographic >}}
infographic sequence-timeline-simple
data
  title Release evidence
  items
    - label Source ready
      desc Scope, copy, attribution, and review are complete
    - label Checks pass
      desc Theme and project-site suites pass
    - label Tag public
      desc The immutable module version resolves
    - label Site deployed
      desc Production routes pass smoke tests
{{< /infographic >}}

The timeline separates four evidence points; a passing test does not skip the public-tag or deployment stages.

Continuous cycle

Use sequence-circular-simple only when the final item genuinely returns work to the first. Do not use a cycle for a process that has a terminal state.

GO-HTML-TEMPLATE
{{< infographic height="480px" >}}
infographic sequence-circular-simple
data
  title Documentation maintenance loop
  items
    - label Observe
      desc Collect support and search signals
    - label Prioritize
      desc Select a reader problem
    - label Improve
      desc Update content and examples
    - label Verify
      desc Test links, rendering, and outcomes
{{< /infographic >}}

Verification produces new observations, so the maintenance loop returns to its first stage.

Selection rule

If removing the arrows or time axis would not change the meaning, use a native list or cards instead. Infographics should reveal a relationship, not decorate an otherwise unrelated set of statements.

4.6.2 - Infographic layouts, funnels, and themes

Present grouped, narrowing, and stylized information without custom JavaScript.

AntV templates combine a structure with item and title treatments. Changing the template changes the implied relationship, so review meaning before appearance. The examples below use flat items data and no remote icons.

Grid of grouped facts

Use list-grid-badge-card for peer facts that share one topic but have no required order.

GO-HTML-TEMPLATE
{{< infographic >}}
infographic list-grid-badge-card
data
  title Documentation quality gates
  items
    - label Accuracy
      desc Commands and versions match the product
    - label Coverage
      desc Required concepts and tasks are present
    - label Language
      desc English and Chinese remain equivalent
    - label Delivery
      desc The hosted route matches the reviewed source
{{< /infographic >}}

The four gates are peers. None should be drawn as a prerequisite for another.

Narrowing funnel

Use sequence-funnel-simple when each stage intentionally reduces a population. Include value fields and repeat the numbers in prose.

GO-HTML-TEMPLATE
{{< infographic height="460px" >}}
infographic sequence-funnel-simple
data
  title Documentation review funnel
  items
    - label Drafted
      value 40
      desc Pages submitted
    - label Fact checked
      value 34
      desc Commands and claims verified
    - label Language reviewed
      value 31
      desc English and Chinese aligned
    - label Published
      value 28
      desc Hosted pages verified
{{< /infographic >}}

Forty drafted pages become 34 fact-checked pages, 31 language-reviewed pages, and 28 verified published pages.

Built-in hand-drawn theme

Themes change styling, not data meaning. The hand-drawn theme is useful for informal planning material; a custom primary color can still align it with the site.

GO-HTML-TEMPLATE
{{< infographic >}}
infographic sequence-stairs-front-simple
data
  title From notes to maintained documentation
  items
    - label Capture
      desc Record the observed behavior
    - label Explain
      desc Add context and reader intent
    - label Verify
      desc Test examples and links
    - label Maintain
      desc Assign an owner and update path
theme hand-drawn
  colorPrimary #2563eb
{{< /infographic >}}

Choose a template family

Relationship Useful starting templates
Ordered handoff list-row-simple-horizontal-arrow, sequence-steps-simple
Chronology or roadmap sequence-timeline-simple, sequence-roadmap-vertical-simple
Repeating loop sequence-circular-simple, sequence-circle-arrows-indexed-card
Peer facts list-grid-badge-card, list-grid-compact-card
Progressive reduction sequence-funnel-simple, sequence-pyramid-simple
Hierarchy hierarchy-tree-*, hierarchy-mindmap-*

Template availability belongs to the bundled AntV version. Before adopting a less common template, render it with realistic English and Chinese content and pin the Oink release whose VENDOR.json provides it.

Layout checklist

  • Keep peer labels grammatically parallel.
  • Use value only when it has a defined unit or meaning.
  • Avoid a fixed height that clips translated text.
  • Use full=true only when the surrounding page and print layout need it.
  • Verify template meaning, contrast, overflow, and reading order separately.
  • Keep remote icon and image references out of network-isolated documentation.

4.7 - Comments with giscus

Add GitHub-backed comments with giscus.

OINK supports giscus through a Hextra-compatible comments configuration under params.comments. giscus gives each content page a comment thread backed by GitHub Discussions and lets readers comment through GitHub OAuth.

How giscus works

When a page loads, giscus searches the configured repository for a Discussion that matches the page. If it does not find one, the giscus bot creates it when a reader submits the first comment or reaction. Maintainers moderate comments in GitHub Discussions.

Anyone can read a public thread. To comment, a reader selects Sign in with GitHub and authorizes the giscus app to post on their behalf. OINK never asks for or stores the reader’s GitHub password or access token.

Prepare GitHub

Before configuring OINK:

  1. Use a public GitHub repository for the comment threads. Visitors cannot read Discussions in a private repository.
  2. Enable GitHub Discussions under the repository’s Settings > Features.
  3. Install the giscus GitHub App for that repository. Without the app, visitors cannot comment or react.
  4. Choose a Discussion category. giscus recommends an Announcements category so that only maintainers and the giscus bot can create new Discussions.

The repository ID and category ID are public identifiers, not credentials. Do not add a GitHub personal access token, OAuth secret, or password to Hugo configuration.

Generate repository settings

Open giscus.app and complete its configuration form:

  1. Select the interface language.
  2. Enter the repository as OWNER/REPOSITORY and wait for the validation to succeed.
  3. Select the page-to-Discussion mapping. pathname is OINK’s default.
  4. Choose the Discussion category and optional features.
  5. Locate the generated <script> block.

Copy these generated values into OINK configuration:

Generated attribute OINK key
data-repo repo
data-repo-id repoId
data-category category
data-category-id categoryId

Choose a stable mapping

The mapping determines which Discussion belongs to each page. pathname is a good default when published paths are stable and the same repository serves multiple domains or preview environments.

Changing mapping, moving a page, or changing its permanent URL can make giscus look for a different Discussion. Choose the mapping before collecting comments and preserve redirects or Discussion titles during a migration. Enable strict matching when similar page paths could otherwise select the wrong thread.

Enable comments site-wide

Add the generated identifiers to the consuming site’s hugo.yml and set enable: true:

YAML
params:
  comments:
    enable: true
    type: giscus
    giscus:
      repo: OWNER/REPOSITORY
      repoId: REPOSITORY_ID
      category: Announcements
      categoryId: CATEGORY_ID
      mapping: pathname
      strict: 0
      reactionsEnabled: 1
      emitMetadata: 0
      inputPosition: top
      theme: auto
      loading: lazy

Replace all uppercase placeholders with the exact values generated by giscus.app. OINK requires repo, repoId, category, and categoryId before it renders giscus. Missing or blank required values produce a Hugo warning and skip giscus instead of failing the build.

Configuration reference

Key Default Purpose
enable false Enables the configured comment provider globally.
type giscus Selects giscus. Other provider names are not supported.
repo Public repository in OWNER/REPOSITORY form.
repoId Repository node ID generated by giscus.app.
category GitHub Discussions category name.
categoryId Category node ID generated by giscus.app.
mapping pathname Maps the current page to a Discussion.
term Supplies the term required by mappings such as specific or number.
strict 0 Uses strict Discussion-title matching when set to 1.
reactionsEnabled 1 Shows reactions for the Discussion’s main post.
emitMetadata 0 Sends Discussion metadata messages to the parent page.
inputPosition top Places the comment editor at top or bottom.
theme auto Follows the OINK theme, or selects a built-in/custom giscus theme.
lang Page language Overrides the automatically selected giscus interface language.
loading lazy Defers iframe loading until the reader approaches the comments.
ariaLabel Comments Labels the comments region for assistive technology.
errorMessage Load-error text Replaces the message shown when giscus cannot load.

Boolean-like feature values accept YAML booleans or giscus-style 0 and 1 values.

Locale, theme, and accessible text

OINK selects the giscus locale from the active Hugo language. Simplified, Traditional, and Hong Kong Chinese map to the corresponding giscus locales; unsupported languages fall back to English. Set lang only when the automatic choice is not appropriate.

With theme: auto, the iframe follows OINK’s light/dark selector and the browser’s preferred color scheme. A built-in giscus theme name or custom theme URL disables that automatic switch.

For a multilingual site, localize the comments-region label and load-error text under each language’s parameters. Language parameters merge with the global repository settings:

YAML
languages:
  en:
    params:
      comments:
        giscus:
          ariaLabel: Comments
          errorMessage: Comments could not be loaded.
  zh:
    params:
      comments:
        giscus:
          ariaLabel: 评论
          errorMessage: 评论加载失败。

Override one page

The comments front matter field overrides the global switch in either direction.

Enable one page

Keep the complete repository configuration in hugo.yml, leave the global switch off, and opt in selected pages:

YAML
---
title: Community design notes
comments: true
---

Disable one page

When comments are enabled globally, opt out pages that should remain static:

YAML
---
title: Security policy
comments: false
---

An explicit comments: false suppresses both giscus and legacy Disqus on that page.

Coexist with Disqus

OINK keeps existing Hugo Disqus configuration compatible during migration. When valid giscus configuration is active for a page, OINK suppresses Disqus so that only one comment system renders. If giscus is enabled but its required settings are incomplete, OINK warns, skips giscus, and can leave configured Disqus as a fallback.

Remove the Disqus service configuration after the migration is complete and every intended page uses giscus.

Content Security Policy

A strict Content Security Policy must permit giscus in both script-src and frame-src. Merge these sources into the site’s existing policy instead of replacing its other directives:

TEXT
script-src 'self' https://giscus.app;
frame-src 'self' https://giscus.app;

OINK’s initializer remains a same-origin bundled asset and is included only on pages where giscus is active. If the external script fails or does not create an iframe, OINK clears the loading state and exposes errorMessage in a live status region.

Verify the integration

  1. Build the site and confirm there is no missing-key warning:

    BASH
    hugo --minify
  2. Start a local preview and open a page where comments should be active:

    BASH
    hugo server --disableFastRender
  3. Confirm that the giscus iframe shows Sign in with GitHub and uses the active page language.

  4. Toggle OINK between light and dark themes and confirm that the comment widget follows it when theme: auto.

  5. Open a page with comments: false and confirm that it has no giscus or Disqus widget.

  6. Submit one test comment, then confirm that the expected Discussion appears in the configured category and can be moderated on GitHub.

A browser-console message saying that the Discussion was not found is expected before the first comment or reaction creates it.

Troubleshooting

  • The build warns about missing keys: regenerate the configuration at giscus.app and copy all four required identifiers without renaming them.
  • The widget does not appear: check params.comments.enable, params.comments.type, the page’s comments front matter, and Hugo’s warning output.
  • GitHub sign-in or posting fails: confirm that the repository is public, Discussions are enabled, and the giscus GitHub App is installed for the repository.
  • The browser blocks giscus: inspect the console and response headers, then allow https://giscus.app in the applicable CSP directives.
  • An existing thread is not found: restore the original mapping and page path, or rename/migrate the Discussion deliberately before changing the URL.
  • The interface language is wrong: verify the Hugo language name and locale, or set params.comments.giscus.lang explicitly.

4.8 - Analytics, user feedback, and SEO

Configure analytics, feedback, and search metadata.

OINK does not contact analytics, form, comment, or advertising services by default. These integrations are site decisions: enable them explicitly, document the data boundary, and provide any consent or policy required by the site’s users and jurisdiction.

Adding analytics

Hugo provides embedded templates for analytics services. When a site configures Google Analytics, browser usage information such as page views and custom events is sent to Google. This is incompatible with a fully air-gapped runtime and may be incompatible with a strict same-origin Content Security Policy.

Setup

Obtain a Google Analytics measurement ID for the site, then use Hugo’s current service configuration:

YAML
services:
  googleAnalytics:
    id: G-YOUR-ID

Do not also set the deprecated top-level googleAnalytics key. Analytics are normally emitted only for a production Hugo environment. Build a production preview and inspect its HTML and browser network log before publication.

If analytics is disabled, OINK emits no Google Analytics request. Remove the configuration entirely rather than inserting a fake identifier.

User feedback

OINK can show a “Was this page helpful?” widget at the bottom of documentation pages. The widget presents Yes and No actions and then displays a configured response, usually with a link to open a documentation issue.

The page asks whether it was helpful and offers Yes and No buttons.
Figure 1. The page feedback widget

The response can remain useful without analytics: it can direct the reader to an issue template, discussion, email address, or another site-owned feedback channel. Collection and event reporting happen only when the site configures an appropriate destination.

How feedback data is useful

Combine feedback with context instead of treating one score as proof. Pages with high traffic and repeated negative feedback are useful review candidates; highly rated pages can reveal patterns worth testing elsewhere.

Make focused editorial changes when possible. For example, update one stale tutorial, or move a code example earlier on a small group of pages, then compare feedback over an appropriate period. Record releases, traffic shifts, support events, and other factors that could explain the change.

Feedback is directional evidence, not a substitute for user research, accessibility review, support data, or technical validation.

Setup

OINK keeps the widget off by default. Set the global default and configure localized responses. For English:

YAML
params:
  ui:
    feedback:
      enable: false
languages:
  en:
    params:
      ui:
        feedback:
          yes: >-
            Glad to hear it! Please <a
            href="https://github.com/OWNER/REPOSITORY/issues/new">tell us how we
            can improve</a>.
          no: >-
            Sorry to hear that. Please <a
            href="https://github.com/OWNER/REPOSITORY/issues/new">tell us how we
            can improve</a>.

For Simplified Chinese, put translated strings in languages.zh.params:

YAML
languages:
  zh:
    params:
      ui:
        feedback:
          yes: >-
            很高兴本页对你有帮助!欢迎<a
            href="https://github.com/OWNER/REPOSITORY/issues/new">告诉我们如何继续改进</a>。
          no: >-
            很抱歉本页没有解决问题。请<a
            href="https://github.com/OWNER/REPOSITORY/issues/new">告诉我们缺少什么</a>。

Visible response HTML is trusted site configuration. Keep it small, review its links, and do not interpolate untrusted values.

When Google Analytics is configured, the widget can emit a custom page_helpful event. A positive action uses params.ui.feedback.max_value (100 by default); a negative action uses 0.

Access feedback data

For Google Analytics, inspect the page_helpful event in the provider’s events report and create a page-level report when needed. An absent event may mean no interaction occurred, analytics was blocked or disabled, consent was not given, or the selected time range is wrong.

Do not enable analytics solely to make the widget visible. A site can keep the response-and-link experience while leaving event collection disabled.

Override feedback on one page

Set feedback in page front matter. The page value overrides the global default in either direction:

YAML
---
title: Feedback example
feedback: true
---

Use feedback: false to hide the widget on a page when the global default is enabled. For compatibility, hide_feedback: true also hides it when feedback is not set.

Set the default for all pages

Set the site parameter. OINK defaults it to false; set it to true only when most documentation pages should show the widget:

YAML
params:
  ui:
    feedback:
      enable: false

Add a contact form with Fabform

Fabform and similar hosted form endpoints are optional online services. After creating an account and reviewing its data handling, a site can post a form to its assigned endpoint:

HTML
<form action="https://fabform.io/f/{form-id}" method="post">
  <label for="email">Your email</label>
  <input id="email" name="email" type="email" autocomplete="email" />
  <button type="submit">Submit</button>
</form>

Replace {form-id}, translate the visible labels, add a privacy notice, and provide error and success states. The form will not work offline. A local or first-party endpoint is preferable when the site must keep submissions within its own boundary.

Search engine optimization metadata

For each page, OINK chooses the HTML meta description from the first available value:

  1. the page’s description front matter field;
  2. Hugo’s computed page summary for non-index pages;
  3. the site description in params.

Write a concise, page-specific description in every language. Do not copy the English description into a Chinese page. Search metadata cannot compensate for thin, duplicated, or inaccurate content.

The theme also emits canonical and alternate-language links from Hugo’s page translations. Use a correct production baseURL, stable translated routes, and explicit translated heading IDs. Add other meta tags through the site’s layouts/_partials/hooks/head-end.html override only when they are not already provided by the theme.

See Hugo’s Google Analytics configuration, page summaries, and Google’s SEO starter guide for the underlying service and content concepts.

4.9 - Repository links and page information

Help readers inspect, edit, and report issues against page source.

OINK’s documentation and blog layouts can show links to the current page’s source repository:

  • View page source opens the source file.
  • Edit this page opens an editable source view.
  • Create child page starts a new file below the current page and can use the site’s assets/stubs/new-page-template.md template.
  • Create documentation issue opens an issue against the documentation repository with page context.
  • Create project issue optionally targets a separate product repository.

The built-in URL patterns target GitHub-style repositories. Verify every action when using another compatible host, and override the relevant partial for a different URL scheme.

A typical site configuration is:

YAML
params:
  github_repo: https://github.com/OWNER/DOCS
  github_project_repo: https://github.com/OWNER/PRODUCT
  github_branch: main
  github_subdir: site

The values can be set globally, per language, in a section cascade, or in page front matter when content comes from more than one repository.

github_repo

The documentation source repository URL. It drives view, edit, child-page, and documentation-issue links:

YAML
params:
  github_repo: https://github.com/pgsty/oink

Omit it to suppress repository-derived page actions. Do not point it at the theme repository when the page source actually lives in a consuming site.

github_subdir (optional)

Set the path from the repository root to the Hugo site source. This project stores its site in oink.pgsty.com:

YAML
params:
  github_subdir: oink.pgsty.com

The value is a repository path, not a local absolute path and not the content directory itself unless that is the actual site root.

github_project_repo (optional)

Set a separate product repository to show Create project issue:

YAML
params:
  github_project_repo: https://github.com/OWNER/PRODUCT

Use the documentation repository for content defects and the product repository for behavior discussed by the page. If that distinction is not clear to readers, omit the second link.

github_branch (optional)

Set the branch used by source and edit URLs:

YAML
params:
  github_branch: main

This is normally the site’s source branch. It is not necessarily the deployed branch, generated Pages branch, or theme revision.

path_base_for_github_subdir (optional)

Use a section cascade when a subtree is mounted from another repository. The path base is removed before the remaining content path is appended to github_subdir:

YAML
---
title: Imported reference
cascade:
  github_repo: https://github.com/OWNER/UPSTREAM
  github_project_repo: https://github.com/OWNER/UPSTREAM
  github_subdir: docs
  path_base_for_github_subdir: content/reference
---

For a source page at content/reference/api/client.md, this configuration maps the repository path to docs/api/client.md.

path_base_for_github_subdir can be a regular expression. A language-directory site might use:

YAML
path_base_for_github_subdir: content/\w+/reference

OINK’s colocated .md / .zh.md layout normally uses the same static base for both languages and does not need the language component in this expression.

When the source file has another name, use a from and to mapping. This example maps a section _index.md to an upstream README.md:

YAML
path_base_for_github_subdir:
  from: content/reference/(.*?)/_index.md
  to: $1/README.md

Test view and edit links from a leaf page, a section page, and both language versions. A regular expression that removes too much can produce a plausible but incorrect repository URL.

github_url (optional)

A legacy page can set a complete custom edit URL in front matter:

YAML
---
title: Imported page
github_url: https://github.com/OWNER/UPSTREAM/edit/main/README.md
---

Pages using this value expose only Edit this page. A site-specific template override is preferable when the destination is not GitHub-compatible.

Each action has a stable CSS class:

Link Class
View page source .td-page-meta__view
Edit this page .td-page-meta__edit
Create child page .td-page-meta__child
Create documentation issue .td-page-meta__issue
Create project issue .td-page-meta__project-issue

Hide an action in assets/scss/_styles_project.scss when the destination does not support it:

SCSS
.td-page-meta__child {
  display: none;
}

Prefer omitting an unavailable global destination in configuration. CSS hiding is useful for selective policy; it does not make a malformed link correct.

Last-modified page metadata

Enable Hugo Git information and configure the source repository:

YAML
enableGitInfo: true
params:
  github_repo: https://github.com/OWNER/DOCS

OINK can then show the last commit date, subject, hash, and source link on documentation and blog pages. CI must fetch enough Git history for the current file; shallow checkouts can produce missing or misleading metadata.

To hide the note for a particular site or section, override its style or the responsible page-meta partial. Do not label a file “last modified” from the build timestamp when Git history is unavailable.

4.10 - AI-agent support

Expose Markdown and discovery metadata to AI agents and tools.

Features

When your site opts in, these are the user-facing and machine-readable behaviors Oink enables:

  • Markdown output format support. Your project’s outputs configuration controls which page kinds publish Markdown.
  • Discovery: page HTML headers include rel="alternate" links to the Markdown version of the page.
  • View Markdown: page meta area includes a View Markdown link to the Markdown version of the page.
  • Open in ChatGPT / Claude: documentation page actions can hand the current browser URL to either assistant. Oink builds the prompt only when the reader activates the link, preserving the deployed host, query string, and fragment.
  • llms.txt: site-root file listing.

The remainder of this page explains how to enable each feature, and discusses validation and metrics supported with examples.

Enable Markdown output

Hugo comes with several built-in output formats, including markdown. To enable Markdown output, add markdown to the Hugo outputs configuration for the page kinds you want to support. For example:

Configuration file:
hugo.yaml
YAML
outputs:
  home: [HTML, markdown]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]
hugo.toml
TOML
[outputs]
home = [ "HTML", "markdown" ]
page = [ "HTML", "markdown" ]
section = [ "HTML", "RSS", "print", "markdown" ]
hugo.json
JSON
{
  "outputs": {
    "home": ["HTML", "markdown"],
    "page": ["HTML", "markdown"],
    "section": ["HTML", "RSS", "print", "markdown"]
  }
}

Opt pages out

To opt pages out of Markdown output, set outputs in page front matter to HTML only, or whatever your page’s default output formats are while excluding markdown. For example:

YAML
---
title: HTML-only test page
outputs: [HTML]
---
...

Enable llms.txt

The llms.txt format is a simple text format for listing machine-readable links to site content. It is designed to be easy for agents to discover and parse, and to complement the richer but more complex Markdown outputs. To learn more, see llmstxt.org.

Oink generates llms.txt at the site root, and includes links to the home page, main menu pages, and Markdown alternates where they exist. To enable it, add LLMS to the Hugo outputs configuration for the home page. For example:

YAML
outputs:
  home: [HTML, markdown, LLMS]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]

For an example of the generated llms.txt for this site, see /llms.txt.

Customize output

Oink renders Markdown output via layouts/all.html and generates llms.txt via layouts/index.llms.txt. You can override these defaults at several levels:

  • Per kind — Add templates such as home.md or _default/single.md under layouts/ in your project to tailor Markdown output for specific Hugo kinds.
  • Per shortcode — Add output-format-specific shortcode templates to project-local shortcodes so they emit Markdown-friendly content when appropriate.
  • Per page — Provide page-specific content or structure for high-value pages that need a curated agent-facing view.

Server-side support

While outside the scope of the theme, sites can facilitate agent discovery and access to Markdown content by implementing server-side content negotiation. For example, honoring Accept: text/markdown on the same URL as HTML.

Validation and metrics

We use AFDocs to assess basic structural support for agent-facing content, and to validate that generated outputs meet the configured checks. We also encourage sites to implement their own monitoring and metrics on agent access patterns—for example logging requests to Markdown URLs or llms.txt, and collecting metrics on their use. For details, see Agent-support checks.

The oink.pgsty.com project contains AFDocs configuration and npm scripts so maintainers can score a deployed URL against checks that overlap with Oink’s agent-support goals, including Markdown URLs, llms.txt, and related categories.

Scorecard examples

For scorecard examples, see:

  • OpenTelemetry agent score online report

  • An AFDocs scorecard for this site:

    oink.pgsty.com scorecard
    TEXT

    Running in oink.pgsty.com…

    Agent-Friendly Docs Scorecard

    http://localhost:1313 · 4/26/2026, 5:43:59 AM

    Overall Score: 100 / 100 (A+)

    Category Scores: Content Discoverability 100 / 100 (A+) Markdown Availability 100 / 100 (A+) Page Size and Truncation Risk 100 / 100 (A+) Content Structure 100 / 100 (A+) URL Stability and Redirects 100 / 100 (A+) Observability and Content Health 100 / 100 (A+) Authentication and Access 100 / 100 (A+)

    Check Results:

    Content Discoverability
        PASS  llms-txt-exists                llms.txt found at 1 location(s)
        PASS  llms-txt-valid                 llms.txt follows the proposed structure (H1, blockquote, heading-delimited link sections)
        PASS  llms-txt-size                  llms.txt is 1,131 characters (under 50,000 threshold)
        PASS  llms-txt-links-resolve         All 13 same-origin links resolve (13 total links)
        PASS  llms-txt-links-markdown        13/13 same-origin links point to markdown content (100%)
        PASS  llms-txt-directive             llms.txt directive found in all 13 pages, near the top of content
      
      Markdown Availability
        PASS  markdown-url-support           13/13 pages support .md URLs (100%)
        PASS  content-negotiation            13/13 pages support content negotiation (100%)
      
      Page Size and Truncation Risk
        PASS  rendering-strategy             All 13 pages contain server-rendered content
        PASS  page-size-markdown             All 13 pages under 50K chars (median 2K, max 9K)
        PASS  page-size-html                 All 13 pages convert under 50K chars (median 2K, 0% boilerplate)
      
      Content Structure
        PASS  tabbed-content-serialization   No tabbed content detected across 13 pages
        PASS  section-header-quality         No tabbed content found; header quality check not applicable
        PASS  markdown-code-fence-validity   All 1 code fences properly closed across 14 pages
      
      URL Stability and Redirects
        PASS  http-status-codes              All 13 pages return proper error codes for bad URLs
        PASS  redirect-behavior              No redirects detected across 13 pages
      
      Observability and Content Health
        PASS  cache-header-hygiene           All 14 endpoints have appropriate cache headers
      
      Authentication and Access
        PASS  auth-gate-detection            All 13 pages are publicly accessible
        SKIP  auth-alternative-access        All docs pages are publicly accessible; no alternative access paths needed
      

    Full spec: https://agentdocsspec.com/spec/

For details on how these checks are configured, see Agent-support checks.


  1. This is contrary to the documented Hugo behavior for front-matter configuration, but it is confirmed with our testing as of Hugo 0.158.0. ↩︎

4.11 - Print support

Configure printable pages and whole-section print output.

Individual documentation pages print well from most browsers as the layouts have been styled to omit navigational chrome from the printed output.

On some sites, it can be useful to enable a “print entire section” feature (as seen in this user guide). Selecting this option renders the entire current top-level section (such as Advanced for this page) with all of its child pages and sections in a format suited to printing, complete with a table of contents for the section.

To enable this feature, add the “print” output format in your site’s hugo.toml/hugo.yaml/hugo.json file for the “section” type:

Configuration file:
hugo.toml
TOML

[outputs]
section = [ "HTML", "RSS", "print" ]
hugo.yaml
YAML

outputs:
  section:
    - HTML
    - RSS
    - print
hugo.json
JSON

{
  "outputs": {
    "section": [
      "HTML",
      "RSS",
      "print"
    ]
  }
}

The site should then show a “Print entire section” link in the right hand navigation.

Further Customization

Disabling the ToC

To disable showing the table of contents in the printable view, set the disable_toc param to true, either in the page front matter, or in hugo.toml/hugo.yaml/hugo.json:

Front matter:
FRONT MATTER:
toml
TOML

+++

disable_toc = true

+++
yaml
YAML

---

disable_toc: true

---
json
JSON

{
  …,
  "disable_toc": true,
  
}
Config file:
hugo.toml
TOML

[params.print]
disable_toc = true
hugo.yaml
YAML

params:
  print:
    disable_toc: true
hugo.json
JSON

{
  "params": {
    "print": {
      "disable_toc": true
    }
  }
}

Layout hooks

A number of layout partials and hooks are defined that can be used to customize the printed format. These can be found in layouts/_partials/print.

Hooks can be defined on a per-type basis. For example, you may want to customize the layouts of heading for “blog” pages vs “docs”. This can be achieved by creating layouts/_partials/print/page-heading-<type>.html such as page-heading-blog.html. It defaults to using the page title and description as a heading.

Similarly, the formatting for each page can be customized by creating layouts/_partials/print/content-<type>.html.

5 - Deployment

Build Oink once, publish its static output, and verify the result.

OINK deployment has two separate stages: Hugo builds a complete public/ directory, then a static host publishes that directory. Keep build verification and hosted verification distinct so a successful local command is not mistaken for a completed production release.

Production build

Run the pinned Hugo Extended version from the site root:

BASH
hugo --gc --minify --cleanDestinationDir

--gc removes unused cached resources, --minify produces production assets, and --cleanDestinationDir removes files left by an earlier build. Review the command before using the last option when publishDir points anywhere other than the site’s dedicated output directory.

The build must finish without warnings that hide missing content, endpoints, or resources. Inspect public/ locally before uploading it.

Local preview

For editing:

BASH
hugo server --disableFastRender

Hugo’s development server proves that the source can render. It is not a production host and its live-reload behavior is not part of the generated site. Always run a clean production build before release.

Build environments and indexing

The plain hugo command uses the production environment. Oink marks HTML from production builds as indexable and uses optimized, fingerprinted assets. For a public preview that search engines must not index, build with another environment:

BASH
hugo --environment preview --baseURL "https://preview.example.com/"

Oink emits noindex, nofollow outside production. A hosting-level X-Robots-Tag header is useful defense in depth, especially for non-HTML files. Rebuild the reviewed source in production before publishing it at the canonical URL; a preview artifact is not a production artifact.

Static hosting

Any host that can serve directories and files can publish OINK:

  • object storage and a CDN;
  • GitHub Pages, GitLab Pages, or similar Git-driven static hosting;
  • Netlify, Cloudflare Pages, or another build-and-publish platform;
  • an Nginx, Caddy, Apache, or internal file server.

Set baseURL to the canonical production URL. If the site is published below a path such as https://example.com/manual/, include that path and test it; OINK’s local assets and component URLs are designed to preserve subpath deployments.

Cloudflare Pages

Connect Pages directly to the source branch. OINK does not require a GitHub Actions workflow that prebuilds and pushes an orphan Pages branch.

Use these settings for an Oink site:

Setting Value
Production branch main, or the reviewed source branch
Root directory the standalone site directory
Build command hugo --gc --minify
Build output directory public
HUGO_VERSION 0.164.0
SKIP_DEPENDENCY_INSTALL 1

As of 2026-08-08, Cloudflare Pages’ v3 build image documents Hugo 0.147.7 as its default, below OINK’s minimum 0.160.1. Set HUGO_VERSION explicitly for both Production and Preview rather than relying on the moving platform default. SKIP_DEPENDENCY_INSTALL=1 prevents the platform’s generic dependency installer from adding a frontend installation step that the site does not need.

For previews that need the generated Pages URL as their canonical build URL:

BASH
hugo --gc --minify --baseURL "$CF_PAGES_URL"

Cloudflare documents public as Hugo’s standard output directory, the HUGO_VERSION override, and the CF_PAGES_URL base-URL pattern. Recheck the platform documentation when changing the build image or pinned Hugo version.

See the Cloudflare Hugo guide and Cloudflare build image reference.

Air-gap deployment

For a disconnected environment, transfer both the site source and a verified theme archive rather than depending on an initial Hugo Module download:

  1. verify the theme archive’s sidecar SHA-256 file;
  2. install a supported Hugo Extended binary inside the environment;
  3. extract the theme into the site’s themes/oink/ directory;
  4. set theme: oink and run hugo --gc --minify in the site;
  5. publish public/ to the internal static server.

Keep PlantUML and Diagrams.net disabled unless a reachable internal endpoint is configured. External links and embeds remain the content author’s responsibility.

Headers and caching

Fingerprint-bearing CSS and JavaScript can use long-lived immutable caching. HTML, search indexes, feeds, and sitemaps should use shorter caching or revalidation so a new deployment becomes visible promptly.

Hosts that recognize the _headers convention can use a site-owned static/_headers file. This is not a portable standard; review security headers against the site’s actual inline content and integrations.

Preview and production URLs

Canonical, hreflang, Open Graph, feeds, and absolute links depend on baseURL. A production build should use the production URL; a preview may use its temporary URL when link validation or social metadata needs to be accurate.

Do not publish a preview build to production without rebuilding against the canonical URL. Conversely, do not reject a preview merely because it contains the intentionally supplied preview host.

Deployment acceptance

Verify each layer independently:

Source and configuration

  • the expected commit and pinned theme version are present;
  • baseURL, languages, menus, repository metadata, and optional endpoints are correct;
  • no unpublished draft or secret enters the public content tree.

Build artifact

  • a clean production build succeeds with the pinned Hugo Extended version;
  • English, Chinese, feeds, sitemaps, search indexes, and 404.html are present;
  • local assets resolve under both root and configured subpath;
  • the artifact contains required license and attribution surfaces.

Hosted site

  • the production URL returns the new artifact;
  • canonical and language-alternate URLs use the production origin;
  • navigation, search, language switching, dark mode, print, and representative components work in a real browser;
  • redirects, custom headers, cache policy, and 404 handling behave as configured;
  • an air-gap claim is backed by a browser network audit.

A green build log completes only the artifact stage. Deployment is complete after the hosted checks pass.

Rollback

Keep the previous known-good static artifact or hosting deployment identifier. If a new release fails hosted validation, restore that artifact first, then diagnose source or platform behavior. Rebuilding an old source commit with a new unpinned toolchain is not equivalent to restoring the original artifact.

5.1 - Serving your site locally

Preview content locally with Hugo’s development server.

Depending on your deployment choice you may want to serve your site locally during development to preview content changes. To serve your site locally:

  1. Ensure you have an up to date local copy of your site files cloned from your repo.

  2. Ensure Hugo Extended and any source-fetch tools required by your chosen installation method are installed, as described in Prerequisites and installation. Node.js and PostCSS are not site-build prerequisites.

  3. Run the hugo server command in your site root. By default your site will be available at http://localhost:1313.

Now that you’re serving your site locally, Hugo will watch for changes to the content and automatically refresh your site. If you have more than one local git branch, when you switch between git branches the local website reflects the files in the current branch.

5.2 - Deployment on GitHub Pages

Build and publish an Oink site with GitHub Actions and Pages.

If your source is hosted on GitHub, GitHub Pages can build and publish the site with a single Actions workflow. The consuming site needs Hugo Extended but does not need Node.js, npm, PostCSS, or a generated deployment branch.

Project sites use a URL such as https://<OWNER>.github.io/<REPOSITORY>/; user and organization sites use https://<OWNER>.github.io/. Custom domains are also supported.

Prepare the repository

Push the complete site source to GitHub and confirm that this command succeeds from the repository root:

BASH
hugo --gc --minify

Set the site’s baseURL to its production URL, or pass the Pages URL with Hugo’s --baseURL option in the workflow. A project site must include the repository path; otherwise CSS, JavaScript, and other resources will resolve from the wrong location.

Add the Pages workflow

Create .github/workflows/pages.yml with the following contents. Keep HUGO_VERSION aligned with a version validated by the theme.

.github/workflows/pages.yml
YAML
 1name: Deploy Hugo site to Pages
 2
 3on:
 4  push:
 5    branches: [main]
 6  workflow_dispatch:
 7
 8permissions:
 9  contents: read
10  pages: write
11  id-token: write
12
13concurrency:
14  group: pages
15  cancel-in-progress: false
16
17env:
18  GO_VERSION: 1.25.5
19  HUGO_VERSION: 0.164.0
20
21jobs:
22  build:
23    runs-on: ubuntu-latest
24    steps:
25      - uses: actions/checkout@v7
26        with:
27          fetch-depth: 0
28          submodules: recursive
29      - uses: actions/setup-go@v6
30        with:
31          go-version: ${{ env.GO_VERSION }}
32      - name: Install Hugo Extended
33        run: |
34          curl -L -o hugo.deb \
35            "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
36          sudo dpkg -i hugo.deb
37      - uses: actions/configure-pages@v6
38        id: pages
39      - name: Build
40        run: >-
41          hugo --gc --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
42      - uses: actions/upload-pages-artifact@v5
43        with:
44          path: public
45
46  deploy:
47    environment:
48      name: github-pages
49      url: ${{ steps.deployment.outputs.page_url }}
50    runs-on: ubuntu-latest
51    needs: build
52    steps:
53      - name: Deploy
54        id: deployment
55        uses: actions/deploy-pages@v5

If the theme is installed as a Git submodule, submodules: recursive checks it out before Hugo runs. A complete offline archive can instead commit or restore the site-owned themes/oink/ directory as part of the repository or build input.

Enable GitHub Pages

In the repository settings, open Pages. Under Build and deployment, set Source to GitHub Actions. Push the workflow to main, then follow its first run in the repository’s Actions tab.

The workflow uploads only the generated public/ directory and publishes it through the Pages deployment API. It does not maintain a gh-pages branch.

For other authentication, domain, and permission options, see GitHub’s Pages documentation and Hugo’s GitHub hosting guide.

5.3 - Deployment on Cloudflare Pages

Build and publish an Oink site with Cloudflare Pages.

Cloudflare Pages can build an Oink site from a connected GitHub or GitLab repository and create preview deployments for reviewed branches. The consumer build runs Hugo Extended directly and does not need a frontend package install.

Configure the project

Import the repository in Workers & Pages, select the production branch, and use these settings:

Setting Value
Production branch main, or the reviewed source branch
Build command hugo --gc --minify
Build output directory public
HUGO_VERSION 0.164.0
SKIP_DEPENDENCY_INSTALL 1

Set HUGO_VERSION in both the Production and Preview environments. Cloudflare Pages’ v3 build image currently defaults to Hugo 0.147.7, which is below Oink’s minimum 0.160.1. Pinning the validated version prevents a build-image update from silently changing the toolchain. SKIP_DEPENDENCY_INSTALL=1 disables the generic dependency installation step that an Oink consumer does not use.

If the Hugo site lives below the repository root, set Root directory to that directory. The output value is relative to the selected root.

Set the base URL

Production builds should use the site’s canonical custom domain in baseURL. For a preview that needs its generated Pages URL in canonical and absolute links, use Cloudflare’s deployment URL:

BASH
hugo --gc --minify --baseURL "$CF_PAGES_URL"

Do not publish that preview artifact as production without rebuilding it for the canonical origin.

Deploy and verify

Save the configuration and inspect the first build log. A normal Oink consumer build should run Hugo without npm, PostCSS, Autoprefixer, or theme-owned CDN downloads. After deployment, verify:

  • the *.pages.dev preview or custom domain serves the expected commit;
  • English and translated routes use the intended canonical origin;
  • search, language switching, dark mode, print, and representative components work;
  • redirects, headers, custom domains, and 404 behavior match the Pages project configuration.

Cloudflare Git integration and Direct Upload are different project modes. Check the current Pages documentation before choosing a mode that must later support an external deployment pipeline.

5.4 - Deployment on Netlify

Deploying your OINK site on Netlify with Hugo alone.

Netlify can build a site from GitHub, GitLab, or Bitbucket and publish a preview for each pull request. An OINK consumer build runs Hugo Extended directly; it does not install Node.js packages or invoke PostCSS.

Configure the site

Push the complete source to your Git provider, import the repository in Netlify, and use these build settings:

Setting Value
Build command hugo --gc --minify
Publish directory public
HUGO_VERSION 0.164.0 or another theme-validated version

If Netlify detects package manifests that exist only for theme-maintainer tooling, disable automatic dependency installation for the site. They are not part of the consumer build contract.

For a theme installed as a Git submodule, enable recursive submodule checkout. For a Hugo module, Netlify also needs the normal Git and Go access required to download the pinned module on a clean build. A complete offline distribution uses the site-owned themes/oink/ directory and avoids that first-build download.

Keep configuration in the repository

The same settings can be committed as netlify.toml:

netlify.toml
TOML
[build]
command = "hugo --gc --minify"
publish = "public"

[build.environment]
HUGO_VERSION = "0.164.0"

Keep production and deploy-preview contexts on the same Hugo version unless a preview is intentionally testing an upgrade. If preview builds need their generated URL as the base URL, add Netlify’s deploy URL to the Hugo command for that context.

To prevent a non-production deployment from being indexed, use a non-production Hugo environment as described in Build environments and indexing.

After saving the settings, trigger a deploy and inspect the build log. A normal consumer build should show one Hugo command and no npm, PostCSS, Autoprefixer, CDN download, or build-time remote-resource step.

5.5 - Deployment with Amazon S3 and CloudFront

Publish Oink output with Amazon S3 and CloudFront.

There are several options for publishing your web site using Amazon Web Services. This section describes the most basic option, deploying your site using an S3 bucket and activating the CloudFront CDN (content delivery network) to speed up the delivery of your deployed contents.

  1. After your registration at AWS, create your S3 bucket, connect it with your domain, and add it to the CloudFront CDN. This blog post has all the details and provides easy to follow step-by-step instructions for the whole procedure.

  2. Download and install the latest version 2 of the AWS Command Line Interface (CLI). Then configure your CLI instance by issuing the command aws configure (make sure you have your AWS Access Key ID and your AWS Secret Access Key at hand):

    CONSOLE
    $ aws configure
    AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
    AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    Default region name [None]: eu-central-1
    Default output format [None]:
    
  3. Check the proper configuration of your AWS CLI by issuing the command aws s3 ls, this should output a list of your S3 bucket(s).

  1. Inside your hugo.toml/hugo.yaml/hugo.json, add a [deployment] section like this one:

    Configuration file:
    hugo.toml
    TOML
    
    [deployment]
    [[deployment.targets]]
    name = "aws"
    URL = "s3://www.your-domain.tld"
    cloudFrontDistributionID = "E9RZ8T1EXAMPLEID"
    hugo.yaml
    YAML
    
    deployment:
      targets:
        - name: aws
          URL: 's3://www.your-domain.tld'
          cloudFrontDistributionID: E9RZ8T1EXAMPLEID
    hugo.json
    JSON
    
    {
      "deployment": {
        "targets": [
          {
            "name": "aws",
            "URL": "s3://www.your-domain.tld",
            "cloudFrontDistributionID": "E9RZ8T1EXAMPLEID"
          }
        ]
      }
    }
  1. Run the command hugo --gc --minify to render the site’s assets into the public/ directory of your Hugo build environment.

  2. Use Hugo’s built-in deploy command to deploy the site to S3:

    CONSOLE
    hugo deploy
    Deploying to target "aws" (www.your-domain.tld)
    Identified 77 file(s) to upload, totaling 5.3 MB, and 0 file(s) to delete.
    Success!
    Invalidating CloudFront CDN...
    Success!
    

    As you can see, issuing the hugo deploy command automatically invalidates your CloudFront CDN cache.

  3. That’s all you need to do! From now on, you can easily deploy to your S3 bucket using Hugo’s built-in deploy command!

For more information about the Hugo deploy command, including command line options, see this synopsis. In particular, you may find the --maxDeletes int option or the --force option (which forces upload of all files) useful.

If S3 does not meet your needs, consider AWS Amplify Console. This is a more advanced continuous deployment (CD) platform with built-in support for the Hugo static site generator. A starter can be found in Hugo’s official docs.

6 - Upgrade

Upgrade Oink, Hugo Extended, or an existing Docsy site safely.

These pages describe the OINK update contract. A target release is the version that you are moving the site to. Read its release note before starting: it records breaking changes, required actions, and the validated Hugo range.

OINK consumer builds do not install Node.js packages. npm remains repository tooling for theme maintainers, not a site-update step.

Before you update

  • Work from a Git branch or another recoverable copy of the site.
  • Record the currently pinned theme revision and Hugo Extended version.
  • Build the current production site once so that new failures can be separated from pre-existing ones.
  • Read every release note between the current and target versions; do not skip intermediate migration actions.

Order of steps

Perform the update in this order:

  1. Update Hugo if the target release changes its supported range.
  2. Update the theme using the site’s installation mode.
  3. Review theme overrides.
  4. Check the site in development and production builds.

Update Hugo

Install a Hugo Extended version supported by the target release. Update the same version in local developer setup, CI, Cloudflare Pages, Netlify, container images, and any cache keys. Verify the selected binary before building:

BASH
hugo version

The current validation baseline is Hugo Extended 0.164.0; the theme’s current minimum is 0.160.1. A release note takes precedence if it changes either value.

Update the theme

Use the page matching the site’s installation mode:

For a released archive, replace the existing theme directory with the target archive only after preserving site-owned overrides. Verify its checksum and keep LICENSE, NOTICE, and VENDOR.json with the distribution.

Review theme overrides

If the site overrides theme files, compare each override with its new theme counterpart and port relevant changes. Check these directories:

  • assets/
  • i18n/
  • layouts/
  • static/

Remove an override when the theme now provides the same behavior. Keep site business components, product pages, and brand assets at the site layer.

Check your site

Run both a development preview and the exact production command. For the Hugo-only contract, the production build is:

BASH
hugo --gc --minify

Verify at least the following:

  • The build completes without errors, warnings, or deprecation notices.
  • English and Chinese home, documentation, ordinary blog, and release-note pages render.
  • Navigation, breadcrumbs, table of contents, stable heading links, and language switching resolve correctly.
  • Local search returns results in both languages.
  • Dark and light modes, mobile navigation, and print output remain usable.
  • Pages load only the local runtimes they use; default pages make no theme-owned third-party subresource requests.
  • Mermaid, KaTeX, Markmap, Swagger UI, Redoc, and any used content components still render.
  • Site-owned shortcodes and business pages remain intact.

Finally, run every release-specific check from the target release note.

6.1 - Upgrade the Oink Hugo Module

Upgrade a site that imports Oink as a pinned Hugo Module.

Pin a version

Production sites should import a release tag or immutable commit, never an unversioned branch. From the site root, update Oink to a specific ref:

BASH
hugo mod get github.com/pgsty/oink@THEME_REF
hugo mod tidy

Replace THEME_REF with the published root tag or commit named by the release.

Test a local checkout

To test the current OINK checkout without changing the committed module path, use an ignored Go workspace with the local theme checkout:

BASH
go work init .
go work edit -replace=github.com/pgsty/oink=/absolute/path/to/oink
export HUGO_MODULE_WORKSPACE=go.work
hugo --gc --minify

Keep go.work out of version control rather than committing a developer-specific absolute path.

Verify the resolved module

Inspect Hugo’s dependency graph:

BASH
hugo mod graph

Confirm that the theme resolves to the intended tag, commit, or local replacement. No hugo mod npm pack or npm install step is required for OINK: browser dependencies already ship with the theme.

Continue with Review theme overrides.

6.2 - Upgrade an Oink Git submodule or clone

Upgrade Oink when its source is a Git submodule or clone.

Use the procedure matching the installation: submodule or clone. Pin the target release tag or immutable commit in both cases.

Update the submodule

From the site root, fetch tags inside the theme and check out the target ref:

BASH
git -C themes/oink fetch --tags
git -C themes/oink checkout THEME_REF
git add themes/oink
git commit -m "Update OINK theme to THEME_REF"

Replace themes/oink if the site uses another directory name. The parent repository records the resulting submodule commit. Push that parent commit so CI and other contributors resolve the same source.

No npm installation is required. If the complete theme has nested source-only submodules for a particular release, initialize them according to that release’s notes; browser runtime assets in the OINK distribution are already present.

Update the clone

If the theme directory is a clone tracked or restored by the site, update it to the target ref:

BASH
git -C themes/oink fetch --tags
git -C themes/oink checkout THEME_REF

Commit, archive, or record the updated theme using the same reproducible method the site already uses. Do not leave production builds following main.

If the clone contains local changes, commit them on a branch before switching refs. Rebase or reapply them after the update and resolve conflicts explicitly. Prefer moving reusable changes upstream into OINK and keeping only true site-specific overrides in the consuming site.

Continue with Review theme overrides.

6.3 - Migrate from the Docsy npm package

Remove the upstream Docsy npm package from an Oink site.

The upstream @docsy/theme npm package is not an OINK distribution channel. OINK ships Bootstrap, Font Awesome, fonts, and browser runtimes directly with the theme so that consumer sites build with Hugo Extended alone.

Remove the npm theme integration

First select an OINK distribution: a versioned archive, Git submodule or clone, or the compatibility Hugo module. Make that theme available to Hugo and confirm that hugo --gc --minify can resolve it.

Then remove @docsy/theme and any dependencies used only to build Docsy assets from the site’s package.json. Remove npm mounts for Bootstrap and Font Awesome from Hugo configuration, along with PostCSS and Autoprefixer build steps that exist only for the old theme pipeline.

Do not delete application dependencies merely because they use npm. The Hugo-only contract covers the documentation theme; a site-owned application or business component may have a separate, intentional toolchain.

Verify the migration

Build from a clean checkout with Hugo Extended and no node_modules directory:

BASH
hugo --gc --minify

Check LTR and RTL pages if the site supports both. Verify local fonts and icons, search, diagrams, API documentation, and any migrated content components. Once the build is clean, remove obsolete lockfiles only if no site-owned tooling uses them.

Continue with Review theme overrides.

6.4 - Migrate an existing Docsy site

Replace copied Docsy shells while preserving site-owned behavior.

OINK is intended to replace copied common shells, runtimes, and shortcodes without forcing a bulk rewrite of ordinary content. A safe migration removes overrides by dependency, keeps product-specific behavior in the site, and validates a temporary copy before changing production.

Migration principles

  • Pin the target implementation; do not migrate production to an unversioned branch.
  • Inventory overrides before deleting them.
  • Remove common theme copies, not the site’s business logic.
  • Preserve content URLs, front matter, and shortcode behavior where the OINK API is compatible.
  • Make unsafe or online exceptions explicit and temporary.
  • Test build output, browser behavior, and hosted behavior as separate layers.

Pin the target

Pin a published tag in go.mod or use the complete versioned archive. During pre-release evaluation, a Hugo Module site can use an ignored Go workspace to resolve a local checkout without editing the committed module version:

BASH
go work init .
go work edit -replace=github.com/pgsty/oink=/absolute/path/to/oink
export HUGO_MODULE_WORKSPACE=go.work
hugo --gc --minify

The site’s hugo.yaml imports github.com/pgsty/oink; the workspace substitutes only the local checkout.

Inventory existing overrides

Group each site-level file into one of four classes:

Class Action
Exact or near-exact copy of common shell Remove after OINK validation
Reusable component now supplied by OINK Remove or mechanically rename
Narrow brand or product customization Keep, then reduce to the smallest hook
Business-specific data or interaction Keep in the site

Search layouts/, assets/, static/, configuration, and build workflows together. A copied shortcode often has a matching JavaScript bundle, stylesheet, vendor file, and CI install step.

Move configuration

Search and brand

Enable theme-owned local search and point the shell at the site’s own logo:

YAML
params:
  logo: img/product.svg
  offlineSearch: true

Continue using title, languages.*, github_repo, github_project_repo, github_branch, page_width, and ui.* in their existing semantic locations. Do not migrate values into an oink.* namespace.

Fonts

The legacy Sass switch $td-enable-google-fonts: true now selects the local Open Sans files shipped by OINK; it no longer requests Google Fonts. $td-web-font-path is not part of the current build. A site that needs another font must provide approved local assets and their licenses.

Remove common overrides

After a temporary build proves equivalence, remove site copies of:

  • layouts/baseof.html and common docs/blog baseof*.html files;
  • common navbar, footer, sidebar, table-of-contents, search, head-CSS partials, and their hooks;
  • old common branded documentation-shell partials;
  • asciinema, echarts, infographic, doc-carousel, details, tab/tabpane, card, and param shortcode copies;
  • JavaScript, Lunr copies, carousel code, and SCSS used only by those removed implementations;
  • consumer PostCSS and Autoprefixer steps that no remaining site asset needs.

Delete by reference, not with a blanket removal of layouts/. Home pages, download pages, and portals may still call local partials such as icons, search dialogs, blog rows, or tag filters.

Keep site-specific behavior

Keep content and code whose semantics belong to the product:

  • product matrices and compatibility data;
  • pricing, downloads, portals, solutions, and catalog pages;
  • site-specific home-page composition;
  • custom redirects, headers, analytics, or identity integrations;
  • content components that encode business data rather than reusable presentation.

For the Pigsty family, pgvers, pgext_matrix, pgext_os_matrix, home-docs, and the current metric implementation remain at the site layer.

Reference-site matrix

The current migration plan uses these boundaries:

Site Remove or migrate Keep
SILO Common docs/blog shell, core shortcodes, duplicate runtimes; set logo: img/silo.svg Home page, download page, product data
PGSTY Common shell and core shortcodes; set logo: img/logo/logo.svg Portal, solution, and enterprise pages
SOW Common docs/blog shell, core shortcodes, duplicate runtimes; set logo: img/sow.svg Home page and repository-specific content
Pigsty Common shell, core shortcodes, duplicate runtimes; set logo: icons/logo.svg and retain reviewed ECharts callbacks Extension matrices, home/pricing pages, catalog styles

The matrix is a starting inventory, not permission to delete every similarly named file. Resolve actual template references in the target checkout.

Rehearsal workflow

Rehearse each migration in a disposable copy of the consuming site. Apply the local Oink workspace, remove one planned override group, block unintended network and frontend-tool access, and run the production build:

BASH
HUGO_MODULE_WORKSPACE=go.work hugo --gc --minify

Do not modify the source workspace during a rehearsal. Retain failed copies for diagnosis and record the exact theme commit, Hugo version, removed files, and output counts.

Current evidence

The latest recorded rehearsal on 2026-08-08 used Hugo Extended 0.164.0:

Site Rehearsal result HTML files
SILO Removed 20 common overrides; built complete English and Chinese content with OINK shell, same-origin search, and site logo 1,095
PGSTY Removed 20 common overrides; built the bilingual portal and a temporary docs page for shell validation 16
SOW Removed 20 common overrides; built complete English and Chinese content with OINK shell, same-origin search, and site logo 128
Pigsty Removed 24 common overrides; kept three business matrix shortcodes and existing ECharts callbacks 2,473

These are temporary-copy build results, not proof that the four production sites have been migrated or deployed.

Production rollout

For each site:

  1. create a dedicated migration branch;
  2. pin the OINK candidate and record its source commit;
  3. remove one coherent override group at a time;
  4. run a clean Hugo-only build and focused automated tests;
  5. compare representative home, docs, blog, special, and 404 pages;
  6. check mobile navigation, both color modes, language switching, search, print, and the site’s retained business components;
  7. deploy a preview and verify its real URLs and requests;
  8. merge and deploy only after review, then perform production smoke tests.

Record deliberate differences instead of forcing pixel equality where OINK intentionally changes the shell.

Rollback

Keep the pre-migration theme pin, site commit, and known-good deployment artifact. A rollback should restore all three consistently. Reintroducing a random subset of copied layouts against the new theme creates a hybrid state that is harder to diagnose than either complete version.