This is the multi-page printable view of this section. .
Customization
- 1: Configuration
- 2: Brand and appearance
- 3: Home and landing pages
- 4: Navigation and menus
- 5: Layouts and page types
- 6: Search
- 7: Command palette
- 8: Keyboard navigation
- 9: Languages
- 10: Versions
- 11: Taxonomies
- 12: Repository links and page info
- 13: Print
- 14: AI-agent support
This section covers site-level configuration: the parameters in hugo.yml, the
data files under data/, and the style entry points under assets/. Writing an
individual page and its front matter is in Authoring.
Find it by what you want to change
| What you want to change | Page |
|---|---|
| Site name, logo, favicon | Brand and appearance |
| Colours, light and dark mode, fonts | Brand and appearance |
| The navbar menu and its dropdowns | Navigation and menus |
| Sidebar width, icon density, outline depth | Layouts and page types |
| The home page and landing pages | Home and landing pages |
| Full-text search and its index scope | Search |
| What appears in the command palette | Command palette |
| Keyboard shortcuts | Keyboard navigation |
| Adding a language | Languages |
| Multi-version sites and the archive banner | Versions |
| Tags and categories | Taxonomies |
| Edit this page, last modified, contributors | Repository links and page info |
| Print and whole-chapter export | |
llms.txt and the per-page .md output |
AI-agent support |
| A parameter’s type and default | Configuration |
Comments, analytics and deployment need an external service; they are in Operations.
1 - Configuration
This is the single home of site parameters. Every key the theme reads has a row in one of the tables below, giving its type, default and a one-line description, and linking to the guide that covers it. The guides give pasteable snippets and never repeat the definitions. Page-level parameters (front matter) are in Page parameters.
The tables are grouped by function, one ## each, and the anchors are
referenceable — for example /docs/customize/config/#sidebar. An empty
default column means the theme has no default: leave the key out and the
feature is off.
The layers of hugo.yml
An OINK site’s configuration has four kinds of key, and which layer you change depends on what you are changing:
| Layer | Examples | Who defines it |
|---|---|---|
| Hugo’s own top-level keys | baseURL title languages markup outputs taxonomies module |
Hugo itself; the behaviour is on gohugo.io |
Top-level params |
logo offline_search github_repo version page_width comments |
Site-level options the theme reads |
params.ui.* |
navbar_enabled sidebar_width_min typography pager_types |
The shell, navigation and reading interface |
params.<runtime> |
mermaid plantuml drawio markmap |
Each content runtime’s own switch and endpoint |
A minimal working configuration needs only the first two layers:
Configuration principles
-
The theme’s defaults are conservative; write only the keys you change. Interactive features (local search, image zoom, comments, feedback, the light/dark menu) are off by default, because the theme does not make policy for a site. Trimming a “complete configuration” leaves behind keys you never needed more readily than adding them as you go.
-
There is no theme master switch. There is no
oink.enabled, noparams.oink.*namespace, and no option that swaps between a “Docsy shell” and an “OINK shell”. A switch you cannot find on this page does not exist. -
An invalid value warns and falls back to the documented default.
params.ui.typography: solarizedreportsinvalid params.ui.typography "solarized" (allowed: technical | system) -- using "technical"and the site still builds;footer_style: thin,page_width: hugeandsection_index: gridbehave the same way. One typo therefore degrades one setting instead of serving HTTP 500 on every URL underhugo server. It cannot ship silently either: every publishing gate builds with--panicOnWarning, which turns the warning back into a hard failure. -
One warning keeps the value instead of dropping it. A
theme_colorthe theme reads as below AA body text (4.5:1) against its own canvas still ships — a custom canvas or a brand mandate is the author’s call — but says so, and prints theignoreLogsid that silences it. Treat it as advice, not a rejection: the fix is either a darker color or one line of configuration, and the publishing gate stops the build until you choose. Only an unparseable hex is dropped outright, and that one falls back to the default palette like every other invalid value. -
The theme itself never stops the build. Its templates contain no
errorfat all: every invalid value takes the warn-and-fall-back path above. A feature needing an external endpoint — PlantUML, Draw.io, Algolia — warns and stays off when the endpoint is missing, because the theme never connects to a public service on your behalf. An incomplete upstream attribution warns and omits the whole notice, because a partial one reads exactly like a complete one. What does stop a build comes from Hugo rather than the theme: a content reference that resolves to nothing, and a Hugo older thanmodule.hugoVersion.min.
Page-level override precedence
Hugo’s .Param lookup lets most parameters be overridden per page, highest
precedence first:
- The page’s own front matter;
cascadein an ancestor section’s_index.md(nearer wins);- Site
params.
Drop the ui. prefix when writing it in front matter. The site’s
params.ui.scroll_spy is simply scroll_spy on a page. A ui: block in front
matter is read by nobody and reported by nobody, so a setting that seems to have
no effect is worth checking against
Page parameters first.
A cascade sets a whole subtree at once:
Overrides are for real differences in content. Rebuilding a visual system page by page tends to fall out of step at the next theme upgrade.
The three Goldmark prerequisites
Hugo does not merge a theme module’s markup configuration into the site,
so these three must be in the site’s own hugo.yml, or attribute lines,
component HTML and mathematics all stop working:
Without attribute.block, {.fields}, {.steps} and {caption=…} render as
literal text; without passthrough, \(x\) never becomes a formula; without
unsafe, the structure of steps and cards is escaped away.
renderer.unsafe: true also lets raw HTML in Markdown through. It is meant for
trusted authors, not as a submission filter. Where content comes from untrusted
sources, the review belongs in the contribution process.
Site identity and brand
Hugo’s own top-level keys:
title,- Site name, shown in the navbar,
<title>and the footer baseURL,- The production domain; include the path segment for a subpath deployment
copyright,- Fallback for the copyright line, rendered as HTML when
params.copyrightis unset enableGitInfo, ,- Required before “last modified” and commit information exist
enableRobotsTXT, ,- Generates
robots.txt enableEmoji, ,- Allows
:smile:shortcodes
Theme parameters:
params.logo, ,- Brand mark; may point at an
assets/resource or astatic/path — see Brand and appearance params.wordmark,- Horizontal wordmark; when set, the navbar uses it instead of “icon + site name”
params.description,- Site description, the meta fallback when a page has no
description params.copyright,- A string renders as Markdown; a map takes
authors,from_yearandto_year(presentmeans this year) params.footer_center_info, ,- Inline Markdown in the centre of the footer; an empty string hides it
params.author,- The RSS author; a map takes
nameandemail params.ui.theme_color,#rgb/#rrggbbhex tinting the shell’s accent grounds; prose links and inline code are unaffected — see Brand and appearanceparams.ui.theme_color_dark, ,- The dark half of the accent; omitted, it is derived from
theme_coloruntil it clears AA on the dark canvas
There is no favicon parameter: the theme scans static/ for conventional names
(favicon.ico, favicon.svg, favicon-NxN.png, apple-touch-icon.png,
apple-touch-icon-NxN.png) — see
Brand and appearance.
Shell types and section roots
The shell follows the page type, not the path. Documentation can live in any
directory, with a cascade giving it type: docs.
params.ui.shell_types, ,- Which types use the reading shell with a sidebar — see Layouts and page types
params.ui.docs_section, ,- The documentation section’s root directory name, used for navigation resolution only
params.ui.blog_section, ,- The blog section’s root directory name
params.ui.docs_sidebar_root, ,- With
section, a docs page’s sidebar roots at the documentation section; withhome, at the site home. An invalid value warns and falls back params.ui.quick_links, ,- Top-level menu identifiers listed by the command palette on an empty query — see Command palette
params.ui.sidebar_root_enabled, ,- Allows a subsection to become its own sidebar tree with
sidebar_root_for: self params.ui.sidebar_root_menu, ,- Shows the section switcher above the sidebar; it degrades to a plain link when there is only one entry
params.ui.section_index, ,- Child list style on a section index:
listorcards, overridable per section params.ui.section_index_columns, ,- Column count when
section_index: cards
Blog
Seven keys shape a blog section. They apply to the section named by
params.ui.blog_section, and each can be overridden per section through front
matter or a cascade on the blog root.
params.ui.featured_image, ,- How an article renders its own featured image:
nonerenders nothing,bannerframes it above the title in a 16:9 figure,washlays it behind the article header at a tenth of its opacity,heropaints it as the shell’s own full-bleed backdrop and moves the opening down — on single pages and section indexes alike. The image is whichever one the page already shares in its card andog:image, so the two cannot disagree. An article with no image renders nothing in any mode params.ui.blog_index, ,- The blog section’s list page:
listis the row list,cardsa grid of content cards with a 16:9 lead image, the date and section line, and a three-line summary,tableone compact row per post — the whole section at once, with no year groups and no pagination. Year grouping, pagination andmanual_linkbehave the same inlistandcards params.ui.blog_index_columns, ,- Column count when
blog_index: cards; two between the md and xl breakpoints, one below md, whatever this says params.ui.blog_index_size, ,- Posts per page on a
listorcardsindex; thetableform always shows everything. Twelve divides by two, three and four, so no card row is left short params.ui.blog_index_toggle, ,- Lets a reader cycle the index through list, cards and table from the index toolbar. Off by default, because it puts all three forms in the document — the hidden ones load no images, but their markup is real
params.ui.toc_style, ,- The right rail’s presentation:
fixedis a panel pinned to the viewport,flowa wider panel in the content flow that starts where the article starts and pins only on scroll params.ui.toc_taxonomies, ,- Taxonomy term clouds on the right rail. A rail left with neither a table of contents nor clouds renders nothing at all
Article authorship and series are taxonomies rather than parameters — see Taxonomies and Writing a blog.
Navbar and footer
params.ui.navbar_enabled, ,- Whether the site navbar renders; overridable with a top-level
navbar_enabledon a page — see Navigation and menus params.ui.navbar_autohide, ,- The navbar retracts above the viewport and returns when the pointer enters the wake zone; inactive below 768px and on coarse pointers
params.ui.footer_style, ,fatis a multi-column grid plus the copyright line,slimis the copyright line only,nonerenders nothing. An invalid value warns and falls backparams.ui.dark_mode, ,trueenables both the dark palette and the theme control; for the control alone writedark_mode: { show_menu: true }params.ui.breadcrumb, ,- Breadcrumbs;
falseturns them off. A top-level section already omits a one-level breadcrumb params.ui.page_context_menu.enable, ,- The page action split button beside the title
params.ui.page_context_menu.assistant_links, ,- Shows “Open in ChatGPT / Claude”; clicking sends the full URL off-site
params.ui.page_context_menu.links, ,- Custom external actions;
urlsupports the{url},{title}and{markdown_url}placeholders params.ui.github_stars,- The star count on the navbar GitHub mark; a local constant, never a request
params.ui.alt_site,- A sibling-site link shown in the footer of a single-language site;
labeland an absolutehttp(s)urlare both required
The fat footer’s column data comes from data/footer/<language>.yaml rather
than from a parameter — see
Navigation and menus.
Sidebar
params.ui.sidebar_menu_compact, ,- Expands only the current branch and its neighbours
params.ui.sidebar_menu_foldable, ,- Lets the reader expand and collapse sections
params.ui.sidebar_menu_truncate, ,- Maximum entries rendered in one section; the rest are truncated
params.ui.sidebar_cache_limit, ,- Above this page count the site reuses shared navigation markup, and the browser restores the active state
params.ui.sidebar_width_min, ,- Lower bound in pixels for drag-resizing on the desktop
params.ui.sidebar_width_max, ,- Upper bound in pixels for drag-resizing
params.ui.sidebar_item_overflow, ,ellipsistruncates a long title,wrapwraps itparams.ui.sidebar_icon_policy, ,- Icon density:
alleverywhere,groupsonly on the root and nodes with children,nonenowhere. An invalid value warns and falls back toall params.ui.sidebar_expand_levels, ,- Tree levels expanded by default
params.ui.sidebar_headings, ,type: bookonly: expands a heading branch under the current sidebar row; an integer from 2 to 4, andtruemeans 2params.ui.sidebar_enabled, ,- The left sidebar;
falseturns it off, usually per page rather than per site params.ui.taxonomy_icons,- Right-column group icons by taxonomy plural, for example
tags: fa-solid fa-tags
How to use the sidebar is in
Layouts and page types; the tree itself comes
from the shape of content/ — see
Organizing content.
Table of contents
The outline’s levels come from Hugo’s own configuration; the theme controls only the tracking behaviour:
markup.tableOfContents.startLevel, ,- Hugo’s own: the highest heading level collected
markup.tableOfContents.endLevel, ,- Hugo’s own: the lowest heading level collected
params.ui.scroll_spy, ,- Scroll position tracking;
truehighlights the active entry
Hide the outline on one page with the front matter notoc: true — see
Page parameters.
Pager and page end
The page-end components are in a fixed order — share → feedback → page information → pager → comments — and each has its own switch; backlinks sit in the right rail beside the table of contents.
params.ui.share, ,- Page-end share targets, in the order given, from
xblueskymastodonfacebooklinkedinreddithackernewstelegramwhatsapplinepinterestweibochatgptclaudeemailcopy. Empty means no bar. Every target is a plain intent link — no SDK, no iframe, no third-party script, no share counts — see Writing a blog. An unknown target warns and is dropped params.ui.pager_types, ,- Which types show previous / next; a page opts out with the front matter
pager: false. An unknown type warns and is dropped params.ui.annotation, ,- The “last modified” and provenance block at the end of the body; the upstream attribution line is driven by the page’s
upstream_linkfamily — see Page parameters params.ui.backlinks, ,- Lists the pages that link to this one as a “Backlinks” group in the right rail beside the table of contents, derived at build time from ordinary links — see Navigation and menus
params.ui.translation_notice, ,- The language code of the authoritative version, so a translated page shows a line pointing back at it; a page opts out with
translation_notice: false params.ui.reading_time, ,- Shows a reading time under the page title
params.ui.book_draft_banner, ,- Adds a banner at the top of a draft Book page
Search and command palette
Local search is off by default, and the command palette appears only once it is
on (the navbar magnifier, Cmd/Ctrl with K, /, \).
params.offline_search, ,- Generates one local index per language and enables the command palette — see Search
params.offline_search_on_serve, ,- Builds the index under
hugo servertoo, so the preview behaves like production; setfalseon a very large site to speed up local rebuilds params.offline_search_index, ,- Index scope, cumulative:
title,heading,summary,content. An invalid value warns and usescontent params.offline_search_summary_length, ,- Word cut-off for the
summaryscope’s excerpt params.offline_search_max_results, ,- Result cap, bounding both Lunr and the CJK substring fallback
params.ui.landing_search, ,- Whether a
layout: landingpage keeps a search entry point params.ui.command_palette.commands, ,- Custom commands, each with either
urlor a built-inaction— see Command palette params.gcs_engine_id,- A Google Programmable Search engine ID; enabling it brings in an external service
params.search.algolia,- Algolia DocSearch;
appId,apiKeyandindexNamemust all be given explicitly, or it warns and DocSearch stays off
A custom command record accepts seven keys only — id, title, description,
icon, keywords, url, action — and id must match ^[a-z][a-z0-9_-]*$
and must not collide with a built-in action ID. Per-language titles go under
languages.<lang>.params.ui.command_palette.commands.
Keyboard
params.ui.keyboard_nav, ,- Single-key navigation (WASD / arrows walk the tree, j/k jump headings, q/e page, palette and shell switches). With
falsethe runtime never enters the bundle — see Keyboard navigation
Image zoom
params.ui.image_zoom, ,- Lets body images open full size; a page overrides it with the front matter
image_zoom. A non-boolean warns and falls back
Which images become zoom candidates is in Images.
Typography
params.ui.typography, ,technicaluses the bundled Inter / Chakra Petch / IBM Plex Mono;systemuses the platform stack only and requests no brand font. An invalid value warns and falls backparams.ui.fonts,- Font-family names for the
ui,body,heading,code,display,meta, andprintroles. The theme validates names but never loads font files; every list should end in a generic family params.page_width, ,- Overall shell width:
normal,wide,full; overridable per page params.reading_width, ,- Reading measure of a Book page’s body:
slim,normal,wide; it does not affect the shell
Use params.ui.fonts when the faces already exist on the reader’s system or the
site has declared them with @font-face. Bundling font files and changing
lower-level typography still use the SCSS/CSS entry points — see
Brand and appearance.
Comments and feedback
params.comments.enable, ,- The site-level comment switch; a page overrides it with the front matter
comments— see Comments params.comments.type, ,- Only
giscusactually renders today params.comments.giscus.repo,- The GitHub repository hosting the discussions; required
params.comments.giscus.repoId,- The repository ID; required
params.comments.giscus.category,- The discussion category name; required
params.comments.giscus.categoryId,- The discussion category ID; required
params.comments.giscus.mapping, ,- How pages map to discussions
params.comments.giscus.term,- The discussion title or number when
mappingisspecificornumber; the attribute is omitted when unset params.comments.giscus.strict, ,- Strict title matching
params.comments.giscus.reactionsEnabled, ,- Shows reactions on the main post
params.comments.giscus.emitMetadata, ,- Sends discussion metadata to the parent page
params.comments.giscus.inputPosition, ,- Whether the input box sits above or below the list
params.comments.giscus.theme, ,- The giscus theme;
autofollows the site’s light/dark state params.comments.giscus.lightTheme, ,- The giscus theme or custom CSS URL used in light mode
params.comments.giscus.darkTheme, ,- The giscus theme or custom CSS URL used in dark mode
params.comments.giscus.loading, ,- The iframe loading strategy
params.comments.giscus.lang, ,- The giscus interface language. Unset, a Chinese site resolves
zh-CN/zh-TW/zh-HK, other languages take the base language code, and anything giscus does not support falls back toen params.comments.giscus.ariaLabel, ,- The
aria-labelon the comment container; the default is English, so a multilingual site writes one per language params.comments.giscus.errorMessage, ,- Text shown when loading fails; the default is English, so a multilingual site writes one per language
params.ui.feedback.enable, ,- The two “was this page helpful?” buttons at the page end; there is no backend, and a structured event is recorded when
gtagis present params.ui.feedback.reasons, ,- Expands four optional reasons after “no”
Missing any one of the four required giscus values leaves the comment section unrendered: no error, and nothing appears.
Repository links and page information
params.github_repo,- The content repository URL, resolving “edit this page”, “view history”, “create child page” and “open a documentation issue” — see Repository links and page info
params.github_project_repo, ,- The product repository URL, for “open a project issue” and the navbar GitHub entry
params.github_branch, ,- The branch edit links point at
params.github_subdir,- The content site’s subdirectory inside a monorepo
params.path_base_for_github_subdir,- Source path rewriting; the map form takes
fromandto params.github_url, ,- Removed; write
params.github_repo. The migration registry that used to name the replacement is gone, so an old key is now simply an unread key params.ui.lastmod_commit, ,- What follows “last modified”:
subjectthe commit subject,hashthe short hash,nonenothing. An invalid value warns and falls back params.images, ,- The site-level social card: fills
og:imagewhen a page has no image of its own. Metadata only; never rendered as a list thumbnail params.upstream_source, ,- Default
data/upstreamsrecord name for pages that declareupstream_link; page front matter can override it params.upstream_modified, ,- Site default for whether attributed material is adapted; a page can override it, and no attribution renders without
upstream_link params.default_featured, ,- Removed; write
params.images, or a sectioncascadecarryingimages. As above, an old key is now simply an unread key
Content runtimes
Mermaid, KaTeX, ECharts, Infographic, Asciinema, Swagger UI and Redoc are detected from the content and load only where a page uses them, and only in that page’s HTML output; they have no site switch. Only these need a switch or an external endpoint:
params.markmap, ,- Enables the mind map fence site-wide — see Markmap
params.mermaid,- Configuration passed to
mermaid.initialize(); keys are lowercase, and dark mode overridesthemeautomatically params.plantuml.enable, ,- Enables the PlantUML fence — see PlantUML
params.plantuml.svg_image_url,- The PlantUML service’s SVG endpoint; required when enabled, and its absence warns and leaves PlantUML off
params.plantuml.svg,- Renders inline SVG instead of an
<img> params.drawio.enable, ,- Enables the edit button on
.drawio.svgimages — see Draw.io params.drawio.drawio_server,- The Draw.io editor address; required when enabled, and its absence warns and leaves Diagrams.net off
params.highlight_classes, ,- Emits Chroma classes for highlighting;
falsereturns to Hugo’s inline styles params.ui.code_copy, ,- The copy button on code blocks;
falseremoves it globally, and a fence’s owncopy=still wins
Mathematics needs no parameter, only the
passthrough prerequisite.
Output formats
The theme declares its custom output formats but does not enable them for a
site: request what you want under outputs. Expensive aggregate and
machine-readable outputs remain explicit opt-ins.
| Format | Output | Description |
|---|---|---|
HTML |
index.html |
The interactive form; required |
markdown |
index.md |
Each page’s plain Markdown twin, which “copy Markdown” and “view source” depend on — see AI-agent support |
LLMS |
llms.txt |
A plain-text format the theme declares, usually attached to home only |
LLMSFULL |
llms-full.txt |
A top-level section opt-in: the same per-page Markdown concatenated in sidebar reading order, one bundle per language |
NAVJSON |
navigation.json |
A home opt-in: the sidebar/pager navigation authority serialized once per language, validated by schema/nav.v1.schema.json |
print |
_print/index.html |
The whole-section print page the theme declares — see Print |
BookManifest |
book.json |
A Book-root opt-in JSON handoff for the EPUB/PDF packaging tools; it is not itself an ebook |
RSS |
index.xml |
Hugo’s own; attach it to section so every section has a feed |
LLMSFULL and BookManifest are enabled in the relevant top-level section’s
front matter rather than globally. NAVJSON belongs on outputs.home. The
complete examples and constraints are in AI-agent support
and Books.
Two parameters for print output:
params.print.toc, ,- Generates a table of contents at the top of the print page;
falseomits it params.print.section_break_wordcount, ,- How many words a section needs before it starts a new print page
Languages and versions
Languages are defined with Hugo’s own languages block, and the theme only
reads the translation relationships it establishes:
defaultContentLanguage, ,- The primary language, served without a path prefix
languages.<lang>.label,- The language’s endonym, shown in the language menu
languages.<lang>.locale,- The full locale, used for
<html lang>and SEO languages.<lang>.weight,- Language order, and the cycle order when clicking the language icon
languages.<lang>.title,- The site name in that language
languages.<lang>.direction, ,- Set
rtlfor a right-to-left language
Paired files, anchor alignment and fallback for untranslated pages are in Languages.
Version parameters:
params.version,- The identifier of this site variant, which need not be a Git ref — see Versions
params.version_menu, ,- The version menu’s title
params.version_menu_pagelinks,- On switching version, try the same path on the target site first
params.versions,- Version entries:
version,url,kind;name: '---'is a divider params.archived_version,- Shows the “this is an archived version” banner at the top
params.url_latest_version,- The link to the current version inside that banner
params.time_format_blog, ,- Blog date format, overridable per language
params.time_format_default, ,- All other date formats, overridable per language
Miscellaneous
taxonomies,- Hugo’s own: enables
tag: tags/category: categories— see Taxonomies params.taxonomy.page_header,- Shows only these taxonomies in a post header; unset shows all
services.googleAnalytics.id,- Hugo’s own: the analytics script is injected in production builds only — see Analytics and SEO
module.hugoVersion.min, ,- The Hugo floor the theme declares; anything older fails the build
module.hugoVersion.extended, ,- Hugo Extended is required (SCSS has to be compiled)
Editor completion via generated schemas
The theme ships two generated JSON Schemas under its schema/ directory:
site-params.schema.json for a site’s hugo.yaml and
front-matter.schema.json for page front matter. They are projections of the
theme’s own hugo.yaml defaults (with the comment documentation as hover
text) and its parameter-scan registry; the theme’s CI regenerates them and
fails on drift, so they can never disagree with the theme you have pinned.
With the VS Code YAML extension, map the site schema in your settings:
Pin the URL to your release tag instead of main to match your go.mod pin.
Front matter completion depends on your Markdown tooling; point it at
front-matter.schema.json the same way. The front-matter schema deliberately
omits type constraints, because keys like share and theme_color accept a
bare-boolean opt-out beside their ordinary type.
Verifying a configuration change
Run a strict build after changing configuration:
It passes only when the output reads Total in … with no ERROR and no WARN.
Common errors and what they mean:
| Error fragment | Cause |
|---|---|
invalid params.ui.typography |
The presets are technical and system |
invalid footer_style … (allowed: fat | slim | none) |
A bad footer style; the error names the page |
invalid page_width … (allowed: normal | wide | full) |
A bad page width |
invalid params.ui.section_index … (allowed: list | cards) |
A bad section index style |
invalid params.offline_search_index |
The scopes are title, heading, summary, content |
params.plantuml.enable requires an explicit params.plantuml.svg_image_url |
PlantUML enabled with no endpoint |
params.drawio.enable requires an explicit params.drawio.drawio_server |
Draw.io enabled with no server address |
params.search.algolia requires explicit appId, apiKey, and indexName |
All three Algolia values are required |
params.ui.image_zoom must be a boolean |
Written as the string "true" |
theme_color … is not a #rgb or #rrggbb hex color |
The value is not a hex color; the default palette is kept |
theme_color … reads at about N:1 against the theme's … canvas |
Advisory: the color ships, and the message prints the id that silences it |
theme_color_dark … has no theme_color to pair with |
The dark half was set without a valid theme_color; it is ignored and the default palette is kept in both modes |
command … must define exactly one of url or action |
A custom command gave both url and action, or neither |
invalid params.ui.sidebar_icon_policy …; using all |
Only a warning, but the value is misspelled |
A configuration change also needs at least three checks: one page in each
language, a page with no translation to see the fallback, and the links under
the production baseURL (easy to miss on a subpath deployment).
The theme’s declared Hugo floor is 0.160.1. OINK’s continuous test toolchain
is pinned to Hugo Extended 0.165.0; configuration changes are tested once
with that pinned version instead of against a version matrix:
The floor is declared in the theme’s hugo.yaml and theme.toml, and a site’s
own module.hugoVersion.min should agree with it. It remains a consumer
compatibility declaration, not a second routine CI test leg.
Related
- Brand and appearance — site name, logo, colours, fonts
- Navigation and menus — navbar menu, page actions, footer
- Layouts and page types — shell, sidebar, table of contents
- Page parameters — the full front matter table
- Troubleshooting — locating a build failure
2 - Brand and appearance
This page covers a site’s appearance: the name and logo live in hugo.yml,
colours and fonts go through SCSS entry points, and page width and footer shape
are parameters. It assumes the site already builds
(Quick start).
There are four things to change: hugo.yml, the icons under static/,
assets/scss/_variables_project.scss, and
assets/scss/_styles_project.scss. Do not edit files inside the theme
directory: the theme is a Hugo Module, and an upgrade replaces the whole
directory.
Site name
The site name appears in the navbar, the browser title and the footer. A multilingual site writes one per language:
The top-level title is the fallback, and languages.<lang>.title wins.
Logo and wordmark
The theme ships assets/icons/logo.svg and uses it by default. To replace it,
put the icon file in the site’s assets/ or static/ and point the
configuration at it.
params.logois the square mark, shared by the navbar, the sidebar and the footer. Underassets/it goes through Hugo’s resource pipeline (and can be fingerprinted); understatic/it is published as is. Either way the path is relative to theassets/orstatic/root.params.wordmarkis the horizontal wordmark. Once set, the navbar uses it instead of “icon + site name”, falling back toparams.logowhen the screen is too narrow. Left unset, “icon + site name” stays.
Crop the source SVG tight to the artwork, or the sizes will not line up. An SVG
needs a viewBox, and its colours should inherit currentColor or hold enough
contrast in both light and dark.
This site leaves both unset: the navbar pairs the theme’s own
assets/icons/logo.svg with the site title, drawn in the display font.
favicon
The favicon has no parameter. The theme scans the site’s static/ directory for
conventional filenames and emits the matching <link> on every page for
whichever it finds:
| File | Link generated |
|---|---|
static/favicon.ico |
rel="icon" |
static/favicon.svg |
rel="icon" type="image/svg+xml" |
static/favicon-32x32.png |
rel="icon" with sizes, emitted in ascending size order |
static/apple-touch-icon.png |
rel="apple-touch-icon" |
static/apple-touch-icon-180x180.png |
rel="apple-touch-icon" with sizes |
A sufficient minimum is favicon.ico plus favicon.svg plus
apple-touch-icon.png. A file with a size suffix has to be square (NxN) or it
is not recognized.
Generate these with any graphics tool. The theme needs no Node.js, and Hugo
simply publishes what is already in static/.
Extra head metadata such as a Web App Manifest is outside the scan; emit it
yourself through the layouts/_partials/hooks/head-end.html hook. To change the
discovery rules themselves (a different directory, more filenames), override
layouts/_partials/favicons.html in the site’s layouts/.
Accent colour and palette
Colour comes in two layers: Bootstrap’s semantic colours (Sass variables, at compile time) and OINK’s brand layer (CSS custom properties, at run time).
Change the semantic colours first; they decide the tone of buttons, links and callouts:
This file is loaded before Bootstrap and the OINK defaults, which is where
Sass variables are overridden. To reference a variable or map Bootstrap has
already defined, use _variables_project_after_bs.scss instead.
The brand layer is a set of CSS custom properties, and light and dark must be overridden in pairs or one mode leaks the original colour:
The brand properties available are --td-brand-elev (overlay ground),
--td-brand-silk (secondary text), --td-brand-copper and
--td-brand-copper-dim (the accent and its muted form),
--td-brand-line-strong (rules), --td-brand-header-bg (navbar background),
--td-brand-shadow-sm / --td-brand-shadow-md (shadows), and
--td-brand-mark-from / --td-brand-mark-to / --td-brand-mark-gradient (the
brand gradient).
Section theme colour
The brand palette above sets the colour of the whole site. theme_color is the
smaller instrument beside it: one hex that tints the accent grounds of the
shell, so a reader can tell which part of the site they are standing in without
being told.
It is more useful per section than site-wide. Written into a section root’s
cascade, it gives that whole section an identity — a navy Docs beside a
violet Blog and an orange Book — while the site default stays the brand colour:
Hugo resolves these cascade values on the section page as well as its descendants, so the pair is declared once. The same resolved pair drives the page’s accent and that section’s mark in the root switcher.
What it touches: the selected sidebar row and the greyed ground its neighbours take under the pointer, hover washes, the outline pill and its travelling rail and dot, a Book chapter’s headings under the pointer, tag and chip hovers, a content card’s hovered edge, a share button’s hover fill, text selection, focus rings, and each root’s mark in the sidebar switcher.
What it deliberately does not touch: prose links, external URLs, and inline code. Those are reading conventions, not brand surfaces — a page dense in identifiers should read as code and prose in every section, and a link should look like a link wherever it is. This is why the accent is its own custom property rather than a repaint of Bootstrap’s link colour.
The dark half is optional. Left out, it is derived by lightening the light
colour toward white until it clears AA body text on the dark canvas, so a
single-colour author cannot produce an unreadable dark palette. Name it
yourself when the derived value no longer matches the brand hue you want.
The light colour is the key: theme_color_dark on its own, or beside an
invalid theme_color, colours nothing in either mode — the theme warns and
keeps the default palette rather than tint dark mode alone.
A page inside a coloured section can decline the colour with the theme’s
bare-boolean idiom: theme_color: false in front matter opts that page out —
inherited dark half included — and reverts to the default palette without a
warning. Any other non-hex value (a number, true, a named colour) warns.
The theme reads your colour against its own canvases and warns when it falls
below AA body text (4.5:1). The colour still ships: a custom canvas or a
brand mandate is your call. The warning carries an ignoreLogs id that
silences it, and because publishing builds with --panicOnWarning, the gate
stops until you either darken the colour or silence the check.
The check reads the colour against the page canvas. Some interactive surfaces reuse it as both ink and a translucent wash — a linked solid badge, for example, uses accent ink over a 12% accent wash — and that pairing is tighter than the canvas check. If a colour only just passes, inspect those surfaces and go one step darker when needed.
Hugo merges params by key, so a page overriding theme_color inside a section
whose cascade also sets theme_color_dark inherits that dark value. Override
both, or neither.
Light and dark mode
The theme does not show a light/dark control by default. To enable it:
A theme control then appears in the navbar: clicking it toggles light and dark,
and hovering or focusing it expands “follow system / light / dark”. The reader’s
choice is stored locally in the browser, and with no choice it follows
prefers-color-scheme. The switching script sets data-bs-theme before the
first paint, so there is no theme flash.
For the dark palette without the control, write
dark_mode: { show_menu: false, enable: true }; dark_mode: false (the
default) enables neither.
Custom components need readable hover, focus, disabled and selected states in both modes, with at least 4.5:1 contrast for body text and 3:1 for large text.
Fonts
There are two font presets, decided at build time with no JavaScript involved:
technical(the default): interface and body text use the bundled Inter (variable weight, with Latin / Cyrillic / Greek / Vietnamese subsets, while Chinese and emoji fall through to platform fonts), display headings use Chakra Petch, and code uses IBM Plex Mono. All font files are local, and Google Fonts is never requested.system: the interface, display, metadata, print and monospace roles all fall back to the platform stack, and the browser requests no brand font. The font files still ship with the theme; they are simply not referenced.
An invalid value warns and falls back to technical, so an ordinary
hugo server stays usable; publishing gates run --panicOnWarning, which is
where that warning becomes a hard failure. The chosen value is written to
<html data-td-typography="…"> and can be confirmed in the browser.
Custom fonts
The font roles are seven CSS custom properties. Override them rather than hunting for component selectors:
| Property | Config key | Where it is used |
|---|---|---|
--td-ui-font-family |
ui |
Navigation, controls and interface text |
--td-body-font-family |
body |
Body text and blog posts |
--td-heading-font-family |
heading |
Headings in the body |
--td-code-font-family |
code |
Code and terminals |
--td-display-font-family |
display |
Wordmark and display headings |
--td-meta-font-family |
meta |
Technical labels and metadata |
--td-print-font-family |
print |
Print body text |
ui is the main face: body resolves through it and heading through body,
so a single line moves the interface, the prose and the headings together.
From configuration
To swap font families and nothing else, skip SCSS and write
params.ui.fonts:
These are family names, not font files. The theme never downloads or loads
a font because of this key: a family here must be one the reader already has,
or one the site declared in an @font-face of its own. End every list with a
generic family (sans-serif, monospace, serif) — that is where a reader
without your face lands.
Values are gated to plain font family syntax: quoted names, bare identifiers,
a leading hyphen (-apple-system), and names spelled in any script (苹方 is
a family name). Semicolons, braces, parentheses, url() and angle brackets do
not pass. An unknown role or an unsafe value warns and is dropped on its
own; the rest of the map still ships. A site that sets nothing gets no style
element in <head> at all.
The block renders after the stylesheet, which is what lets an authored face
outrank the typography preset at equal specificity.
From a stylesheet
To ship a font file of your own, or to change the face for one kind of content
only, use a stylesheet. Put the .woff2 in the site’s static/webfonts/,
declare the face in the project stylesheet, then rewrite the roles:
Roles inherit by ordinary CSS rules, so changing the font for one kind of content needs no component selectors either:
A monospace stack needs a CJK fallback, or mixed code blocks fail to align:
A site migrating from Docsy need not change how it writes this. The old Sass
variables still feed the corresponding roles, still work from
_variables_project.scss, and take precedence over the preset defaults:
| Legacy Sass variable | Font role it feeds | Note |
|---|---|---|
$td-fonts-serif |
--td-ui-font-family / --td-body-font-family |
Docsy’s interface stack, assigned to $font-family-sans-serif |
$font-family-sans-serif |
--td-ui-font-family / --td-body-font-family |
Once a project supplies its own stack, the technical preset stops putting Inter in front of it |
$font-family-base |
--td-ui-font-family / --td-body-font-family |
Bootstrap’s body variable, reaching the role through --bs-body-font-family |
$headings-font-family |
--td-heading-font-family |
Unset, headings inherit the body role |
$font-family-code |
--td-code-font-family |
Code, terminals and pre / code / kbd |
$td-font-family-monospace |
--bs-font-monospace |
Assigned to $font-family-monospace |
$font-family-monospace |
--bs-font-monospace |
Under the system preset, an explicit project value beats the platform monospace stack |
Docsy’s three Google Fonts variables — $td-enable-google-fonts,
$td-google-font-name and $td-web-font-path — are no longer read by the
theme. Leaving them in _variables_project.scss breaks nothing and does
nothing: what ships with the theme is Inter, Chakra Petch and IBM Plex Mono, and
neither preset requests anything from Google Fonts. The print role
--td-print-font-family follows the body role, and the theme ships no separate
font for paper.
YAML accepts font family names only — neither a remote font URL nor arbitrary CSS. Font files and styles must both be auditable local inputs, and an ordinary build makes no network request for a font.
Page width
page_width controls the overall shell width and can be overridden per page or
per section by cascade. Book pages additionally have reading_width (slim /
normal / wide), which changes the reading measure of the body rather than
the shell. An invalid value in either key warns and falls back during ordinary
preview; the warning fails a publishing build with --panicOnWarning.
Footer
fat(the default): a multi-column link grid plus the copyright line;slim: the copyright line only;none: no footer at all.
Page front matter (including a section cascade) can override it; this site’s
documentation section uses footer_style: slim. An unrecognized value warns
and falls back to fat in ordinary preview; strict publishing rejects the
warning.
The grid’s data lives in data/footer/<language>.yaml — see
Navigation and menus. With fat
configured but no data, it degrades to slim automatically, so it can be
enabled before the content exists.
params.copyright accepts a Markdown string, or a map of authors /
from_year / to_year (present means this year). footer_center_info is
inline Markdown in the centre of the footer, and setting it explicitly to an
empty string hides that region.
SCSS entry points, and what not to do
A site’s SCSS overrides join the theme’s single style bundle, and a production
build still emits one fingerprinted stylesheet with an integrity attribute.
Three entry files go under the site’s assets/scss/:
| File | When to use it |
|---|---|
_variables_project.scss |
Sass variables set before Bootstrap and the OINK defaults ($primary, the font variables) |
_variables_project_after_bs.scss |
Variables or maps that depend on Bootstrap’s own definitions |
_styles_project.scss |
Selectors and CSS custom properties written after the theme’s component styles |
The compilation order is: Bootstrap functions → project variables → OINK defaults and Bootstrap → post-Bootstrap project variables → OINK components and the brand layer → project styles.
The CSS interface has a defined boundary. The seven font roles in
Fonts and the --td-brand-* properties are public, and the theme
keeps their names and meanings across minor versions. Component aliases such as
--td-asciinema-font-family promise only to work within that component, and
undocumented variables such as the --td-shell-* family are implementation
detail that may be renamed or removed at any time.
What not to do:
- Do not edit any file inside the theme directory (
hugo modoverwrites it); - Do not
@importthe theme’s internal partials individually — they are not a public Sass interface and their import order may change; - Do not override
baseof.htmlto change one colour. Use a design variable where one exists, and otherwise write the narrowest selector that works; - Do not reference a remote stylesheet or a font CDN.
For additional third-party CSS, publish a local resource through the
layouts/_partials/hooks/head-end.html hook rather than writing a <link> in
Markdown.
Verify
- The build prints
Total in …with no ERROR and no WARN; - The page source has
data-td-typography="technical"(or your chosen preset) on<html>; - In the browser the navbar shows your logo and site name, and the tab shows your favicon;
- Switch to dark mode and look again at body text, tables, callouts, code blocks and focus rings. A colour change is easy to verify in only one mode;
- Switch language and confirm the site name changes with it.
To check whether the font really was replaced, inspect any paragraph’s
font-family in the browser’s developer tools: it should be the face you
declared rather than Inter.
Related
- Configuration — types and defaults of the brand parameters
- Navigation and menus — navbar menu, page actions and footer data
- Layouts and page types — shell, sidebar and table of contents
- Images — images in the body, light/dark pairs and captions
- Home and landing pages — hero, sections and landing data
3 - Home and landing pages
The home page is not a template but a data file: the sections list in
data/home/<language>.yaml decides which sections the page has from top to
bottom, and each section’s content is looked up by name in the same file. An
ordinary page with layout: landing uses the same sections.
Every section is rendered on the server. Prices, star counts, screenshots, avatars and download states all have to exist in the repository before Hugo starts; no section fetches data in the browser.
A site migrating from Docsy’s blocks/* home page has to rewrite it: the theme
has no blocks/cover, blocks/section or blocks/feature shortcodes, and
keeping them fails the build with
template for shortcode "blocks/cover" not found. The two ways forward are the
data/home/<language>.yaml described here, or layout: landing on an ordinary
page.
Where the home page’s data lives
The home page’s content file keeps only a title and a description:
Section data is a file per language:
home page data
- data/
- home/
- en.yamlEnglish home page
- zh.yamlChinese home page
- home/
The lookup order is data/home/<current language>.yaml → data/home/en.yaml →
data/home.yaml for a single-language site.
The file has only two levels: a sections list, and the same-named keys that
list references.
That is how this site’s home page is written; the complete file is
data/home/en.yaml in the repository.
A minimal working home page
Paste the following, replace the text and links, and it publishes. Write
internal links as site paths without a leading slash, and the theme adds the
current language prefix (docs/start/ → /docs/start/).
Hero
The hero is the first screen, and the only section with a large title and an illustration.
Without title_lines it uses title, and without either the site title. The
image is a CSS background: with an alt the container carries role="img", and
without one it is hidden from assistive technology.
align: center gives a text-only centred first screen: the text block widens
and centres, the title balances its line breaks, and note moves below the
buttons. When an image is present too, ordinary preview warns and falls back to
start so the image is preserved; a strict publishing build rejects the warning.
The section registry
There are 22 section types, named with hyphens (underscores in older data are
normalized). Apart from the hero, each shares the three heading fields
eyebrow / title / desc (or text) plus a class.
| Type | What it holds |
|---|---|
hero |
The first screen: large title, buttons, a theme-following image |
metrics |
Numeric facts, with optional count animation and source links |
capabilities |
Alternating left-right capability narratives with a dedicated visual panel |
principles |
Numbered product principles |
cards |
A general card set: features, scenarios, entry points |
logo-wall |
Tools and partners, as a grid or a pure-CSS marquee |
gallery |
A wall of screenshots |
testimonials |
Quotations with attribution |
contributors |
People, roles, avatars and links |
faq |
Questions and answers, collapsible or flat |
markdown |
A stretch of free Markdown |
cta |
The closing call to action |
pricing |
Pricing tier cards |
pricing-compare |
A tier-by-feature comparison matrix |
command-box |
One copyable command |
steps |
An ordered procedure, optionally with commands |
timeline |
Dated milestones |
code-plate |
Code inside a presentation panel |
preview |
A stretch of Markdown source beside what it renders as |
case-study |
A case: metrics plus a quotation plus a source |
download |
One or more data/download/ records |
bar-chart |
Numeric comparison without any chart JS |
A misspelled type does not silently disappear: the build emits an
unknown section type warning and skips the section. Adding
--panicOnWarning in CI turns that into a build failure.
The commonest sections, minimally
Cards and capability panels are the two used most. cards controls its column
count with columns:
capabilities is one capability per screen with a structured visual panel on
the right, and visual.type must be one of shell, components, code,
image or card:
These fragments come from the theme repository’s executable regression
fixture tests/site/data/landing/demo/en.yaml,
and the field names can be copied.
The download section consumes exactly the data/download/<key>.yaml from
Releases and downloads, introducing no second version
model.
Turning any page into a landing page
Two lines of front matter make an ordinary content page a landing page: a full-width canvas that keeps the navbar, the command palette and the footer, and drops the sidebar and the outline.
The data lives in a directory parallel to the home page’s, likewise split by language:
landing page data
- data/
- landing/
- pricing/
- en.yaml
- zh.yaml
- pricing/
- landing/
A non-home landing page looks for its data in this order. When nothing is found, ordinary preview warns and renders the landing shell with no sections; a strict publishing build rejects the warning:
sectionsin the page’s front matter;data/landing/<key>/<exact language>.yaml;- The exact-language entry inside a single
data/landing/<key>.yaml; - The English or language-less record.
Small amounts of data can go in front matter, but landing: and sections: are
mutually exclusive:
Writing a section entry
Each item in sections is either a type-name string or a map:
| Key | What it does |
|---|---|
type |
The section type; omitted, key is used as the type |
key |
Which key to read data from, defaulting to the same name as type; use it to distinguish two uses of one section |
data |
Inline data, so no top-level key is looked up |
id |
The section’s anchor ID, generated from key / type by default |
enabled: false |
Disables the section while keeping its data |
partial |
Swaps in the site’s own partial. A local template convention, not portable landing data |
Languages and local facts
Narrative text belongs in per-language files (zh.yaml / en.yaml). A shared
record of facts can also fall back field by field: <field>_<exact language> →
<field>_<base language> → <field>, with - in a language tag normalized to
_. A Chinese site resolves title_zh_cn, then title_zh, then title.
camelCase suffixes are not accepted.
Display text inside a section is site data, not the theme’s i18n strings. Only the theme’s own controls — marquee pause, pricing states — use translation keys. Configuring a multilingual site as a whole is in Languages.
A few optional facts on the landing shell are local too, written in hugo.yml
and never fetched at runtime:
The footer is not home page data: it reads data/footer/<language>.yaml (or
data/footer.yaml on a single-language site), and this site has one per
language. A leftover footer key in data/home/<language>.yaml warns and is
ignored in ordinary preview; --panicOnWarning rejects it while naming the new
location. How to write it is in
Navigation and menus.
Output
| Output | What appears |
|---|---|
| HTML | The full static section content, plus landing.js loaded on demand for reveal, counting, copying and theme image switching |
| Content kept; dynamic surfaces such as the marquee become a static grid, and controls are removed | |
| Markdown | Titles, prose, lists, tables and code, with no component classes |
| RSS | Landing sections are not emitted |
With JavaScript disabled the server-rendered document is still complete. The marquee’s duplicate track stays out of the accessibility tree, and pausing uses a checkbox that needs no JavaScript; with the reader’s reduced-motion preference on, movement and reveal are switched off.
Verify
- The build is warning-free:
hugo --printPathWarnings --panicOnWarning. A misspelled type, a missing data key, andlandingalongsidesectionsall surface here. - Open the home page and any landing page, compare each section against the data file, and look at every language.
- Reload with JavaScript disabled: the content is still there, only without motion.
- Look at both light and dark, confirming
image.lightandimage.darkare each correct. - When deploying to a subpath, confirm internal links and images all carry the prefix.
Related
- Brand and appearance — site name, logo, colours and fonts
- Navigation and menus — navbar, footer and the language menu
- Releases and downloads — the data behind the
downloadsection - Languages — enabling languages and splitting data by language
- Configuration — full definitions of
params.ui.landing_searchand the rest
4 - Navigation and menus
This page covers the ways a reader moves between pages: the navbar menu, the section switcher, breadcrumbs, page actions, previous / next, and the footer. The sidebar tree and the outline belong to Layouts and page types.
Navigation has no second information architecture: the navbar comes from Hugo’s
menus.main, and the sidebar from the shape of content/. The theme reads no
parallel navigation tree such as a docs.json or a navigation.yaml.
The navbar menu
Top-level entries go in each language’s menus.main:
A lower weight comes first. pageRef points at a site page and url at an
external one; an external link automatically gains target="_blank",
rel="noopener noreferrer" and an external-link mark. identifier is the
stable handle configuration uses to reference the entry (quick_links and
sidebar_root_menu match on it), name is translated per language, and the
identifier is not.
A menu entry can also hang off a page’s front matter, which suits “this page is itself a top-level entry”:
The GitHub entry at the right of the navbar is not a menu item: it comes
from params.github_project_repo (falling back to params.github_repo). A menu
entry identified as github is skipped by the menu area and never shows. To
change that entry’s target, change the repository parameters — see
Repository links and page info.
Dropdowns
Use Hugo’s parent to establish a parent-child relationship. Only one level
of children is supported:
- Every entry is one icon and one title on its own row, in one
moderate-width column. A child’s
params.descriptionis configuration data only; the panel never renders it. - The parent is itself an ordinary link: hovering or focusing it expands the panel, and clicking or pressing Enter goes to the parent page. There is no separate expand arrow, and a touch reader lands on the parent page, whose body lists the same links.
- Keyboard: the down arrow expands and focuses the first item, Esc closes and returns focus to the link, and clicking outside closes it.
- The 0.5
params.columnsparameter is retired: setting it emits a build warning and the panel keeps its single column. - A third level warns at build time and degrades to a static group heading; it does not produce a third-level flyout. Put deeper levels in the sidebar.
Menu icons
Below lg a menu entry is reduced to its icon, so every top-level entry should
have one. Icons resolve in this order:
iconin the target page’s front matter;- The menu entry’s own
params.icon; - A built-in default matched by identifier or section name (
docs,blog,examples,community,about,download,githuband others); fa-solid fa-linkwhen none matched.
An icon is one Font Awesome class pair, with the free faces supplied locally by the theme:
Taxonomy menus
A top-level entry pointing at a taxonomy page (/tags/, /categories/) needs
no hand-written submenu: the panel renders a grid of “term + count” chips,
ordered by descending count.
Enabling taxonomies is in Taxonomies.
Navbar controls
The navbar is 50px tall and holds, left to right: the brand (logo or wordmark), the menu area, search, version, language, theme, GitHub. Home and Landing pages keep a final drawer menu button at the right edge. The navbar renders on every layout; documentation, blog and taxonomy pages use the same controls without that Landing drawer.
The navbar has a full desktop tier and a compact icon tier:
| Viewport | State |
|---|---|
lg and above |
Full: brand, menu entries with text, all utility controls; Home/Landing ends with the drawer button |
Below lg |
Compact: the brand stays, everything else becomes right-aligned icons |
Below md |
Only search and the drawer button remain in the navbar; version, language, theme and keyboard help remain in the footer’s bottom bar |
The individual controls are switched on elsewhere: the search icon needs
params.offline_search (see Search), the version
menu needs params.versions (see Versions), the
language menu appears automatically with two or more languages configured (see
Languages), and the theme control needs
params.ui.dark_mode (see
Brand and appearance).
Auto-hide
With it on, the navbar leaves the normal flow and rests above the viewport, sliding out only when the pointer enters the middle 60% of the area above its original position (or keyboard focus arrives), and it overlays the body rather than pushing it down. 64px at each side is outside the wake zone, so it does not cover the collapsed sidebar and outline restore buttons.
It is disabled automatically below 768px, on a coarse pointer, and on a
touch-only device, where the navbar stays visible. A top-level
navbar_autohide in page front matter or a section cascade overrides it per
section.
Turning the navbar off
It can also be turned off for one page or one section:
With it off, the theme restores the interface the navbar carried: mobile subnavigation, a brand and search row at the top of the sidebar, and utility buttons on the outline rail. The switch suits pages that must own the viewport; it is not a general layout preference. This site’s documentation section uses it: documentation pages navigate through the sidebar, and the navbar is one row too many.
The section switcher
The row at the top of the sidebar is the section switcher, deciding which tree
is shown. Its entries are built in order and deduplicated: every top-level
section → every section anywhere with sidebar_root_for: self → the currently
resolved root.
To let a large subtree become a root of its own (a versioned API reference, a
self-contained handbook), in its _index.md:
self makes the section index and all its descendants use the new tree;
children leaves the index in the parent tree and binds only the descendants.
To keep a top-level section out of the switcher, set sidebar_root_menu: false
in its front matter.
With one entry the switcher degrades to a borderless link; two or more make it a dropdown. The tree below it still has the section index as its first link: the switcher picks a tree, and the root link picks a document.
Breadcrumbs and page actions
An ordinary content page has a breadcrumb row above its title, and that row’s right end carries the page actions. A top-level section omits a single-level breadcrumb that would only repeat the title, and the action buttons stay where they are.
Breadcrumb labels use the localized linkTitle, and the hierarchy matches the
sidebar.
The page action menu
Page actions are the split button at the end of the title row: the left half copies this page’s Markdown in one click (turning into a green tick on success), and the arrow on the right expands the full menu. The menu has two groups — taking the content away, and changing or producing it:
| Action | When it appears |
|---|---|
| Copy as Markdown | The site enabled the markdown output format |
| Open in ChatGPT | page_context_menu.assistant_links: true |
| Open in Claude | The same |
| View Markdown source | The markdown output format |
| View history | params.github_repo can resolve the source path |
| Edit this page | params.github_repo |
| Create child page | params.github_repo |
| Open a documentation issue | params.github_repo |
| Open a project issue | params.github_project_repo |
| Print the whole section | The section enabled the print output format |
The assistant entries are off by default: on a click the full current URL
(query and fragment included) goes to a third party with a localized prompt,
while the body is not uploaded. Before enabling it, confirm no sensitive
information appears in URLs, and disclose the boundary in the privacy statement.
A page can narrow the site policy with a boolean assistant_links in front
matter, but cannot enable it on the site’s behalf.
Custom external actions come last in the menu, and url supports three
URL-encoded placeholders:
The placeholders are {url} (the page’s full address), {title} (the page
title) and {markdown_url} (the Markdown version’s address).
On the blog root section and its first-level subsections, the left half becomes the RSS subscription link while “copy as Markdown” stays in the menu. A page with no Markdown output loses the left half, and the arrow becomes an “Actions” button with a label.
These actions are also entries in the command palette.
The pager
Previous / next at the end of the body are two text links, ordered by the
sidebar’s visible tree: root page → first page → through to the last. The root
has no previous, and the last page has no next. Where a site provides
data/docs_nav.json, that explicit tree decides the paging order too — and the
section index on a docs or book section the file declares, so the sidebar, the
pager and the index can no longer show the same children in three different
orders. A section the file does not declare, and a site without the file, keep
walking the content tree. See
Layouts and page types.
pager_types accepts only docs, book and blog; any other value warns and
is dropped. A page opts out through front matter:
The same order is written into <head>: with a previous or next page, it emits
<link rel="prev"> and <link rel="next"> so browsers and crawlers can see the
reading sequence.
Paging applies to HTML output only. Print, Markdown and RSS have neither the
links nor the two rel relationships.
The pager is the third of the four page-end components (feedback → annotation → pager → comments), in a fixed order with four independent switches.
Backlinks
The pages that link to a page can be listed in the right rail, as a “Linked
from” group with a link icon below the table of contents and above the taxonomy
clouds, expanded by default; below the xl breakpoint it moves into the sidebar
drawer with the table of contents. A reader who arrived from search sees which
pages consider this one worth pointing at, and where it sits in the rest of the
site. It is off until a site asks for it:
A page overrides it in front matter, and a section cascades it to everything below:
The index is derived at build time from what authors already write: an ordinary
Markdown link, or a ref / relref shortcode, in the page source. There is no
new syntax to learn, nothing to migrate, and no JavaScript — the list is in the
HTML. Fenced and inline code are stripped before scanning, several links to one
target merge into a single entry, and self links, external links, mailto: and
same-page anchors never count. A fragment is dropped when identifying the target
page, and each language has its own graph, so a Chinese page never appears under
an English one. Entries are sorted by stable page path, so the same content
builds the same order every time, and the block is absent entirely — no heading,
no empty container — when nothing links in.
The first eight entries are visible and the rest fold behind a native “Show N more” disclosure, so a heavily referenced page cannot swallow the rail; no JavaScript is involved. Each entry carries its source page’s description, shown on hover.
Reading the source has a known limit: a URL inside a custom shortcode’s
parameters, or a raw <a href>, does not become an edge, and a destination that
cannot be resolved is dropped without a warning. This is a navigation
enhancement, not a link checker; keep using a link checker for broken links.
A non-boolean value warns, falls back to off and fails a build run with
--panicOnWarning, while hugo server keeps working.
The page’s Markdown output carries the same list, introduced by “Backlinks:”.
RSS omits it, and the print output format omits it with the rest of the rail.
This site enables it site-wide: look at this page’s right rail for the real thing, and the most-referenced page — Configuration — lists more than forty inbound links, most of them folded behind the disclosure.
The footer
The footer’s shape comes from params.ui.footer_style (fat / slim / none
— see Brand and appearance). The fat link
grid reads data/footer/<language>.yaml. It is not a menu, and the theme has no
menus.footer:
- Without
brand.nameandbrand.logoit falls back to the site’s own brand name, logo and wordmark;taglineandsloganrender Markdown. - An internal
urlresolves against the current language root;external: trueopens in a new tab withrel="noopener noreferrer". - The grid has as many columns as the data does.
- A single-language site can use
data/footer.yaml. - With
fatconfigured but no data, it degrades toslimautomatically, so it can be enabled before the content exists.
The fat footer’s copyright row has a collapse arrow at its right end, hiding
or restoring the link grid above it. It starts expanded, and the reader’s choice
is kept in localStorage under td-footer-collapsed across pages. slim and
none have no such button, and it is unrelated to focused reading mode.
Every rendered bottom bar ends with the same icon dock: version, language,
theme, then keyboard help. Each configured menu opens upward; the version
trigger stays icon-only while its choices keep their full labels. The fat
footer’s collapse arrow follows those four controls. The sidebar has no second
copy of the dock, and footer_style: none removes the bar with the footer.
The copyright row and the centre note are parameters — see Configuration.
Verify
After changing navigation, check each of these:
- The build has no
Navbar menu … supports one interactive child levelwarning; one means the menu is three levels deep; - On the desktop: clicking a parent goes to the parent page, hovering expands the panel, and Esc closes it;
- Narrow the window below
lg: every top-level entry still has an icon, and one without an icon is blank at this width; - Below
md: Home and Landing navbars keep search and the drawer button on the right; version, language, theme and keyboard help stay in the persistent footer bottom bar; - The switcher at the top of the sidebar lists every top-level section, with the current one marked;
- On any documentation page, E / Q page in sidebar order, and the page source has matching
rel="prev"/rel="next"; - With backlinks on,
grep td-backlinks public/<a page that is linked to>/index.htmlfinds the block, and a page nothing links to has no such markup at all; - Open the page action menu and confirm what should be there is, and what should not is not (for example “open a project issue” with no
github_project_repoconfigured).
Related
- Layouts and page types — sidebar tree, outline and shell types
- Configuration — defaults of the navigation parameters
- Command palette — page actions and custom commands
- Repository links and page info — the edit, history and issue links
- Organizing content — how the directory structure decides the sidebar
5 - Layouts and page types
type decide which shell a page uses, then adjust sidebar width and icons, outline depth, section index style and page width.This page covers a page’s skeleton: whether it has a sidebar, how wide that is, how deep the outline goes, and whether a section index is a list or cards. Where content goes is in Organizing content; this page is only about the shell.
The rule is that the shell follows type, not the path. Documentation can
live anywhere under content/ as long as it has type: docs.
Shell types
params.ui.shell_types lists the types that use the reading shell with a
sidebar:
| type | Shell |
|---|---|
docs |
The documentation shell: left sidebar (section switcher + tree) + body + right-hand outline |
book |
The documentation shell, plus numbered targets, the reading_width measure and the draft banner |
blog |
The documentation shell, with the sidebar expanded by default and RSS as the left half of the title row |
swagger |
The documentation shell, with the body handed to Swagger UI or Redoc — see API reference pages |
| Any other type | An ordinary page: navbar + single-column body + footer, with no sidebar |
Taxonomy and term pages are not in this table but use the same shell.
Assigning a type to a subtree uses a cascade, which is how documentation ends up at an arbitrary path:
Section roots are only navigation starting points
These two keys do not decide the shell. They tell the theme where the
documentation and blog trees are rooted, for resolving the sidebar root, quick
links and default icons. The content/handbook/ example above still has the
documentation shell, and leaving docs_section at docs does not affect it.
To make a docs page’s sidebar root the site home rather than the documentation section:
Those are the only two values. Anything else warns and uses section in
ordinary preview; --panicOnWarning rejects the warning during publishing.
Documentation at the site root
A documentation-first site can publish the docs section at the URL root while
the source stays in content/docs/. Three pieces of configuration are needed
together.
The first uses Hugo’s own permalinks to drop the docs/ segment from URLs:
The second keeps the physical site root index usable as a link target while no
longer competing for the same output path. Every language’s site root index
(content/_index.md, content/_index.zh.md) needs it:
The third declares the sidebar root to be the site home, so the sidebar and the pager share one tree:
With docs_sidebar_root: home, every top-level section of the site home enters
that tree. Overview sections that are not part of the reading sequence — blog,
community, download — opt out with toc_root: true in their own _index.md,
which keeps them out of the tree and out of the paging order:
Documentation then shares the URL root with blog, community and the rest. Build
with --printPathWarnings and resolve every duplicate target before publishing.
Landing pages
Any page with layout: landing uses the landing layout: navbar + a body
assembled from sections + footer, with no sidebar. How to write the data is in
Home and landing pages.
landing_search: false removes the search entry point from the landing shell
and affects no other page.
Sidebar
The sidebar tree comes from the shape of content/, ordered by weight and
labelled with linkTitle where one exists. What is adjustable is density and
size:
sidebar_menu_compactexpands only the current branch and its neighbours;falseexpands the whole tree.sidebar_menu_foldablelets the reader expand and collapse sections manually. Blog sections are expanded by default; to collapse one by default, writesidebar_expanded: falsein its_index.md.sidebar_expand_levelsis how many levels are expanded by default.sidebar_menu_truncateis the maximum entries rendered per section, so a thousand-page tree does not inflate the HTML past usability.sidebar_width_min/sidebar_width_maxbound drag-resizing on the desktop, in pixels. The reader’s adjusted width is kept locally, and double-clicking the divider restores the default.sidebar_item_overflowdefaults toellipsis(long titles truncate); a site with many long titles can usewrap.
Fold state, width and scroll position are stored locally per language. Below
md the sidebar becomes a drawer with a backdrop.
To drop the sidebar on one page, use front matter:
An explicit navigation tree, data/docs_nav.json
The sidebar tree is derived from content/ by default. A site may also supply
an explicit navigation manifest, and the theme renders from it when three
conditions hold together:
- The site has a
data/docs_nav.jsoncontaining asectionskey; - The page’s type is
docsorbook; - The resolved sidebar root is not the site home.
The file is a nested node tree. Each node’s page points at a content path,
url is its link, and children are its children; active_path_by_url records
the ancestor chain for each URL, used for highlighting the current entry:
URLs have their language prefix stripped before comparison, so one file serves every language.
That tree also decides the paging order, so the sidebar and previous / next
never disagree. An empty sections array warns and falls back to the content
tree; a page that does not exist warns and skips that entry. Strict publishing
rejects either warning. Placeholder nodes with manual_link and
sidebar_divider rows stay in the sidebar without becoming paging targets.
It suits a site whose navigation order is generated by an external tool — a
manual migrated from a Sphinx toctree that has to freeze its existing chapter
order, say. Where order is maintained by weight in content/, the file is not
needed.
Sidebar icon density
An icon in a page’s front matter appears in the sidebar. Icons on every leaf
page reduce readability, so a density policy controls them:
| Value | Effect |
|---|---|
all |
Every entry with an icon shows it (the compatibility default when unset) |
groups |
Only the root and nodes with children show icons |
none |
No entry icons in the sidebar |
An invalid value only warns and falls back to all rather than failing the
build. This site uses groups.
Expanding headings in the sidebar
Book pages can expand an h2–h4 branch under the current sidebar row, which helps navigation inside a long chapter:
An integer sets the deepest level expanded (2–4), true means 2 (h2 only), and
false turns it off. Out of range warns and disables the branch in ordinary
preview; strict publishing rejects the warning. It applies to type: book
pages only, and expands only under the current sidebar row.
Table of contents
The right-hand outline is generated by Hugo from the Markdown headings, and the levels collected are Hugo’s own configuration:
The theme governs only the tracking behaviour:
Scroll tracking is off by default. Set to true, the outline draws a
continuous rail, highlights the current section and marks the position. The
reader can collapse the right column entirely, and that state is kept locally.
Below xl the right column is hidden and its content moves into the sidebar
drawer.
To hide the outline on one page, use the front matter notoc: true.
Only headings that reach Hugo’s table of contents appear in the outline:
headings emitted by a Markdown-form shortcode ({{%/* … */%}}) do, and those
from an ordinary shortcode ({{</* … */>}}) usually do not. Structural headings
belong in the Markdown.
Section index style
A section with an _index.md lists its child pages automatically, in one of two
styles:
list(the default): one title plus description paragraph per child page;cards: a grid of cards reading each child’stitle(orlinkTitle),descriptionandicon.
It can be overridden per section. An invalid value warns and falls back during
ordinary preview; --panicOnWarning rejects it at the publication gate:
Related page-level switches: no_list: true lists no children;
simple_list: true emits a bulleted list with no descriptions; and a child page
with hide_summary: true removes itself from the list. Do not hand-write a
child list: a hand-written one goes out of step with the sidebar.
Page width
normal is the usual reading width, wide widens the content column, and
full fills the viewport. It can be overridden per page or per section; wide
tables, large images and API reference pages often use wide:
Book pages additionally have reading_width (slim / normal / wide), which
changes the body’s own reading measure without touching the shell. An invalid
value in either key warns and falls back during ordinary preview; strict
publishing rejects the warning.
Navbar and footer switches
The navbar and footer are per-page layout decisions, written at the top
level of front matter (not under ui), and can be set once with a section
cascade:
The behaviour is in Navigation and menus and Brand and appearance, and the key definitions in Page parameters.
Verify
- The build prints
Total in …with no ERROR and no WARN; - A newly created
type: docspage has a left sidebar. If not, check whether the cascade reaches that page and whethershell_typescontains the type; - Drag the sidebar divider, reload and confirm the width persists; double-click restores the default;
- Below
mdthe sidebar becomes a closable drawer, and belowxlthe outline moves into the drawer; - A section index has as many cards as the sidebar has child pages;
- A page with
page_width: wideis wider than its neighbours; - With documentation at the site root,
hugo --printPathWarningsreports no duplicate output paths.
Related
- Configuration — defaults for the shell, sidebar and outline parameters
- Organizing content — directory structure,
weightand the sidebar tree - Navigation and menus — navbar, section switcher and pager
- Home and landing pages — writing the data for
layout: landing - Page parameters — the front matter keys used for per-page overrides
6 - Search
OINK’s search is local search: Hugo generates one JSON index per language at build time, the reader’s browser downloads it, and the search runs in the browser. No crawler, no account, no CDN, and no network access. The theme leaves it off, and one line of configuration turns it on.
The entry point to search is the command palette; how to open it and what else it holds are in Command palette.
Turning on local search
This one key decides whether the index, the Lunr runtime and the search dialog reach a page. Three conditions must hold together:
params.offline_searchis true;- The page is the home page, or uses a shell layout (
docs/book/blog/swagger— see Layouts and page types), or is a landing page withparams.ui.landing_searchon; - The current output is not print.
If any one fails, the build puts no dialog, no index reference and no Lunr into that page. Those resources are not hidden; they are never generated.
Under hugo server the index is generated as well by default, so the
preview behaves like production. On a very large site, where rebuilding the
whole index on every change slows the preview noticeably, turn it off:
Controlling index size
offline_search_index decides how much of each page goes into the index, and so
decides two things at once: whether a reader can find words from the body, and
how large the first search’s download is.
| Value | What is indexed | When to use it |
|---|---|---|
title |
Title, tags, categories, search_keywords |
A very large site where titles alone locate a page |
heading |
The above plus every heading in the page | When headings are specific enough |
summary |
The above plus description and summary | Sites in the thousands of pages; this site uses it |
content |
The above plus the full plain text | The default, suitable up to a few hundred pages |
Any other value warns and uses content during ordinary preview; a strict
publishing build fails on invalid params.offline_search_index.
offline_search_summary_length is where a result row’s excerpt is cut (default
70), and offline_search_max_results caps the number of results (default 10).
The full definitions are in Configuration.
The reader downloads the whole index before searching for a first word. Past
that size, step offline_search_index down from content to summary.
Adjusting ranking
A page influences its own ranking from front matter:
search_keywords adds matching terms and takes either a string or an array. It
is the more useful of the two: a reader searching pg or GUC reaches a page
whose title only says “PostgreSQL parameters”. In ranking, keywords weigh less
than the title and more than the body.
search_boost is a positive multiplier on the final score, defaulting to 1.0
and applied on top of the text match score. 1.5 does not pin a page to first
place; it moves the page up among results it already matched. Zero, a negative
number and a non-number all warn and are treated as 1.0.
Set a section-wide default once with a cascade:
A page’s own value overrides the inherited one. Pages under this site’s docs/
use search_keywords in exactly this way: each lists the Chinese phrasing, the
English term and the configuration key name.
Keeping a page out of the index
search_exclude is the only spelling. The removed exclude_search and
excludeSearch keys are not read and therefore do not protect a page; the
migration checker reports them. A page with an empty body is not indexed.
Do not put content that should stay private on the site, and do not use
search_exclude to protect it.
Chinese and CJK
Lunr cannot reliably tokenize Chinese. When the palette detects a CJK character
in the query, the whole query switches to substring matching: it compares title,
keywords, in-page headings, description and body in turn, scores whichever layer
matched, and finally multiplies by search_boost as usual. Both paths rank by
the same rules.
Three things follow:
- A CJK query is a substring match. Searching 主从复制 finds only where those four characters appear consecutively; 复制主从 returns nothing.
search_keywordstherefore pays off most on a Chinese site: write in the synonyms, English terms and abbreviations a reader might use.- While an input method is composing, the palette does not recompute; it searches once the text is committed, so typing Chinese does not refresh results character by character.
When a Chinese query finds nothing, first confirm the Chinese page reached the Chinese index (see Verify below) before suspecting tokenization.
Optional: hosted search
Besides local search, the theme keeps two hosted integrations, both off by
default. Enable only one at a time: with more than one configured the build
warns You have more than one site-search option configured.
Enabling hosted search means accepting that service’s crawling behaviour, availability and privacy boundary, all of which belong in the site’s privacy statement.
Algolia DocSearch
All three values must be written explicitly, and a missing one stops the build: OINK never falls back to another project’s public index. The DocSearch JS and CSS ship with the theme rather than loading from a CDN, but every query is a request to Algolia. Real credentials and a real index are needed for it to work, so nothing is rendered here.
Google Programmable Search
A landing page for the results is needed too:
The search box submits the query to <baseURL>/search/?q=…, and Google’s script
renders the results on that page, which needs access to cse.google.com. It is
likewise an external service and is not rendered here.
Verify
-
Build, and confirm one index per language was generated:
In a development build the filename is
offline-search-index.zh.json; a production build fingerprints it, as inoffline-search-index.zh.7ab….json. One file per language, and a missing one means that language’s pages never reached an index. -
Look inside the index — the first step in diagnosing “Chinese finds nothing”:
The entry count should be close to the number of Chinese pages, and the
keywordsandboostfields should show what the front matter set. -
Open the site, press /, and search once with an English word and once with a Chinese one. Results are grouped by content root, each group named after the first breadcrumb segment.
-
On a subpath deployment (the site under something like
https://example.com/docs/), open the browser’s network panel and confirm the index request carries the subpath. An index request hitting the domain root and returning 404, while the rest of the page works, is the most common cause of “search returns nothing”.
Related
- Command palette — search’s entry point, and the commands and page actions beside it
- Keyboard navigation — the four single keys that open search and commands
- Languages — per-language indexes and untranslated fallback
- Configuration — full definitions of the
offline_search*keys - Page parameters —
search_keywords/search_boost/search_exclude
7 - Command palette
The command palette is the site’s one modal entry point: searching pages,
copying this page’s Markdown, switching language, switching version and jumping
to a site’s own links all happen in one dialog. It is assembled together with
local search: with params.offline_search off, the palette, the index and Lunr
all stay out of the page — see Search.
Opening the palette
| How to open it | What opens |
|---|---|
| Click the search box in the navbar or sidebar | Full search mode |
| ⌘ / Ctrl + K | Full search mode; press again to close |
| / | Full search mode |
| The backslash key | Command-only mode (equivalent to a prefilled >) |
| f / c | The same two, provided by keyboard navigation |
Typing a query beginning with > in the box |
Command-only mode |
/, backslash, f and c are all bare single keys
and stand down for typing: while focus is in an input, textarea, select or
contenteditable, and while an input method is composing, they type an ordinary
character. The modified ⌘/Ctrl + K has no such
restriction and opens the palette even from inside a text box.
Inside the palette: ↑ ↓ select, Enter runs, and Esc closes and returns focus to whatever opened it.
What the palette holds
With nothing typed, the palette lists four groups in a fixed order:
| Group | Contents | Decided by |
|---|---|---|
| Quick links | A few entry points chosen from the navbar’s top-level menu | params.ui.quick_links |
| Page actions | Copy Markdown, view Markdown source, edit this page, view history, create a child page, open an issue, print the section | Repository configuration and whether this page has a Markdown output |
| Preferences | Switch version → switch language → switch theme | Whether the site configures versions, languages and the light/dark menu |
| Commands | Open the GitHub repository, then the site’s own commands | params.github_project_repo (falling back to github_repo) and ui.command_palette.commands |
The three preferences follow the same order as the navbar controls (version, language, theme); palette and navbar share one ordering. Choosing something like “switch language” does not jump immediately — the palette expands the options in place for a second choice.
As soon as text is typed, page results come first, grouped by content root (the group name is the first breadcrumb segment, and the groups follow the navbar’s top-level menu order), with commands and actions merged into one group at the end.
A query starting with > lists commands and actions only and searches no pages.
Use it when you are unsure which menu holds a feature.
An unavailable item is still listed when the reason can be stated. With no repository configured, “edit this page” stays in the list with an “unavailable” note rather than disappearing.
Quick links
Quick links are selected from Hugo’s main menu by identifier rather than written out a second time:
The values are the identifier of entries in menus.main. Left unset, it
defaults to the documentation and blog sections (params.ui.docs_section and
blog_section). Configuring the menu itself is in
Navigation and menus.
Custom commands
A site’s own commands go under params.ui.command_palette.commands, after the
built-in ones, in the order written:
That is the one this site uses. There are seven fields. An unsupported key or invalid record warns and drops that command during ordinary preview; strict publishing rejects the warning:
idis required, starts with a lowercase letter, and holds only lowercase letters, digits, underscores and hyphens; it must not collide with a built-in action ID.titleis what the palette shows;descriptionis the smaller line beneath it;iconis one Font Awesome class pair.keywordsis an array that takes part in matching without being displayed, for the search terms a reader might type.urlandactionare mutually exclusive and one is required.urlaccepts a fullhttp/httpsaddress, a site path, or an in-page anchor beginning with#; an address with a host opens in a new tab.actionreferences a built-in action ID.
action:Built-in actions are already in the palette, and wrapping one makes the same feature appear twice under two names.
A multilingual site writes the commands under
languages.<lang>.params.ui.command_palette.commands so titles and keywords can
be localized. The order comes from the default language’s list: an entry with
the same id in another language overrides fields only, and a new id is
appended at the end. Command order is therefore identical across languages, and
nothing moves when a reader switches.
Configuration can only supply a link or reference a built-in action; it cannot inject a JavaScript callback. What the palette reads is a plain data manifest.
Page actions
The palette’s “page actions” and the split button beside a documentation title are one implementation: the same action descriptors, the same URL generation, the same executor. The button’s left half copies this page’s Markdown, and the arrow on the right expands every action.
To turn the whole group off, or off on certain pages:
enable: false removes only the button beside the title; the corresponding
items stay in the palette, which is itself the command entry point. A single
page overrides it with the front matter page_context_menu: false.
assistant_links is off by default because clicking one sends the current
page’s full URL — including query string and anchor — to a third party, while
the body is never uploaded. That is a site-level choice, and a page’s
assistant_links in front matter can only narrow it, never enable it on the
site’s behalf.
links adds external actions that appear only in the menu beside the title, not
in the palette:
The three placeholders {url}, {title} and {markdown_url} are replaced with
the current page’s values.
Whether “edit this page”, “view history” and “open an issue” are available
depends on the repository configuration — see
Repository links and page info. “Copy Markdown”
and “view Markdown source” need the page to have the markdown output — see
AI-agent support.
How it relates to full-text search
One dialog, two independent data sources:
- Page results come from the local search index. When the index was never generated or fails to download, the palette still opens and still runs commands, and the page section reads “the page index is unavailable; actions still work”.
- Commands and actions come from a JSON manifest embedded in the page and need no network.
The palette is not assembled in print state, so print output has none of it.
With offline_search off there is likewise no palette, and f and
c stay silent without disturbing normal typing.
Verify
-
After a build, confirm the command manifest reached the page:
Its absence means local search is off, or this page is not in a shell layout.
-
Open the site and press ⌘/Ctrl + K without typing: quick links, page actions, preferences and commands should appear in that order.
-
Type
>: only commands and actions remain. A newly added command should sit after “open the GitHub repository”. -
Repeat step 3 in another language, and confirm the command titles changed while the order did not.
-
A print preview (⌘/Ctrl + P) should show no trace of the palette.
Related
- Search — where the palette’s page results come from
- Keyboard navigation — f, c and the other single keys
- Navigation and menus — the source of quick links and group order
- Repository links and page info — prerequisites for the edit, history and issue actions
- Configuration — full definitions of
ui.command_paletteandui.page_context_menu
8 - Keyboard navigation
OINK’s interactive pages come with a set of single-key shortcuts: WASD moves through the sidebar tree, J K jump between headings, Q E page back and forward, and a few more toggle the theme, the language and the command palette. They are on by default, every binding stands down for typing, and they can be turned off per site or per page.
Keyboard navigation keeps no second copy of any state: expanding and collapsing reuses the sidebar’s own arrow buttons, section jumps read the right-hand outline, and switching language and theme reuse the command palette’s actions. Keyboard order and mouse order are therefore the same order.
Sidebar
| Key | Behaviour |
|---|---|
| W S ↑ ↓ | Move focus to the previous / next visible entry |
| A D ← → | Collapse / expand a group; on a leaf, A goes to the parent and D does nothing |
| Enter Space G | Open the focused page |
| Esc | Leave the tree; focus returns to the body |
The four letter keys need no prior entry into the tree: with focus still in the body, S takes the current page’s sidebar entry as its starting point, moves down one and takes focus. The focused row is shaded a step darker than the “current page” shading, so the two are distinguishable.
When the sidebar is in a drawer on a narrow screen, or collapsed on the desktop, the first press of one of these keys opens it first. On a page with no sidebar tree they are silent.
The arrow keys act on the tree only after focus has entered the sidebar; in the body they keep native browser scrolling. In a right-to-left language ← → swap with the reading direction, while A D always mean “collapse / expand”.
Reading
| Key | Behaviour |
|---|---|
| J K | Jump to the next / previous section along the page outline |
| N | Home page only: jump to the next top-level section (a mnemonic alias for J there) |
| Q E | Previous / next page |
| H | Focused reading: hide / restore the navigation shell |
J K take their target sequence from the same source as the right-hand outline, so they land where clicking the outline lands. The jump is a fixed 100 ms ease regardless of distance, and successive presses need not wait for the previous animation. Once you have read some way into a section, K returns to that section’s start first and only jumps to the previous section on a second press. On a page with no headings it degrades to a short scroll.
Q E page in the sidebar tree’s visible order, not
by date. A section index is itself an entry in the tree, so a blog’s section
boundary reads as “last post of the previous section → next section’s index →
first post of the next section”. A collapsed branch is not in that order:
paging order and focus order are the same order. On a page with no sidebar tree
it falls back to the page-end pager, and without one to rel=prev/next in
<head>.
H hides only the navbar and footer on the home page, and on a documentation page hides the left and right columns and the floating buttons too. The state is kept in the tab’s session and restored before the first frame, so paging through with Q E neither loses it nor flickers. While the shell is hidden, WASD will not send focus into an invisible sidebar.
Appearance, language and routing
| Key | Behaviour |
|---|---|
| L Y | Cycle the language (the two keys are equivalent) |
| T | Toggle light and dark |
| R | Cycle among same-origin top-level navbar entries |
These three work on any interactive page, not only inside the documentation shell. L on a single-language site, T with the light/dark menu off, and R with only one top-level entry are all silent. R cycles only same-origin top-level menu items; external links and navbar utility controls take no part.
Search and commands
| Key | Behaviour |
|---|---|
| F or / | Open the command palette in full search mode |
| C or the backslash key | Open the command palette in command-only mode |
| ⌘ with K or Ctrl with K | Open the palette; press again to close |
/ and backslash belong to search itself and keep working with
keyboard navigation off; F C are aliases keyboard
navigation adds, pointing at the same palette instance. Backslash is awkward on
some non-US layouts, and typing a > prefix in the palette reaches command-only
mode just as well. What the palette holds is in
Command palette.
Keys deliberately left free
? is reserved and unbound. The cheatsheet hangs off the question mark button in the footer’s bottom bar, opens on hover, keyboard focus or touch, and lists the keys actually available on the current page: a single-language site never sees the language row.
G G, Shift with G and the digits are likewise reserved, as possible future jump sequences.
When shortcuts stand down
Every binding is a bare single key, and all of them are disabled wherever they could collide with typing or an overlay:
- Focus is in an input, textarea, select or
contenteditableregion; - An input method is composing (a hard requirement on a Chinese site);
- A modifier is held: ⌘ with C is still copy, Shift with ↓ still belongs to the browser;
- The command palette or another dialog is open, and the keyboard belongs to that overlay.
The comment section lives in an iframe, where key events do not bubble to the page, so no extra isolation is needed.
Focus order and accessibility
- Skip link: the first Tab after landing on a page reveals “skip to main content”, stepping past the navbar and sidebar in one move.
- Real focus: navigating the tree moves actual DOM focus rather than a virtual cursor. A screen reader therefore announces the link name and the “current page” marker, Enter is the link’s native behaviour, and the Tab order is not rewritten.
- High contrast: the focused row’s background drops out under
forced-colorsand degrades to a system highlight outline. - Reduced motion: with
prefers-reduced-motionon, section jumps and paging scroll become instant positioning rather than an ease. - The key caps in the cheatsheet share their styling with the Kbd component used in the body.
Turning it off
Site-wide:
For one page (interaction-heavy demonstration pages often need this), or for a whole section by cascade:
The key accepts a boolean only; "false" or any other value warns and uses the
site default during ordinary preview. A strict publishing build fails on
params.ui.keyboard_nav must be a boolean. The full definition is in
Configuration.
Turned off, the runtime never enters the JavaScript bundle rather than loading and then checking. /, backslash and ⌘ with K belong to search and keep working; the arrows on the footer’s collapsible link grid are unaffected.
Verify
-
After a build, confirm the cheatsheet button is in the page:
With keyboard navigation off and local search not enabled, the button is not generated at all.
-
Open a documentation page, leave the cursor in the body and press S repeatedly: the sidebar should step down from the current page’s entry while the body stays put.
-
Press E several times and check the paging order matches the sidebar top to bottom; collapse a group and page again — the collapsed pages should be skipped.
-
Click into the search box and press J: the page should not scroll, and the character should type normally. The same holds while typing with a Chinese input method.
-
Turn on “reduce motion” in the system and press J: it should position instantly with no glide.
Related
- Command palette — the dialog F C open
- Search — where the palette’s page results come from
- Layouts and page types — which pages have a sidebar and outline, and so which keys apply
- Kbd — writing key caps in your own documentation
- Configuration — the full definition of
ui.keyboard_nav
9 - Languages
OINK uses Hugo’s multilingual model and adds no directory conventions of its
own: configure a languages block, and keep a translation beside its original
in the same directory, distinguished by a filename suffix. What follows covers
what a single-language site has to change to become bilingual, plus the two
things bilingual sites get wrong: resource ownership and heading anchors.
Enabling a second language
That is this site’s configuration. What the four fields do:
labelis the name shown in the language picker, written in that language’s own script:简体中文, notChinese.localeis the standard language tag, and reaches<html lang>, thehreflangalternate links and the Open Graph metadata.weightdecides both language order and the picker’s cycle order, lowest first.paramsis a per-language override: a key not written here inherits the global value of the same name. Date formats usually need one per language.
The default language carries no path prefix (English at /docs/…), and each
other language takes one (Chinese at /zh/docs/…). To give the default language
a prefix too, add defaultContentLanguageInSubdir: true. That changes every URL
on the site, so a live site needs redirects arranged at the same time.
File naming and resources
A translation sits beside its original, distinguished by suffix, and Hugo treats the shared base filename as making them two language versions of one page:
- content/docs/
- install.mdEnglish
- install.zh.mdChinese
- _index.md
- _index.zh.md
Page bundles work the same way: index.md and index.zh.md in one directory.
Resources in a page bundle follow one rule: a resource whose filename has no language suffix is shared by every language, and one with a suffix belongs only to that language.
- content/docs/install/
- index.mdEnglish page
- index.zh.mdChinese page
- topology.webpavailable to both languages
- screenshot.zh.webpavailable to the Chinese page only
When the body references a suffixed resource, write the name without the
suffix: , and Hugo resolves it for the current
language.
That rule has a corollary: in a page bundle holding only index.zh.md with no
English counterpart, unsuffixed resources are not handed to the Chinese page —
they belong to the default language, which has no page in that bundle. Every
resource then needs the .zh. suffix, which is how the Chinese page bundles
under this site’s docs/ are arranged.
What needs translating:
- Translate:
title,description, summaries, menu labels, tag names, image alt text, callout bodies, and reader-facing shortcode parameters. - Keep identical: dates,
weight, aliases, and any metadata affecting routing. A mismatch makes the sidebar order differ between languages. - Do not translate: commands, configuration keys, filenames, URLs, version numbers, product names, shortcode names.
Per-language configuration
Three things live outside content/ and need one copy per language.
Menus are written under their own language:
The identifier must match across languages: the command palette’s quick links
and the grouping order of search results both match on it. Configuring menus
fully is in Navigation and menus.
Home page data is chosen by language: data/home/en.yaml,
data/home/zh.yaml. Without a file for the current language it falls back to
en.yaml; a single-language site needs only one data/home.yaml. See
Home and landing pages.
Interface strings: the theme ships 32 complete interface catalogs: the 31
locale filenames supported by Docsy, plus generic zh. Every catalog contains
all 192 OINK keys in its native language; none relies on generated English
fallback blocks. zh and zh-cn use Simplified Chinese, while zh-tw uses
Traditional Chinese. The exact locale and placeholder contract is recorded in
Architecture. To change a
string, create a file of the same name under the site’s own i18n/ and write
only the keys you override:
Keep the concrete locale: zh-CN shown for the non-default language when
supporting Hugo 0.160.x with regional Chinese catalogs present. Bare
locale: zh is safe in the same configuration from Hugo 0.161 onward.
Untranslated fallback and the language picker
The language picker’s icon is itself a link: clicking it moves to the next
language by weight (wrapping at the end), while hovering or focusing it
expands a menu of every language. On a touch screen the menu does not expand and
a tap switches directly. A bilingual site therefore toggles back and forth in
one click.
The menu always lists every configured language, whether or not the current page is translated:
- The target language has a translation → jump to that page;
- The target language has none → jump to that language’s home page.
Falling back to the home page beats dropping the reader into a 404. The cost is that the reader may not notice being sent there, so a bilingual site should check “every page has a counterpart” as a constraint rather than relying on the fallback.
When a Chinese page does not exist, the Chinese site does not have that page at all: it is absent from the sidebar, the search index and the paging order.
Search indexes are also per language: searching from a Chinese page matches Chinese content only. CJK queries use substring matching, detailed in Search.
Heading anchors must align
Hugo derives an ID from the heading text, so a Chinese heading yields a Chinese
ID: /docs/install/#prerequisites and /zh/docs/install/#前置条件 point at the
same place through two anchors that do not connect, and cross-language deep
links, contents and in-page jumps all break.
The remedy is to write the original’s ID explicitly on the translated heading:
Two disciplines:
- Take the ID from the HTML the English page renders, not from the heading text. When a heading contains inline code, a badge or a shortcode, the generated ID does not match the heading text.
- Corresponding pages must have the same number of headings, in the same order, with the same IDs. Where a translation genuinely needs an extra section, give it an independent, stable ID that does not collide with the English side.
This site turns that constraint into a CI check with a script that compares rendered HTML rather than source:
Writing explicit English {#id} anchors from the moment a page is created costs
less than retrofitting them.
Right-to-left languages
Declare the writing direction under the language:
<html dir> changes with it, and the theme additionally loads Bootstrap’s RTL
stylesheet. The theme’s own CSS uses logical properties throughout
(margin-inline-start rather than margin-left), so mirroring happens by
itself. A site’s own CSS needs logical properties too, or it will be misplaced
under RTL.
Verify
-
Build, and confirm both languages’ output and indexes exist:
-
Check
hreflang: each page’s<head>should carry onerel="alternate"per language plus arel="canonical"pointing at itself. -
On a translated page, expand the language picker and choose the other language; confirm you stay on the same document. Repeat on an untranslated page and confirm you land on that language’s home page rather than a 404.
-
Search the same concept once in each language and confirm both return results.
-
Wire the heading alignment check into CI on a bilingual site, using the script above.
Related
- Search — per-language indexes and CJK queries
- Navigation and menus — per-language menus and where the picker sits
- Home and landing pages —
data/home/<lang>.yaml - Writing pages — how to write explicit heading IDs
- Analytics and SEO — how
hreflangand the sitemap are consumed by search engines
10 - Versions
When a product has several supported versions, its documentation usually needs versions too. The theme provides two things: a version switcher in the navbar, and an archive banner on older sites. The deployment layout is the site’s decision — the theme does no cross-version build, and each version is its own Hugo build.
The version switcher
List the versions that should appear in the menu under params.versions. When
that list is non-empty, a branch-icon menu appears in the navbar’s utility area,
with the same content in an icon-only upward menu in the footer’s bottom bar.
A menu entry shows its version value by default, or name when one is given.
The current entry is marked selected, decided by either the entry’s version
equalling params.version or the entry’s url equalling the site’s baseURL.
An entry with no url renders as an unclickable grey item, usable as a group
heading; name: '---' is a divider (a url on a divider warns). name accepts
inline Markdown:
The same list feeds “switch version” in the command palette, so menu and palette never disagree.
The trade-off in page-for-page links
version_menu_pagelinks: true appends the current page’s path to the target
version’s URL, so switching version keeps the reader on the same document.
The cost is that the target version may not have that page: documentation structure evolves between versions, an older version lacks a newly added page, and the reader who switches lands on a 404. This site leaves the option off.
A single entry can override the global setting:
Turn it on where structure is stable and off where it moved. One extra step to a version’s home page still beats a 404.
The archive banner
On the site of a version no longer maintained, tell the reader it is a snapshot:
With archived_version: true, a banner appears at the top of the body on every
documentation and book page, saying the current version is no longer actively
maintained and linking to url_latest_version. The wording is localized to the
site’s language and needs no authoring; version is the version number the
banner shows.
The banner appears on documentation and book pages only, not on blog or landing pages.
params.version versus params.versions
Two similar names with different jobs:
params.versionsis a cross-site list: which versions the menu can reach and where each lives. It describes other sites.params.versionis this build’s own version identifier. It decides which menu entry is marked selected and which version number the archive banner shows, and it is the fallback whendata/download/*.yamlomitsversion(see Releases and downloads).
It need not be a Git ref. Where a resolvable release tag is needed — the one an
install command references, say — declare a parameter of your own rather than
reusing params.version. The full definitions of both keys are in
Configuration.
Deployment layouts for multiple versions
| Layout | baseURL |
Characteristics |
|---|---|---|
| Subdomain | https://v1-9.docs.example.com/ |
Versions are fully independent; each needs its own DNS and certificate |
| Subpath | https://docs.example.com/v1.9/ |
One domain, SEO weight concentrated; the host must route by path to different artifacts |
Each version is an independent build: check the content out from its branch or
tag, build with that version’s own hugo.yml, and publish to the matching
address. The current version’s site lists every version; an older version’s site
lists them and adds the archive banner.
baseURL must include the path segmentOtherwise the search index, page actions and asset links all point at the domain root: the page looks fine and search returns nothing. This is the most common subpath failure; deployment details are in Deploy.
Verify
-
After a build, confirm the version menu reached the page:
With
params.versionsempty or unset, the menu is not generated at all. -
Check whether the current version is marked selected:
None at all means
params.versiondoes not match any entry’sversionfield, orbaseURLdoes not match that entry’surl(mind the trailing slash). -
Visit each link in the menu. With
version_menu_pagelinkson, try it once from a document an older version lacks and confirm the landing is acceptable. -
On an archived site, open any documentation page: the banner should sit at the top of the body, in the site’s language, linking to the current version.
-
Press ⌘/Ctrl + K to open the command palette; “switch version” should list the same set.
Related
- Navigation and menus — where the version menu sits in the navbar and sidebar
- Command palette — “switch version” in the palette
- Deploy —
baseURL, subpaths and multi-target publishing - Releases and downloads — download data falling back to
params.version - Configuration — full definitions of
version/versions/archived_version
11 - Taxonomies
A directory tree gives a page one path; a taxonomy gives it a second. The same
PostgreSQL backup document sits under an “Operations” directory and is also
reachable from a “backup” tag page. Enabling it needs only Hugo’s taxonomies:
configuration: the term pages, filter chips, rail cloud and navbar panel are all
generated by the theme, with no template to write.
This page carries a category. The “Categories: Customization” line under the title, and the counted chips under the outline in the right column, need no configuration on the page itself.
Enabling a taxonomy
Taxonomies are Hugo’s, and the theme adds no switch of its own. Write
taxonomies: at the top level of hugo.yml, with the singular name as the
key and the plural as the value:
That is this site’s configuration. Three things to note:
- Writing
taxonomies:makes it the complete list, not an addition. To keeptags/categoriesalongside a custom taxonomy, list them too. - The plural is also the URL segment:
/tags/,/categories/. - To turn them all off:
disableKinds: [taxonomy, term].
Adding one of your own, for instance grouping by product module:
Display names: the six keys tag, tags, category, categories, module,
modules have a localized title in every one of the theme’s language files. Any
other taxonomy uses the humanized plural (products → Products). To name one
yourself, write title / linkTitle in content/<plural>/_index.md and
_index.zh.md, and the theme prefers it:
Tagging a page
The front matter key is the plural (the value column of taxonomies), and
the value is always a list, even with one entry:
Where a whole section shares one category, write it in the section index’s
cascade rather than repeating it on every page:
All six documentation sections on this site are configured that way. A page’s
own categories: replaces the cascade rather than merging with it: to add one
beside the section’s category, write both.
The term line on a page
Documentation and blog pages render a line of assigned terms under the title and
summary, each linking to its term page — the “Categories: Customization” at the
top of this page. Its container is .taxonomy-terms-article, with an additional
.taxo-<plural> class per taxonomy; use those two selectors to style it.
By default it lists every taxonomy on the page, except the two reserved
plurals authors and series — each of those has a surface of its own (a
byline and a series strip), so repeating them as chips would say the same thing
twice. Naming one in page_header puts it back.
To show only some, in a fixed order:
The key is catalogued in Configuration. It cannot be used to hide the line — see Limits.
Two taxonomies the theme knows by name
authors and series are ordinary Hugo taxonomies, declared the ordinary way —
the theme adds no parameter for either. What it adds is a rendering of each, so
the declaration alone is the whole switch:
| Plural | What the declaration turns on | The term page becomes |
|---|---|---|
authors |
Portraits and linked names in the article head, names on list rows, one <dc:creator> per author in the feed |
The author’s profile: the display name is the term page’s link title (linkTitle, else title), description the one-line introduction, the body the long one, and the avatar whatever the featured-image resolver picks |
series |
A strip above the article body naming the series, this article’s position, the next part, and the whole list behind a <details> |
The series introduction, listing its members in reading order rather than newest-first |
Both are covered in full, with the front matter each expects, in Writing a blog. Two things worth knowing here:
- There is deliberately no
data/authorsfile. The profile is the term page, so nothing can disagree with it. - A series term page is the one term page that is not in reverse-date order. Members with a
series_weightcome first in ascending order, the rest by ascending date. A term page cannot supply an order to Hugo, so the theme resolves it once and both surfaces read the result.
Term and taxonomy pages
Each taxonomy generates two levels of page:
| Page | URL | Contents |
|---|---|---|
| Taxonomy list | /categories/ |
Titled with the taxonomy’s localized name (“Categories”), followed by filter chips for every term, each with a count, the first being “All” |
| Term page | /categories/customization/ |
Titled “Categories: Customization”, listing every page with that term newest first, styled like the blog list |
A Chinese term’s URL uses Chinese characters (the address bar shows 定制站点 and
the HTML is percent-encoded); Hugo does not transliterate. Where ASCII URLs are
wanted, use English terms and give each a display name with title in
content/categories/<term>/_index.zh.md — Hugo’s
term page content file
mechanism.
A term page has no fixed place in the content tree, so it borrows one: when every member of a term sits under one top-level section, the term page renders that section’s sidebar tree and root link, and a reader clicking a tag from the documentation stays inside the documentation navigation. Where members span sections, it falls back to the site-level tree. The “All” filter chip follows the same rule: with one section it points at that section’s index, and across sections at the taxonomy list page.
Filter chips appear on the taxonomy list page only; term pages carry the rail cloud instead.
The rail cloud
Documentation, blog and term pages carry one group per taxonomy in the right column (under the outline), with counted, collapsible chips. The group is automatic and has no switch: it appears wherever a taxonomy is defined and the current scope has terms.
The count is not site-wide but per top-level section: it first looks for a
section named after the page’s type (a type: docs page uses the /docs/
tree), and otherwise uses the top-level section the page is in. “Tags: release
4” on a blog page means four posts in the blog, not four on the site.
Icons are configured by plural name:
Those two are already the defaults for categories and tags; any other
taxonomy defaults to fa-solid fa-shapes. An icon is one Font Awesome class
pair, written as everywhere else on the site.
The taxonomy panel in the navbar
A main-menu entry pointing at a taxonomy list page automatically becomes a panel of term chips (by descending usage, with counts), needing no hand-written dropdown:
Both pageRef: /tags and the older url: /tags/ are recognized: a URL-form
menu entry is resolved to a site page before its kind is judged, so migrating
from an older configuration needs no rewrite. Other ways to write menus are in
Navigation and menus.
Bilingual terms
Hugo counts and links taxonomies per language: /categories/ and
/zh/categories/ are two unrelated trees, and a Chinese page enters only the
Chinese one. Terms are written once per language in each front matter:
Two things to watch:
- The same word written identically in both languages (say
release) still yields two term pages,/categories/release/and/zh/categories/release/, each counting only its own language’s pages. Do not write English terms on Chinese pages for the sake of uniformity: the rail chips would then show English. - A taxonomy’s display name follows the language (for the six built-in keys), but a term’s name does not: a term is exactly the string written in front matter, and the theme does not translate it. Write
高可用on an English page and the English site’s chip reads高可用.
The rest of running a multilingual site is in Languages.
Switching by content type
The theme has no “show on documentation, hide on blog” switch; the control is which pages you tag. What this site does:
| Content | categories | tags | Effect |
|---|---|---|---|
content/docs/** |
Section-level cascade (the six sections) | none | The term line has one row, Categories |
content/blog/** |
Per post (release, oink) |
Per post (Oink, Release) |
Two rows in the term line, two chip groups in the rail |
To make a whole section disappear from the taxonomy, delete categories from
the section index’s cascade; nothing else is needed. To keep one page out, write
categories: [] in its own front matter — an empty list overrides the cascade.
Verify
Three things to look at on the page:
- A “Categories: Customization” line under this page’s title;
- Chips grouped by taxonomy under the outline in the right column, each with a count;
- /categories/ showing filter chips for every term, each leading to its term page.
On the command line, check the output:
The theme repository has a dedicated check verifying that no taxonomy pages are
generated without taxonomies:, and that term page titles are correct in both
languages:
Limits
page_header: []does not hide the term line: an empty list is treated as unset and falls back to “list every taxonomy”. To remove the line, stop tagging those pages, or hide.taxonomy-terms-articleinassets/scss/_styles_project.scss.- The rail cloud has neither a switch nor a cap; a site with very many terms should reduce its taxonomies, as there is no way to trim it in configuration.
- Term pages have no cross-language pairing: switching language on a term page does not guarantee landing on the same term in the other language.
Related
- Page parameters —
categories/tagsand the other front matter keys - Blog posts — how the blog list works with taxonomies
- Navigation and menus — how to write navbar entries
- Languages — per-language content and menus
- Configuration — full definitions of
params.taxonomy.*andparams.ui.taxonomy_icons
12 - Repository links and page info
The repository-related entries in the action menu at the right of the
breadcrumb row are derived from a few github_* parameters, and the “last
modified” line at the page end comes from git history. Both assume the content
lives in a GitHub-style repository.
Four keys wire up every link
Every repository-related entry in the action menu derives from these keys:
That is this site’s real configuration. With it filled in, this page’s action menu points at:
| Menu entry | Target |
|---|---|
| Edit this page | …/edit/main/content/docs/customize/repository.md |
| View history | …/commits/main/content/docs/customize/repository.md |
| Create child page | …/new/main/content/docs/customize?filename=change-me.md&value=<template> |
| Open a documentation issue | …/issues/new?title=Repository links and page info |
| Open a project issue | https://github.com/pgsty/oink/issues/new |
A few conventions:
github_repopoints at the repository holding the content, not the theme repository. Naming the theme repository sends a reader’s change to the wrong place. Omit it and all five rows above disappear.github_project_repois a second repository, receiving product bugs rather than documentation errors. Do not configure it where readers cannot tell the two apart.github_branchdefaults tomainand names the content branch — not the deployment branch, and not the branch Pages generates.github_subdiris the path inside the repository. Leave it empty when the site source is at the repository root; set it towebsitewhen the source sits in a subdirectory (a repository holding both code andwebsite/, say).
All of these can be set at site level, per language, in a section cascade or in a page’s front matter, which matters when content comes from several repositories. The full definitions are in Configuration.
Content from another repository
When a subtree is mounted from an upstream repository, override the repository
parameters with a section cascade, then use path_base_for_github_subdir to
tell the theme: strip the local path prefix, and append what remains to
github_subdir.
content/reference/api/client.md therefore maps to the upstream’s
docs/api/client.md.
The value of path_base_for_github_subdir is a regular expression. Where the
source filename differs from the local one, use a from / to mapping instead
— for example, matching each section’s _index.md to the upstream README.md:
OINK keeps .md and .zh.md side by side in one directory, so both languages
share a path prefix and the expression needs no language directory. After
changing it, click “edit this page” once from a leaf page, once from a section
index and once in each language: when the expression strips too much, the
generated URL looks plausible and is a 404.
Turning individual entries off
Every menu entry carries a stable action ID:
| Menu entry | Action ID |
|---|---|
| Copy as Markdown | copy_markdown |
| View Markdown source | view_markdown |
| Open in ChatGPT / Claude | open_chatgpt / open_claude |
| View history | view_history |
| Edit this page | edit_page |
| Create child page | create_child_page |
| Open a documentation issue | create_issue |
| Open a project issue | create_project_issue |
| Print the whole section | print_section |
Where a host does not support one, hide it with CSS:
The command palette uses the same IDs, so hiding a menu entry does not remove it from the palette. A target the whole site cannot use should have its key omitted from the configuration rather than covered with CSS: CSS can hide a link, but it cannot make a wrong link right.
The whole menu can also be turned off per page with page_context_menu: false
in front matter — see Page parameters.
The new-page template that “create child page” prefills comes from the theme’s
assets/stubs/new-page-template.md; a site replaces it with its own skeleton by
placing a file of the same name at assets/stubs/new-page-template.md.
Last modified
This line’s data comes from git, not from a file’s mtime. Turn on Hugo’s git support:
The page end then reads “Last modified August 17, 2026 · …/commit/<hash>. The three values
of lastmod_commit:
| Value | What is shown |
|---|---|
subject (default) |
The commit subject plus the abbreviated hash |
hash |
commit a1b2c3d |
none |
The date only, with no commit link |
Any other value warns and uses subject during ordinary preview; a strict
publishing build fails on invalid params.ui.lastmod_commit.
Two things to watch:
- CI needs enough git history. A shallow clone (
fetch-depth: 1) cannot reach a file’s last commit, and the date goes missing or wrong. Setfetch-depth: 0in GitHub Actions. - An uncommitted file has no git time. Previewing a newly written page locally, this line is simply absent.
Where git history is unavailable, do not substitute the build time for “last modified”: build time is not when the content changed.
This line belongs to the annotation component, which is on by default and
sits after feedback and before the pager. Turn it off for a page with
annotation: false.
The line is not all the annotation block renders. The same block also carries two kinds of provenance, both driven by page front matter and needing no template override:
- Upstream attribution: a page derived from elsewhere writes
upstream_linkplus the four required keysupstream_name,upstream_copyright,upstream_licenseandupstream_notice, and the page end gains an attribution line naming the work, the copyright holder, the licence and a link to the full notice. Addingupstream_modified: trueappends a “modified downstream” line. - Translation notice:
params.ui.translation_noticeholds the language code of the authoritative version, and a translated page then shows a line pointing back at the original; a page authored natively in this language opts out withtranslation_notice: false.
Both families are defined in full in Page parameters.
Where customization really is needed, three override points cover one layer each:
| Partial to override | What it changes |
|---|---|
layouts/_partials/annotation-items.html |
Add, remove or reorder the lines, keeping the theme’s markup, icons, print rules and accessible label |
layouts/_partials/page-meta-lastmod.html |
Replace the markup those lines render as |
layouts/_partials/page-annotation.html |
Replace the block’s outer container |
What the page end is made of
The five components are in a fixed order, and every reading layout shares one implementation:
| Order | Component | Theme default | Page switch |
|---|---|---|---|
| 1 | Share | Off (params.ui.share is empty) |
share: false, or the page’s own list |
| 2 | Feedback | Off | feedback: true / false |
| 3 | Annotation | On | annotation: false |
| 4 | Pager | On for docs / book / blog | pager: false |
| 5 | Comments | On when fully configured | comments: false |
The order follows what a reader does after the last paragraph: hand the page on, say whether it helped, see where it came from, go to the next one, join the discussion. Share leads because it is the only block that points outward, and because a reader who has decided to pass a page on decided it before being asked how the page went. Configuring the bar is in Writing a blog.
Configuring comments is in Comments.
The feedback widget
One question and two buttons: “Did this page solve your problem?” → yes / no. Choosing no expands four optional reasons. It is off by default:
To enable it for the documentation section only, use a cascade (a blog usually keeps just comments):
Where the boundaries are:
- A click completes it. There is no text box, no submit button and no sign-in.
- The choice is written to the browser’s
localStorageper page and language, so a returning reader sees and can change it. - Where the site already has Google Analytics (
gtag), it sends adocs_feedbackevent withresult(solved/not_solved),page_pathandlanguage; choosing a reason sends a second event carryingreasonandrefinement: true, distinguishing it from the first count. Without analytics the widget still works, simply reporting nothing — it needs no backend at all. - Where the page has comments enabled, an anchor link reading “add details in the comments” appears under the result. Feedback and giscus are two independent data flows, and the theme never writes a comment on the reader’s behalf.
This page sets feedback: true in its front matter (the docs section defaults
it off), so the real widget is visible at the page end.
The contributor wall
The contributors shortcode renders a wall of GitHub avatars from a file under
the site’s data/ directory, and never contacts GitHub at build time:
The fields: github is required and validated as a GitHub username; a
duplicate warns and skips the repeated entry, and strict publishing rejects
the warning. name defaults to github; role is optional;
url defaults to https://github.com/<github>; avatar is optional, and
without it an initial placeholder block is rendered with no network request at
all, while a value must be http(s):// or a site-root-relative path.
Several lists mean several data files, selected with data=:
In Markdown and RSS output the wall degrades to a list of
- [@handle](url) — role.
data/contributors.yamlThe example above therefore does not render on this page. Drop a data file
into data/ to see it.
Verify
- Open the action menu at the right of this page’s breadcrumb row: “edit this page” should point at
github.com/<your repository>/edit/<branch>/<source path>, with the path matching the repository segment for segment. - Click it again from a section index (
_index.md): a section index is the likeliest thing for apath_base_for_github_subdirexpression to get wrong. - The page end should have a “last modified” line; its absence on a locally created, not-yet-committed page is expected.
- Check the generated links from the command line:
Related
- Page parameters —
annotation/feedback/pager/page_context_menuand the other page switches - Configuration — full definitions of
github_*,ui.lastmod_commitandui.feedback - Comments — the last block at the page end
- Analytics and SEO — where feedback events land
- AI-agent support — the Markdown and assistant entries in the action menu
13 - Print
Printing one page needs no configuration: the shell (sidebar, outline, navbar,
buttons) all carries d-print-none, so the browser’s Cmd/Ctrl+P yields a
clean body. That is why the theme has no per-page “print this page” button.
What does need configuration is the other thing: assembling a whole section (or a whole book) and all its pages into one continuous document with a table of contents. What follows covers enabling it, the structure of the print view, and how to exclude pages.
Enabling whole-section print
print is a custom output format the theme declares and does not enable for a
site. Add it to section in the site’s own hugo.yml:
That is this site’s configuration. Each key under outputs is a wholesale
replacement rather than a merge: adding print means writing back every
format that kind already had (HTML, RSS, markdown), and omitting one loses
that output.
Once on, every section gains a URL. The _print segment comes first, after the
language prefix:
| Page | Print view |
|---|---|
/docs/customize/ |
/_print/docs/customize/ |
/docs/ |
/_print/docs/ |
/blog/release/ |
/_print/blog/release/ |
“Print the whole section” also appears in the page action menu, and the same
entry is searchable in the command palette (action ID print_section). It
prints the current section: clicking it on /docs/customize/print/ produces
the entire Customization section, not this one page.
The structure of a print view
Opening any of those links, from top to bottom:
- A notice bar: “This is the multi-page printable view of this section. Click here to print. Return to the regular view of this page.” It carries
d-print-noneand appears on screen only, never on paper. - The section title and summary.
- A whole-section table of contents, numbered
1:,2:,2.1:by level, linking to in-document anchors. - Each page in turn, its title becoming “number - title” as in `1 - Configuration", its description a standfirst, and its body rendered as it stands.
Page order is sidebar order (weight), with subsections expanded recursively.
Every page from the second onwards starts a new sheet; whether the first does
depends on whether the section index’s own body exceeds 50 words, so an index of
one sentence does not take a sheet to itself. The threshold is adjustable:
To drop the table of contents:
It can also be turned off for one section, in the section index’s front matter:
Excluding pages
Link-only pages, pages that are one redirect note, and pages that are one
enormous screenshot are not worth paper. Give them no_print:
It affects the whole-section print view only; the page’s own HTML and the
browser’s Cmd/Ctrl+P are unaffected. Sidebar dividers (sidebar_divider) are
excluded automatically.
How components look in print
Print is one of the four outputs, and every component has a defined print shape. The whole-section print view and the browser printing one page follow the same rule: anything interactive degrades to static, and anything collapsible is expanded.
| Component | Print shape |
|---|---|
| Callouts | Static blocks, with every collapsible kind (- / + / DETAILS) expanded; borders go grey and backgrounds drop |
| Tabs | The tab bar disappears and every panel is expanded in turn, each with its own heading |
| Code Blocks | Copy and fold controls removed, max height and scrolling dropped, long lines wrapped |
| Tables | Full-width static tables with no horizontal scroll; headers repeat across pages |
| Images | Image and caption kept, zoom attributes stripped, width brought inside the measure |
| Gallery | The grid becomes a vertical stack |
| FileTree | A static panel with every directory expanded and the split frozen at its build-time width |
| Fields | A complete definition list, identical in both forms |
| Math | Statically rendered KaTeX / MathML |
| Mermaid · Markmap · PlantUML | Still rendered as diagrams: the print view is an HTML page, and these runtimes load as usual |
| ECharts · Infographic | Degrade to the fence source block; no chart is drawn |
| Asciinema · OpenAPI | A labelled static link showing the recording or specification address; none of the three runtimes loads |
| Cards / steps / badges / keys | Static, with content unchanged |
The page shell never reaches paper: sidebar, outline, navbar, the page action menu, the feedback widget, heading anchor links and inline copy buttons.
For the three diagram kinds above that a browser runtime draws (Mermaid, Markmap, PlantUML), confirm they have finished drawing before triggering print.
Browser print styles
The theme ships a layer of @media print rules shared by single-page and
whole-section printing:
A4paper with18mm 16mm 20mmmargins; 10.5pt body text; the light palette forced.- Fonts switch to the
--td-print-font-familytypography token — see Brand and appearance. - Headings do not separate from their body (
break-after: avoid-page), and paragraphs and list items keep three-line orphan and widow control. - Tables, images, blockquotes, callouts, cards and tabs avoid breaking across pages where possible; code blocks may break, and wrap rather than truncate.
- Links are underlined and turned dark blue, and the URL text is not printed after them. A site that wants that behaviour adds it:
- A closed
<details>is always expanded: collapsed callouts and file tree directories are complete on paper.
Custom print styling goes in a @media print block in
assets/scss/_styles_project.scss and needs no template change.
Replacing the print templates
To change the structure — adding a running header, or changing the numbering
format — override the narrowest partial. They are all under
layouts/_partials/print/:
| Partial | Responsibility |
|---|---|
print/render.html |
The whole-section skeleton: notice bar, contents, recursive content |
print/page-heading.html |
The title and standfirst at the top of the document |
print/content.html |
How one page appears inside the whole-section view |
print/toc-li.html |
One row of the table of contents |
The last three additionally support per content type: create
print/page-heading-blog.html or print/content-book.html and the theme
prefers the type-suffixed one.
Printing a whole book (type: book) takes a different path, where chapter
numbers, figure numbers and cross-references stay continuous across the book —
see Books.
Verify
Then look at the page:
- Open
/_print/docs/customize/in a browser and confirm the contents has as many rows as the section has pages (minus those withno_print: true). - Press
Cmd/Ctrl+Pin that view: the print preview should show no notice bar, no navbar and no buttons. - Find a page with tabs and a collapsed callout (for example Tabs) and confirm every panel is expanded in the preview.
- Print a PDF and read the pagination through, adjusting
section_break_wordcountwhere the threshold does not suit.
Related
- Books — numbering, indexes and print for a whole book
- Organizing content — print order is sidebar order
- Brand and appearance — the print font token
- AI-agent support — the other non-HTML output
- Configuration — full definitions of
outputsandparams.print.*
14 - AI-agent support
.md twin, the site root an llms.txt, and the reader a way to hand the current page to ChatGPT or Claude.An HTML page carries a sidebar, scripts and styles, and a model has to strip
that shell before reading it. OINK emits the same content a second time as plain
Markdown: one .md per page, one llms.txt index at the site root, and a “copy
as Markdown” button on the page. All three are build-time artifacts, with no
runtime service and no content negotiation.
All three have to be declared by the site under outputs; the theme does not
turn them on. Two further artifacts, equally opt-in, serve an agent that wants
more than one page at a time: a full-text bundle per section and a navigation
tree per language.
A .md per page
markdown is one of Hugo’s built-in output formats. Add it to the page kinds
that need it:
That is this site’s configuration. Each key under outputs is a wholesale
replacement rather than a merge: adding markdown means writing back every
format that kind already had (RSS, print), and omitting one loses that
output.
The URL rule is the page URL plus index.md:
| Page | Markdown |
|---|---|
/docs/customize/agents/ |
/docs/customize/agents/index.md |
/docs/customize/ (section index) |
/docs/customize/index.md |
/ (site home) |
/index.md |
Each HTML page’s <head> also carries a discovery link, so a crawler need not
guess the URL:
What the .md contains
It is not rendered HTML converted back to Markdown but the source you wrote: the front matter becomes an H1 and a blockquoted summary, and the body follows verbatim, with shortcodes expanded in place into their own Markdown forms.
Components in their native Markdown form (callouts, tables, field lists, image
attribute lines, code fences, data fences) keep their source in the .md, so
what the model reads is what you wrote. A section index additionally appends a
Section pages: list of child links after the body.
Shortcode forms each have a defined degradation: a
badge becomes emphasized text or a link, a
key becomes Ctrl + K,
tabs become a run of **Label** subsections, and
fields become an item list. Each component page’s
Output section states its own row.
Where the site has not enabled the LLMS output, that LLMS index: line does
not appear: the theme never points at a file it did not publish.
llms.txt
llms.txt is a plain-text manifest at the site root
telling a model what the site holds and where the machine-readable versions
are. Add the LLMS output format to the home page to generate it:
A multilingual site gets one per language: /llms.txt and
/zh/llms.txt. The content is a generated site index:
Where the three sections come from: Site index is this language’s home page
plus the site’s main menu (menus.main, linking the Markdown version where an
entry has one, and carrying description where present); Documentation index
is the docs section’s subsections and the level of pages beneath them, indented
by level, each row carrying that page’s description; Site locales is every
language in the site configuration. Menu entries pointing off-site (GitHub, an
issue tracker) are dropped: they belong to the navigation shell rather than to
this site’s content.
The way to improve llms.txt is through the main menu and each section index’s
description, not through this template.
Full-text bundle
One .md per page suits an agent that already knows which page it wants; an
agent that wants the whole manual has to crawl it page by page. The LLMSFULL
output collapses that into one file per top-level section: llms-full.txt,
holding every page of the section in reading order. It is new in OINK 0.8.0 and
stays off until a section asks for it.
The switch is the section index’s own front matter rather than the site configuration:
Front matter outputs replaces the site-level list for that page, so write back
the formats the section already had: omitting markdown or print here costs
the section index those outputs. Front matter is per language, so a bilingual
site repeats the line in _index.zh.md to get the Chinese bundle.
The result is one file per language at the section root — /docs/llms-full.txt
and /zh/docs/llms-full.txt. The order is the reading order the sidebar and the
pager present: the explicit data/docs_nav.json tree where a docs or book
section declares one, the weighted content tree otherwise. Pages held out of the
sidebar (toc_hide) stay out of the bundle too.
Each page is introduced by a separator carrying its source URL, and the body
that follows is byte-identical to that page’s own .md:
Source: points at the page’s Markdown output, falling back to its HTML URL
where the page publishes no .md.
Only a top-level section can carry a bundle. Listing LLMSFULL further down the
tree warns — “LLMSFULL output requires a top-level section” — and emits nothing,
so hugo server keeps working while a publishing build with --panicOnWarning
stops there.
Where at least one section has a bundle, llms.txt grows a ## Full-text bundles list of this language’s bundles: discovery stays in the file an agent
already fetches.
This site’s docs section has it enabled: https://oink.pgsty.com/docs/llms-full.txt is the entire English documentation in one fetch.
Navigation JSON
The sidebar is the site’s table of contents, and an agent that can read it plans
a route before fetching anything. The NAVJSON output publishes it as data:
navigation.json, one file per language at the language root. Like the bundle
it is new in OINK 0.8.0 and off by default; the site turns it on for the home
page:
That yields /navigation.json and /zh/navigation.json. The tree is the one
the sidebar and the pager already read — the explicit data/docs_nav.json tree
where a docs or book section declares one, the weighted content tree
everywhere else:
| Key | What it holds |
|---|---|
id |
The page’s path with the language prefix removed, so the same page carries the same id in every language |
url |
The absolute URL of this language’s HTML page |
markdown |
The absolute URL of the page’s .md, present only where the page publishes one |
title |
The navigation title (linkTitle, falling back to title) |
description |
The page’s description, where it has one |
kind |
home, section or page for real pages; external or link for placeholders |
children |
The ordered children, where the node has any |
Array order is the contract, and weight is never serialized: the ordering has
already been applied, and a consumer re-sorting the array would disagree with
the sidebar the array came from.
Placeholder rows keep the shape the sidebar gives them: a manual_link entry
becomes a node of kind external carrying the URL as authored, a
manual_link_relref entry becomes kind link with the reference resolved.
Neither has page identity, so neither carries an id or a markdown URL.
Sidebar dividers and pages Hugo never renders drop out, while their children
stay in place.
The contract is versioned: schemaVersion is 1, and the JSON Schema ships in
the theme repository as
schema/nav.v1.schema.json
— validate against it if you consume the file. Where the site publishes it,
llms.txt lists navigation.json for its own language in the site index.
This site has it enabled: https://oink.pgsty.com/navigation.json is a live instance of the tree.
Agent actions on the page
Four entries in the action menu at the right of the breadcrumb row relate to agents:
| Entry | What it does | When it appears |
|---|---|---|
| Copy as Markdown | Fetches this page’s .md into the clipboard (prefetched on hover, so a click has no perceptible wait) |
This page has a markdown output |
| View Markdown source | Opens the .md in a new tab |
This page has a markdown output |
| Open in ChatGPT | Jumps to ChatGPT with a prompt | assistant_links: true |
| Open in Claude | The same, to Claude | assistant_links: true |
The first two exist as soon as the markdown output is on. “Copy” is the left
half of the split button (the clipboard icon), and shows a brief tick on
success.
The last two are off by default and must be enabled explicitly:
Where the boundary lies once enabled: on a click, the runtime composes a prompt
using the full URL from the address bar (real domain, query string and anchor
included) — in English, “Please read the contents of
A page may narrow the site policy but not reverse it: front matter
page_context_menu: { assistant_links: false } turns the assistant links off
for that page, while writing true where the site has not enabled them has no
effect. To turn the whole menu off for a page, use page_context_menu: false —
see Page parameters.
Both assistant actions are also searchable in the command palette, from the same action manifest — see Command palette.
Opting a page out of .md output
Rewrite outputs in the page’s front matter. It is likewise a wholesale
replacement, so write only the formats you keep:
To keep RSS and drop only Markdown, list the rest:
Customizing the output
The theme renders Markdown output with layouts/all.md, generates llms.txt
with layouts/index.llms.txt, and owns the two opt-in formats in
layouts/list.llmsfull.txt and layouts/index.navjson.json. A site replaces
any of them wholesale by placing a file of the same name under its own
layouts/, but consider a narrower approach first:
- Per content type: a typed path such as
layouts/blog/single.mdorlayouts/docs/list.mdaffects only that kind of content, which is how the theme’s own print templates are specialized (layouts/blog/single.print.html). Check the template lookup order for your combination. - Per shortcode: a site’s own shortcode can have an output-format-specific template giving it a more machine-readable form in Markdown output.
- Per page: hand-writing the content of a few high-value pages costs less than changing a template.
The content of llms.txt follows the site’s structure, so before changing the
template, confirm the problem is not in the main menu or a description.
Replacing index.navjson.json also takes over the nav.v1 contract: whatever
you emit still has to satisfy schema/nav.v1.schema.json for a consumer that
validates.
Verify
With curl, against production or a local preview:
Then check four things:
- Any page’s HTML
<head>hasrel="alternate" type="text/markdown"; - Clicking the copy button at the right of the breadcrumb row and pasting yields Markdown rather than HTML;
llms.txtcontains no off-site links;- Where you enabled them: every page in
llms-full.txtopens with aSource:line, and the same page carries the sameidin each language’snavigation.json.
Limits
- The machine-readable surface the theme produces is four build-time files: a
.mdper page,llms.txt, and — where you opt in —llms-full.txtper top-level section andnavigation.jsonper language. The sitemap is still Hugo’s ownsitemap.xml. - A bundle belongs to a top-level section. There is no whole-site
llms-full.txt: an agent that wants everything reads one bundle per section, listed inllms.txt. LLMS,LLMSFULLandNAVJSONare all declared as non-alternative formats, so none of them appears in the<head>alternate links or gains a page action. They are discovered by their conventional paths and by the entriesllms.txtcarries for them.- Server-side content negotiation (one URL returning Markdown for
Accept: text/markdown) is outside the theme’s scope and belongs to the hosting layer. - Markdown output follows the source path: content generated only in the browser by JavaScript (a runtime-drawn chart) appears in the
.mdas fence source, not as a diagram.
Related
- Print — the other non-HTML output
- Command palette — the other entry point to the assistant actions
- Page parameters —
outputs/assistant_links/page_context_menu - Navigation and menus —
llms.txt’s site index comes from the main menu - Configuration — full definitions of
outputsandparams.ui.page_context_menu.*