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

Return to the regular view of this page.

Site configuration

Navigation menus, languages, versions, and repository links.

Once the site runs, this chapter covers site-level structural configuration: how readers navigate, how many languages the content has, how many versions the documentation carries, and which repository the page actions point at.

How to write individual pages is in Authoring; the available building blocks are in Components.

In this chapter

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
  copyright:
    authors: '[Example Authors](https://example.org/)'
    from_year: 2026
  footer_center_info: 'Powered by [Oink](https://oink.pgsty.com)'
  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 navbar, the sidebar drawer, and the footer. It accepts the same asset and static/ paths as params.logo. The compact navbar falls back to params.logo on its own, because a wordmark would consume the whole row; if wordmark is absent entirely, 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:
    navbar_enabled: true
    footer_style: fat # fat | slim | none
    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
    taxonomy_icons:
      categories: fa-solid fa-folder
      tags: fa-solid fa-tags
    page_context_menu:
      enable: true
      assistant_links: false
      links: []
    readingtime:
      enable: true

navbar_enabled and footer_style decide whether each page carries the site navbar and which footer shape it uses. Both default to on (true and fat), apply to every layout, and can be overridden per section through a cascade or per page in front matter; an unknown footer_style fails the build. See Navigation and menus and Site footer.

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. taxonomy_icons sets the right-rail group icon per plural taxonomy name, defaulting to a folder for categories, tags for tags, and a generic shape elsewhere.

The page actions are a split button in the breadcrumb row: the primary half copies the page’s Markdown, and its menu keeps Copy Markdown, the assistant links, View markdown, View edit history, Edit this page, Create child page, the documentation and project issues, and Print entire section reachable at every viewport width. Built-in Open in ChatGPT / Claude actions are disabled by default. Set assistant_links: true to show them on file-backed pages; when a reader activates one, the full current URL — including its query string and fragment — leaves the site inside a localized prompt. Oink does not upload the page body. Avoid secrets in URLs and disclose this third-party boundary. A page can override the site policy with boolean assistant_links front matter.

View edit history appears when github_repo can resolve the same repository path used by Edit this page. 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
      assistant_links: 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 site footer is no longer part of that file — it now renders on every layout and reads data/footer/<language>.yaml. See Site footer.

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.4.0 provides 21 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
pricing Product tiers, prices, features, and calls to action
pricing-compare Feature comparison matrices across pricing tiers
command-box A focused command with Copy support and an optional note
steps Ordered procedures with optional command examples
timeline Dated milestones, roadmaps, and release histories
code-plate Static code or line arrays in a presentation panel
case-study Evidence-led stories with metrics, quotation, and source
download Validated rolling and pinned download channels
bar-chart Comparable non-negative values rendered without chart JS

The homepage and regular layout: landing pages use the same registry. See Landing pages for data resolution, the nine scenario-oriented section contracts, no-JavaScript behavior, and output rules.

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,
      }

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.

Below the landing sections, every page ends with the same site footer: the column grid when footer_style is fat, then the bottom bar. Its left side retains Docsy’s params.copyright API: use a Markdown string or a map with authors, from_year, and to_year; when omitted, Hugo’s top-level copyright value is rendered as-is. OINK’s params.footer_center_info accepts inline Markdown, defaults to Powered by Oink, and can be set to an empty string to hide the center region. The right side contains the language controls.

A site that still keeps a footer block in data/home/<language>.yaml is read as before, but that data now feeds the footer on every page rather than the homepage alone. Move it to data/footer/<language>.yaml when convenient.

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/components/layout/#asciinema,
            }
          - {
              title: Mermaid,
              icon: fa-solid fa-share-nodes,
              url: docs/components/diagrams/#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
navbar_enabled: false
footer_style: slim
hide_feedback: true
hide_readingtime: true
ui:
  no_left_sidebar: false
  scrollSpy:
    disable: false
---

navbar_enabled and footer_style are read from front matter directly, not from a ui block, so a section can set them once in its cascade.

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.

2 - Navigation and menus

Configure navigation, language switching, sidebars, and outlines.

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

The navbar is built from Hugo’s main menu plus OINK-generated controls: the version selector, the language selector, the color-mode control, search, and the project repository link. It renders on every layout — landing pages, docs, blog, Swagger, and taxonomy pages alike — so the same site-level navigation is one click away from anywhere.

navbar_enabled defaults to true. Turn the navbar off for the whole site, for one section through a front-matter cascade, or for a single page:

hugo.yaml
YAML
params:
  ui:
    navbar_enabled: false
YAML
---
title: Standalone report
navbar_enabled: false
---

Front matter wins over the site parameter, and an explicit false is honored at every level. Without the navbar OINK restores the chrome the navbar replaced: the mobile subnav, the sidebar’s brand and search rows, the utility buttons on the TOC rail, and the sidebar footer utilities. Use it for a page that has to own the full viewport, not as a general layout preference.

The navbar has exactly two states:

Width State
lg and up Full: brand, menu labels, and the utility controls
Below lg Compact: the logo, then every item as a right-aligned icon

Compact is not a reduced menu. Menu entries keep their icons, search stays a magnifier, and the version, language, and theme controls stay where they are — nothing collapses into a hamburger, because there is no separate mobile menu to collapse into. The one width-gated control is on shell pages below md, where an extra icon opens the sidebar drawer.

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.

Nested dropdowns

Top-level menus support one level of dropdown. Use Hugo’s parent to establish the relationship:

hugo.yaml
YAML
menus:
  main:
    - identifier: docs
      name: Docs
      pageRef: /docs
      weight: 20
    - identifier: docs-tutorial
      parent: docs
      name: Get started
      pageRef: /docs/tutorial
      weight: 10
      params:
        icon: fa-solid fa-route
        description: Install OINK and build your first site

A child’s params.description renders under its title in the dropdown, helping a reader decide where to go.

One interaction detail matters: the parent is a plain link. Its panel opens on hover and on keyboard focus, and clicking or pressing Enter navigates to the parent page. There is no disclosure caret to press, and no state in which the parent page is captured by its own menu. Escape closes the panel and leaves focus on the link; touch users navigate straight to the parent page, which carries the same links in its own content.

Version menu

The selector appears when params.versions is configured. It is a branch icon that opens its list on hover or keyboard focus, sharing one popover style with the language and theme controls. 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, the language icon advances to the next language by weight on click, while hovering for half a second or focusing it 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 shows a theme control. Clicking it toggles light and dark; hovering or focusing it opens a System / Light / Dark picker, where System follows the reader’s operating system. See Light/dark-mode menu.

Search is a magnifier icon in the navbar. It opens the Command Palette, as do Cmd/Ctrl-K and, outside editable controls, /. The icon appears when offline search is enabled; with the navbar disabled the search row returns to the top of the sidebar. 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".

Below lg the icon is all that remains of a menu entry, so give every top-level entry a pre icon. An entry without one has nothing to show in the compact state.

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. Blog sections start expanded; set sidebar_expanded: false in a section’s front matter to start it collapsed.
  • 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
---

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

An icon on every leaf page produces noticeable visual noise. Control the density with sidebar_icon_policy:

hugo.yaml
YAML
params:
  ui:
    sidebar_icon_policy: groups # all | groups | none
Value Effect
all Every eligible sidebar entry shows its icon
groups Only roots and nodes with children show icons; plain leaves do not
none Sidebar entry icons are omitted entirely

The compatibility default for an unset value is all. New sites should set groups explicitly — it keeps the semantic marker on groups while removing the noise at leaf level. This site uses that setting.

An invalid value warns and falls back to all.

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.

The sidebar tree is rooted at the reader’s current top-level section, and the row above the tree names that root. A large sub-tree — a versioned API reference, a separate handbook — can become a root of its own so readers can switch into it without leaving the section:

YAML
params:
  ui:
    sidebar_root_enabled: true
    sidebar_root_menu: true

Then set a descendant 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. Rooted sections can nest, but redundant or invalid values produce build warnings.

The switcher is scoped to the current top-level section. Its entries are that section itself, which is the default, plus every descendant that sets sidebar_root_for: self. Sibling top-level sections are not listed — moving between Docs and Blog is the navbar’s job. A section with no switchable descendant therefore shows no dropdown at all: the row is a plain, unboxed link to the section landing page, flush with the tree’s top-level rows.

Taxonomy term pages have no content ancestry, so a term adopts the top section its members share. Following a tag from a docs page keeps the docs tree and the docs root link instead of falling back to the site-wide tree; a term whose members span several sections shows no root row.

Table of contents (TOC)

Hugo builds the right-side page outline from Markdown headings. OINK renders it as the first group in a fixed right rail, followed by the taxonomy clouds for the current section. Readers can collapse the rail; 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.

Right-rail groups

Every group in the rail uses the same header row: an icon, a title, and a chevron, with the whole row highlighting as one item. The outline group is titled Content, and its icon is a three-line glyph that collapses the rail rather than a decoration. In the sidebar drawer the same group keeps a static three-line icon so it reads like the taxonomy heads beside it.

Taxonomy group icons are configurable by plural taxonomy name:

hugo.yaml
YAML
params:
  ui:
    taxonomy_icons:
      categories: fa-solid fa-folder
      tags: fa-solid fa-tags
      projects: fa-solid fa-diagram-project

categories defaults to a folder and tags to tags; any other taxonomy gets a generic shapes glyph until it is named here. See Taxonomy support for how the clouds themselves are scoped.

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, and that row also carries the page actions. Top-level section pages keep their one-crumb breadcrumb so the row stays anchored at every depth. Disable breadcrumbs 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.

Page actions

The page actions are an icon-only split button at the end of the breadcrumb row. The primary half copies the page’s Markdown and flips to a green check on success; the caret opens a menu of ten actions in two halves. The reading half takes the page somewhere else:

  • Copy Markdown
  • Open in ChatGPT
  • Open in Claude
  • View markdown
  • View edit history

A separator follows, then the acting half, which changes or produces something:

  • Edit this page
  • Create child page
  • Create docs issue
  • Create project issue
  • Print entire section

Configured page_context_menu.links come last, after a second separator. Every entry appears only when it can resolve: the Markdown actions need the markdown output format, the repository actions need github_repo, the project issue needs github_project_repo, and the assistant actions need params.ui.page_context_menu.assistant_links.

On the blog root and its first-level sections the primary half is the RSS link instead of the copy control, and the menu still offers Copy Markdown. Blog leaf pages carry no feed icon. A page with no Markdown output drops the primary half and renders a labeled Actions button instead.

create_child_page, create_project_issue, and print_section are first-class registry actions, so they appear in the Command Palette too. The page-level print action is retired; readers use the browser’s own Cmd/Ctrl+P.

The footer renders on every layout and has three shapes, selected with footer_style:

Value Renders
fat The column grid above the copyright line (the default)
slim The copyright line only
none No footer at all
hugo.yaml
YAML
params:
  ui:
    footer_style: fat

Front matter — including a section cascade — overrides the site value:

YAML
---
title: Embedded reference
footer_style: slim
---

An unrecognized value fails the build instead of falling back silently.

The column grid reads data/footer/<language>.yaml, or data/footer.yaml on a single-language site:

data/footer/en.yaml
YAML
brand:
  name: Product Docs
  tagline: A short **Markdown-enabled** description.
  slogan: Clear answers, close to the product.
columns:
  - title: Documentation
    links:
      - { label: Docs, url: /docs/ }
      - { label: Blog, url: /blog/ }
  - title: Project
    links:
      - { label: GitHub, url: https://github.com/pgsty/oink, external: true }

brand.name and brand.logo fall back to the site’s own brand name, logo, and wordmark. tagline and slogan render Markdown. Internal url values resolve against the language root; external: true opens the link in a new tab with rel="noopener noreferrer". The grid’s track count follows the number of columns in the data.

A fat footer with no data degrades to slim, so a site can keep the default while it writes the columns.

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 - Languages

Language configuration, translation layout, stable anchors, and RTL support.

OINK uses Hugo’s multilingual page model directly and introduces no site-specific domain convention or template assumption. This site treats English as the primary language and Simplified Chinese (zh) as the second.

Configure languages

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日
label , string , required

The name shown in the language selector, written in that language — 简体中文, not Chinese.

locale , string

The standard language tag used for <html lang>, hreflang alternates, and Open Graph metadata.

weight , integer

Sets both the language order and the selector’s cycle order; lower comes first.

title , string

The site title in that language.

params.* , map

Language-level parameters override the global value of the same name; anything undefined is inherited. Date formats usually need a per-language value.

When menu labels differ by language, define menus under each language.

Organize translations

A translation sits beside its source in the same directory, distinguished by a filename suffix:

  • content/docs
    • install.md
    • install.zh.md

The shared base filename is what makes Hugo treat them as one page in two languages.

Keep identical: dates, weights, aliases, page resources, and every piece of metadata that affects routing.

Translate: front matter title and description, summaries, menu labels, tags, image alt text, callouts, and visible shortcode parameters.

Do not translate: commands, identifiers, configuration keys, filenames, URLs, and product names.

Stable heading anchors

This is where multilingual documentation most often breaks. Hugo derives heading IDs from heading text, so a Chinese heading produces a Chinese ID and /docs/page/#install and /zh/docs/page/#安装 become two unrelated anchors.

Write the source ID explicitly in the translation:

MARKDOWN
## 安装 {#install}

When translating an existing page, take the ID from the rendered English HTML. Do not guess from the heading text — headings containing shortcodes or inline code often generate something other than what you expect.

This site enforces identical heading count, order, and IDs with a script:

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

Language selector behavior

The selector reads each page’s .Translations:

  • the target language has a translation → it links straight to that page;
  • the target language has none → it falls back to that language’s home page.

The fallback is deliberate, not a defect. Sending a reader to a URL that does not exist would be worse.

Search and languages

With offlineSearch: true, each language gets its own index:

TEXT
public/offline-search-index.en.json
public/offline-search-index.zh.json

A reader searching from a Chinese page matches only Chinese content.

Chinese queries use the theme’s CJK substring fallback: Lunr cannot tokenize Chinese reliably, so the Command Palette switches to substring matching when it detects CJK characters. Both paths apply the same ranking boost.

Right-to-left languages

Declare the writing direction on the language:

hugo.yaml
YAML
languages:
  ar:
    label: العربية
    locale: ar
    languageDirection: rtl
    weight: 3

OINK loads Bootstrap’s RTL stylesheet, and the theme’s own CSS uses logical properties (margin-inline-start rather than margin-left), so mirroring is automatic.

Site-authored CSS should use logical properties too, or it will break under RTL.

UI translations

The theme ships interface strings for 32 locales. English, Simplified Chinese (zh-cn and generic zh), and Traditional Chinese (zh-tw) are fully reviewed; the rest keep their inherited Docsy translations, and OINK-only labels currently fall back to English.

To override one string, create a file of the same name under the site’s i18n/:

i18n/zh.yaml
YAML
ui_search: 搜索文档

Translation checklist

  • every page.md has a matching page.zh.md
  • Chinese headings carry explicit IDs matching the rendered English IDs
  • routing-affecting front matter is consistent
  • commands, configuration keys, and URLs are untranslated
  • the language selector is verified on pages with and without translations
  • search returns results in both languages

Next steps

4 - Versions

Let readers move between documentation versions, and mark archived ones.

When a product has several supported releases, the documentation usually follows. OINK provides two things: a version switcher and an archived version banner.

How each version is deployed is up to you — commonly one subdomain or subpath per version, each built separately.

Version menu

List the versions that should appear in the menu under params.versions:

hugo.yaml
YAML
params:
  version_menu: v2.1
  versions:
    - version: v2.1
      url: https://docs.example.com
    - version: v2.0
      url: https://v2-0.docs.example.com
    - version: v1.9
      url: https://v1-9.docs.example.com
version_menu , string

The label on the menu button, usually the current version.

versions[].version , string , required

The version identifier shown on the menu entry.

versions[].url , string , required

That version’s documentation address. An entry with no URL renders as unavailable.

version_menu_pagelinks , boolean , default: false

Whether to append the current page path to the target version’s URL.

Insert a separator with - name: '---' to divide supported from historical releases:

hugo.yaml
YAML
params:
  versions:
    - name: '**Current**'
    - version: v2.1
      url: https://docs.example.com
    - name: '---'
    - name: '**Historical**'
    - version: v1.9
      url: https://v1-9.docs.example.com

The page-level switching trade-off

version_menu_pagelinks: true appends the current page path to the target version’s URL, so a reader switching versions stays on the same document.

The cost is that the target version may not have that page. Documentation structure evolves between releases, an older version may not contain a newly written page, and the reader lands on a 404.

hugo.yaml
YAML
params:
  version_menu_pagelinks: true
  versions:
    - version: v2.1
      url: https://docs.example.com
    - version: v1.9
      url: https://v1-9.docs.example.com
      pagelinks: false # structure differs too much; go to the home page

pagelinks: false on an individual entry overrides the global setting so that version only ever receives its home page.

Archived version banner

On a site for a release you no longer maintain, say so explicitly:

hugo.yaml
YAML
params:
  archived_version: true
  version: v1.9
  url_latest_version: https://docs.example.com
archived_version , boolean , default: false

When true, shows an archive notice at the top of every page.

version , string

The version shown in the banner.

url_latest_version , string

The current version’s address; the banner links to it.

The banner text is localized with the site language; you do not write it.

Deployment layout

Two common arrangements:

Layout baseURL Character
Subdomain https://v1-9.docs.example.com/ Fully independent versions
Subpath https://docs.example.com/v1.9/ One domain; needs path routing

Each version is built independently: check out the content from its branch or tag, build with that version’s own hugo.yaml, and publish the output to the matching address. OINK does not build several versions in one pass.

Next steps

5 - 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. They live in the page actions menu at the end of the breadcrumb row:

  • View markdown opens the generated Markdown alternate when that output is enabled.
  • View edit history opens the source file’s commit history.
  • 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 edit, history, 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 Edit this page but not View edit history: the opaque URL has no repository path from which OINK can derive a history destination. A site-specific template override is preferable when the destination is not GitHub-compatible.

Every entry in the menu carries a stable action ID in data-oink-action:

Link Action ID
View generated source view_markdown
View edit history view_history
Edit this page edit_page
Create child page create_child_page
Create documentation issue create_issue
Create project issue create_project_issue

Hide an action in assets/scss/_styles_project.scss when the destination does not support it:

SCSS
.td-page-actions__item[data-oink-action='create_child_page'] {
  display: none;
}

The same IDs name the actions in the Command Palette, so hiding the menu entry alone leaves the command reachable there.

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.