================ Source: https://oink.pgsty.com/docs/index.md ================ # OINK Documentation > OINK is a local-first Hugo documentation framework — components stay readable in Markdown, assets ship with the theme, multilingual sites work out of the box, and one source serves readers and agents. --- LLMS index: [llms.txt](/llms.txt) --- OINK is a Hugo theme for technical documentation. Components are part of the Markdown syntax rather than a second template language; the fonts, icons, search and diagram runtimes the browser needs ship with the theme; the only build dependency is one Hugo Extended binary, with no Node.js and no CDN request. The current release is v1.0.0. ## Five ways in {#five-entries} - [Get started](/docs/start/) — create an OINK Starter repository, establish a local baseline, customize it in layers, deploy. - [Components](/docs/components/) — one page per component, source first and rendered result after it. - [Write Beautiful Docs](/book/) — a hands-on tutorial from first preview to a maintained publication. - [Case studies](/case/) — production sites explained as reusable design and migration patterns. - [Design and development](/docs/design/) — contracts, accepted decisions, research evidence, and active proposals for OINK maintainers. {.cards} ## Find it by task {#where-to-go} | What you want to do | Where to go | | ----------------------------------------------------- | ---------------------------------------------- | | Decide whether it fits | [What is OINK](/docs/about/) | | Install and preview | [Get started](/docs/start/) | | Write a documentation page | [Writing pages](/docs/write/pages/) | | Turn a directory tree into a sidebar | [Organizing content](/docs/write/organize/) | | Look up a component's syntax | [Components](/docs/components/) | | Change the name, logo, colours and fonts | [Brand and appearance](/docs/customize/brand/) | | Look up a configuration key's default | [Configuration](/docs/customize/config/) | | Run a bilingual or multilingual site | [Languages](/docs/customize/i18n/) | | Learn OINK end to end | [Write Beautiful Docs](/book/) | | Study a production implementation | [Case studies](/case/) | | Deploy | [Deploy](/docs/admin/deploy/) | | Upgrade, or migrate from Docsy | [Upgrade](/docs/admin/upgrade/) | | Maintain the theme, review a contract, or write a PRD | [Design and development](/docs/design/) | The seven Docs sections are ordered the way they are read: understand, install, write content, look up components, adjust the site, run the release, then study or maintain the contracts and design records behind it. --- Section pages: - [What is OINK](/docs/about/): A local-first Hugo documentation framework evolved from Docsy. Its components stay readable in Markdown, its assets ship with the theme, and fifteen production sites exercise it. - [Get started](/docs/start/): Start from the official OINK Starter, establish a working local baseline, then customize content, language, brand, integrations, and deployment in that order. - [Authoring](/docs/write/): Writing documentation pages, blog posts, books, release pages and API references — what a page looks like, and how content is organized. - [Components](/docs/components/): Every component available for writing, one page each, examples from the simplest upwards, with the parameter table at the end. - [Customization](/docs/customize/): Site-level configuration — brand, navigation, layout, search, languages, versions, print and agent output. - [Operations](/docs/admin/): Running the site from a laptop to production — local preview, deployment, comments, analytics and SEO, upgrades and troubleshooting. - [Design and development](/docs/design/): OINK maintainer contracts, accepted decisions, dated research, and proposals in one canonical bilingual section. ================ Source: https://oink.pgsty.com/docs/about/index.md ================ # What is OINK > A local-first Hugo documentation framework evolved from Docsy. Its components stay readable in Markdown, its assets ship with the theme, and fifteen production sites exercise it. --- LLMS index: [llms.txt](/llms.txt) --- OINK is a standalone [Hugo](https://gohugo.io/) theme for medium and large technical documentation sites. It evolved from [Docsy](https://github.com/google/docsy): the content model and the multilingual behaviour are kept, while the shell, navigation, search and content components are replaced. A consuming site's only build dependency is one Hugo Extended binary. There is no Node.js, no npm, no PostCSS and no CDN request. Bootstrap, Font Awesome, the fonts, local search, the diagram runtimes and the API reference runtimes are all committed to the theme repository and shipped only to the pages that use them. Components are not a second template language: `> [!NOTE]` is a callout, a table with a `{.fields}` line is a parameter list, and an image followed by `{caption=}` has a caption. [Fifteen production sites](/docs/about/showcase/) run on it today, this one among them. ![OINK turns Markdown content, configuration and local assets into one static documentation site](/images/hero-light.webp) {width="900" height="600" caption="One Hugo build produces a static site ready to host"} ## What the theme provides {#what-oink-provides} - The documentation and blog shell: navigation, sidebar tree, table of contents, breadcrumbs, pager, dark mode, print view and accessible interaction. - The multilingual frame: translation routing, fallback for untranslated pages, language weighting, RTL, and 32 interface language packs. - Local runtimes: Mermaid, KaTeX, Markmap, Swagger UI, Redoc, Asciinema, ECharts, Infographic and local full-text search. - Content components: callouts, tabs, steps, cards, field lists, file trees, galleries, badges, keys and more — most with a native Markdown form. - Content types: beyond ordinary documentation, built-in book numbering and cross-references, release and download pages, data-driven landing pages, and OpenAPI reference pages. The theme does not handle source hosting or deployment: a site can live on GitHub, GitLab or a private Git server, and the static files Hugo produces can be published anywhere. A site's own content, brand and business components stay with the site; the theme supplies the shell and the reusable components. ## Is OINK for me {#is-oink-for-me} | A good fit when | A poor fit when | | --- | --- | | There are many pages and mixed content types: documentation, blog, a book, release pages and an API reference in one site | There are one or two pages and no need for structured navigation; a README or a lighter Hugo theme is simpler | | You need real multilingual support, not a translation link bolted onto an English site | The site is mostly application UI rather than documentation: OINK can carry the documentation part while business components stay at the site layer | | Reproducible builds and network isolation matter, and the build machine has no outbound access | You need interactive components inside the prose (React / MDX) | | Several sites share one shell, so layouts and shortcodes are not copied around | You want one switch that swaps in a different look: the theme has no brand switch, and appearance changes go through CSS tokens and partial overrides | | The team has no front-end engineers and maintains no Node toolchain | You need a built-in CMS or a WYSIWYG editor | ## How it differs from other documentation systems {#comparison} The table below lists structural differences only, and only what can be confirmed from each project's own documentation and repository. Versions change; check each project's current documentation before choosing. | Dimension | OINK | Docsy | Hextra | Docusaurus | | --- | --- | --- | --- | --- | | Build tool | Hugo Extended, one binary | Hugo Extended + Node/npm | Hugo | Node.js toolchain | | Does a consuming site need npm | No | Yes: Bootstrap and Font Awesome are mounted from `node_modules/` | No | Yes | | Where front-end assets come from | All committed to the theme repository; `VENDOR.json` records version, source, licence and checksum | jQuery is loaded from a CDN on every page unconditionally; Mermaid, KaTeX and others also fetch from a CDN at build time | Prebuilt artifacts committed to the repository | npm dependencies | | How components are written | Native Markdown attributes and fences first, 29 shortcodes as the fallback | Shortcodes (19) | Shortcodes (29) first; callouts also have a `> [!NOTE]` native form | MDX (React components) | | Multilingual | Hugo multilingual + 32 interface language packs | Hugo multilingual (OINK's packs are inherited from it) | Hugo multilingual + 21 interface language packs | Built-in i18n framework | | Book numbering and cross-references / release and download pages / data-driven landing pages | Built into the theme | None | None | Build your own or find a plugin | Two qualifications. Per-page Markdown output and `llms.txt` are not unique to OINK — Docsy and Hextra have them too, and all three need the site to opt in under `outputs`. Only the last row is exclusive to OINK, and it comes from PGSTY's own production sites rather than from what a general documentation site needs. The theme's interactive features are off by default: search, zoom, comments and feedback all require the site to turn them on. OINK is not a skin layered over Docsy but a theme that forked and evolved separately. Docsy's source history, its Apache-2.0 obligations and its attribution are kept intact; the details are in [License and acknowledgements](/docs/about/license/). ## Start here {#start-here} - [Get started](/docs/start/) — use the official Starter, customize it in layers, and publish it. - [Components](/docs/components/) — one page per component, source first and rendered result after. - [Showcase](/docs/about/showcase/) — fifteen production sites and which part of OINK each one uses. {.cards} [Highlights](/docs/about/features/) lists what the theme provides capability by capability, each entry linking to the guide that covers it. --- Section pages: - [Highlights](/docs/about/features/): What separates OINK from an ordinary Hugo theme, one item at a time, each linking to the guide that covers it. - [Case Guide](/docs/about/showcase/): Find the OINK production case closest to your documentation, book, landing page, or interactive tool. - [License and acknowledgements](/docs/about/license/): Which licence applies to which layer — Apache-2.0 for the theme, CC BY 4.0 for the documentation, and their own terms for every third-party runtime shipped with the theme. --- Backlinks: - [Introducing OINK](/blog/oink/oink-announcement/) - [Docs](/docs/) - [Highlights](/docs/about/features/) - [License](/docs/about/license/) ================ Source: https://oink.pgsty.com/docs/about/features/index.md ================ # Highlights > What separates OINK from an ordinary Hugo theme, one item at a time, each linking to the guide that covers it. --- LLMS index: [llms.txt](/llms.txt) --- This page lists what separates OINK from an ordinary Hugo theme, each item ending with the guide that covers it. To install straight away, see [Quick start](/docs/start/). ## Components are written in Markdown {#native-components} A callout is a `> [!NOTE]` blockquote (ten semantic types plus one neutral disclosure). A field list is a table with a `{.fields}` line. Steps and cards are lists with `{.steps}` / `{.cards}`. A caption is a `{caption="…"}` line under an image. Tabs are adjacent fences each carrying a `{tab="…"}`; file trees, galleries, Mermaid and ECharts are data fences named after their language. On GitHub or in any plain Markdown reader these degrade to blockquotes, tables, lists and code blocks, and nothing is lost. 29 shortcodes cover what the native forms cannot express: cards with icons and images, field entries whose body is several paragraphs of Markdown. → [Components](/docs/components/) ## One Hugo binary is enough {#hugo-only} A consuming site's entire build dependency is Hugo Extended 0.160.1 or newer. SCSS is compiled by Hugo's embedded Sass transpiler; the theme never invokes `postCSS`. There is no npm, no webpack and no build-time download. Installing the theme as a Hugo Module needs Go on the machine to resolve the module; an offline archive or a submodule does not. "Hugo only" refers to the build dependency. The interface still runs JavaScript in the browser: search, the command palette, diagrams and tabs are page scripts. The difference is that those scripts ship with the theme and are delivered per page according to what that page actually uses. → [Quick start](/docs/start/) ## Local-first {#local-first} Everything the browser needs is committed to the theme repository: Bootstrap, Font Awesome, four fonts, Lunr, Mermaid, KaTeX, Markmap, Swagger UI, Redoc, Asciinema, ECharts, Infographic. `VENDOR.json` records the version, source, licence file and SHA-256 checksum of each of the 26 dependencies; updating a runtime means updating artifact, licence and checksum together. Where a feature could cause a network request, the theme leaves it off rather than reaching out silently: PlantUML without `params.plantuml.svg_image_url`, Diagrams.net without `params.drawio.drawio_server`, and Algolia without `appId` / `apiKey` / `indexName` each warn and stay disabled, and a publishing gate built with `--panicOnWarning` turns that warning into a failure. Local-first does not extend to what an author adds. All of these are explicit network choices: external links, remote images and video, iframes, remote API specifications; hosted search such as Algolia or Google Programmable Search; analytics, comments and other SaaS integrations; and PlantUML or Diagrams.net once the author configures a remote renderer. Pages using them are still valid pages, but a site should stop claiming those pages work fully offline. → [License and acknowledgements](/docs/about/license/) · [Configuration](/docs/customize/config/) ## One source, four outputs {#four-outputs} Every component has a defined shape in all four outputs: interactive HTML; a print page with zoom and copy controls stripped and disclosures fully expanded; plain Markdown; and RSS. The print view is generated per section (this one is `/_print/docs/about/`), and the Markdown version is the same page address plus `index.md`. A site chooses which of them it wants under `outputs`; the theme does not decide for it. → [Print](/docs/customize/print/) · [AI-agent support](/docs/customize/agents/) ## Two languages and 32 interface locales {#multilingual} Multilingual support uses Hugo's own mechanism: translation routing, a language picker ordered by weight, fallback for untranslated pages, RTL, and canonical and alternate metadata. Interface strings come in 32 language packs sharing one key schema. English, Simplified Chinese (`zh-cn` and the generic `zh`) and Traditional Chinese (`zh-tw`) are human-reviewed; the other locales keep the translations inherited from Docsy, with English fallbacks for the keys OINK added. → [Languages](/docs/customize/i18n/) ## Full-text search that stays on the site {#search} With `params.offline_search` on, Hugo generates one index per language. The browser searches Latin text with a local Lunr index and falls back to substring matching for CJK text; no query leaves for a third party. A page can adjust its weight with `search_boost` and add synonyms with `search_keywords`. → [Search](/docs/customize/search/) ## Command palette {#command-palette} `Cmd/Ctrl + K` opens the command palette; a bare `/` enters search mode and a bare `\` enters command-only mode. The palette holds pages, commands and page actions (switch language, switch theme, copy Markdown) together, so searching and acting share one entry point. → [Command palette](/docs/customize/panel/) ## Keyboard navigation {#keyboard} On by default, and switchable off per site or per section. `w` and `s` move up and down the sidebar tree, `a` and `d` collapse and expand, `q` and `e` go to the previous and next page, `j` and `k` jump along the page's table of contents, `t` toggles light and dark, `l` switches language, `h` hides the reading shell. Every single-key shortcut stands down while an input or textarea has focus or an input method is composing. The question-mark button in the footer's bottom bar opens the cheatsheet. → [Keyboard navigation](/docs/customize/keyboard/) ## Backlinks {#backlinks} Turn on `params.ui.backlinks` and every page lists the pages that link to it — derived at build time from the ordinary links you already write, with no new syntax and no JavaScript. This site enables it site-wide: look at the "Linked from" group in this page's right rail, and the more a page is referenced, the longer its list — past eight entries it folds. → [Backlinks](/docs/customize/navigation/#backlinks) ## Four content types beyond documentation {#content-types} The theme also has four kinds of page that need extra structure: - Books: chapter numbering, figures / tables / equations / examples numbered with `{#id num=}` and cross-referenced with `xref`, indexes generated by `book-toc` and `book-figures` and friends, and a printable whole. - Release and download pages: `data/download/*.yaml` produces release cards, asset tables and checksums, with a controlled publication state. - Landing pages: `data/home/.yaml` assembles the home page sections; any page with `layout: landing` can use data under `data/landing/`. - API references: Swagger UI and Redoc are both local runtimes, and the specification can live on the site. → [Books](/docs/write/book/) · [Releases and downloads](/docs/write/releases/) · [Home and landing pages](/docs/customize/home/) · [API reference pages](/docs/write/openapi/) ## Output for AI assistants {#agent-output} Add `markdown` to `outputs` and every page gains a `.md` twin, the HTML `` gains a `rel="alternate"` pointing at it, and the page actions gain "Copy Markdown" and "View source". The `LLMS` output format writes an `llms.txt` inventory at the site root (this site's is ). 0.8.0 adds two more: a section that enables `LLMSFULL` becomes one `llms-full.txt` an agent fetches in a single request, and a site that enables `NAVJSON` publishes `navigation.json` per language — the sidebar's tree readable as data. Both are live on this site: and are the real artifacts. "Open in ChatGPT / Claude" is off by default: clicking it hands the current URL to a third party, so the site must turn on `params.ui.page_context_menu.assistant_links` explicitly. → [AI-agent support](/docs/customize/agents/) ## Versions {#versions} Configure `params.versions` and a version menu appears in the navbar, while an archived version shows a banner at the top of the page pointing readers at the current one; whether the menu jumps page-for-page is the site's choice. The versions are separately built and separately deployed static sites, so nothing is needed at runtime. → [Versions](/docs/customize/versions/) ## See for yourself {#verify} This site has most of the above enabled. Four checks: 1. Press `Cmd/Ctrl + K` on any page and type `postgres` to see local search results; press `\` for command-only mode. 2. Append `index.md` to the current page address to get this page's Markdown version. 3. Open , the site inventory written for AI assistants; it leads to the docs section's `llms-full.txt` and to `navigation.json`. 4. Look at this page's right rail: "Backlinks" lists the pages that link here. ## Related {#related} - [What is OINK](/docs/about/) — scope, fit and comparisons - [Showcase](/docs/about/showcase/) — how production sites use these features - [Quick start](/docs/start/) — from clone to deploy - [Configuration](/docs/customize/config/) — where to look up the parameters named above --- Backlinks: - [Introduction](/docs/about/) - [License](/docs/about/license/) ================ Source: https://oink.pgsty.com/docs/about/showcase/index.md ================ # Case Guide > Find the OINK production case closest to your documentation, book, landing page, or interactive tool. --- LLMS index: [llms.txt](/llms.txt) --- The canonical [Case library](/case/) turns fifteen production sites into short, reusable implementation patterns, and the home page mirrors the same fifteen. All of them run OINK, including this documentation site itself as a self-referential case. Use this guide when you know the shape of the site you want to build. Follow a case for its architecture and trade-offs, then use the linked documentation for the exact configuration. Counts in individual cases describe the audited snapshot rather than a permanent property of a live site. ## Distribution documentation {#pigsty-sites} ### [pigsty.io](/case/pigsty-io/) {#pigsty-io} A very large English site combining a distribution manual, editorial blog, extension catalogue, taxonomies, version navigation, and pricing landing pages. ### [pigsty.cc](/case/pigsty-cc/) {#pigsty-cc} The Chinese peer deployed as an independent single-language site—a useful trade-off when both language corpora have become products in their own right. ### [pgsty.pro](/case/pgsty-pro/) {#pgsty-pro} A bilingual version archive that renders many release pages from reusable, structured release data. ## Product documentation {#product-sites} ### [PIG](/case/pig/) {#pig-pgsty-com} A compact bilingual CLI manual with a data-driven home page and a much larger companion blog. ### [SOW](/case/sow/) {#sow-pgsty-com} A bilingual operations manual with a dedicated download content type fed by release metadata. ### [SILO](/case/silo/) {#silo-pgsty-com} A large upstream migration whose checked manifest generates the bilingual documentation navigation. ### [PG Exporter](/case/pg-exporter/) {#exp-pgsty-com} A metrics manual combining generated navigation, a structured catalogue, and a system-font presentation. ## Books {#book-sites} ### [Designing Data-Intensive Applications](/case/ddia/) {#ddia-vonng-com} A multilingual, multi-edition book and the strongest example of numbered figures, cross-references, chapter navigation, and indexes. ### [The Product-Minded Engineer](/case/tpme/) {#tpme-vonng-com} A focused bilingual publication that needs only OINK's Book shell. ### [PG Internal](/case/pg-internal/) {#pgint-vonng-com} A finished Chinese translation published as a deliberately single-language Book, with no documentation tree and nothing to switch languages to. ## Aggregate, landing, and custom sites {#other-sites} ### [pgsql.cc](/case/pgsql-cc/) {#pgsql-cc} An aggregate operations library where several upstream manuals and partially translated language trees share one search and visual system. ### [pgsty.com](/case/pgsty-com/) {#pgsty-com} A small bilingual corporate site showing that OINK can primarily be a data-driven landing-page system. ### [Capslock](/case/capslock/) {#caps-vonng-com} A two-page-per-language project whose custom shell hosts an interactive, data-driven configuration generator. ### [oink.pgsty.com](/case/oink/) {#oink-pgsty-com} The full reference site: public documentation, live component examples, design contracts, multiple content shells, and regression coverage in one repository. ### [pgext.cloud](/case/pgext-cloud/) {#pgext-cloud} The PostgreSQL extension catalog: a queryable dataset as the primary object of a site, indexing 2,241 extensions and 576 packaged builds across 16 platforms. ## Choosing a starting point {#choosing-a-starting-point} - For a conventional product manual, begin with [PIG](/case/pig/) or [SOW](/case/sow/). - For a large migration, compare [SILO](/case/silo/) and [pgsql.cc](/case/pgsql-cc/). - For a book, compare [TPME](/case/tpme/) with the more elaborate [DDIA](/case/ddia/) implementation, or [PG Internal](/case/pg-internal/) for a single-language one. - For a landing or interactive site, start with [pgsty.com](/case/pgsty-com/) or [Capslock](/case/capslock/). - For the broadest reference, use [OINK Docs](/case/oink/). - For a queryable dataset presented as the primary object of a site, see [ext.pgsty.com](/case/pgext-cloud/). The theme repository's `tests/site/` is an internal CI fixture, not a starter template. Its pages exist to exercise rendering behavior; the production cases above are the better design references. → [Browse all cases](/case/) · [Quick start](/docs/start/) · [Repository tour](/docs/start/anatomy/) --- Backlinks: - [Introduction](/docs/about/) - [Highlights](/docs/about/features/) ================ Source: https://oink.pgsty.com/docs/about/license/index.md ================ # License and acknowledgements > Which licence applies to which layer — Apache-2.0 for the theme, CC BY 4.0 for the documentation, and their own terms for every third-party runtime shipped with the theme. --- LLMS index: [llms.txt](/llms.txt) --- OINK is three layers of material: the theme source, the documentation content, and the third-party assets shipped with the theme. None of them is relicensed into a single combined work. Every table below points at the authoritative file in the repository; **where a summary and the licence text disagree, the file wins**. ## Which licence covers what {#license-map} | Scope | Licence | Authoritative file | | --- | --- | --- | | OINK theme source (layouts, partials, shortcodes, SCSS, JS, i18n) | Apache License 2.0 | Theme [`LICENSE`](https://github.com/pgsty/oink/blob/main/LICENSE), [`NOTICE`](https://github.com/pgsty/oink/blob/main/NOTICE) | | This site's own code, build scripts and material derived from Docsy | Apache License 2.0 | Site [`LICENSE`](https://github.com/pgsty/oink.pgsty.com/blob/main/LICENSE), [`NOTICE`](https://github.com/pgsty/oink.pgsty.com/blob/main/NOTICE) | | This site's original documentation content, except where stated otherwise | Creative Commons Attribution 4.0 International | Site [`LICENSE-CC-BY-4.0`](https://github.com/pgsty/oink.pgsty.com/blob/main/LICENSE-CC-BY-4.0) | | Browser libraries, fonts and icons shipped with the theme | Each component's own licence | Theme [`VENDOR.json`](https://github.com/pgsty/oink/blob/main/VENDOR.json) and the licence files beside each asset | Two boundaries are worth keeping straight. CC BY 4.0 covers the original documentation content only, not the theme code, the trademarks, the screenshots or the third-party assets. And the theme being Apache-2.0 does not turn its bundled dependencies into Apache-licensed works. ## Upstream: Docsy {#upstream-docsy} What the theme's `NOTICE` records: - OINK is derived from [Docsy](https://github.com/google/docsy), Copyright 2018 Google LLC and Docsy contributors. - OINK's own theme work is Copyright 2026 PGSTY contributors. - The project and its upstream are both under Apache License 2.0. The licence, source, version and checksum of every third-party browser dependency are recorded in `VENDOR.json`, and each NOTICE file a dependency requires is distributed beside the asset it belongs to. - The Docsy name and Google's trademarks belong to their respective holders; naming them here identifies the upstream project and **implies no endorsement**. This site is likewise derived from the Docsy project website, and that lineage is recorded in the site's own `NOTICE`. Docsy is OINK's only code upstream: the source history, the Apache-2.0 obligations and the copyright notices are kept intact, and as Apache-2.0 requires, modified files carry a modification notice. ## Third-party runtimes shipped with the theme {#vendored-runtimes} The theme commits everything the browser needs to the repository (`assets/third_party/`, `assets/js/third_party/`, `static/webfonts/`), so a consuming site needs no npm and downloads nothing at build time. `VENDOR.json` is the machine-readable manifest for that material: for each entry it records the name, the pinned version, the source URL, the licence file path and the SHA-256 of every selected artifact, plus an aggregate checksum for each of the three asset trees. The table below is a snapshot of that manifest (`VENDOR.json` generated 2026-08-17, schema 1, 26 entries). Versions change with each theme release, so **the `VENDOR.json` in the repository is authoritative**. Every source is the npm registry (`https://registry.npmjs.org/…`). | Package | Version | Licence | What it does in the theme | | --- | --- | --- | --- | | bootstrap | 5.3.8 | MIT | Grid, components and the RTL stylesheet | | @popperjs/core | 2.11.8 | MIT | Overlay positioning for Bootstrap | | @fortawesome/fontawesome-free | 7.3.1 | CC-BY-4.0 AND OFL-1.1 AND MIT | Icons throughout the site | | @fontsource-variable/inter | 5.3.0 | OFL-1.1 | Interface and body font | | @fontsource/chakra-petch | 5.3.0 | OFL-1.1 | Brand display font | | @fontsource/ibm-plex-mono | 5.3.0 | OFL-1.1 | Code font | | lunr | 2.3.9 | MIT | Local full-text search | | @docsearch/js | 5.0.1 | MIT | The optional Algolia DocSearch front end | | @docsearch/css | 5.0.1 | MIT | Its stylesheet | | mermaid | 11.16.1 | MIT | Mermaid diagrams | | katex | 0.18.4 | MIT | Mathematics | | markmap-autoloader | 0.18.12 | MIT | Mind maps | | markmap-lib | 0.18.12 | MIT | Mind maps | | markmap-view | 0.18.12 | MIT | Mind maps | | markmap-toolbar | 0.18.12 | MIT | Mind map toolbar | | d3 | 7.9.0 | ISC | Markmap dependency | | @highlightjs/cdn-assets | 11.12.0 | BSD-3-Clause | Markmap dependency | | webfontloader | 1.6.28 | Apache-2.0 | Markmap dependency | | swagger-ui-dist | 5.32.13 | Apache-2.0 | OpenAPI reference pages | | redoc | 2.5.3 | MIT | OpenAPI reference pages | | asciinema-player | 3.17.0 | Apache-2.0 | Terminal recording playback | | echarts | 6.1.0 | Apache-2.0 | Charts | | @antv/infographic | 0.2.19 | MIT | Infographics | | pako | 3.0.1 | MIT AND Zlib | Decompression (diagram data) | | external-svg-loader | 1.7.1 | MIT | Inlining external SVG | | idb-keyval | 6.2.0 | Apache-2.0 | Browser-side caching | Licence texts sit beside the asset they belong to — for example `assets/third_party/bootstrap/LICENSE` and `assets/third_party/katex/LICENSE` — and Swagger UI, Redoc and ECharts also ship their own `NOTICE` or bundled-declaration files. Lunr is the one exception: its code is in `assets/js/third_party/` while its licence is at `assets/third_party/lunr/LICENSE`. Redistributing the theme means carrying all of this licence and notice material with it. Updating a runtime means updating the artifact, the licence file, the source and the checksum in the same change. ## Fonts and icons {#fonts-and-icons} All three fonts (Inter, Chakra Petch, IBM Plex Mono) are under the SIL Open Font License 1.1, and the font files are committed to `static/webfonts/`: fourteen Inter subset files, four brand-font files, and Font Awesome's three, twenty-one in all. Font Awesome Free 7.3.1 carries a composite licence — CC BY 4.0 for the icon artwork, SIL OFL 1.1 for the font files, MIT for the code — with the text in `assets/third_party/Font-Awesome/LICENSE.txt`. The theme makes no request to a remote font service: there is no Google Fonts link in the repository, and fonts are always served from the site's own `baseURL`. To change fonts or switch to the platform stack, see [Brand and appearance](/docs/customize/brand/). ## Design references {#design-references} Docsy is the only code upstream. The projects below are references for the design language. They are neither a source of code nor a runtime dependency, and OINK has ported no code from them: | Project | What was learned from it | | --- | --- | | [Fumadocs](https://www.fumadocs.dev/) | Content-first presentation, information hierarchy, and writing components such as file trees and field lists (the theme's `NOTICE` records this acknowledgement) | | [Nextra](https://nextra.site/) | A spare documentation shell, filename and copy affordances on code blocks, per-page layout switches | | [Hextra](https://imfing.github.io/hextra/) | A Hugo-native approach to implementation, file trees, badges, tabs | | [Mintlify](https://mintlify.com/) | Layered navigation structure, synchronized code groups, the reading experience of an API reference | [Hugo](https://gohugo.io/) is the build platform, and Go resolves modules when the theme is installed as a Hugo Module. Both are prerequisites, and the theme redistributes neither binary. Naming these projects describes lineage, dependency or inspiration and **implies no endorsement by them**; project and product names belong to their respective holders. ## Reusing this documentation {#reusing-the-docs} CC BY 4.0 permits sharing and adaptation for any purpose, provided you give attribution, link to the licence, state whether you made changes, and do not imply that OINK, PGSTY or any upstream project endorses your adaptation. A sufficient attribution reads: > Adapted from the OINK documentation by PGSTY contributors, licensed under > [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), with modifications. Images or quotations that carry their own attribution on a page keep their own credit and licence; removing the footer does not discharge the attribution obligation. ## Reusing the theme {#reusing-the-theme} Apache-2.0 permits using, modifying and distributing the theme source and its build output under its terms, provided you keep the licence, copyright and attribution notices, keep the contents of `NOTICE`, and state which files you changed when distributing modified source. A theme distribution should include `LICENSE`, `NOTICE`, `VENDOR.json`, and every third-party licence file the manifest references. Apache-2.0 grants no trademark rights, and it does not turn third-party assets into Apache-licensed works. ## Related {#related} - [What is OINK](/docs/about/) — what the project is and where it came from - [Highlights](/docs/about/features/) — what local-first means in practice - [Configuration](/docs/customize/config/) — which features bring in an external service - [Brand and appearance](/docs/customize/brand/) — changing fonts and icons --- Backlinks: - [Introduction](/docs/about/) - [Highlights](/docs/about/features/) ================ Source: https://oink.pgsty.com/docs/start/index.md ================ # Get started > Start from the official OINK Starter, establish a working local baseline, then customize content, language, brand, integrations, and deployment in that order. --- LLMS index: [llms.txt](/llms.txt) --- The recommended path for a new site starts from [`pgsty/oink-starter`](https://github.com/pgsty/oink-starter), not from a copy of this documentation and regression repository. The Starter is a public GitHub template: it pins OINK v1.0.0, builds as-is, and contains only neutral project content and deployment workflows. > [!IMPORTANT] Two version numbers have different jobs > OINK's declared compatibility floor is Hugo Extended > 0.160.1. The current Starter and its CI use Hugo Extended > 0.165.0 and Go 1.27. Use that pinned Starter toolchain for the path below; use > the lower floor only when maintaining an existing site that deliberately > supports it. ## Choose a path {#choose} | Starting point | Recommended path | Result | | --- | --- | --- | | New documentation or project site | [OINK Starter](/docs/start/starter/) | A small three-language Docs, Blog, and Book site with two deployment workflows | | Existing Hugo site | [From scratch](/docs/start/from-scratch/) | Add the OINK module and required Goldmark settings without replacing content | | Existing Docsy or older OINK site | [Upgrade](/docs/admin/upgrade/) | Preserve content, migrate supported syntax, and review site overrides | ## Five-minute baseline {#baseline} 1. ### Install the tools {#tools} Install Git, Go 1.27 or newer, and Hugo Extended 0.165.0 or newer. The Hugo output must contain `extended`: ```console $ go version go version go1.27.0 darwin/arm64 $ hugo version hugo v0.165.0+extended+withdeploy darwin/arm64 ``` On macOS, `brew install git go hugo` supplies them. On Linux and Windows, use the official [Hugo installation guide](https://gohugo.io/installation/) and [Go downloads](https://go.dev/dl/); choose Hugo **Extended**. 1. ### Create or clone the site {#clone} For a repository you intend to keep, open the Starter and select **Use this template**, then clone the repository GitHub created for you. To evaluate the untouched original locally: ```bash git clone https://github.com/pgsty/oink-starter.git my-docs cd my-docs hugo server ``` 1. ### Open the baseline {#open} Open . The default Starter also publishes Chinese at `/zh/` and French at `/fr/`. Confirm that Docs, Blog, Book, search, language switching, and light/dark mode all work before editing anything. 1. ### Make one visible change {#first-change} Change the title and canonical URL at the top of `hugo.yaml`, then edit one sentence in `data/home/en.yaml`. A browser reload that shows both changes is the first useful proof that configuration, content, and the pinned theme are connected correctly. {.steps} ## Customize from shallow to deep {#learning-path} - [Use OINK Starter](/docs/start/starter/) — identity first, then languages, home page, content, navigation, brand, integrations, and deployment. - [Starter repository tour](/docs/start/anatomy/) — which file owns each part of the site, what to replace, and what can be removed. - [Writing pages](/docs/write/pages/) — front matter, headings, links, images, drafts, and the page-end controls. - [Components](/docs/components/) — add expression only after the content tree is stable. - [Brand and appearance](/docs/customize/brand/) — logo, accent, typography, width, and CSS extension points. - [Deploy](/docs/admin/deploy/) — use the supplied GitHub Pages or Cloudflare Pages workflow, then verify the real public routes. {.cards} This order is deliberate. A site that first proves its build and content tree is easier to debug than one that changes languages, navigation, CSS, analytics, and hosting at the same time. ## Publication gate {#publication-gate} Before the first push, run the same warning-strict production build the Starter workflows use: ```bash hugo --cleanDestinationDir --gc --minify --environment production \ --printPathWarnings --panicOnWarning ``` Success means the command ends with `Total in …`, prints no warning or error, and `public/` contains the language roots and representative Docs, Blog, and Book routes. It does not yet prove deployment: a local build, a commit, a push, a green workflow, and correct public rendering are separate gates. ## Next {#next} Start with the [complete Starter tutorial](/docs/start/starter/). If the template deliberately carries more structure than your project needs, use the [repository tour](/docs/start/anatomy/) to remove it safely. Use [From scratch](/docs/start/from-scratch/) only when adding OINK to an existing site or when you explicitly want to assemble every file yourself. --- Section pages: - [Use OINK Starter](/docs/start/starter/): Turn the official starter into your project site, one controlled layer at a time — identity, languages, home page, content, navigation, brand, integrations, and deployment. - [Starter repository tour](/docs/start/anatomy/): A file-level map of oink-starter — what owns identity, languages, home, content, navigation, brand, deployment, and the pinned theme. - [From scratch and other install methods](/docs/start/from-scratch/): Build a minimal OINK site in an empty directory, and weigh the four install methods — Module, submodule, offline archive, pinned clone. --- Backlinks: - [Docs](/docs/) - [Introduction](/docs/about/) - [Highlights](/docs/about/features/) - [Cases](/docs/about/showcase/) - [Cards](/docs/components/cards/) - [Brand and appearance](/docs/customize/brand/) - [From scratch](/docs/start/from-scratch/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/start/starter/index.md ================ # Use OINK Starter > Turn the official starter into your project site, one controlled layer at a time — identity, languages, home page, content, navigation, brand, integrations, and deployment. --- LLMS index: [llms.txt](/llms.txt) --- [`pgsty/oink-starter`](https://github.com/pgsty/oink-starter) is the supported starting point for a new OINK site. It is deliberately smaller than `oink.pgsty.com`: no theme documentation, analytics account, comment repository, browser regression suite, or PGSTY-specific brand is copied into your project. The current template pins OINK v1.0.0, Go 1.27, and Hugo Extended 0.165.0. Its default three-language, English-only, and English–Chinese profiles have all been built warning-strictly against that release. ## What the template contains {#contents} | Surface | Included baseline | First decision | | --- | --- | --- | | Languages | English, Simplified Chinese, French | Keep all three, or select a supplied single/bilingual profile | | Content | Docs, Blog, and a short Book tutorial | Rewrite the examples; delete a whole surface only when you do not need it | | Home | One compact `data/home/.yaml` per language | Replace the project promise and destinations | | Brand | Neutral logo and favicon | Keep them until real project artwork exists | | Integrations | Repository, Giscus, analytics, share, and feedback examples are commented | Enable only complete configurations you intend to operate | | Deployment | GitHub Pages and Cloudflare Pages Direct Upload workflows | Choose one production path and verify its real URL | The Starter's own Book at `/book/` is a four-chapter tour from preview to deployment. This page is the maintainer-grade version: it explains the order of changes, the boundaries between them, and the checks after each layer. ## Create your repository {#create-repository} ### GitHub template, recommended {#github-template} Open the [Starter repository](https://github.com/pgsty/oink-starter), select **Use this template → Create a new repository**, then clone the repository created under your account or organization: ```bash git clone https://github.com/OWNER/PROJECT-DOCS.git cd PROJECT-DOCS hugo server ``` This gives your site its own Git history and keeps the original Starter as an upstream reference rather than as a remote you might accidentally push to. ### Clone the original to evaluate it {#clone-original} For a disposable local evaluation: ```bash git clone https://github.com/pgsty/oink-starter.git cd oink-starter hugo server ``` Do not start a real project by deleting this clone's `.git` directory. GitHub's template operation already creates the clean project boundary and preserves an auditable first commit. ## Preview before changing anything {#preview} Open these routes: - `/`, `/zh/`, `/fr/` — the three home pages; - `/docs/`, `/blog/`, `/book/` — the three content surfaces; - one translated page, then the language switcher; - search and the light/dark control at a narrow viewport. Also record the resolved module: ```bash hugo mod graph | grep github.com/pgsty/oink ``` It should resolve `github.com/pgsty/oink@v1.0.0`. This unchanged preview is the baseline against which every later edit is judged. ## Customize in layers {#customize} ### Layer 1: identity {#identity} Change the two marked values at the top of `hugo.yaml`: ```yaml {title="hugo.yaml"} title: &siteTitle Project Name baseURL: https://example.org/ ``` The YAML anchor carries the title to all enabled languages. Then change the copyright holder and, after the new repository exists, uncomment its links: ```yaml {title="hugo.yaml"} params: copyright: authors: '[Project contributors](https://example.org/community/)' from_year: 2026 github_repo: https://github.com/OWNER/PROJECT-DOCS github_branch: main ``` Run `hugo server` again and check the browser title, footer, edit/history links, and canonical URL. Do not change the logo yet unless the project has final artwork; text identity is easier to review first. ### Layer 2: language profile {#languages} The root configuration enables English, Chinese, and French. Before making other configuration edits, choose one of the supplied profiles when that is not your intended language set: ```bash cp examples/hugo.single.yaml hugo.yaml # English only cp examples/hugo.bilingual.yaml hugo.yaml # English + Chinese ``` These are complete minimal configurations, not fragments: copying one replaces the commented integration examples in the root file. Do it at the beginning; if `hugo.yaml` already contains project changes, merge the `languages` and `disableLanguages` sections instead of overwriting it. Disabled languages stay declared so Hugo recognizes `.zh.md` and `.fr.md` as translations and safely ignores them. If you remove a language permanently, remove its content and home data only after the selected profile builds. ### Layer 3: home page {#home} The home page is data rather than an opaque layout override: ```text data/home/en.yaml data/home/zh.yaml data/home/fr.yaml ``` Edit one language first. In each file, `sections` fixes the order; `hero`, `cards`, and `cta` provide the content. Replace the promise, destination URLs, and sample card copy while keeping the structure. After the first language is right, translate the same information into the enabled peers. For another composition, use the full registry in [Home and landing pages](/docs/customize/home/); do not copy the Starter home partial, because there is no site-specific template to copy. ### Layer 4: content and navigation {#content-navigation} Rewrite or remove sample leaf pages under `content/`. Keep section roots until you decide whether that whole surface belongs in your project: ```text content/docs/ reference and task documentation content/blog/ posts, design records, and release announcements content/book/ a sequential long-form guide ``` The content tree becomes the sidebar. Top navigation lives in `menus.main` on the translated `_index` roots, so renaming Docs, Blog, or Book happens beside the content it names rather than in a second global menu tree. Keep translated files side by side and give corresponding headings the same explicit IDs: ```text page.md page.zh.md page.fr.md ``` Follow [Organizing content](/docs/write/organize/) before creating a custom navigation data file; the generated tree is enough for most sites. ### Layer 5: brand and reader features {#brand-features} Replace `assets/icons/logo.svg` and `static/favicon.svg` when real assets are ready. Then enable the smallest useful configuration changes, one at a time: ```yaml {title="hugo.yaml"} params: ui: theme_color: '#245f94' typography: system image_zoom: true share: [mastodon, linkedin, email, copy] ``` For custom local fonts, use `params.ui.fonts` for family names or declare font files in site CSS. For layout, sidebar, search, and component settings, consult the [Configuration reference](/docs/customize/config/) rather than copying the much larger configuration of `oink.pgsty.com`. ### Layer 6: integrations {#integrations} The Starter leaves repository actions, Giscus, Google Analytics, feedback, and sharing off or commented. Enable an integration only after all of its required facts are known: - repository links need the real owner, repository, and branch; - Giscus needs its repository/category names and immutable IDs; - Google Analytics needs a project-owned measurement ID; - feedback records structured `gtag` events only when analytics is present; - assistant links send the current URL to a third party and therefore require an explicit policy choice. An incomplete optional block should remain commented. See [Comments](/docs/admin/comments/), [Analytics and SEO](/docs/admin/analytics/), and [Repository links](/docs/customize/repository/) for the operating boundary of each integration. ## Build and deploy {#build-deploy} ### Strict local build {#strict-build} Before enabling a hosting workflow: ```bash hugo --cleanDestinationDir --gc --minify --environment production \ --printPathWarnings --panicOnWarning ``` Commit `hugo.yaml`, `go.mod`, and `go.sum`; never commit generated `public/`, `resources/`, module caches, or a local module replacement. ### GitHub Pages {#github-pages} The Starter already contains `.github/workflows/github-pages.yaml`. In **Settings → Pages**, select **GitHub Actions** as the source. A push to `main` builds with the pinned toolchain, asks GitHub for the correct project subpath, and publishes `public/` through the Pages deployment API. ### Cloudflare Pages {#cloudflare-pages} The supplied `.github/workflows/cloudflare-pages.yaml` uses Direct Upload. Create a Pages Direct Upload project, add `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, then run the workflow manually once. Set the repository variable `CLOUDFLARE_PAGES_ENABLED=true` for automatic deploys, and `CLOUDFLARE_SITE_URL` when the canonical address is not the default `pages.dev` domain. Use either Direct Upload or Cloudflare Git integration for one project, not both. The complete host comparison and `baseURL` rules are in [Deploy](/docs/admin/deploy/). ## Verify and remove samples {#verify} Before calling the site ready: 1. Search for placeholders such as `Project Name`, `example.org`, `OWNER`, and `PROJECT`, then decide whether each remaining occurrence is intentional. 1. Open every enabled language root and representative Docs, Blog, and Book pages on desktop and mobile. 1. Confirm language switching lands on peers, not the home page. 1. Test search, dark mode, one component, Markdown output, print, 404, canonical URLs, and repository actions. 1. Check the deployed workflow and the public URL separately from the local build. {.steps} Delete the sample Book or Blog only after removing its top-menu root and any home-page card that links to it. A warning-strict rebuild after each whole surface is removed keeps failures attributable to one change. ## Next {#next} Use the [Starter repository tour](/docs/start/anatomy/) as a file-level map, then continue with [Writing pages](/docs/write/pages/) and [Configuration](/docs/customize/config/). For an existing site that should not inherit the Starter's content model, use [From scratch](/docs/start/from-scratch/). --- Backlinks: - [OINK v0.8.1](/blog/release/0.8.1/) - [OINK v1.0.0](/blog/release/1.0.0/) - [Start with a working site](/book/01-start/) - [Get started](/docs/start/) - [Repository tour](/docs/start/anatomy/) - [From scratch](/docs/start/from-scratch/) ================ Source: https://oink.pgsty.com/docs/start/anatomy/index.md ================ # Starter repository tour > A file-level map of oink-starter — what owns identity, languages, home, content, navigation, brand, deployment, and the pinned theme. --- LLMS index: [llms.txt](/llms.txt) --- This page describes the repository created from [`pgsty/oink-starter`](https://github.com/pgsty/oink-starter). It is not a tour of the much larger `oink.pgsty.com` documentation and regression repository. The theme source is not copied into either site: `go.mod` pins it as a Hugo Module, and Hugo stores the resolved source in the Go module cache. ## Top-level map {#layout} ```filetree {title="oink-starter/"} - oink-starter/ - hugo.yaml # identity, languages, outputs, parameters, module import - go.mod # site module and exact OINK release - go.sum # module checksums - examples/ - hugo.single.yaml # English-only complete profile - hugo.bilingual.yaml # English + Chinese complete profile - data/ - home/ - en.yaml # one compact landing page per language - zh.yaml - fr.yaml - content/ - _index.md # language home roots - _index.zh.md - _index.fr.md - docs/ # Introduction, Get Started, Tutorial, Reference - blog/ # posts, design records, release announcements - book/ # sequential tutorial about the Starter - assets/ - icons/logo.svg # processed project logo - static/ - favicon.svg # copied unchanged to the site root - i18n/ - fr.yaml # Starter-specific French interface overrides - .github/workflows/ - github-pages.yaml # strict build and GitHub Pages deployment - cloudflare-pages.yaml # strict build and Cloudflare Direct Upload - README.md # operating summary for repository maintainers - LICENSE # template source license ``` Generated `public/`, `resources/`, `.hugo_build.lock`, and module caches are ignored build state, not source. ## What to change first {#change-first} | Path | Responsibility | Initial action | | --- | --- | --- | | `hugo.yaml` | Identity, canonical URL, languages, outputs, theme features, optional integrations | Change the two marked values; choose a language profile before other edits | | `data/home/` | Home-page promise, cards, calls to action | Rewrite every enabled language after one language is approved | | `content/` | All reader-facing material | Replace example leaves; keep a section root until deciding to remove that whole surface | | `assets/icons/logo.svg` | Processed logo | Replace only with final artwork | | `static/favicon.svg` | Browser icon | Replace together with the logo review | | `params.github_*` in `hugo.yaml` | Edit/history/new-page/issue links | Uncomment only after the destination repository exists | ## What to keep {#keep} - `go.mod` and `go.sum`: together they pin and verify OINK v1.0.0. Commit both. - The three Goldmark settings in `hugo.yaml`: native Steps, Cards, Fields, image attributes, and Book targets depend on them. - `outputs`: removing `markdown`, `LLMS`, or `print` intentionally removes the corresponding Markdown, agent-index, or print surfaces. - `fetch-depth: 0` in workflows when `enableGitInfo` stays on: last-modified and contributor facts need repository history. - `GOWORK: off` and `HUGO_MODULE_WORKSPACE: off` in CI: a developer's local workspace must not replace the published release being verified. ## Optional surfaces {#optional} Docs, Blog, and Book are independent top-level surfaces. To remove one safely: 1. delete its `content//` tree; 1. remove any home-page card or link that targets it; 1. confirm no other page links to it; 1. run a warning-strict build and inspect the remaining top navigation. {.steps} Do not delete only translated section roots: that creates language-specific navigation and fallback behaviour that is difficult to distinguish from a mistake. Remove a surface in all enabled languages or document the asymmetry. The two configuration profiles under `examples/` are optional after the language decision. They are useful references, but the root `hugo.yaml` is the only active site configuration. ## Content and navigation {#content-navigation} Under Docs and Book, directory structure and `weight` form the sidebar and pager sequence. Top navigation comes from `menus.main` on section roots. A translated root repeats the same `identifier`, `parent`, and weight while translating visible labels. The Starter intentionally demonstrates the Documentation System model: - Introduction explains what and why; - Get Started gets a new user to a result; - Tutorial teaches an end-to-end task; - Reference records exact supported behaviour. Rename or reshape those sections for the project, but preserve the separation between learning paths rather than mixing every kind of answer into one tree. ## Language model {#languages} English source files end in `.md`; Chinese and French peers end in `.zh.md` and `.fr.md`. Home data uses language keys under `data/home/`. The root profile declares the languages, their locale, order, and site description. The single and bilingual profiles keep disabled languages declared. This is intentional: Hugo then recognizes the unused suffixes as translations instead of rendering several files onto one English URL. Copy a profile only before project-specific configuration begins; afterwards merge changes by hand. ## Where OINK lives {#theme} Two files establish the module boundary: ```yaml {title="hugo.yaml"} module: imports: - path: github.com/pgsty/oink hugoVersion: extended: true min: '0.160.1' ``` ```go-mod {title="go.mod"} module github.com/OWNER/PROJECT-DOCS go 1.27.0 require github.com/pgsty/oink v1.0.0 ``` `hugo mod graph` shows the resolved version. Production follows the exact tag in `go.mod`; a local `HUGO_MODULE_REPLACEMENTS` value is a development override and must never be committed or treated as release proof. ## Deployment files {#deployment} The GitHub Pages workflow runs automatically on pushes to `main`; repository settings must select GitHub Actions as the Pages source. The Cloudflare workflow runs manually, or automatically only after the repository variable `CLOUDFLARE_PAGES_ENABLED=true` is set. Its required account ID and API token remain repository secrets. Keep only the workflows for deployment paths you operate. Cloudflare Direct Upload and Cloudflare Git integration are alternative ownership models for the same project, not two gates to run together. ## Safe customization order {#order} 1. Prove the untouched preview. 1. Change identity and select languages. 1. Replace one home page and then its translations. 1. Replace content and verify navigation. 1. Change brand and reader features one group at a time. 1. Enable complete external integrations. 1. Run the strict production build. 1. Deploy, then verify production independently. {.steps} Commit between layers when the repository is already yours. Small boundaries make a later regression or rollback attributable to one decision. ## Verify {#verify} ```bash hugo mod graph | grep github.com/pgsty/oink hugo --cleanDestinationDir --gc --minify --environment production \ --printPathWarnings --panicOnWarning git status --short ``` The module graph names the pinned release, the build emits no warning or error, and Git status contains source edits but no `public/` or cache files. Then open the enabled language roots and one Docs, Blog, and Book route before moving to deployment. ## Related {#related} - [Use OINK Starter](/docs/start/starter/) — the complete layered workflow - [From scratch](/docs/start/from-scratch/) — add OINK without adopting this content model - [Organizing content](/docs/write/organize/) — sidebar, pager, and menu authority - [Configuration](/docs/customize/config/) — every current site parameter - [Deploy](/docs/admin/deploy/) — host-specific setup and production checks --- Backlinks: - [pig.pgsty.com](/case/pig/) - [Cases](/docs/about/showcase/) - [Get started](/docs/start/) - [From scratch](/docs/start/from-scratch/) - [OINK Starter](/docs/start/starter/) ================ Source: https://oink.pgsty.com/docs/start/from-scratch/index.md ================ # From scratch and other install methods > Build a minimal OINK site in an empty directory, and weigh the four install methods — Module, submodule, offline archive, pinned clone. --- LLMS index: [llms.txt](/llms.txt) --- This is the manual alternative to the recommended [OINK Starter](/docs/start/starter/). It builds a minimal site in an empty directory: a small `hugo.yml` plus one `hugo mod get` gives a single-language site you can preview. The cost is that the home page, example content, deployment workflow, and every component usage are yours to assemble. An existing Hugo site needs no scaffolding: install the theme module, add the three Goldmark prerequisites (see [Writing `hugo.yml`](#config)), and leave the content alone. For an existing Docsy site, see [Upgrade](/docs/admin/upgrade/). The second half weighs four install methods: Hugo Module, Git submodule, offline archive, and pinned clone. For the current v1.0.0 release path, use Go 1.27 and Hugo Extended 0.165.0 unless an existing site deliberately targets the theme's lower declared compatibility floor. ## From an empty directory to the first page {#scaffold} 1. ### Create the skeleton and fetch the theme {#skeleton} ```bash hugo new site --format yaml my-docs cd my-docs hugo mod init github.com/example/my-docs hugo mod get github.com/pgsty/oink@v1.0.0 ``` What follows `hugo mod init` is your own site's module path, usually the repository address. `hugo mod get` writes `go.mod` and `go.sum`, and both are committed. The newest version number is on [GitHub Releases](https://github.com/pgsty/oink/releases); the `v1.0.0` on this page is what this site currently pins. A production site pins a release tag rather than following `main`: `@latest` is a one-off resolution, not a version policy. 1. ### Writing `hugo.yml` {#config} Rename the `hugo.yaml` that `hugo new site` generated to `hugo.yml` (Hugo accepts both; this documentation uses the latter throughout) and replace its contents with the following, which builds as it stands: ```yaml {title="hugo.yml" collapse=30} title: Product Docs baseURL: https://docs.example.com/ defaultContentLanguage: en # enableGitInfo: true # the "last modified" time comes from git; run git init before enabling languages: en: label: English locale: en-US weight: 1 title: Product Docs params: description: Everything about running Product in production menus: main: - { name: Docs, pageRef: /docs, weight: 20 } - { name: Blog, pageRef: /blog, weight: 50 } # The three Goldmark prerequisites: OINK's native Markdown components depend on them markup: goldmark: renderer: unsafe: true # allow inline HTML in content parser: attribute: block: true # attribute lines such as {.steps} {.cards} {caption=} wrapStandAloneImageWithinParagraph: false # only a block-level image can carry an attribute line highlight: noClasses: false # code colours follow light and dark mode params: offline_search: true github_repo: https://github.com/example/product-docs copyright: authors: '[Example Inc.](https://example.com/)' from_year: 2026 ui: dark_mode: true sidebar_menu_foldable: true section_index: cards outputs: home: [HTML, markdown, LLMS] page: [HTML, markdown] section: [HTML, RSS, print, markdown] module: imports: - path: github.com/pgsty/oink hugoVersion: extended: true min: '0.160.1' ``` What each of the five blocks governs: | Block | Governs | Consequence of omitting it | | --- | --- | --- | | Top level + `languages` | Site name, domain, languages and navbar menu | A wrong `baseURL` sends every absolute link astray in production | | `markup.goldmark` | The three component prerequisites | An attribute line becomes a literal `{.steps}` in the prose | | `params` | Search, repository links, shell switches | Interactive features stay off; the theme does not decide for the site | | `outputs` | The per-page `.md`, `llms.txt` and print pages | No "Copy as Markdown" in the page menu, and no print view | | `module` | References the theme and declares the Hugo floor | The build cannot find the theme | Mathematics additionally needs Goldmark's passthrough extension; see [Math](/docs/components/math/). Every key's full meaning and default is in [Configuration](/docs/customize/config/). 1. ### Write the first page {#first-page} Every top-level directory under `content/` is a section, and the directory structure is the sidebar structure. A documentation section needs at least an `_index.md`: ```markdown {title="content/docs/_index.md"} --- title: Docs linkTitle: Docs description: Everything about running Product in production. weight: 20 --- Start with [Install](/docs/install/). ``` ````markdown {title="content/docs/install.md"} --- title: Install description: Install Product on a fresh machine. weight: 10 --- ## Prerequisites {#prerequisites} > [!IMPORTANT] > Product needs PostgreSQL 18 or newer. ## Install {#install} ```bash curl -fsSL https://get.example.com | bash ``` ```` Write explicit `{#id}` anchors on headings: when a translation is added later, the two languages' anchors have to correspond. How to write a page is in [Writing pages](/docs/write/pages/). 1. ### Preview {#preview} ```bash hugo server ``` Open and the sidebar shows Docs → Install. Edits hot-reload in milliseconds. {.steps} ## Other install methods {#install-methods} The steps above use a Hugo Module. The other three address particular constraints: network isolation, a platform that requires the build input to contain the whole theme tree, or an organization that reviews its own copy of the theme. Apart from `hugo mod vendor`, none of them creates a Go module, and the site references the theme with `theme: oink` rather than `module.imports`. The shared cost is that version resolution and integrity checking become your responsibility. ### Hugo Module (recommended) {#hugo-module} ```bash hugo mod init github.com/example/product-docs hugo mod get github.com/pgsty/oink@v1.0.0 ``` ```yaml {title="hugo.yml"} module: imports: - path: github.com/pgsty/oink ``` The only method where Hugo resolves the version itself, verifies the checksum, and leaves an audit record in `go.sum`. `hugo mod graph` shows what actually resolved and `hugo mod get -u` upgrades. It needs Go on the machine. ### Git submodule {#git-submodule} Record an exact theme commit in the site repository: ```bash git submodule add https://github.com/pgsty/oink.git themes/oink git -C themes/oink fetch --tags git -C themes/oink checkout v1.0.0 git add .gitmodules themes/oink ``` ```yaml {title="hugo.yml"} theme: oink ``` CI must initialize the submodule before running Hugo, or `themes/oink` is an empty directory: ```bash git submodule update --init --recursive ``` ### Offline archive {#offline-archive} For network-isolated environments. Two paths, both prepared on a connected machine and carried in whole. **With `hugo mod vendor`**, the resolved theme source is frozen into the site directory, and later builds need neither the network nor Go. ```bash hugo mod vendor # writes _vendor/, holding the theme's full source tree tar czf my-docs.tgz . # carry _vendor/ into the isolated environment with everything else ``` When `_vendor/` exists Hugo prefers it (`hugo mod graph` prints `+vendor`), and `module.imports` in `hugo.yml` stays as it is. This step needs Go; the builds after it do not. Upgrading the theme means returning to a connected environment and running `hugo mod get` and `hugo mod vendor` again. `_vendor/` collects only the directories the theme mounts (`assets`, `data`, `i18n`, `layouts`, `static`) plus `hugo.yaml` and `theme.toml`. It does not include `LICENSE`, `NOTICE` or `VENDOR.json`. To redistribute that archive, take those three files from the theme repository as well. **With a tag source archive**, no Go module is created; a version of the theme is simply unpacked into `themes/oink/`. ```bash curl -L -o oink.tar.gz \ https://github.com/pgsty/oink/archive/refs/tags/v1.0.0.tar.gz mkdir -p themes/oink tar xzf oink.tar.gz -C themes/oink --strip-components=1 ``` ```yaml {title="hugo.yml"} theme: oink ``` The theme repository's root is the module root, so unpacking lands directly on `layouts/`, `assets/`, `i18n/` and `static/` with no further level to descend into. Redistribution must keep `LICENSE`, `NOTICE` and `VENDOR.json`; the last records each third-party runtime's version, source, licence path and SHA-256, and is what an offline audit rests on. When moving between machines, generate the archive and its checksum from an immutable tag on the connected side: ```bash git clone --branch v1.0.0 --depth 1 \ https://github.com/pgsty/oink.git oink git -C oink archive --format=tar.gz --prefix=oink/ \ --output=../oink-v1.0.0.tar.gz v1.0.0 shasum -a 256 oink-v1.0.0.tar.gz \ > oink-v1.0.0.tar.gz.sha256 ``` Carry the archive and its `.sha256` into the isolated environment, verify, then unpack: ```bash shasum -a 256 -c oink-v1.0.0.tar.gz.sha256 mkdir -p themes tar -xzf oink-v1.0.0.tar.gz -C themes ``` An archive produced this way is your own artifact, not a project release. Whether a given tag's release page carries an archive and a checksum file varies by release; verify the checksum independently when using a public attachment. Before building offline, confirm the archive is complete. All eleven of these must be present: ```filetree {title="themes/oink/"} - oink/ - go.mod # module path declaration, used when resolving as a Hugo Module - hugo.yaml # theme default parameters and the Hugo version floor - theme.toml # theme metadata, required by the theme: oink method - LICENSE # Apache-2.0 - NOTICE # upstream attribution; must be kept on redistribution - VENDOR.json # third-party runtime manifest: version, source, licence path, SHA-256 - assets/ # SCSS, JS and the third-party runtimes shipped with the theme - layouts/ # templates, partials, shortcodes, render hooks - static/ # font files, published as is - i18n/ # 32 interface language files - data/ # the SPDX licence table behind the page-end attribution line ``` ### Pinned clone {#pinned-clone} For a hosting platform that requires the build input to contain the whole theme tree: ```bash git clone https://github.com/pgsty/oink.git themes/oink git -C themes/oink checkout v1.0.0 ``` The difference from a submodule is that the theme files enter your repository history directly, without the `.gitmodules` indirection. Record the commit that was finally resolved and the procedure for restoring it. ### The four methods compared {#comparison} | Method | Needs Go | Version auditable | Theme source in your repository | Use when | | --- | --- | --- | --- | --- | | **Hugo Module** | Yes | `go.sum` verifies automatically | No | The default | | Git submodule | No | The repository records the commit | By reference | The theme source has to be in the repository | | Offline archive | No | Checksums verified by hand | Yes | Network isolation | | Pinned clone | No | You record it yourself | Yes | The platform requires a complete tree | > [!TIP] A consuming site needs no front-end toolchain > Bootstrap, Font Awesome, the fonts, and the search and diagram runtimes all > ship with the theme. A site needs no `node_modules`, no PostCSS, no RTLCSS and > no CDN. Tutorials that install npm dependencies for a Docsy site describe > upstream Docsy's process and do not apply to OINK. ## Developing against a local theme checkout {#local-theme-checkout} This section applies only when changing the theme and the site together. Clone the two repositories as siblings: ```text {title="sibling directory layout" copy=false} ~/pgsty/ ├── oink/ # the theme └── product-docs/ # your site ``` Use the `HUGO_MODULE_REPLACEMENTS` environment variable to substitute the local checkout temporarily, leaving `go.mod` untouched: ```bash cd ~/pgsty/product-docs HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> ../oink' hugo server ``` The documentation site's `Makefile` is an alias for exactly these commands, and `make dev` and `make check` expect the theme checkout at the sibling `../oink`: ```makefile {title="Makefile: as the documentation site writes it"} build: hugo --cleanDestinationDir --minify check: HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> $(abspath ../oink)' npm test dev: HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> $(abspath ../oink)' hugo server --renderToMemory ``` A Go workspace (`go work init` plus `HUGO_MODULE_WORKSPACE=go.work`) is an equivalent alternative. Both apply to the local machine only: CI and production builds use the version in `go.mod`, and `go.work` is never committed. ## Verify {#verify} ```bash hugo mod graph # which theme version actually resolved hugo --gc --minify --printPathWarnings --panicOnWarning ``` It passes when the build ends with `Total in …` and no `WARN` or `ERROR`. Then confirm: - `/docs/` opens and the sidebar holds the page you wrote - The navbar has a search box that finds the heading you just wrote - The light/dark toggle is present, and code block colours follow it (which shows `markup.highlight.noClasses: false` took effect) - `git status` shows `go.mod` and `go.sum`, and no `public/` or `resources/` ## Related {#related} - [Get started](/docs/start/) — choose between Starter, an existing Hugo site, and migration - [OINK Starter](/docs/start/starter/) — the recommended new-site path - [Starter repository tour](/docs/start/anatomy/) — what each template directory owns - [Configuration](/docs/customize/config/) — every `hugo.yml` key and its default - [Writing pages](/docs/write/pages/) — how to keep writing after the first page - [Upgrade](/docs/admin/upgrade/) — upgrading the theme module, and migrating from Docsy --- Backlinks: - [OINK v0.8.1](/blog/release/0.8.1/) - [OINK v1.0.0](/blog/release/1.0.0/) - [Start with a working site](/book/01-start/) - [Deploy](/docs/admin/deploy/) - [Local preview](/docs/admin/preview/) - [Upgrade](/docs/admin/upgrade/) - [Get started](/docs/start/) - [Repository tour](/docs/start/anatomy/) - [OINK Starter](/docs/start/starter/) ================ Source: https://oink.pgsty.com/docs/write/index.md ================ # Authoring > Writing documentation pages, blog posts, books, release pages and API references — what a page looks like, and how content is organized. --- LLMS index: [llms.txt](/llms.txt) --- This section covers the content types OINK supports: documentation pages, blog posts, books, release and download pages, and OpenAPI references. They share one Markdown dialect and one front matter schema, and each adds its own conventions. ## What a documentation page is made of {#anatomy} A documentation page is one Markdown file. Between the two `---` lines at the top is the front matter — the page's metadata: title, short sidebar name, description, ordering. The rest is the body: ordinary Markdown plus OINK's native components. Here is a complete page: ```markdown {title="content/docs/install.md"} --- title: Install Pigsty linkTitle: Install description: Get a working PostgreSQL cluster onto a clean EL 9 machine. weight: 20 --- ## Prerequisites {#prerequisites} A Linux machine you can reach over SSH, passwordless `sudo`, and Python 3.11 or newer. > [!IMPORTANT] > The installer rewrites `/etc/yum.repos.d/`. Back it up first. ``` Save it as `content/docs/install.md`, run `hugo server`, and the page appears at `/docs/install/` with an "Install" entry in the sidebar. ## Content types and where they are covered {#map} | What you are writing | Where to go | | --- | --- | | A documentation page: front matter, heading anchors, links, images, drafts | [Writing pages](/docs/write/pages/) | | The tree and the sidebar: `_index.md`, `weight`, icons, folding, multiple sidebar roots | [Organizing content](/docs/write/organize/) | | Looking up what a front matter key means | [Page parameters](/docs/write/frontmatter/) | | A blog post, a release announcement, RSS | [Blog posts](/docs/write/blog/) | | A book: chapter numbering, figures and tables, cross-references, whole-book print | [Books](/docs/write/book/) | | A release and download page: version cards, asset tables, checksums | [Releases and downloads](/docs/write/releases/) | | An OpenAPI reference page | [API reference pages](/docs/write/openapi/) | | Writing in two languages: paired files, aligned anchors, fallback for untranslated pages | [Languages](/docs/customize/i18n/) | | A component's syntax and parameters | [Components](/docs/components/) | --- Section pages: - [Writing pages](/docs/write/pages/): Creating a documentation page — where the file goes, what the front matter says, why heading anchors are written by hand, how links and images work, and what appears at the end of a page on its own. - [Organizing content](/docs/write/organize/): The directory structure is the sidebar tree — `_index.md` and weight, section index styles, icons and folding, hiding pages, and putting documentation at any path. - [Page parameters](/docs/write/frontmatter/): The full front matter table — every page key the theme actually reads, grouped by sidebar, shell, search, output, page end, Book, landing and release pages. - [Blog posts](/docs/write/blog/): Setting up a blog section — directory conventions, a post's front matter, featured images, the year-grouped list page, and RSS. - [Books](/docs/write/book/): Turn a directory tree into a book with `type: book`: chapter numbering, numbered figures and tables, cross-references, generated indexes and whole-book print. - [Releases and downloads](/docs/write/releases/): Record versions, tags, archive links, checksums and install commands as local facts, then let release cards, asset tables, download blocks and index pages derive from that one record. - [API reference pages](/docs/write/openapi/): Put an OpenAPI specification on the site and render it as a browsable API reference with the bundled Swagger UI or Redoc, without touching a CDN. --- Backlinks: - [Cards](/docs/components/cards/) - [Customization](/docs/customize/) ================ Source: https://oink.pgsty.com/docs/write/pages/index.md ================ # Writing pages > Creating a documentation page — where the file goes, what the front matter says, why heading anchors are written by hand, how links and images work, and what appears at the end of a page on its own. --- LLMS index: [llms.txt](/llms.txt) --- This page covers writing a documentation page end to end: where the file goes, the front matter, heading anchors, links, images, drafts, and the page end. It assumes the site already builds locally; if it does not yet, start with [Quick start](/docs/start/). ## Creating a page {#new-page} A page is a Markdown file under `content/`, and its URL follows its position there: `content/docs/install.md` is published as `/docs/install/`. The Chinese translation is a `.zh.md` file of the same name in the same directory, sharing one logical path with the English page. A page with no attached resources is a single file. When a page carries images, cast files or example configuration, make it a directory instead, name the page itself `index.md`, and put the resources beside it — Hugo calls this a [page bundle](https://gohugo.io/content-management/page-bundles/): ```filetree {title="the two page shapes inside content/"} - content/ - docs/ - _index.md # section index, English - _index.zh.md # section index, Chinese - install.md # single-file page → /docs/install/ - install.zh.md # its Chinese translation - anatomy/ # page bundle → /docs/anatomy/ - index.md - index.zh.md - shell.webp # page resource, shared by both languages ``` `hugo new content docs/install.md` generates an empty file with front matter from an archetype — see the [Hugo documentation](https://gohugo.io/commands/hugo_new_content/) — and writing the file by hand works just as well. > [!IMPORTANT] > When a Chinese page has no English counterpart, Hugo does not hand it > resources that carry no language suffix. In that case the resource filename > needs the `.zh.` infix (`shell.zh.webp`) while the body still writes > `shell.webp`. ## The front matter you need {#front-matter} Between the two `---` lines at the top of the file is YAML front matter. Four keys belong on every page: ```yaml {title="content/docs/install.md"} --- title: Install Pigsty # page heading, browser title, search result title linkTitle: Install # short name in the sidebar and breadcrumbs; falls back to title description: Get a working PostgreSQL cluster onto a clean EL 9 machine. weight: 20 # ordering among siblings; use multiples of 10 to leave room --- ``` Let `description` say in one sentence what the page lets the reader accomplish. It appears on the section index cards, in search results and on social cards. `weight` decides the sidebar order, and only equal weights fall back to alphabetical order. The remaining keys are optional — icon, draft, search weight, comment switch, page shell and so on. The full table is in [Page parameters](/docs/write/frontmatter/). ## Heading levels and stable anchors {#headings} Start sections at `##` in the body and leave `#` to `title`. The theme already renders the page heading, so another `#` in the body produces two top-level headings. The outline in the right column starts at `##`, and how deep it goes is decided by Hugo's `markup.tableOfContents` — `####` on this site. Write an explicit English anchor `{#id}` on every `##` and `###`: ```markdown {title="Source"} ## Prerequisites {#prerequisites} ### Disk and memory {#disk-and-memory} ``` There are two reasons: - Cross-language alignment. 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 semantic place through two different anchors, which no translation audit can compare. Give the translated heading the English page's ID and both sides share one fragment. - Link stability. Heading text changes as wording is revised, and a public link should not break with it. An explicit ID is a public route once published; when a rename is needed, leave an empty anchor for the old ID: ```markdown {title="Source: leaving a target behind for the old anchor"} ## Getting started {#quickstart} ``` Use lowercase English with hyphens, unique within the page. This site's translation audit compares the heading IDs rendered by the English and Chinese pages and fails on a mismatch. ## Writing links {#links} Three forms, for different purposes: | Form | Example | When to use it | | --- | --- | --- | | Absolute site path | `[Configuration](/docs/customize/config/)` | The default. It points at a published route, is easy to audit and replace site-wide, and survives source files moving | | Relative path | `[another page](../organize/)`, `![diagram](shell.webp)` | Resources inside the same page bundle, or a neighbouring page that should deliberately follow the source directory | | The `ref` / `relref` shortcode | `[Configuration]({{< ref "/docs/configure/overview" >}})` | When the target's existence must be checked at build time; a missing target fails the build instead of leaving a dead link | All three carry a trailing slash and point at directory-style routes (`/docs/write/pages/`), matching Hugo's default permalinks. The theme has no link render hook: links go to Goldmark untouched. External links get no automatic `target="_blank"`; write HTML where a new tab is needed, or handle it in the site's own `layouts/_markup/render-link.html`. Plain Markdown links are not checked for existence. So: - Prefer absolute paths for internal links, and `grep` to replace them site-wide after a restructure; - When moving a page, add `aliases` for the old path and update internal links to the new route — do not let an alias carry navigation indefinitely; - Use `ref` for a target you are unsure of, and let the build check it for you. In a bilingual site, link to the logical page (`/docs/write/pages/`) rather than to a `.zh.md` filename, and keep fragment IDs language-neutral. ## Where images go {#images} A page's own screenshots go in its page bundle, images shared by several pages go in `assets/images/`, and large files that need no processing go in `static/`. All three are written `![alt text](source)` in the source, and an attribute line controls caption, size, zoom and numbering — see [Images](/docs/components/image/). ## Drafts and publishing {#drafts} A page with `draft: true` never reaches the build output: ```yaml {title="front matter"} --- title: Migration guide, not yet final draft: true --- ``` Preview with `hugo server -D` to show drafts (`-D` is `--buildDrafts`). A page whose `date` is in the future is excluded too; `-F` shows those. A production build uses neither switch, and plain `hugo` publishes only finished content. ## OINK's Markdown extensions at a glance {#extensions} The body is standard Markdown (Goldmark) plus the native forms below. Each is ordinary Markdown syntax with one attribute line, and each stays readable as source on GitHub: | Component | Shortest syntax | Page | | --- | --- | --- | | Callouts | `> [!NOTE]` on the first line of a blockquote | [Callouts](/docs/components/callout/) | | Tabs | Two adjacent fences each carrying `{tab="Homebrew"}` | [Tabs](/docs/components/tabs/) | | Steps | An ordered list followed by a `{.steps}` line | [Steps](/docs/components/steps/) | | Cards | A list of links followed by a `{.cards}` line | [Cards](/docs/components/cards/) | | Field lists | A table followed by `{.fields meta="type default"}` | [Fields](/docs/components/fields/) | | Table extras | A table followed by `{.matrix}` or `{caption="…"}` | [Tables](/docs/components/table/) | | Code blocks | `{title="hugo.yml" copy=false}` on the fence info line | [Code Blocks](/docs/components/code/) | | Images | A standalone image followed by `{caption="…" width="600"}` | [Images](/docs/components/image/) | | File trees | A `filetree` fence, one `- name/ # comment` per line | [FileTree](/docs/components/filetree/) | | Mathematics | A `math` fence, or display maths wrapped in `$$` | [Math](/docs/components/math/) | | Diagrams | A `mermaid` fence (also `plantuml`, `markmap`, `echarts`) | [Mermaid](/docs/components/mermaid/) | The few remaining components — badges, keys, file includes, terminal recordings, the Book figure and table family — are shortcodes, with syntax and parameters in [Components](/docs/components/). A combined example: code fences and a callout inside steps. ````markdown {title="Source"} 1. Install Hugo Extended, 0.160.1 at the oldest: ```bash brew install hugo ``` 1. Clone OINK Starter and preview it: ```bash git clone https://github.com/pgsty/oink-starter my-docs cd my-docs && hugo server ``` > [!TIP] > Add `-D` to preview drafts as well. {.steps} ```` 1. Install Hugo Extended, 0.160.1 at the oldest: ```bash brew install hugo ``` 1. Clone OINK Starter and preview it: ```bash git clone https://github.com/pgsty/oink-starter my-docs cd my-docs && hugo server ``` > [!TIP] > Add `-D` to preview drafts as well. {.steps} ## What appears at the end of a page {#page-end} Four blocks are generated by the theme in a fixed order, and none is written in the body: | Position | What it is | Default | Where to configure | | --- | --- | --- | --- | | 1 | Feedback: the two "Was this page helpful?" buttons | Off | [Repository links and page info](/docs/customize/repository/) | | 2 | Last modified: the time and the most recent commit subject, linked to GitHub | On when Git information is available | [Repository links and page info](/docs/customize/repository/) | | 3 | Pager: previous and next, in sidebar tree order | On for docs / book / blog | [Navigation and menus](/docs/customize/navigation/) | | 4 | Comments: giscus | When configured and enabled | [Comments](/docs/admin/comments/) | The action menu beside the title (copy Markdown, edit this page, view history, open an issue, print) is automatic too, and is configured in the same place, [Repository links and page info](/docs/customize/repository/). To turn one of them off for a single page, use front matter: `feedback: false`, `annotation: false`, `pager: false`, `comments: false`. The keys are described in [Page parameters](/docs/write/frontmatter/). ## Verify {#verify} After writing a page, run a strict build: ```bash hugo --printPathWarnings --panicOnWarning ``` - The output must end with `Total in …` and no ERROR and no WARN. A disallowed key on an attribute line, an invalid component parameter, or a `ref` whose target is missing all fail here naming the file and the line; the theme never degrades silently. - `--printPathWarnings` reports two pages resolving to the same output path, which turns up most often in multilingual sites or after changing `permalinks`. Then confirm three things in the browser: 1. The page is in the sidebar, in the position `weight` implies; 2. The right-hand outline lists the `##` headings you wrote, and clicking one puts an English anchor in the URL; 3. The English and Chinese versions of the same heading share an anchor (this site audits that with `node scripts/check-doc-translations.mjs --public public`). ## Related {#related} - [Organizing content](/docs/write/organize/) — how the directory structure decides the sidebar - [Page parameters](/docs/write/frontmatter/) — the full front matter table - [Components](/docs/components/) — each component's syntax and parameters - [Languages](/docs/customize/i18n/) — paired bilingual files and fallback for untranslated pages - [Local preview](/docs/admin/preview/) — the `hugo server` switches worth knowing --- Backlinks: - [Structure the content](/book/02-structure/) - [Docs](/docs/) - [Callouts](/docs/components/callout/) - [Languages](/docs/customize/i18n/) - [Get started](/docs/start/) - [From scratch](/docs/start/from-scratch/) - [OINK Starter](/docs/start/starter/) - [Authoring](/docs/write/) - [Blog posts](/docs/write/blog/) - [Page parameters](/docs/write/frontmatter/) - [API reference](/docs/write/openapi/) - [Organizing content](/docs/write/organize/) ================ Source: https://oink.pgsty.com/docs/write/organize/index.md ================ # Organizing content > The directory structure is the sidebar tree — `_index.md` and weight, section index styles, icons and folding, hiding pages, and putting documentation at any path. --- LLMS index: [llms.txt](/llms.txt) --- OINK needs no separate navigation configuration: the directory structure under `content/` is the sidebar tree. This page covers how directories and files are arranged, section indexes, ordering, icons, folding, hiding, and multiple sidebar roots. ## Directories are the sidebar {#tree-is-sidebar} A directory is a section (Hugo's term), the Markdown files inside it are its pages, and a nested directory is a subsection. The sidebar renders that tree level by level, ordered by `weight`, labelled with `linkTitle` and falling back to `title`. The tree on the left comes from this source: ```filetree {title="the first two levels of content/docs/"} - content/ - docs/ - _index.md # section root: type: docs + cascade - about/ # Introduction {open=false} - _index.md - features.md - start/ # Get started {open=false} - _index.md - write/ # Authoring (this section) - _index.md # weight: 30 - pages.md # weight: 10 - organize.md # weight: 20 - frontmatter.md # weight: 30 - components/ # Components {open=false} - _index.md ``` ## Every directory needs an `_index.md` {#index-pages} A section index is the `_index.md` inside the directory (`_index.zh.md` for Chinese). Without one Hugo still creates the section, but it has no title, description, icon or `weight`: the sidebar row shows the directory name and the ordering is out of your control. ```yaml {title="content/docs/deploy/_index.md"} --- title: Deploy linkTitle: Deploy description: Publish the site to GitHub Pages, Cloudflare Pages or your own Nginx. weight: 50 icon: fa-solid fa-cloud-arrow-up --- ``` A section `_index.md` has one further power: `cascade` pushes shared settings down the whole subtree once, instead of repeating them on every page. ```yaml {title="content/docs/reference/_index.md"} --- title: Reference weight: 90 cascade: pager: false # no previous / next on any page in this subtree search_boost: 0.8 # reference pages rank slightly lower in search --- ``` ## Ordering: use multiples of 10 for weight {#weight} Pages in a section are sorted by ascending `weight`, and only equal weights fall back to date and `linkTitle`. Always use multiples of 10 (10, 20, 30) so a page can be inserted between two others without touching the rest. A section's own `weight` decides its position among its siblings. A page with no `weight` counts as 0, and Hugo places those after every page that does have one, ordered among themselves by date and title. That order drifts as content changes, so give every page a `weight`. ## Single file or page bundle {#bundles} A page with no resources of its own is a single `slug.md`. A page carrying images, cast files or example files becomes a directory with an `index.md` and the resources beside it. The two shapes look identical in the sidebar and produce the same URL. See [Writing pages](/docs/write/pages/#new-page). ## List or cards on a section index {#section-index} After the body of an `_index.md`, the theme appends an index of the child pages in one of two styles: ```yaml {title="hugo.yml: the site-wide default"} params: ui: section_index: cards # list | cards ``` `list` is the theme default — one line per child page with its title and description. `cards` is a grid of link cards reading each child's `icon`, `linkTitle` and `description`. This site uses `cards`, and this section's index page is the example. Override it in a single section's front matter when that section needs the other style: ```yaml {title="content/docs/reference/_index.md"} section_index: list cascade: section_index: list # and its descendant sections too ``` Two page-level switches are independent of the style: `simple_list: true` renders a compact bulleted list, and `no_list: true` generates no index at all, for a page whose body writes its own navigation. > [!TIP] > In the card style, `description` is the card body. Keep it to one sentence > that fits on a single line. ## Sidebar icons {#icons} Write one Font Awesome class pair in a page's or section's front matter: ```yaml {title="content/docs/deploy/_index.md"} icon: fa-solid fa-cloud-arrow-up ``` Icon density is a site-level policy, so that leaf pages do not all carry icons: ```yaml {title="hugo.yml"} params: ui: sidebar_icon_policy: groups # all | groups | none ``` | Value | Effect | | --- | --- | | `all` | Every entry that declares an `icon` shows it (the compatibility default when unset) | | `groups` | Only the root and nodes that have children show icons; ordinary leaf pages do not | | `none` | No entry icons in the sidebar | A new site is better off writing `groups` explicitly: the semantic markers on groups stay and the leaf-level icons go. This site uses that setting, so only the six sections on the left carry icons. ## Expanding and folding {#folding} A section with children carries a fold arrow in the sidebar, and the reader's expansion state is kept locally. The default behaviour: the path containing the current page is expanded and everything else is collapsed; blog-type sections are expanded by default. ```yaml {title="content/docs/reference/_index.md"} sidebar_expanded: true # this section is always expanded by default ``` Site-level folding, compact mode, initial expansion depth, width and truncation are configured in [Layouts and page types](/docs/customize/layout/); the full key definitions are in [Configuration](/docs/customize/config/). ## Hiding from the sidebar {#hiding} | Front matter | Effect | | --- | --- | | `toc_hide: true` | The page is absent from the sidebar tree (it is still published, and links to it still work) | | `hide_summary: true` | The page is absent from the section index | | `sidebar_divider: true` | The entry stops being a link and becomes a group heading in the sidebar | | `manual_link: https://…` | The sidebar row points elsewhere; pair it with `manual_link_title` and `manual_link_target: _blank` | `toc_hide` and `hide_summary` control two different entry points, so set both only when the page should appear in neither. ## The shell follows `type`, not the path {#type-and-shell} The documentation shell (sidebar, table of contents, breadcrumbs, pager) does not depend on the directory name. It depends only on whether the page's `type` is listed in `params.ui.shell_types`: ```yaml {title="hugo.yml: the theme default"} params: ui: shell_types: [docs, book, blog, swagger] ``` Documentation can therefore live at any path, with `type` assigned by a cascade. To put a handbook at `content/handbook/`, the section root reads: ```yaml {title="content/handbook/_index.md"} --- title: Operations handbook type: docs sidebar_root_for: self # the sidebar tree roots here rather than falling back to /docs cascade: type: docs # the whole subtree uses the documentation shell --- ``` > [!IMPORTANT] > When the documentation directory is not called `docs`, `sidebar_root_for: self` > is needed alongside `type: docs`. Otherwise the sidebar looks for its root at > `params.ui.docs_section` (default `docs`), and a reader under `/handbook/` > sees the `/docs/` tree. ## Multiple sidebar roots {#sidebar-roots} By default the sidebar tree roots at the top-level section the reader is in, and a row above the tree names the current root. A large subtree can become a root of its own — a versioned API reference, say, or a self-contained handbook: ```yaml {title="content/docs/api/v2/_index.md"} --- title: API reference v2 sidebar_root_for: self # self | children --- ``` | Value | Meaning | | --- | --- | | `self` | The section's index page and all its descendants take it as their sidebar root | | `children` | The index page stays in the parent tree; only the descendants root here | The switcher above the root is site-wide: it lists every top-level section plus every section anywhere that declares `sidebar_root_for: self`. With only one entry it degrades to a plain link; two or more make it a dropdown. To keep a top-level section out of the switcher, write `sidebar_root_menu: false` in its `_index.md`. Below the switcher, the section index remains the first link in the tree: the switcher picks a tree and the root link points at a document. `sidebar_root_link_self: false` makes that row point at the parent section instead. ## Verify {#verify} ```bash hugo --printPathWarnings --panicOnWarning ``` It must reach `Total in …` with no ERROR and no WARN. `--printPathWarnings` reports two pages resolving to the same output path, which happens most often while changing the directory structure. Then confirm each of these in the browser: 1. The sidebar order matches the `weight` values you wrote, and a new section appears where expected; 2. The section index lists every child (a missing one comes from `hide_summary` or a missing `_index.md`); 3. Breadcrumbs and the pager follow the same order as the sidebar, because the pager reads the same tree; 4. The tree has the same shape after switching language (every `_index.md` needs a `.zh.md` counterpart). When sidebar entries exceed `params.ui.sidebar_menu_truncate`, the build warns and says what to raise it to. That warning cannot be ignored: truncated entries never appear in the sidebar. ## Related {#related} - [Writing pages](/docs/write/pages/) — how to write a single page - [Page parameters](/docs/write/frontmatter/) — the full definition of every front matter key used here - [Layouts and page types](/docs/customize/layout/) — site-level shell, sidebar and table-of-contents settings - [Navigation and menus](/docs/customize/navigation/) — the navbar menu, breadcrumbs and pager - [Languages](/docs/customize/i18n/) — keeping a bilingual tree consistent --- Backlinks: - [Structure the content](/book/02-structure/) - [Docs](/docs/) - [Cards](/docs/components/cards/) - [FileTree](/docs/components/filetree/) - [Configuration](/docs/customize/config/) - [Layouts and page types](/docs/customize/layout/) - [Navigation and menus](/docs/customize/navigation/) - [Print](/docs/customize/print/) - [Repository tour](/docs/start/anatomy/) - [OINK Starter](/docs/start/starter/) - [Authoring](/docs/write/) - [Blog posts](/docs/write/blog/) - [Books](/docs/write/book/) - [Page parameters](/docs/write/frontmatter/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/write/frontmatter/index.md ================ # Page parameters > The full front matter table — every page key the theme actually reads, grouped by sidebar, shell, search, output, page end, Book, landing and release pages. --- LLMS index: [llms.txt](/llms.txt) --- This page is the complete table of page-level parameters, listing only the keys the OINK theme reads. Keys the theme reads solely to warn that they were renamed or removed are not listed here — they are in [Migration](/docs/design/migration/), and they are also kept out of the generated editor schema. Hugo's own front matter fields (`slug`, `url`, `build`, `sitemap`, `expiryDate` and the rest) work as usual; their meaning is in the [Hugo documentation](https://gohugo.io/content-management/front-matter/). Site parameters (`params.*` in `hugo.yml`) are in [Configuration](/docs/customize/config/). ## How to read the tables {#how-to-read} Precedence, highest first: 1. The page's own front matter; 2. The nearest `cascade` (when several cascade layers set the same key, the one closest to the page wins); 3. The site parameter in `hugo.yml`. Keys whose *Default* column says "site value" fall back to the site parameter of the same name when unset. Page keys are written at the top level of the front matter, and the key name is the site key with its `ui.` prefix dropped: the site's `params.ui.section_index` is the page's `section_index`. Front matter never carries a `ui:` block; the keys sit at the top level. A `ui:` block written there is not read and not reported, so check the key name against this page when a setting seems to have no effect. ```yaml {title="content/docs/wide-reference.md"} --- title: Compatibility matrix weight: 40 page_width: wide footer_style: slim image_zoom: true section_index: list --- ``` Inside a `cascade` the key names are unchanged, just one level deeper: ```yaml {title="content/docs/reference/_index.md"} cascade: pager: false section_index: list ``` An invalid value does not stop the build. The theme warns — naming the key, the value it got and the fallback it used — and renders the page with the default in the table, so one typo degrades one setting instead of serving HTTP 500 on every URL under `hugo server`. It still never ships: every publishing gate builds with `--panicOnWarning`, which turns that warning back into a hard failure where it counts. No front matter key stops the build; the theme's templates never raise an error. Where carrying on would publish something wrong rather than merely plain — an incomplete upstream attribution, for instance, because a partial notice reads exactly like a complete one — the warning is followed by omitting that block entirely rather than by a fallback. The one thing here that does stop a build belongs to Hugo, not the theme: a reference that cannot resolve. ## Basics {#basic} | Key | Type | Default | Description | | --- | --- | --- | --- | | `title` | string | — | Page heading, browser title, search result title. Required on every page | | `linkTitle` | string | `title` | Short name in the sidebar, breadcrumbs, pager and cards | | `description` | string | — | One-sentence summary: section cards, search snippet, `meta description`; rendered as a standfirst above the body on blog pages | | `weight` | integer | `0` | Ordering among siblings; use multiples of 10. `0` (unset) sorts after every page that has a weight — see [Organizing content](/docs/write/organize/#weight) | | `draft` | boolean | `false` | A draft never reaches the build output; `hugo server -D` previews it — see [Writing pages](/docs/write/pages/#drafts) | | `date` | date | — | Blog date, and the sort key for release pages; a future date is excluded by default | | `lastmod` | date | Git commit time | The page-end "last modified"; not needed by hand when the site enables `enableGitInfo` | | `aliases` | string array | — | Redirects an old path to this page; for page migration, not for everyday navigation | | `type` | string | top-level directory name | Decides the template and the shell: `docs`, `book`, `blog`, `swagger` — see [Organizing content](/docs/write/organize/#type-and-shell) | | `layout` | string | — | Picks a layout for one page: `landing`, `releases` | | `cascade` | map | — | Pushes the keys below down the whole subtree | {.fields meta="type default"} ## Sidebar and navigation {#navigation} The guide is [Organizing content](/docs/write/organize/). | Key | Type | Default | Description | | --- | --- | --- | --- | | `icon` | Font Awesome class pair | — | Icon in the sidebar, section cards and search results, e.g. `fa-solid fa-rocket` | | `toc_hide` | boolean | `false` | Absent from the sidebar tree and from the pager sequence | | `hide_summary` | boolean | `false` | Absent from the section index | | `sidebar_divider` | boolean | `false` | The row renders as a sidebar group heading: not a link, and not in the pager sequence | | `sidebar_expanded` | boolean | `true` for blog sections, `false` otherwise | This section is expanded by default in the sidebar | | `sidebar_root_for` | `self` / `children` | — | Makes this section a sidebar tree root; `self` includes the section index, `children` covers descendants only. Any other value warns and is ignored | | `sidebar_root_link_self` | boolean | `true` | The root row links to itself; `false` links to the parent section instead. A non-boolean warns and uses `true` | | `sidebar_root_menu` | boolean | `true` | Whether a top-level section appears in the root switcher | | `toc_root` | boolean | `false` | When the sidebar root is the site home, excludes this whole top-level section from the tree and the pager sequence | | `manual_link` | URL | — | The sidebar and section index row points elsewhere | | `manual_link_relref` | content reference | — | The same, resolved with `relref`; a missing target fails the build | | `manual_link_title` | string | `title` | Hover title for the manual link | | `manual_link_target` | string | — | For example `_blank`; the theme adds `noopener` | | `no_list` | boolean | `false` | The section index generates no child list | | `simple_list` | boolean | `false` | The child index renders as a compact bulleted list | | `section_index` | `list` / `cards` | site value (`list`) | Style of the child index. An invalid value warns and falls back | | `section_index_columns` | integer | `2` | Column count in the card style | | `notoc` | boolean | `false` | Hides the right-hand page outline | | `pager` | boolean | decided by `params.ui.pager_types` | `false` turns off previous / next for this page. A non-boolean warns and is ignored | | `navbar_enabled` | boolean | site value (`true`) | Whether this page renders the navbar | | `navbar_autohide` | boolean | site value (`false`) | The navbar hides itself on pointer devices | | `breadcrumb` | boolean | shell default | Whether this page renders breadcrumbs; Docs/Book default on and Blog defaults off | | `theme_color` | string | site value | `#rgb`/`#rrggbb` hex tinting this page's accent grounds. On a section root's `cascade` it gives the whole section an identity — see [Brand and appearance](/docs/customize/brand/#theme-color) | | `theme_color_dark` | string | derived | The dark half of the accent. A page overriding `theme_color` under a cascade that also sets this key inherits that dark value, so override both. `theme_color: false` opts the page out of an inherited section color entirely | | `page_context_menu` | boolean | site value (`true`) | The page action menu on the title row (copy Markdown, edit this page, print, …) | | `page_context_menu.assistant_links` | boolean | site value (`false`) | The ChatGPT / Claude handover items, written `page_context_menu: { assistant_links: false }`. A page may only narrow the site policy, never enable it alone | {.fields meta="type default"} ## Page shell {#shell} Site-level defaults and what they do are in [Layouts and page types](/docs/customize/layout/). | Key | Type | Default | Description | | --- | --- | --- | --- | | `page_width` | `normal` / `wide` / `full` | `normal` | Width of the content column. An invalid value warns and falls back | | `reading_width` | `slim` / `normal` / `wide` | `normal` | Reading measure on Book pages; applies to `type: book` only | | `footer_style` | `fat` / `slim` / `none` | site value (`fat`) | Footer shape. An invalid value warns and falls back | | `body_class` | string | — | A class appended to `` for the site's own CSS | | `reading_time` | boolean | site value | Whether this page shows a reading time; `false` hides it | | `sidebar_enabled` | boolean | `true` | Whether this page shows the left sidebar; `false` hides it | | `scroll_spy` | boolean | site value | Scroll tracking in the outline; `true` enables it | | `keyboard_nav` | boolean | site value (`true`) | Single-key keyboard navigation — see [Keyboard navigation](/docs/customize/keyboard/). A non-boolean warns and falls back | | `lastmod_commit` | `subject` / `hash` / `none` | `subject` | How the commit is shown after "last modified". An invalid value warns and falls back | | `sidebar_expand_levels`, `sidebar_menu_compact`, `sidebar_menu_foldable`, `sidebar_item_overflow` | as the site parameter | site value | Sidebar behaviour can be overridden per page too; the values are in [Configuration](/docs/customize/config/) | | `sidebar_width_min`, `sidebar_width_max` | positive integer | site value (`220` / `480`) | Per-page lower and upper bounds for desktop sidebar resizing; a minimum above its maximum warns and restores the site pair | | `code_copy` | boolean | site value (`true`) | Default copy control for code blocks on this page; an explicit fence `copy=` still wins | | `toc_style` | `fixed` / `flow` | site value (`fixed`) | Fixed right-rail panel or a wider rail beginning in the content flow | | `toc_taxonomies` | boolean | site value (`true`) | Whether taxonomy clouds join the right-rail outline | | `taxonomy_icons` | map | site value | Per-taxonomy icon overrides for this page or section cascade | {.fields meta="type default"} ## Search {#search} The guide is [Search](/docs/customize/search/). | Key | Type | Default | Description | | --- | --- | --- | --- | | `search_keywords` | string or string array | — | Extra search terms, including synonyms and other languages | | `search_boost` | positive number | `1.0` | Ranking multiplier; the final score is the text match score times this value. A non-numeric, non-finite, zero or negative value warns and falls back to `1.0` | | `search_exclude` | boolean | `false` | Keeps the page out of the local index | {.fields meta="type default"} ## Output formats {#outputs} The guides are [AI-agent support](/docs/customize/agents/) (`.md` and `llms.txt`) and [Print](/docs/customize/print/). | Key | Type | Default | Description | | --- | --- | --- | --- | | `outputs` | string array | site `outputs` | Which output formats this page generates; `[HTML]` stops the `.md` twin | | `no_print` | boolean | `false` | Excluded from the whole-chapter and whole-book print aggregate | {.fields meta="type default"} ## Page end: comments, feedback and provenance {#page-end} The order is fixed as feedback → provenance → pager → comments; see [Writing pages](/docs/write/pages/#page-end). | Key | Type | Default | Description | | --- | --- | --- | --- | | `comments` | boolean | site `params.comments.enable` (`false`) | Whether this page shows the giscus comment section — see [Comments](/docs/admin/comments/) | | `feedback` | boolean or map | site `params.ui.feedback` (off) | The map form takes `enable` and `reasons`. Anything else warns and falls back | | `annotation` | boolean | site `params.ui.annotation` (on) | The "last modified / provenance" block at the page end. Only a boolean is accepted; anything else warns and falls back | | `backlinks` | boolean | site `params.ui.backlinks` (off) | Whether the right rail shows the "Backlinks" group beside the table of contents; a section can cascade it. Only a boolean is accepted; anything else warns and falls back — see [Navigation and menus](/docs/customize/navigation/#backlinks) | | `translation_notice` | language code or `false` | site `params.ui.translation_notice` (off) | The language code of the authoritative version, so a translation can say so and link back; write `false` on a page authored natively in this language | {.fields meta="type default"} ### Upstream attribution {#upstream} When a page is derived from material elsewhere, `upstream_link` declares the source and the page-end provenance line gives the work, the copyright holder, the licence and a link to the full notice. This family resolves site parameters → the `data/upstreams` entry named by `upstream_source` → this page's front matter, so the most specific declaration wins. `upstream_link` is read from front matter only (a cascade counts, site parameters do not) — a site-wide value would make every page claim the same source. A companion key without `upstream_link` warns and the attribution is omitted. | Key | Type | Default | Description | | --- | --- | --- | --- | | `upstream_link` | URL | — | The address of the material this page is derived from. An empty string opts out of an inherited cascade value | | `upstream_name` | string | — | The upstream work, as the attribution names it. Required once `upstream_link` is set | | `upstream_copyright` | string | — | The copyright notice, retained as upstream wrote it. Required | | `upstream_license` | SPDX identifier | — | Must be found in `data/licenses`, or it warns and the attribution is omitted. Required | | `upstream_notice` | site path or URL | — | The page carrying the full notice (licence text, warranty disclaimer, upstream NOTICE, snapshot pin). Required | | `upstream_ref` | string | — | The tag or commit the snapshot pins, shown in parentheses after the work | | `upstream_source` | string | site parameter | The entry name in `data/upstreams`, for upstream facts shared by many pages; a missing entry warns and the attribution is omitted | | `upstream_modified` | boolean | `false` | Changes the credit verb to say the work was adapted, and adds a "view history" link to that same sentence when the site has repository information — one line, not two. A non-boolean warns and the page is treated as unmodified | {.fields meta="type default"} Missing any one of the four required keys (`upstream_name`, `upstream_copyright`, `upstream_license`, `upstream_notice`) warns and omits the attribution: a partial attribution is worse than an obvious omission. The theme ships an SPDX table at `data/licenses.yaml`, and a site adds to or overrides it with a file of the same name. ## Image zoom {#image-zoom} | Key | Type | Default | Description | | --- | --- | --- | --- | | `image_zoom` | boolean | site value (`false`) | Whether images on this page open full size — see [Images](/docs/components/image/). A non-boolean warns and falls back | {.fields meta="type default"} ## Blog posts {#blog} The guide is [Blog posts](/docs/write/blog/). | Key | Type | Default | Description | | --- | --- | --- | --- | | `author` | string | — | Post byline; inline Markdown is allowed. Ignored on a page that has `authors` | | `authors` | string array | — | Terms of the `authors` taxonomy, in byline order — see [Authors and bylines](/docs/write/blog/#authors). Needs `author: authors` under `taxonomies:` | | `series` | string array | — | Terms of the `series` taxonomy. The strip above the body uses the first one — see [Series](/docs/write/blog/#series) | | `series_weight` | integer | — | Place in the series. Weighted members come first in ascending order, the rest follow by ascending date | | `tags` | string array | — | Tags — see [Taxonomies](/docs/customize/taxonomy/) | | `categories` | string array | — | Categories, likewise | | `images` | string array | — | The first entry becomes the post's featured image and share card; put it in a section `_index.md` cascade for a section-wide default. `images: []` opts the page out of an inherited cascade value; it does not suppress an image the page bundle already supplies under a `featured`, `cover` or `thumbnail` name | | `byline` | string | — | Credit shown with the resolved featured image when that image is rendered | | `featured_image` | `none` / `banner` / `wash` / `hero` | site value (`none`) | How this article renders its own featured image; `hero` paints the immersive full-bleed shell. An invalid value warns and falls back | | `blog_index` | `list` / `cards` / `table` | site value (`list`) | Written on a blog root, the index form for that section. `table` lists the whole section without pagination. An invalid value warns and falls back | | `blog_index_columns` | positive integer | site value (`3`) | Card columns at wide breakpoints; medium and narrow layouts retain their responsive limits | | `blog_index_size` | positive integer | site value (`12`) | Posts per page for `list` and `cards`; `table` always lists the whole section | | `blog_index_toggle` | boolean | site value (`false`) | Publishes all three index forms and lets the reader switch among them; hidden forms do not load images | | `share` | string array or `false` | site `params.ui.share` (empty) | The page-end share targets, replacing any inherited list; `false` opts this page out — see [Share](/docs/write/blog/#share). An unknown target warns and is dropped | | `summary` | string | — | Fallback excerpt for post rows on tag and category pages; `description` wins | {.fields meta="type default"} ## Book {#book} The guide is [Books](/docs/write/book/). A whole book sets `type: book` through a section `cascade`. | Key | Type | Default | Description | | --- | --- | --- | --- | | `book_number` | string | — | Chapter number, shown before the page title and the sidebar entry | | `book_status` | `draft` | — | Marks a draft chapter: flagged in the sidebar and contents, and left out of the indexes by default | | `sidebar_headings` | `false` / `true` / integer 2–4 | site value (`false`) | Expands the h2–h4 branch under the current sidebar entry. Out of range warns and falls back | | `book_draft_banner` | boolean | site value (`false`) | Adds a banner at the top of a draft chapter. A non-boolean warns and falls back | {.fields meta="type default"} ## Landing {#landing} The guide is [Home and landing pages](/docs/customize/home/). Any page with `layout: landing` uses the landing shell. | Key | Type | Default | Description | | --- | --- | --- | --- | | `landing` | string | — | Data is taken from `data/landing//.yaml` | | `sections` | array | — | Section definitions inlined in front matter, taking precedence over `landing`. Anything but an array warns and no sections render | {.fields meta="type default"} ## Release pages {#releases} The guide is [Releases and downloads](/docs/write/releases/). A section with `layout: releases` ignores `weight` and sorts by release date and SemVer, newest first. | Key | Type | Default | Description | | --- | --- | --- | --- | | `release_url` | string | — | One GitHub release URL, `https://github.com///releases/tag/`. The theme derives the project, tag, date and asset list from it. Anything else warns and the release block is skipped | {.fields meta="type default"} ## Related {#related} - [Writing pages](/docs/write/pages/) — the handful of keys every page needs - [Organizing content](/docs/write/organize/) — what the sidebar and navigation keys actually do - [Configuration](/docs/customize/config/) — the full table of site parameters in `hugo.yml` --- Backlinks: - [Comments](/docs/admin/comments/) - [Troubleshooting](/docs/admin/troubleshooting/) - [Cards](/docs/components/cards/) - [Fields](/docs/components/fields/) - [Includes](/docs/components/include/) - [AI-agent support](/docs/customize/agents/) - [Configuration](/docs/customize/config/) - [Layouts and page types](/docs/customize/layout/) - [Repository links](/docs/customize/repository/) - [Search](/docs/customize/search/) - [Taxonomies](/docs/customize/taxonomy/) - [Authoring](/docs/write/) - [Blog posts](/docs/write/blog/) - [Books](/docs/write/book/) - [Organizing content](/docs/write/organize/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/write/blog/index.md ================ # Blog posts > Setting up a blog section — directory conventions, a post's front matter, featured images, the year-grouped list page, and RSS. --- LLMS index: [llms.txt](/llms.txt) --- A blog post's body is written exactly like a documentation page; the shell is what differs. A post carries a date, an author, tags and a featured image, the list is grouped by year newest first, and the section has an RSS feed. This page covers creating the blog section, a post's front matter, featured images, list pagination and feeds. ## The blog directory {#layout} A blog is a section under `content/`, and `type: blog` gives it the blog shell. Subdirectories divide it by publisher and audience, with posts sitting flat inside. Do not create year directories: the year grouping is generated by the list page. ```filetree {title="this site's content/blog/"} - content/ - blog/ - _index.md # type: blog + cascade - _index.zh.md - oink/ # engineering notes and announcements - _index.md # cascade: images: [/images/oink.webp] - oink-announcement.md - oink-announcement.zh.md - release/ # versioned release notes - _index.md # cascade: images: [/images/releasenote.webp] - 0.4.0.md - 0.4.0.zh.md ``` The section root pushes the type down the whole subtree and sets the behaviour that section shares: ```yaml {title="content/blog/_index.md"} --- title: Blog description: OINK engineering notes and release announcements type: blog icon: fa-solid fa-blog sidebar_root_for: self # the blog has its own sidebar tree cascade: type: blog feedback: false # posts do not ask "was this page helpful?" comments: true # but they do take comments --- ``` `params.ui.blog_section` (default `blog`) names where the blog root is. Rename the directory and either change that parameter or use `sidebar_root_for: self` as above. Blog sections are expanded by default in the sidebar and ordered by date, newest first; giving one post a `weight` pins it to the top. ## A post's front matter {#front-matter} ```yaml {title="content/blog/release/0.4.0.md"} --- title: Oink 0.4.0 — scenario components for a complete release workflow linkTitle: Oink v0.4.0 # short name in the sidebar and pager date: 2026-08-14 # publication date; decides ordering and grouping lastmod: 2026-08-14 description: >- Oink 0.4.0 delivers sequential reading and release surfaces, reusable landing pages, book publishing with stable references, and a keyboard-first site shell. author: The OINK maintainers categories: [Release] tags: [Oink, Release] --- ``` Where it differs from a documentation page: - `date` is required. It decides the post's place in the list, its year group and its RSS timestamp. A date in the future is not built by default; `hugo server -F` previews it. - `description` is rendered as a standfirst above the body, not only as a search snippet, so write it as a sentence for the reader. - `author` accepts inline Markdown, so `[Vonng](https://vonng.com)` works. For more than one author, a portrait, or a profile page, use the `authors` taxonomy below instead; the two do not interfere, and a post keeps rendering `author` wherever `authors` is absent. - The date display format comes from `params.time_format_blog` and can be set per language (this site uses `Monday, January 02, 2006` in English and `2006年1月2日` in Chinese). Bilingual posts are stored in pairs, keeping `date`, `author`, `weight` and `aliases` identical across the two. Titles, descriptions and tags are translated; commit IDs, version numbers, commands and URLs are not. ## Featured image {#featured-image} Each row on a list page or a tag page has a thumbnail on the left, resolved in this order, first match winning: 1. `images` in the post's front matter, first entry; 2. An image resource in the page bundle whose filename contains `featured` (it is cropped to a thumbnail, and the resource's own `byline` becomes its caption); 3. An `images` value inherited from an ancestor section's `cascade`, nearest first. A section-wide default uses Hugo's native `cascade` over the whole subtree; this site sets one for each of its two subsections: ```yaml {title="content/blog/release/_index.md"} cascade: images: [/images/releasenote.webp] ``` To drop the image on one post, write `images: []` in its front matter; to drop it for a whole subsection, put `images: []` in that level's `cascade`. The site-level `params.images` is unaffected — it feeds the share card only and is never rendered as a list thumbnail. ### On the article itself {#featured-image-article} By default the resolved image appears on list rows and in the social card, and the article itself shows nothing — write the hero by hand and it will disagree with the card sooner or later. `params.ui.featured_image` renders it from the same resolver instead: | Mode | What the article shows | | --- | --- | | `none` | Nothing. The theme default, so a site that renders nothing today renders exactly the same bytes | | `banner` | The image above the title in a fixed 16:9 figure, so a run of articles keeps one rhythm | | `wash` | The image behind the article header at a tenth of its opacity, masked to nothing before the text starts — the post takes a colour from its subject without spending any contrast on it | ```yaml {title="hugo.yml"} params: ui: featured_image: banner ``` The page key is `featured_image`, so a `cascade` on one subsection turns it on for that tree and a single post can opt out. A post with no image renders nothing in either mode, which is why a section can carry the switch for a run of posts that do not all have art. Neither mode adds a script or a bundle member. ```yaml {title="content/blog/release/_index.md"} cascade: featured_image: wash ``` ## List pages and pagination {#list} After the body of the section `_index.md`, the theme appends the post list: grouped by year ("Posted in 2026"), years newest first, each row showing the title, date, subsection, tags, thumbnail and the first 250 characters of the body as a summary. Pagination uses Hugo's native paginator, ten posts per page by default, adjusted in `hugo.yml`: ```yaml {title="hugo.yml"} pagination: pagerSize: 20 ``` The values and the remaining pagination options are in the [Hugo documentation](https://gohugo.io/configuration/pagination/). ### The card form {#list-cards} `params.ui.blog_index: cards` renders the same list as a grid of content cards instead of rows: a 16:9 crop of the post's image above the title, the date and subsection line, and a three-line summary. ```yaml {title="hugo.yml"} params: ui: blog_index: cards blog_index_columns: 3 ``` The choice is presentational only — year grouping, pagination and `manual_link` behave identically, and the row output is unchanged to the byte. The column count applies above the xl breakpoint; between md and xl the grid is two columns and below md it is one. Front matter `blog_index` on a blog root, or its `cascade`, sets it per section. Term and taxonomy pages keep the row list, and there is no reader-side switch between the two forms. Card images go through Hugo's `.Fill` whenever the resource can be processed, so a grid of posts does not download a full-size original per card. ## RSS {#rss} Which pages produce a feed is decided by `outputs`. Adding `RSS` to `section` gives every section its own feed: ```yaml {title="hugo.yml"} outputs: home: [HTML, markdown, LLMS] page: [HTML, markdown] section: [HTML, RSS, print, markdown] ``` Writing `outputs` at all replaces Hugo's defaults wholesale, so `RSS` has to be written back explicitly. Omitting it turns off the feed for that page kind, and the build does not complain. This site therefore has `/blog/index.xml` (the whole blog) and `/blog/release/index.xml` (release notes only). A section feed recursively includes every subsection's posts, so subscribing to `/blog/` covers everything. An individual post has no `.xml` of its own. Each language has its own feed at that language's route plus `index.xml`. The item limit is Hugo's `services.rss.limit`. On the blog root and its first-level subsection pages, the first action button beside the title row is the RSS link, so a reader need not assemble the address by hand. To drop feeds site-wide, turn the kind off with `disableKinds`, which is more thorough than removing `RSS` from each page kind: ```yaml {title="hugo.yml"} disableKinds: [RSS] ``` Components degrade to their static shape in a feed: disclosures are expanded and interactive controls are removed. The four-output rules are the same for blog posts as for documentation. ## Categories and tags {#taxonomy} `tags` and `categories` are Hugo's taxonomies, and the theme renders them as chips in the post header, a tag cloud in the right column, and a filter menu in the navbar. Enabling them, bilingual term labels, and switching them per content type are covered in [Taxonomies](/docs/customize/taxonomy/). ## Release notes {#release-notes} A versioned release announcement is an ordinary post, conventionally under `blog/release/`, with the version in `linkTitle` (`Oink v0.4.0`). For a download page with release cards, asset tables and checksums, see [Releases and downloads](/docs/write/releases/). ## Components in a post {#components} Callouts, tabs, code blocks, images and tables work exactly as on a documentation page; the syntax is in [Components](/docs/components/). Headings in a post body take explicit English `{#id}` anchors too. The four blocks at the end of a post — feedback, last modified, pager, comments — behave as on a documentation page; see [Writing pages](/docs/write/pages/#page-end). A blog usually turns feedback off and keeps comments. ## Authors and bylines {#authors} Declaring the taxonomy is the entire switch; the theme adds no parameter: ```yaml {title="hugo.yml"} taxonomies: category: categories tag: tags author: authors ``` A post then names its authors in order: ```yaml authors: [vonng, ada-example] ``` The article head renders portraits and linked names in exactly that order — the front matter sequence is both the set and the order — a list row renders the names, and the blog feed emits one `` per author per item beside the site-level `managingEditor`. Names are separated by a CSS gap rather than a connector word, because "and" is a per-locale decision and there are 32 locales. An author's profile is simply the term page, so there is no `data/authors` file to disagree with it: ```markdown {title="content/authors/vonng/_index.md"} --- title: Vonng description: Maintainer of OINK and Pigsty. images: [portrait.webp] --- The long introduction, rendered on the profile page under the name. ``` The display name is the term page's **link title** — `linkTitle` when it has one, `title` otherwise — so a profile can carry a full name and byline a short handle. `description` is the one-line introduction, the body the long one, and the avatar is whatever the featured-image resolver selects for that page — so `images:` and a bundled portrait follow the same rules an article's own image follows. A bilingual profile is an `_index.zh.md` beside it. A name a post uses but nobody gave a profile page still bylines: the link title, an initial, and a link to its archive. The 0.4 `author:` string is untouched wherever `authors` is absent, and neither form warns about the other. ## Series {#series} A series is a reading path through articles that each stand alone. Numbering, cross-references and aggregate output belong to [Book](/docs/write/book/); this is the lighter thing. Declaring the taxonomy is again the whole switch: ```yaml {title="hugo.yml"} taxonomies: series: series ``` An article names the series and may place itself in it: ```yaml series: [shell-internals] series_weight: 20 ``` It then carries a strip above its body naming the series, its position, the next part, and the whole list behind a `
` — no JavaScript, no bundle member. The term page `content/series//_index.md` is the introduction, and an `_index.zh.md` beside it makes the pair bilingual. Reading order is the theme's own, because a term page cannot supply one: Hugo's taxonomy weight reaches neither `Page.Weight` nor `GroupByParam`. Weighted members come first in ascending `series_weight`, the rest follow by ascending date, and `Path` breaks a tie. The strip and the term page read the same resolver, so they can never disagree about which article is part 2 — which also means a series term page lists oldest-first rather than newest-first, unlike every other term page. That is the feature. A member of several series shows one strip, for the first term it names. A series of one shows none. Neither `authors` nor `series` appears in the generic taxonomy chip row on an article, because each has a surface of its own. Name one in `params.taxonomy.page_header` to put it back. ## Share {#share} `params.ui.share` puts a share bar at the top of the page end. It is empty by default, so nothing renders until a site names its targets, in the order it wants them: ```yaml {title="hugo.yml"} params: ui: share: [x, bluesky, mastodon, reddit, hackernews, email, copy] ``` Sixteen targets are available: `x`, `bluesky`, `mastodon`, `facebook`, `linkedin`, `reddit`, `hackernews`, `telegram`, `whatsapp`, `line`, `pinterest`, `weibo`, `chatgpt`, `claude`, `email`, and `copy`. An unknown name warns and is dropped. Discord is absent on purpose: it publishes no share-intent URL at all, so `copy` stands in for it rather than the theme guessing at a private scheme. The page key is `share`, so a `cascade` scopes the bar to one tree, a page's own list replaces the inherited one, and `share: false` opts a single page out: ```yaml {title="content/blog/_index.md"} cascade: share: [x, bluesky, email, copy] ``` Only a regular page renders the bar — a list, a term page and the home page have no single thing being shared — and print, Markdown and RSS carry none of it. What the bar does not do is why it can ship in this theme at all. There is no share count, no platform SDK, no iframe, and no third-party script or stylesheet, which is what those three usually arrive as: one request per page to a company the reader never chose. Every target is a plain `` intent link carrying only the page's own permalink and title, with no campaign parameters, plus one local copy button. Nothing is fetched when the site builds or when the page loads; the only request a share can cause is the navigation the reader starts by clicking. A build with every target enabled passes `bin/check-output-security.py` with no `--third-party` allowance. `chatgpt` and `claude` hand that same build-time permalink to an assistant with a prompt asking it to read the page. They are not the "open in ChatGPT" / "open in Claude" entries of the page action menu, which the runtime rewrites at activation time to the live browser URL and which therefore stay behind `page_context_menu.assistant_links`. The copy button is the built-in `copy_link` action, which means the Command Palette carries it on every page of every site whether or not a bar is configured. ## Verify {#verify} ```bash hugo --printPathWarnings --panicOnWarning ``` It must reach `Total in …` with no ERROR and no WARN. Then confirm: 1. The post appears under the right year group at `/blog/`, with the date in the expected format; 2. `public/blog/index.xml` exists, contains the post, and its links are complete absolute addresses; 3. The thumbnail shows in the list (a missing one means none of the three featured-image sources matched); 4. Tag chips lead to the corresponding tag page. ## Related {#related} - [Writing pages](/docs/write/pages/) — how to write the body - [Page parameters](/docs/write/frontmatter/) — the full definition of `author`, `images` and the rest - [Organizing content](/docs/write/organize/) — directories and the sidebar - [Taxonomies](/docs/customize/taxonomy/) — tags and categories - [Releases and downloads](/docs/write/releases/) — version cards and asset tables --- Backlinks: - [Configuration](/docs/customize/config/) - [Repository links](/docs/customize/repository/) - [Taxonomies](/docs/customize/taxonomy/) - [Authoring](/docs/write/) - [Page parameters](/docs/write/frontmatter/) - [Releases and downloads](/docs/write/releases/) ================ Source: https://oink.pgsty.com/docs/write/book/index.md ================ # Books > Turn a directory tree into a book with `type: book`: chapter numbering, numbered figures and tables, cross-references, generated indexes and whole-book print. --- LLMS index: [llms.txt](/llms.txt) --- A book is a content tree of `type: book`: the directory decides chapter order, front matter decides chapter numbers, and figures, tables, equations and examples each carry a hand-written number and a stable anchor. Cross-references resolve in all four outputs, and the book's root page can generate a whole-book print HTML. Two prerequisites: the site's `markup.goldmark` has attribute lines and passthrough enabled (see [Components](/docs/components/)); and `params.ui.shell_types` still contains `book` (the theme default includes it). ## A book's directory {#layout} The book root is an ordinary Hugo section, chapters are its subdirectories, and sections are the pages inside a chapter. There is no second chapter list: the sidebar, the pager and the generated contents all read this one tree. ```filetree {title="content/handbook/, one book"} - content/handbook/ - _index.md # book home: type: book + cascade, holding book-toc and the indexes - ch01/ - _index.md # chapter 1 front page: book_number: 1 - install.md # section 1.x - bootstrap.md - ch02/ - _index.md # chapter 2: numbered with book_number, optionally marked draft - replication.md - failover.md - appendix.md # an unnumbered appendix, still in the sidebar and the reading order ``` Chapter numbers are written by hand: `book_number` displays exactly what you write, and the theme never numbers by directory order. The `num` on a figure, table, equation or example works the same way — a string the author controls (`2-1`, `5.3` and `A-2` are all valid), not an index computed at render time. Rearranging the tree therefore never shifts a number that has already been printed. ## The book home and chapter pages {#front-matter} The book root declares the type, cascades it to descendants, and explicitly requests the `print` output. That aggregate is expensive to build, so the theme does not turn it on for a consuming site: ```yaml {title="content/handbook/_index.md"} --- title: The PostgreSQL operations handbook type: book book_number: B cascade: type: book outputs: [HTML, print, markdown] --- ``` A book that is a section maps to Hugo's `section` output kind; `home` applies only when the book sits at the site root: ```yaml {title="hugo.yml"} outputs: section: [HTML, print, markdown] params: ui: sidebar_headings: 3 # project an h2–h3 heading tree under the current entry book_draft_banner: true # draft chapters get a localized banner above the body ``` A chapter page needs only its number and its order: ```yaml {title="content/handbook/ch02/_index.md"} --- title: Replication and failover book_number: 2 book_status: draft weight: 20 --- ``` `book_number` appears before the page title, in the sidebar and in the generated contents. `book_status: draft` is a visible editorial label and does not change Hugo's publication state: a draft chapter builds and publishes as usual. `sidebar_headings` accepts `false`, `true` (h2 only) or a maximum level from 2 to 4. Give every heading that will be referenced an explicit ID, such as `## Synchronous replication {#sync-replication}`: a generated slug is fine for navigation and unfit as a long-lived reference target. The full key definitions are in [Configuration](/docs/customize/config/) and [Page parameters](/docs/write/frontmatter/). ## Numbering: the native form {#numbering-native} Each of the four numbered kinds has a native form: one Markdown block followed immediately by an attribute line. On that line `num=` is the number, `#id` is the anchor, and `caption=` is a plain-text caption. ### Figures {#figure} An attribute line follows the image block. Omitting `#id` defaults it to `fig-`. ```markdown {title="Source"} ![The OINK release notes page](/images/releasenote.webp) {#book-release-note num="2-1" caption="The release notes page is also the single source of release facts." width=600 height=300} ``` ![The OINK release notes page](/images/releasenote.webp) {#book-release-note num="2-1" caption="The release notes page is also the single source of release facts." width=600 height=300} The native figure form requires the site to set `markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false`; otherwise the attribute line attaches to the paragraph and is ignored. The alternative text comes from the Markdown image itself and is never replaced by the caption. ### Tables {#table} An attribute line follows a pipe table, and the default ID is `tbl-`. ```markdown {title="Source"} | Isolation level | Dirty read | Non-repeatable read | Phantom read | | --- | --- | --- | --- | | Read Committed | Not possible | Possible | Possible | | Repeatable Read | Not possible | Not possible | Possible | | Serializable | Not possible | Not possible | Not possible | {#tbl-2-1 num="2-1" caption="Anomalies permitted at each PostgreSQL isolation level."} ``` | Isolation level | Dirty read | Non-repeatable read | Phantom read | | --- | --- | --- | --- | | Read Committed | Not possible | Possible | Possible | | Repeatable Read | Not possible | Not possible | Possible | | Serializable | Not possible | Not possible | Not possible | {#tbl-2-1 num="2-1" caption="Anomalies permitted at each PostgreSQL isolation level."} ### Equations {#equation} An attribute line follows a `$$` block, and the default ID is `eq-`. The number and caption sit on one non-wrapping line to the right of the formula, so a long caption squeezes the formula column until it becomes a horizontally scrolling region. Keep an equation caption short. ```markdown {title="Source"} $$ A = \frac{\mathrm{MTBF}}{\mathrm{MTBF} + \mathrm{MTTR}} $$ {#eq-2-1 num="2-1" caption="Availability from MTBF and MTTR."} ``` $$ A = \frac{\mathrm{MTBF}}{\mathrm{MTBF} + \mathrm{MTTR}} $$ {#eq-2-1 num="2-1" caption="Availability from MTBF and MTTR."} The native form depends on the site enabling Goldmark passthrough. Without it, use the `eq` shortcode below, which goes through local server-side KaTeX. ### Examples {#example} A code fence with `num=` and `caption=` is a numbered example, and the default ID is `eg-`. An `#id` written on the fence names the enclosing `
` — the reference target — rather than the code block itself. The caption is required: a lone caption is ignored and a lone number is dropped with a warning; strict publishing rejects the warning. A numbered example renders as one framed unit: the caption is the frame's header and the body sits inside it, and a body that is exactly one code block sits flush against the frame instead of drawing a second border. ````markdown {title="Source"} ```sql {num="2-1" caption="Daily write volume on the primary." #eg-2-1} SELECT date_trunc('day', ts) AS day, count(*) FROM pg_stat_statements_history GROUP BY 1 ORDER BY 1 DESC LIMIT 7; ``` ```` ```sql {num="2-1" caption="Daily write volume on the primary." #eg-2-1} SELECT date_trunc('day', ts) AS day, count(*) FROM pg_stat_statements_history GROUP BY 1 ORDER BY 1 DESC LIMIT 7; ``` ## Numbering: the shortcode form {#numbering-shortcodes} The four shortcodes `fig`, `tbl`, `eq` and `eg` render a `
` identical to the native form, register into the same target table, and sort by source position. Use them only where the native form cannot reach: an image that needs an outbound link, several tables under one number, a site without passthrough, or an example body made of several fences and prose. `fig` takes `src=` (it also accepts inner Markdown content, and the two are mutually exclusive) and additionally supports `link`, `alt`, `width`, `height`, `class`, and the migration alias `title`: ```markdown {title="Source"} {{< fig num="2-2" src="/images/docsy.webp" alt="The default Docsy shell" caption="OINK's upstream: the Docsy content model is still underneath." width="600" height="300" />}} ``` **Figure 2-2.** OINK\'s upstream\: the Docsy content model is still underneath\. ![The default Docsy shell](/images/docsy.webp) `tbl` wraps the label, the table, the caption and the anchor in one semantic figure: ```markdown {title="Source"} {{< tbl num="2-2" caption="How a numbered component appears in each of the four outputs." >}} | Output | Label | Anchor | | --- | --- | --- | | HTML | Visible | Stable | | Print | Visible | Stable | {{< /tbl >}} ``` **Table 2-2.** How a numbered component appears in each of the four outputs\. | Output | Label | Anchor | | --- | --- | --- | | HTML | Visible | Stable | | Print | Visible | Stable | `eq` hands its content to local server-side KaTeX, so it does not depend on passthrough: ```markdown {title="Source"} {{< eq num="2-2" caption="Connection pool saturation." >}}U = \frac{\lambda}{\mu \cdot c}{{< /eq >}} ``` **Equation 2-2.** Connection pool saturation\. $$ U = \frac{\lambda}{\mu \cdot c} $$ A bare `{{< eq >}}` with no parameters is the unnumbered display-maths escape hatch: it registers no target, cannot be reached by `xref`, and does not appear in the equation index. `eg` is a wrapping shortcode whose body renders under the page's Markdown policy, usually holding one or more fences: ````markdown {title="Source"} {{< eg num="2-2" caption="Bringing up a new replica with pg_basebackup." >}} ```bash pg_basebackup -h primary -U replicator -D /pg/data -Fp -Xs -P -R ``` {{< /eg >}} ```` **Example 2-2.** Bringing up a new replica with pg\_basebackup\. ```bash pg_basebackup -h primary -U replicator -D /pg/data -Fp -Xs -P -R ``` IDs must be unique within a page, and within one kind a number maps to exactly one ID. A duplicate warns and keeps the first registration; strict publishing rejects the warning, which names the line that claimed it first. > [!IMPORTANT] Footnotes cannot appear in a shortcode body > Hugo renders a shortcode body as its own Goldmark document, and footnotes are > page-level. A `[^label]` inside the body of `tbl`, `eg`, `fig`, `card`, `tab`, > `field` or `include` warns, naming the file, line, and label. Strict > publishing rejects the warning. > With the definition on the page, the reference would print literally as > `[^label]`; with the definition in the body, it would build a second footnote > list whose `fn:N` ids collide with the page's own. Neither belongs in > published output. > > A table or code block that needs footnotes uses the native form instead: a > table, image or fence carrying `{num=… caption=…}` keeps its content in the > page document, where a footnote numbers, links and backlinks like any other. > The rendered figure is the same either way, so this is usually a one-line > change. Footnote-shaped text in code — a `[^0-9]` character class in a > listing, or a code span — is left alone. ## Cross-references {#xref} A target on the same page can be reached with a plain Markdown link: [Table 2-1](#tbl-2-1) points at the isolation table above. The cost is that the label and the number are hand-written, so changing a number means finding them yourself. `xref` composes the label, the number and the anchor in one place, and works across pages and languages: ```markdown {title="Source"} See {{< xref fig="2-2" />}} and {{< xref eg="2-1" />}}; with an explicit anchor: {{< xref fig="2-1" anchor="book-release-note" />}}. ``` See [Figure 2-2](#fig-2-2) and [Example 2-1](#eg-2-1); with an explicit anchor: [Figure 2-1](#book-release-note). The rules: - At most one kind key (`fig`, `tbl`, `eq`, `eg`). The kind supplies the localized label (Figure / Table / Equation / Example) and derives the default anchor `-`. - `anchor=` overrides the derived anchor, for a target that wrote an explicit `#id`. - `page=` references another page through Hugo's page lookup in the current language, so the source never hard-codes a `/zh/` prefix. - Without a kind, both `anchor=` and inner link text are required: `{{< xref page="../ch01/install" anchor="sync-replication" >}}synchronous replication{{< /xref >}}`. - A reference may precede its target: nothing reads the registry at render time, so forward references are valid. A plain cross-page Markdown link is still a site URL inside the whole-book print. A reference that must also jump within the aggregate document is written as an `xref`. ## Indexes: contents and lists of figures {#indexes} Five index shortcodes walk the same book tree, triggering descendant content and aggregating what it registered. They usually sit on the book home (`_index.md`) or on a dedicated "list of figures" page. ```markdown {title="content/handbook/_index.md"} {{< book-toc depth=3 >}} ## List of figures {#lof} {{< book-figures >}} ## List of tables {#lot} {{< book-tables >}} ## List of equations {#loe} {{< book-equations >}} ## List of examples {#lox} {{< book-examples >}} ``` These five appear here as source only. They walk down from the navigation root the current page belongs to, so placing one in an ordinary documentation tree would list the whole docs tree as a book. For the real effect, read [Write Beautiful Docs](/book/) and inspect its [`content/book/_index.md`](https://github.com/pgsty/oink.pgsty.com/blob/main/content/book/_index.md) source. - `book-toc` takes a `depth` of 1 to 3: 1 lists chapters, 2 adds nested sections, 3 also projects each page's heading tree. `drafts=false` filters `book_status: draft` rows out of this generated list only, and does not affect publication. - `book-figures`, `book-tables`, `book-equations` and `book-examples` take no parameters. Each lists one kind, with entries like "Figure 2-1 — caption" linked to the stable ID. - In whole-book print, all of these links become in-document fragments. ## Sequential reading and drafts {#reading} The pager is on by default for the `docs`, `book` and `blog` types, and its order is a pre-order walk of the sidebar tree: a section index first, then its children by `weight`. Turn a whole type off with `params.ui.pager_types`, and a single page off with `pager: false`. ```yaml {title="hugo.yml"} params: ui: pager_types: [docs, book] ``` Entries hidden with `toc_hide`, `manual_link` link-only placeholders and `sidebar_divider` rows never become pager destinations. Besides the "draft" label in the sidebar, a draft chapter can carry a banner above its body: ```yaml {title="hugo.yml"} params: ui: book_draft_banner: true ``` The banner appears only on pages that are both `type: book` and `book_status: draft`, and its wording comes from the localization key `book_draft_notice`. ## Printing the whole book {#print} Once the book root has the `print` output, it generates a cover, a local table of contents, the root page's body and every descendant chapter in visible reading order, all inside one HTML document. Pages with `no_print: true`, link-only nodes, divider rows and hidden placeholders never become chapters. Inside the aggregate, the IDs of numbered components are preserved byte for byte. Markdown heading and footnote IDs within a page are prefixed with their source page to avoid collisions when several chapters share an anchor such as `summary` or each start with `fn:1`; generated links are rewritten to match. A page rendered alone as Print keeps the same page-local IDs as ordinary HTML — only a multi-page section or whole-Book aggregate adds the namespace. The output is print-oriented HTML. An opt-in `BookManifest` output records that same reading order as JSON, and the theme ships `bin/book-epub.py` and `bin/book-pdf.py`, which turn the manifest and the print HTML into EPUB and PDF. The switches themselves, and per-chapter print, are covered in [Print](/docs/customize/print/). ## Migrating an existing manuscript {#migrate} An existing manuscript usually expresses figure and table numbering with the site's own `figure` shortcode, bold pseudo-captions, and bare links to `#fig_*`. The theme repository ships a migration script that rewrites those legacy forms into `fig`, `tbl` and `xref` while preserving the public anchors already published. Pin the site to a released OINK version that includes the Book components first, then migrate the content. ```bash {title="Dry run: diff and report only, no files changed"} python3 ~/pgsty/oink/bin/migrations/book_figures.py \ --profile tpme \ --root /path/to/your-book \ --report /tmp/book-migrate.json > /tmp/book-migrate.diff ``` Four profiles cover the legacy conventions of three real manuscripts (DDIA contributes one each for v1 and v2), and each recognizes only the forms actually observed in them: | `--profile` | Legacy form it recognizes | | --- | --- | | `tpme` | A pseudo-h6 caption beside an image, a caption beside a table, and bare `/en/...#fragment` links | | `ddia-v2` | The site's own `figure` shortcode, classified by number into figure / table / code example | | `ddia-v1` | A bare image with an adjacent bold numbered caption, with the ID derived from the image filename | | `pg-internal` | A bold or italic "Figure N" caption in Chinese or English next to an image, and a numbered table caption next to a table | | Option | What it does | | --- | --- | | `--profile` | Required; one of the four values above | | `--root` | Required; the consuming repository's root | | `--path` | Restricts to a file or directory under `--root`; repeatable. The default scans the whole content tree | | `--write` | Applies the rewrite. The default is a dry run that writes nothing | | `--no-diff` | Suppresses the diff while keeping the summary and the report | | `--report` | Writes the machine-readable JSON report | {.fields} The diff goes to standard output, the summary to standard error, and the report carries `files_scanned`, `files_changed`, `counts`, `skipped` and `idempotent`. The script rewrites only targets it can determine uniquely: where the number is unclear, the caption is not unique, or the marker form is unrecognized, the text is left as it stands and recorded in `skipped` for a human. Bold text, inline code and formulas inside a legacy caption degrade to plain text, because a Book caption is plain text by contract. After reviewing the diff, apply it on a dedicated branch and run a second pass to confirm idempotency: ```bash {title="Apply, then verify idempotency"} python3 ~/pgsty/oink/bin/migrations/book_figures.py \ --profile tpme --root /path/to/your-book --write \ --report /tmp/book-migrate-written.json python3 ~/pgsty/oink/bin/migrations/book_figures.py \ --profile tpme --root /path/to/your-book --no-diff \ --report /tmp/book-migrate-second.json ``` The second report should read `files_changed: 0`, an empty `counts` and `idempotent: true`; the script signals idempotency with exit code 0. The profiles recognize only the legacy forms actually observed in those three manuscripts. Where a manuscript's conventions fall outside the four, the script does not apply and the rewrite is manual, following [Numbering: the native form](#numbering-native). The theme repository's `bin/check-book-migrations.py` covers all four profiles with a dry-run and an idempotency check. ## Verify {#verify} 1. The build is warning-free: `hugo --printPathWarnings --panicOnWarning`. A malformed number, a duplicate ID and a missing caption all fail here. 2. The page should show a localized label such as "Figure 2-1", clickable `xref` links, and anchors that land correctly. 3. Compare the chapter order across all four places: sidebar, pager, `book-toc` and whole-book print. 4. Check the Markdown output: `curl -s http://localhost:1313/handbook/ch02/index.md`. The shortcode form should degrade to `**Figure 2-2.** caption` plus the original body, and the native form should keep its source block and attribute line as they are. 5. Run the anchor check from the theme repository against the build output: ```bash python3 ~/pgsty/oink/bin/check-book.py --site-public public ``` It verifies that every reference's target anchor exists, that kind and number agree, that page-local IDs are unique, and that a numbered image has alternative text worthy of its caption. ## Book shortcode parameters {#reference} | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `num` | string | — | Required (except for the bare `eq` form). Matches `[0-9A-Za-z.-]+` and must be quoted | | `id` | string | `fig-` / `tbl-` / `eq-` / `eg-` | Matches `[A-Za-z][A-Za-z0-9_.:-]*` and is preserved byte for byte | | `caption` | plain text | empty | Required for `eg`; optional for `fig`, `tbl` and `eq`. Not Markdown | | `class` | class token | — | Appended to the `
`; requires `num` | | `src` | image path | — | `fig` only. Mutually exclusive with inner content, and follows the shared image resolution order | | `link` `alt` `width` `height` | — | — | `fig` only. Width and height are positive integers | | `title` | plain text | — | `fig` only. A migration alias for `caption`, mutually exclusive with it | {.fields meta="type default"} `xref`: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `fig` `tbl` `eq` `eg` | number string | — | At most one. Supplies the localized label and derives the anchor | | `anchor` | ID | derived from kind and number | Required when no kind is given, together with inner link text | | `page` | page reference | current page | Resolved through page lookup in the current language; a missing page warns and renders text without a link | {.fields meta="type default"} `book-toc`: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `depth` | integer 1–3 | `2` | 1 chapters / 2 with nested sections / 3 with the heading tree | | `drafts` | boolean | `true` | `false` filters draft chapters out of the generated list | {.fields meta="type default"} `book-figures`, `book-tables`, `book-equations` and `book-examples` take no parameters. ## Limits {#limits} - There is no automatic numbering. Chapter, figure and table numbers are all written by hand; changing one is a deliberate edit, not a side effect of a build. - The attribute line must touch its block, with no blank line between. An attribute line a tool like Prettier has moved fails silently, and the figure degrades to a plain image. - `book_kind` and `book_part` are metadata keys the contract acknowledges but the current templates do not render. The ones with a visible effect are `book_number` and `book_status`. - The index shortcodes trigger descendant content rendering, which noticeably lengthens the build on a very large tree. The same reason is why whole-book `print` has to be requested explicitly. - A footnote reference cannot appear in a shortcode body; it warns and names the native form to use instead, and strict publishing rejects it — see [Numbering: the shortcode form](#numbering-shortcodes). - Packaging is opt-in and runs outside the build. `BookManifest` plus `bin/book-epub.py` / `bin/book-pdf.py` produce EPUB and PDF, but no Hugo build emits either file on its own, and typeset pagination, font embedding and index compilation remain outside the contract. ## Related {#related} - [Organizing content](/docs/write/organize/) — how the tree becomes the sidebar and the reading order - [Images](/docs/components/image/) — captions, sizing, zoom and image processing - [Tables](/docs/components/table/) — table attribute lines and full-width tables - [Math](/docs/components/math/) — KaTeX and passthrough configuration - [Print](/docs/customize/print/) — per-chapter and whole-book print --- Backlinks: - [DDIA](/case/ddia/) - [pgint.vonng.com](/case/pg-internal/) - [TPME](/case/tpme/) - [Highlights](/docs/about/features/) - [Code Blocks](/docs/components/code/) - [Draw.io](/docs/components/drawio/) - [Images](/docs/components/image/) - [Math](/docs/components/math/) - [Tables](/docs/components/table/) - [Configuration](/docs/customize/config/) - [Print](/docs/customize/print/) - [Consumer evidence](/docs/design/research/consumer-evidence/) - [Authoring](/docs/write/) - [Blog posts](/docs/write/blog/) - [Page parameters](/docs/write/frontmatter/) ================ Source: https://oink.pgsty.com/docs/write/releases/index.md ================ # Releases and downloads > Record versions, tags, archive links, checksums and install commands as local facts, then let release cards, asset tables, download blocks and index pages derive from that one record. --- LLMS index: [llms.txt](/llms.txt) --- OINK keeps release facts in two local places: a `release_url` in a page's front matter names the GitHub release this page is about, and `data/download/.yaml` says how to install it. Release cards, asset tables, download blocks and index pages all derive from those two. Nothing contacts GitHub at build time, and nothing claims a tag or an asset already exists. > [!NOTE] This page carries demonstration release facts > Its front matter holds a `release_url` (OINK v0.4.0), and the card, asset > table and download block below are really rendered. The checksums and asset > filenames are fabricated: the URLs are derived locally from the repository and > the tag, the files they point at do not exist in any real release, and the > hashes here must not be used to verify anything. ## Components and where the facts come from {#overview} | What you want | What renders it | Facts come from | | --- | --- | --- | | A version summary card (tag, date, archives, repo) | `release-card` | The page's `release_url` | | A checksum asset table | The `checksums` fence / `release-assets` | `sha*sum` lines in the body | | A multi-channel download block | `download` | `data/download/.yaml` | | A chronological release index | `layout: releases` | Each page's `release_url`, or its title | ## The page owns the release facts {#release-facts} One key in the release page's front matter is the whole record — the exact-tag GitHub release URL: ```yaml {title="content/blog/release/0.4.0.md"} release_url: https://github.com/pgsty/oink/releases/tag/v0.4.0 ``` The owner, the project, and the tag come out of the URL, and the date is the page's own `date`. A value that is not an exact-tag GitHub release URL warns and skips the release block — and fails a `--panicOnWarning` build. The 0.5 `release` map (product / version / repo / tag / date / prev / checksums) and its string shorthand are gone; a page still carrying one gets a warning that names `release_url`. Put a parameterless shortcode wherever the summary belongs; the call itself accepts no facts: ```markdown {title="Source"} {{< release-card >}} ``` **v0\.4\.0 · 0001-01-01** - [View release](https://github.com/pgsty/oink/releases/tag/v0.4.0) - [Source · tar\.gz](https://github.com/pgsty/oink/archive/refs/tags/v0.4.0.tar.gz) - [Source · zip](https://github.com/pgsty/oink/archive/refs/tags/v0.4.0.zip) - [pgsty\/oink](https://github.com/pgsty/oink) The card carries the four links the URL alone can name — the release, both source archives, and the repository — all derived locally. Checksum files belong in the asset table below a note, and comparisons live on GitHub. ## The release index page {#release-index} A section can switch to the release index layout. It lists every regular page of the section, newest first — the page date, with the tag's version as the tiebreaker inside one day (SemVer precedence, with a deterministic fallback for non-SemVer tags): ```yaml {title="content/blog/release/_index.md"} --- title: Releases layout: releases --- ``` An entry whose `release_url` parses reads as `project tag` — `oink v0.4.0` — over the page's description; a page without one keeps its own title, so a plain note between releases is a plain entry, not a warning. The 0.5 `release_products` filter and `release_group_by_product` grouping are gone; naming either warns. This site's [Releases](/blog/release/) currently uses the ordinary blog list. Switch to `layout: releases` when a strict chronology is wanted. ## Checksum assets {#assets} The `checksums` fence is the native form of a checksum table, holding the verbatim output of a `sha*sum` command: ````markdown {title="Source"} ```checksums 1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4 oink-0.4.0-linux-amd64.tar.gz 7b3d9e0c145a8f26d0b7e93c48156aa2f0d9c7b31e846a5029df1b6c7a3e8250 *oink-0.4.0-darwin-arm64.tar.gz ``` ```` ```checksums 1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4 oink-0.4.0-linux-amd64.tar.gz 7b3d9e0c145a8f26d0b7e93c48156aa2f0d9c7b31e846a5029df1b6c7a3e8250 *oink-0.4.0-darwin-arm64.tar.gz ``` Only two line shapes are accepted: `` and `*`. Blank lines and lines starting with `#` are ignored. The hash length decides the algorithm (MD5 / SHA-1 / SHA-256 / SHA-512), and one block holds one algorithm. A malformed line warns and is skipped with its line number; strict publishing rejects the warning. A filename must be a single path segment. The type, operating system and architecture badges are inferred from the filename; they are decoration, and nothing shows when the inference fails. The base for asset links: with `release_url` front matter on the page it is derived as `https://github.com//releases/download//`; a page without release facts must write `base=` explicitly. Having both is an error. ````markdown {title="a page with no release front matter"} ```checksums {base="https://repo.pigsty.io/oink/v0.4.0/" algo="sha256"} 1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4 oink-0.4.0-linux-amd64.tar.gz ``` ```` `release-assets` is the shortcode form of the same parser and renderer. It adds one thing the fence lacks, `src=`, so the checksum file itself can be committed as a page resource or a global asset (`src` and inner content are mutually exclusive); `group="auto"` groups by platform and architecture: ```markdown {title="Source"} {{< release-assets group="auto" >}} 5a0c7d1e93b4826f0ad35c9e17b6402d8f1c95ae63d70b28c4e19a5f38207db6 oink-0.4.0-1.el9.x86_64.rpm c93f16a8d052b7e41ac68d3907b25fe0a41d8c7362b95e0187ac4d63f9520ea8 oink-0.4.0-1.el9.aarch64.rpm {{< /release-assets >}} ``` ### `.rpm` | File | Checksum | | --- | --- | | [oink\-0\.4\.0\-1\.el9\.x86\_64\.rpm](https://github.com/pgsty/oink/releases/download/v0.4.0/oink-0.4.0-1.el9.x86_64.rpm) | SHA-256 · `5a0c7d1e93b4826f0ad35c9e17b6402d8f1c95ae63d70b28c4e19a5f38207db6` | | [oink\-0\.4\.0\-1\.el9\.aarch64\.rpm](https://github.com/pgsty/oink/releases/download/v0.4.0/oink-0.4.0-1.el9.aarch64.rpm) | SHA-256 · `c93f16a8d052b7e41ac68d3907b25fe0a41d8c7362b95e0187ac4d63f9520ea8` | In HTML the hash is shown truncated while the full value stays in the accessible name and in what the copy button copies, and that button comes from a local runtime loaded on demand. With JavaScript disabled it is still a complete linked table. Print expands the full hash without controls, and Markdown and RSS emit a pipe table of full hashes. ## Download channel data {#download-data} How to install belongs to the product rather than to one release, so it lives in `data/download/.yaml`. This site's real record is `data/download/prd5.yaml`: ```yaml {title="data/download/prd5.yaml"} version: 0.4.0 repo: pgsty/oink published: true channels: - id: script kind: rolling title: Install script title_zh: 安装脚本 icon: fa-solid fa-bolt note: The rolling channel deliberately contains no version interpolation. note_zh: 滚动渠道刻意不插入版本号。 steps: - title: Install title_zh: 安装 code: curl -fsSL https://repo.example.org/oink/install | bash lang: bash - id: source kind: pinned title: Source archive title_zh: 源码归档 icon: fa-solid fa-code-branch url: https://github.com/pgsty/oink/archive/refs/tags/${tag}.tar.gz steps: - title: Clone the tag title_zh: 克隆标签 code: git clone --branch ${tag} https://github.com/pgsty/oink.git lang: bash - id: assets kind: pinned title: Release assets title_zh: 发布资产 icon: fa-solid fa-box-open checksums: | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa oink-0.4.0.tar.gz ``` The record has exactly five top-level fields — `version`, `repo`, `tag`, `published`, `channels`. An extra key warns and skips the record; strict publishing rejects the warning. `version` may be omitted here and supplied by the site's `params.version` instead. | Field | Type | Default | Description | | --- | --- | --- | --- | | `version` | string | site `params.version` | Missing in both places warns and skips the block | | `repo` | `owner/name` | — | Required once a pinned channel has a link or assets | | `tag` | string | `v{version}` | URL-safe characters only | | `published` | boolean | `true` | `false` means the immutable release does not exist yet | | `channels` | array | — | Must be non-empty | {.fields meta="type default"} Each channel: | Field | Type | Default | Description | | --- | --- | --- | --- | | `id` | `^[a-z][a-z0-9-]*$` | — | Unique within the record; used as the anchor | | `kind` | `rolling` \| `pinned` | — | Decides whether release facts may be interpolated | | `title` | localized string | — | Must resolve to a non-empty value | | `note` | localized string | — | One line of explanation under the channel | | `icon` | Font Awesome class pair | — | For example `fa-solid fa-bolt` | | `url` | http(s) or a site path | — | Interpolatable on `pinned` only | | `steps[]` | `title` / `code` / `lang` | `lang: text` | Code steps go through OINK's enhanced code renderer | | `checksums` | `sha*sum` text | — | `pinned` only; mutually exclusive with `checksums_src` | | `checksums_src` | asset path | — | Reads the checksum file as a Hugo asset | {.fields meta="type default"} Two rules: - Localization resolves by suffix: `_` → `_` → ``. A Chinese site resolves `title_zh_cn`, then `title_zh`, then `title`. camelCase aliases are not accepted. - Only a pinned channel's `url` and `steps[].code` interpolate `${version}` and `${tag}`. A rolling channel refuses interpolation, so a stable install command is never bound to one version. Titles and notes never interpolate. ## Rendering the download block {#download-shortcode} `download` takes exactly one positional parameter, the data key: ```markdown {title="Source"} {{< download "prd5" >}} ``` ## Install script The rolling channel deliberately contains no version interpolation. **Install** ```bash curl -fsSL https://repo.example.org/oink/install | bash ``` ## Source archive [Source archive](https://github.com/pgsty/oink/archive/refs/tags/v0.4.0.tar.gz) **Clone the tag** ```bash git clone --branch v0.4.0 https://github.com/pgsty/oink.git ``` ## Release assets | File | Checksum | | --- | --- | | [oink\-0\.4\.0\.tar\.gz](https://github.com/pgsty/oink/releases/download/v0.4.0/oink-0.4.0.tar.gz) | SHA-256 · `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | In HTML it renders a row of anchor chips plus one section per channel; code steps reuse the enhanced code block and its on-demand copy runtime, and a checksum channel reuses the asset table above. Print statically expands the same content, Markdown emits the titles, source fences and full hashes, and RSS omits the component. Before the tag is cut and the assets are uploaded, mark the record unpublished: ```yaml {title="data/download/.yaml"} published: false ``` Rolling channels keep working. Pinned channels become an unclickable "pending release" state, omit the pinned commands, and disable asset links and copy controls. Flip the switch once the tag and the assets resolve, rather than writing a guessed link into the prose first. The same record can also feed a landing page's `download` section, with no second version model — see [Home and landing pages](/docs/customize/home/). ## How this relates to blog release notes {#release-notes} The two have different jobs: - A release note in the blog (this site keeps them in `content/blog/release/`) is the narrative: what changed, how to upgrade, what breaks. Its front matter carries `release_url`, and a `release-card` can sit at the top. How to write one is in [Blog posts](/docs/write/blog/). - The download data is the operation: which channel, which command, which hash. It is decoupled from the version number, so an upgrade edits one place. The order for a release: update `version` in `data/download/.yaml` → write a new `content/blog/release/.md` with its `release_url` → flip `published` to `true` once the tag and assets are in place. ## Verify {#verify} 1. The build is warning-free: `hugo --printPathWarnings --panicOnWarning`. A malformed hash line, mixed algorithms, a missing `base` and a misspelled channel field all fail here. 2. On the page: the card's tag and date match the repository, and every asset row opens a real download URL. 3. Check the hashes against the actual artifacts by hand: the component only lays them out and verifies nothing. 4. Confirm the hashes are complete in non-HTML output: ```bash curl -s http://localhost:1313/docs/write/releases/index.md | grep -c '^| ' ``` 5. Rehearse with `published: false` first and switch to `true` only once the tag and assets really exist; test each language and a subpath deployment. ## Related {#related} - [Blog posts](/docs/write/blog/) — where release notes live and how they are ordered - [Code Blocks](/docs/components/code/) — code rendering and copying inside download steps - [Home and landing pages](/docs/customize/home/) — the landing `download` section - [Configuration](/docs/customize/config/) — `params.version` and the related site parameters - [Upgrade](/docs/admin/upgrade/) — how a consuming site tracks theme versions --- Backlinks: - [pgsty.pro](/case/pgsty-pro/) - [sow.pgsty.com](/case/sow/) - [Highlights](/docs/about/features/) - [ECharts](/docs/components/echarts/) - [Home and landing pages](/docs/customize/home/) - [Versions](/docs/customize/versions/) - [Authoring](/docs/write/) - [Blog posts](/docs/write/blog/) - [Page parameters](/docs/write/frontmatter/) ================ Source: https://oink.pgsty.com/docs/write/openapi/index.md ================ # API reference pages > Put an OpenAPI specification on the site and render it as a browsable API reference with the bundled Swagger UI or Redoc, without touching a CDN. --- LLMS index: [llms.txt](/llms.txt) --- An API reference page is one OpenAPI specification plus one shortcode. The Swagger UI and Redoc runtimes ship with the theme (versions 5.32.13 and 2.5.3 respectively, per the repository's `VENDOR.json`), load only on a page that uses them in its HTML output, and reach no external service at build time or in the browser. Swagger UI's online validator is pinned off (`validatorUrl: null`), so a published API page never sends its specification address anywhere. Three steps: put the specification file under `static/`, create a page with the shortcode, and change the page `type` to `swagger` if it needs the dedicated shell. ## Where the specification file goes {#spec-file} The specification goes under `static/`, is published unchanged at the site root, and both shortcodes then receive a URL the browser can fetch: ```filetree {title="where the specification lives"} - static/ - openapi/ - docs-demo.yaml # published as /openapi/docs-demo.yaml - content/ - docs/ - write/ - openapi.md # this page ``` Do not put the specification beside the page. `redoc` looks for a file of that name in the content directory and builds a URL from it, but a `.yaml` in the content directory is a page resource, and Hugo publishes one only when it is referenced or processed. `redoc` builds a URL without referencing the resource, so the browser gets a 404. A remote specification (starting `https://…`) is accepted by both shortcodes, but that is a network dependency, and it exposes the reader's metadata to that host. Intranet deployments and sites with a CSP should use a same-origin specification. Only `http` and `https` are accepted: any other scheme, a protocol-relative `//host`, or an empty value warns and the shortcode renders nothing. The examples below use the real `/openapi/docs-demo.yaml`, a demonstration cluster-management API with no reachable server behind it. ## Swagger UI {#swaggerui} `swagger` has one named parameter, `src`, whose value is a URL from the site root. It passes through the theme's URL validation, so a subpath deployment resolves correctly: ```markdown {title="Source"} {{< swagger src="/openapi/docs-demo.yaml" >}} ``` It renders a container with `class="td-swagger-ui"` carrying the specification address in `data-td-spec-url`; a single cacheable `js/chunks/swagger-init.js` mounts every container on the page. The container ID is derived from the page address and the shortcode's ordinal (`td-swagger--`), so one page can hold several. This page shows the source without rendering Swagger UI: the markup it generates carries axe WCAG AA violations (the server dropdown has no accessible name, and the version stamp is a scrollable region without keyboard access), and this site's accessibility gate requires zero violations per page. The Redoc below is really rendered — but be aware that both widgets are scoped out of that gate, because Redoc's operation descriptions have their own colour-contrast defect. Neither is a fully accessible interface; see [Limits](#limits). ## Redoc {#redoc} `redoc` takes exactly one positional parameter, the specification path. A second parameter warns and the shortcode renders nothing. ```markdown {title="Source"} {{< redoc "openapi/docs-demo.yaml" >}} ``` [OpenAPI specification](https://oink.pgsty.com/openapi/docs-demo.yaml) Path resolution has three branches, in order: anything starting with `http` is a remote URL; a file of that name found in the content directory yields `baseURL + page directory + filename`; otherwise it is `baseURL + the path as written`. So a `redoc` path must not begin with a slash — `/openapi/…` would produce a doubled slash such as `https://example.com//openapi/…`. Unlike `swagger`, it generates an absolute URL based on `baseURL`. The theme pins five attributes — `hide-hostname`, `hide-logo`, `suppress-warnings`, `lazy-rendering`, `native-scrollbars` — and hides the Redocly brand mark with CSS. Redoc's remaining attributes are not exposed to authors; a site that needs them overrides `layouts/_shortcodes/redoc.html`. ## The dedicated page shell {#shell} API reference pages tend to be wide and long, which is what the `swagger` page type is for: ```yaml {title="content/api/_index.md"} --- title: Cluster management API type: swagger page_width: wide cascade: type: swagger --- ``` `swagger` is one of the theme's default shell types (`params.ui.shell_types` defaults to `[docs, book, blog, swagger]`, and a site that overrides the list needs to keep it). It differs from the `docs` shell in exactly two ways: an extra `td-swagger` class on `` for styling hooks, and no version banner. Sidebar, table of contents, breadcrumbs, pager and page end all behave normally. Shells and page width are covered fully in [Layouts and page types](/docs/customize/layout/). ## Output {#outputs} | Output | What appears | | --- | --- | | HTML | The full interactive Swagger UI / Redoc; the runtime loads on demand from local files, with no CDN, and only in this output | | Print | A labelled static link showing the specification's address; neither runtime loads | | Markdown | A plain Markdown link, `[OpenAPI specification](/openapi/example.yaml)`; it does not degrade into an endpoint list | | RSS | The same plain link | Outside HTML an API reference is a pointer, not a reference. To put endpoint information into print or agent output as well, describe the key endpoints in prose on the same page; body text outside the shortcode survives intact in all four outputs. ## Limits {#limits} - Both components derive their container ID from the page address and the shortcode's ordinal, so several on one page never collide. - The two can coexist on one page, but the page becomes long and its HTML output loads both runtimes. Pick one for a production site. - Neither interface is fully accessible, and both come from upstream distributions the theme does not rewrite. Swagger UI's markup has axe WCAG AA violations (`select-name`, `scrollable-region-focusable`); Redoc's operation descriptions fail AA colour contrast. This site excludes `.td-swagger-ui` and `.td-redoc` from its zero-violation gate for that reason — a site with such a gate has to do the same, and should say so rather than assume either widget passes. - `redoc` accepts no attribute parameter: a second positional argument warns and the shortcode renders nothing. - A `redoc` path must not start with `/`, or the URL gains a doubled slash. - The specification must be fetchable by the browser: put it in `static/` and confirm the file exists under `public/` after a build. - There is no mock server: Swagger UI's "Try it out" makes a real request to whatever `servers` names, and the address in the sample specification is not reachable. ## Verify {#verify} 1. The build is warning-free: `hugo --printPathWarnings --panicOnWarning`. 2. The specification really was published: `ls public/openapi/docs-demo.yaml`, or open `http://localhost:1313/openapi/docs-demo.yaml`. 3. Endpoints expand on the page and their schemas appear; the browser console shows no 404 and no cross-origin error. 4. Reload once with the network off: the runtimes are local, and with a same-origin specification the interface should still appear. ## Related {#related} - [Writing pages](/docs/write/pages/) — page front matter and body basics - [Layouts and page types](/docs/customize/layout/) — `shell_types`, page width and the sidebar - [AI-agent support](/docs/customize/agents/) — why a component that is interactive only in HTML needs prose beside it - [Code Blocks](/docs/components/code/) — the lighter alternative of request / response examples instead of a whole UI --- Backlinks: - [Highlights](/docs/about/features/) - [Deploy](/docs/admin/deploy/) - [Layouts and page types](/docs/customize/layout/) - [Print](/docs/customize/print/) - [Authoring](/docs/write/) ================ Source: https://oink.pgsty.com/docs/components/index.md ================ # Components > Every component available for writing, one page each, examples from the simplest upwards, with the parameter table at the end. --- LLMS index: [llms.txt](/llms.txt) --- This section answers one question: how do I write this component in Markdown? Every page has the same shape — the shortest example, progressively richer examples, the output matrix, the parameter table, the limits. For syntax at a glance, use the cheatsheet below. ## Two forms {#two-forms} A component's first form is Markdown itself: blockquotes, lists, tables, images, fences — plus a single `{…}` attribute line right after them. The native form stays readable on GitHub and in any Markdown editor, and the Markdown output keeps the source rather than the rendered HTML. Whatever the native form cannot express is a shortcode: tabs in running text, parameter tables with block-level descriptions, cards with icons and badges, terminal recordings. Five rules cover them: - Every shortcode is written `{{< name >}}`. Only `{{% steps %}}` uses the `%` delimiter, because its body is page-level Markdown. - Nested names (`tab`, `card`, `field`) are valid only inside their parent. - A bad author parameter never degrades silently. An ordinary preview warns, names the source position, and uses the documented fallback or omits the unsafe part; a publishing build with `--panicOnWarning` fails on that warning. - Public string parameters (captions, labels, titles) are plain text and are not parsed as Markdown. Only bodies are Markdown: `tab`, `card` and `field` bodies, files pulled in by `include`, and the Book `fig` / `tbl` / `eg` bodies. - A component the page never used ships no runtime. HTML references only the stable capability chunks the page actually needs; print, Markdown and RSS load no interactive runtime. ## Site prerequisites {#prerequisites} Components depend on three Goldmark settings. OINK Starter provides them already configured; copy the snippet when starting from scratch: ```yaml {title="hugo.yml"} markup: goldmark: renderer: unsafe: true # keep HTML that content emits parser: attribute: block: true # enable {…} attribute lines wrapStandAloneImageWithinParagraph: false # standalone images are not wrapped in

``` - `renderer.unsafe: true` — Goldmark drops raw HTML in content by default; with it off, HTML nested inside component bodies disappears. - `parser.attribute.block: true` — the master switch for attribute lines. With it off, `{.steps}` and `{caption="…"}` are just a line of text. - `parser.wrapStandAloneImageWithinParagraph: false` — a standalone image is no longer wrapped in `

`, so it can become a captioned figure and an attribute line can follow it. A few components have their own prerequisites: mathematics needs Goldmark passthrough, PlantUML and Draw.io need a rendering server you run yourself. Each page says so. The complete set of configuration keys is in [Configuration](/docs/customize/config/). ## Cheatsheet {#cheatsheet} Values in the *Form* column: native = Markdown syntax plus an attribute line; fence = a fenced block with a language tag; shortcode = `{{< … >}}`. The *Runtime* column says whether the component ships JavaScript to the page. | Component | In one line | Shortest form | Form | Runtime | | --- | --- | --- | --- | --- | | [Callouts](/docs/components/callout/) | Separate prerequisites, warnings and asides from the prose | `> [!NOTE]` | native | none | | [Images](/docs/components/image/) | Captions, sizing, zoom, numbering and build-time processing | `![alt](oink.webp)` | native | site switch | | [Code Blocks](/docs/components/code/) | Highlighting, titles, copy, folding, linkable lines | ```` ```sh ```` | fence | per page | | [Tabs](/docs/components/tabs/) | One thing, several platforms or languages | attribute `{tab="Linux"}` | native + shortcode | per page | | [Tables](/docs/components/table/) | Plain tables plus full-width, matrix, caption and numbering | `{.full-width}` | native | none | | [Fields](/docs/components/fields/) | Parameter lists with type / required / default chips | `{.fields meta="type default"}` | native + shortcode | none | | [Steps](/docs/components/steps/) | A procedure with an order | `{.steps}` | native + shortcode | none | | [Cards](/docs/components/cards/) | A set of parallel destinations | `{.cards}` | native + shortcode | none | | [FileTree](/docs/components/filetree/) | Directory structure with an aligned comment column | ```` ```filetree ```` | fence | per page | | [Math](/docs/components/math/) | KaTeX inline and display formulas | `$$ … $$` | native | per page | | [Mermaid](/docs/components/mermaid/) | Flowcharts, sequence diagrams, Gantt charts | ```` ```mermaid ```` | fence | per page | | [PlantUML](/docs/components/plantuml/) | UML diagrams; needs a rendering server | ```` ```plantuml ```` | fence | site switch | | [Markmap](/docs/components/markmap/) | A Markdown outline becomes a mind map | ```` ```markmap ```` | fence | site switch | | [Draw.io](/docs/components/drawio/) | Diagrams that stay editable; needs a server | `![alt](arch.drawio.svg)` | native | site switch | | [ECharts](/docs/components/echarts/) | Declarative statistical charts | ```` ```echarts ```` | fence | per page | | [Infographic](/docs/components/infographic/) | AntV infographics | ```` ```infographic ```` | fence | per page | | [Gallery](/docs/components/gallery/) | A set of images sharing one zoom dialog | ```` ```gallery ```` | fence | site switch | | [Badge](/docs/components/badge/) | Inline status markers | `{{< badge text="Beta" >}}` | shortcode | none | | [Kbd](/docs/components/kbd/) | Key names and chords | `{{< kbd "Ctrl" "K" >}}` | shortcode | none | | [Includes](/docs/components/include/) | Pull in files, print site parameters, drop build-time notes | `{{< include file="parts/x.md" >}}` | shortcode | none | | [Asciinema](/docs/components/asciinema/) | Terminal recordings | `{{< asciinema file="images/x.cast" >}}` | shortcode | per page | Four notes on the *Runtime* column: - A code block loads `code-block.js` only when a block on the page has a copy or fold control; a file tree loads `filetree.js` only when the tree has a comment column, which is the runtime that drags the split. - Images and galleries share one zoom dialog runtime. It needs `ui.image_zoom` on for the site and at least one eligible image on the page. - Mathematics is rendered to HTML and MathML by KaTeX at build time. The page gains a KaTeX stylesheet and its fonts, and no script. - Draw.io loads only on pages whose rendered content contains PNG or SVG candidates, then inspects each distinct image URL once. Every component has a defined shape in all four outputs — HTML, print, Markdown and RSS. See the *Output* section on each page. --- Section pages: - [Callouts](/docs/components/callout/): Write notes, warnings and collapsible asides — with colour, icon and title — as `> [!NOTE]` blockquotes, no shortcode involved. - [Images](/docs/components/image/): Plain Markdown image syntax plus one attribute line gives you captions, sizing, zoom, links, numbering and Hugo image processing. - [Code Blocks](/docs/components/code/): A plain Markdown fence plus one attribute line gives you a filename title, exact copy, line numbers, highlighting, wrapping, folding and linkable lines. - [Tabs](/docs/components/tabs/): A `{tab=}` attribute on adjacent fences or tables makes a tab set; add a group and it becomes linkable, synchronized and remembered. - [Tables](/docs/components/table/): A plain GFM table plus one attribute line becomes a captioned table, a compatibility matrix, a field list, a numbered table or a tab set; wide tables scroll on their own. - [Fields](/docs/components/fields/): A plain table plus `{.fields}` documents configuration keys, command flags and API fields — name, type, default and description each in place, readable on a narrow screen, every entry individually linkable. - [Steps](/docs/components/steps/): An ordered list plus `{.steps}` becomes a numbered procedure with dots and a connecting rule; switch to the steps shortcode when each step needs a heading in the table of contents. - [Cards](/docs/components/cards/): A link list plus `{.cards}` lays out a grid of navigation cards; switch to the shortcode when you need icons, badges or images. - [FileTree](/docs/components/filetree/): A `filetree` fence draws an annotated directory structure — aligned comment column, per-entry icons, collapsible directories, a draggable split. - [Math](/docs/components/math/): Inline and display mathematics with KaTeX, rendered at build time — the reader downloads no script. - [Mermaid](/docs/components/mermaid/): A `mermaid` fence turns text into flowcharts, sequence diagrams, Gantt charts, class diagrams and state diagrams — rendered locally, theme-aware, diff-friendly. - [PlantUML](/docs/components/plantuml/): A `plantuml` fence writes sequence, class, component, activity and use-case diagrams; rendering requires a PlantUML server you configure yourself. - [Markmap](/docs/components/markmap/): A `markmap` fence turns a Markdown outline into an expandable, zoomable mind map — and the source stays a readable outline. - [Draw.io](/docs/components/drawio/): Put a `.drawio.svg` that carries an editable copy on the page as an ordinary image; hovering gives the reader a button that opens the Draw.io editor. - [ECharts](/docs/components/echarts/): Write ECharts options as YAML or JSON in an `echarts` fence; Hugo validates them at build time and the browser draws a theme-aware chart with the local ECharts. - [Infographic](/docs/components/infographic/): An `infographic` fence picks an AntV template and renders a title plus a list of items as a flow, timeline, funnel, grid or hierarchy. - [Gallery](/docs/components/gallery/): A `gallery` fence arranges related screenshots in a responsive grid, each with an optional description or link, reusing the page's image zoom dialog. - [Badge](/docs/components/badge/): Put a semantic status label next to a feature name, a version or a table cell — five tones, no custom colours. - [Kbd](/docs/components/kbd/): Write shortcuts with `kbd` — one shortcode, a list of key names, a semantic key sequence that stays readable in print and in Markdown output. - [Includes](/docs/components/include/): Pull an external file in with include, print a site parameter with param, and write a note that reaches no output at all with comment. - [Asciinema](/docs/components/asciinema/): Put a .cast terminal recording on the page — the text stays selectable text, and the player ships with the theme rather than coming from a CDN. --- Backlinks: - [Introducing OINK](/blog/oink/oink-announcement/) - [Oink v0.2.0](/blog/release/0.2.0/) - [Oink v0.3.0](/blog/release/0.3.0/) - [Compose the page](/book/03-compose/) - [OINK Docs](/case/oink/) - [Docs](/docs/) - [Introduction](/docs/about/) - [Highlights](/docs/about/features/) - [Upgrade](/docs/admin/upgrade/) - [Components](/docs/design/components/) - [Get started](/docs/start/) - [Authoring](/docs/write/) - [Blog posts](/docs/write/blog/) - [Books](/docs/write/book/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/components/callout/index.md ================ # Callouts > Write notes, warnings and collapsible asides — with colour, icon and title — as `> [!NOTE]` blockquotes, no shortcode involved. --- LLMS index: [llms.txt](/llms.txt) --- A callout is a GitHub / Obsidian style blockquote: `> [!TYPE]` on the first line, the body underneath. Use it to lift a prerequisite, a warning or an aside out of the running text; if a sentence in the prose says it, a callout is not needed. ## Shortest form {#minimal} ```markdown {title="Source"} > [!NOTE] > Hugo Modules need Go on the machine; an offline archive does not. ``` > [!NOTE] > Hugo Modules need Go on the machine; an offline archive does not. Without a title the localized type name is used ("Note" on an English site, 「注意」 on a Chinese one). The source renders as a GitHub callout on GitHub and as a plain blockquote in any other Markdown reader — nothing is ever lost. ## Ten types {#types} The first five match GitHub; the other five are semantic types OINK adds. Every type has a default icon and accent colour. ```markdown {title="Source"} > [!TIP] > `hugo server -D` previews drafts. > [!IMPORTANT] > The floor is Hugo Extended 0.160.1; anything older fails the build outright. > [!WARNING] > `hugo --cleanDestinationDir` empties `public/`. > [!CAUTION] > The first build after deleting `resources/_gen` is much slower. > [!SUCCESS] > Build passed with zero warnings — ship it. > [!DANGER] > Never commit `go.work`. > [!QUESTION] > Should the site have comments? See [enabling comments](/docs/admin/comments/). > [!EXAMPLE] > `pgsty.com` is a documentation site built from callouts and tables alone. > [!QUOTE] > Documentation is a love letter that you write to your future self. ``` > [!TIP] > `hugo server -D` previews drafts. > [!IMPORTANT] > The floor is Hugo Extended 0.160.1; anything older fails the build outright. > [!WARNING] > `hugo --cleanDestinationDir` empties `public/`. > [!CAUTION] > The first build after deleting `resources/_gen` is much slower. > [!SUCCESS] > Build passed with zero warnings — ship it. > [!DANGER] > Never commit `go.work`. > [!QUESTION] > Should the site have comments? See [enabling comments](/docs/admin/comments/). > [!EXAMPLE] > `pgsty.com` is a documentation site built from callouts and tables alone. > [!QUOTE] > Documentation is a love letter that you write to your future self. Type names are case-insensitive. ## Custom title {#title} Text after the marker on the same line becomes the title and accepts inline Markdown — code, bold, links. ```markdown {title="Source"} > [!WARNING] Rewrites `public/` > Check that `baseURL` points at the production domain before a production > build, or every absolute link will be wrong. ``` > [!WARNING] Rewrites `public/` > Check that `baseURL` points at the production domain before a production > build, or every absolute link will be wrong. ## Body content {#body} The body is page-level Markdown: lists, fenced code, tables, images, nested callouts. Every line starts with `>`, fences included. ````markdown {title="Source"} > [!TIP] Three commands to a live preview > > 1. Clone: `git clone https://github.com/pgsty/oink-starter my-docs` > 2. Enter the directory and preview: > ```bash > cd my-docs && hugo server > ``` > 3. Open > > | Port | Purpose | > | --- | --- | > | 1313 | Hugo development server | ```` > [!TIP] Three commands to a live preview > > 1. Clone: `git clone https://github.com/pgsty/oink-starter my-docs` > 2. Enter the directory and preview: > ```bash > cd my-docs && hugo server > ``` > 3. Open > > | Port | Purpose | > | --- | --- | > | 1313 | Hugo development server | ## Collapsing {#collapsible} A `-` after the type starts the callout closed, a `+` starts it open. Both render as a native `

`; no JavaScript is loaded. Use them for full command output, alternatives, background — anything that need not be visible by default. ```markdown {title="Source"} > [!NOTE]- Why is Go needed? > Hugo downloads themes through Go's module system (`hugo mod get`). A submodule > or an offline archive works without Go installed. > [!TIP]+ Open by default, but the reader can close it > The closed state is not remembered; a reload returns to the default. ``` > [!NOTE]- Why is Go needed? > Hugo downloads themes through Go's module system (`hugo mod get`). A submodule > or an offline archive works without Go installed. > [!TIP]+ Open by default, but the reader can close it > The closed state is not remembered; a reload returns to the default. ## The neutral disclosure, DETAILS {#details} `[!DETAILS]` is a disclosure without a semantic colour: closed by default, `[!DETAILS]+` open. Use it for long output, whole configuration files, anything that has to be foldable. ````markdown {title="Source"} > [!DETAILS] Full `hugo version` output > ```text > hugo v0.165.0+extended+withdeploy darwin/arm64 > ``` ```` > [!DETAILS] Full `hugo version` output > ```text > hugo v0.165.0+extended+withdeploy darwin/arm64 > ``` ## Custom icon {#icon} The line right after the blockquote can carry `{icon="fa-solid fa-xxx"}` — one Font Awesome class pair — replacing the type's default icon. The attribute line must follow the blockquote immediately, with no blank line between them. ```markdown {title="Source"} > [!TIP] PostgreSQL 18 is supported > Pigsty v4 installs PostgreSQL 18 by default. {icon="fa-solid fa-database"} ``` > [!TIP] PostgreSQL 18 is supported > Pigsty v4 installs PostgreSQL 18 by default. {icon="fa-solid fa-database"} ## Nesting {#nesting} Callouts nest (one more `>` per level) and can sit inside list items or steps. One level of nesting is plenty. ```markdown {title="Source"} > [!WARNING] Back up before upgrading > A theme version bump can change how a page renders. > > > [!TIP]- How to back up > > `git tag pre-upgrade` is enough — rolling back is `git checkout pre-upgrade`. ``` > [!WARNING] Back up before upgrading > A theme version bump can change how a page renders. > > > [!TIP]- How to back up > > `git tag pre-upgrade` is enough — rolling back is `git checkout pre-upgrade`. ## Unknown types and common slips {#pitfalls} An unknown type name neither fails the build nor loses content: the block renders as an ordinary blockquote with the `[!TYPE]` marker still visible. ```markdown {title="Source"} > [!NOTICE] Not a valid type > The marker stays on the page to tell you so. ``` > [!NOTICE] Not a valid type > The marker stays on the page to tell you so. Other things that bite: - Title merged into the body. In files that pass through Prettier and friends, keep an empty `>` line under the title line, or the formatter folds the title into the body. - Attribute line moved by a formatter. Wrap marker lines such as `{icon=…}` in `` / ``. - `style`, `onclick`, and unsupported attributes warn and are ignored: the attribute line accepts `icon` and `class` only. Strict publishing rejects the warning (see the table below). ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | Static types are `
`; collapsible types are a native `
` + `` | | Print | All static and expanded; disclosures carry a `data-td-callout-collapsible` marker | | Markdown | The source blockquote is preserved, `[!TYPE]` marker and title included | | RSS | Same as print — static and expanded | Callouts load no script. ## Parameter reference {#reference} The marker line `> [!TYPE]± Title`: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `TYPE` | enum | — | `NOTE` `TIP` `IMPORTANT` `WARNING` `CAUTION` `SUCCESS` `DANGER` `QUESTION` `EXAMPLE` `QUOTE` `DETAILS`; case-insensitive; an unknown value renders as a plain blockquote | | `±` | `-` / `+` / none | none | `-` collapses closed, `+` collapses open; bare `DETAILS` is closed | | Title | inline Markdown | the localized type name | On the same line as the marker | {.fields meta="type default"} The attribute line `{…}`, immediately after the blockquote: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `icon` | Font Awesome class pair | the type's icon | For example `fa-solid fa-database`; `DETAILS` has no default icon | | `class` | space-separated classes | — | Passed through verbatim for site CSS | {.fields meta="type default"} `style`, `on*`, and other keys warn and are ignored; strict publishing rejects the warning. ## Limits {#limits} - Colours cannot be customized: the type decides. When you need a new meaning, pick the closest type and write your own title. - The collapsed state is not persisted. - Callouts work inside `{.steps}` list items and `{{% steps %}}` steps (see [Steps](/docs/components/steps/)); every line of the blockquote starts with `>` and lines up with the list item's indent. ## Related {#related} - [Steps](/docs/components/steps/) — callouts inside a procedure - [Tabs](/docs/components/tabs/) — the same note split per platform - [Writing pages](/docs/write/pages/) — when to use a callout and when to use prose --- Backlinks: - [Components](/docs/components/) - [Badge](/docs/components/badge/) - [Cards](/docs/components/cards/) - [FileTree](/docs/components/filetree/) - [Markmap](/docs/components/markmap/) - [Steps](/docs/components/steps/) - [Tabs](/docs/components/tabs/) - [Print](/docs/customize/print/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/components/image/index.md ================ # Images > Plain Markdown image syntax plus one attribute line gives you captions, sizing, zoom, links, numbering and Hugo image processing. --- LLMS index: [llms.txt](/llms.txt) --- There is one way to write an image: Markdown's `![alt text](source "title")`. An image standing alone as its own paragraph can be followed by a `{…}` attribute line, making it a captioned figure, a zoom candidate, a numbered figure, or a derivative processed by Hugo. The theme has no image shortcode. ## Shortest form {#minimal} ```markdown {title="Source"} ![The OINK documentation shell: sidebar, article and table of contents](oink-shell.webp) ``` ![The OINK documentation shell: sidebar, article and table of contents](oink-shell.webp) This image sits in the same directory as the page (a page bundle), so the theme reads its intrinsic size and writes `width`/`height`, and the page does not shift while loading; every image is lazy-loaded. Alternative text serves screen readers and search engines and should always be written; an empty alt marks a decorative image, which zoom skips. ## Where images come from {#sources} Sources resolve in the following order, written the same way in each case: | Placement | How it is written | Suited to | | --- | --- | --- | | Beside the page (a bundle: `index.md` plus the image) | `![…](oink-shell.webp)` | A screenshot only this page uses; it travels with the page and is shared by translations | | Global resource `assets/images/…` | `![…](images/logo/oink.webp)` | Images several pages share, especially ones needing processing (resize / crop) | | Static directory `static/images/…` | `![…](/images/hero-light.webp)` | Large images and downloads that need no processing; supply `width`/`height` where the theme cannot measure them | | Remote URL | `![…](https://example.com/a.png)` | Rare: nothing is downloaded at build time and nothing can be processed | A relative path is looked up first as a page resource and then as a global resource; failing both, it is emitted as a static path. The theme does not check whether a static path or a remote URL exists. When processing cannot resolve a processable resource, ordinary preview warns and leaves the image unprocessed; strict publishing rejects the warning. ## Inline versus block {#inline-vs-block} An image inside a line of text is an inline image, rendered as one `` and unable to carry attributes; an image standing alone as its own paragraph is a block image and can carry an attribute line. ```markdown {title="Source"} This little one ![shell thumbnail](oink-mini.webp) sits inside a sentence — an inline image. ![shell thumbnail](oink-mini.webp) {width="100" height="64"} ``` This little one ![shell thumbnail](oink-mini.webp) sits inside a sentence — an inline image. ![shell thumbnail](oink-mini.webp) {width="100" height="64"} An inline image displays at its own size (50×32 here). An SVG with no intrinsic size stretches to the container width when inlined, so an SVG belongs as a block image with explicit `width`/`height`. > [!NOTE] > Block images depend on the site setting > `markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false` (this site > has it; see [Configuration](/docs/customize/config/)). Without it, Goldmark > wraps a standalone image in `

` and the attribute line is treated as prose. ## Captions {#caption} An attribute line with `caption="…"` renders the image as a `

` plus a `
`. A caption is plain text and is not parsed as Markdown. ```markdown {title="Source"} ![Release card: version, publication date and asset buttons](release-note.webp) {caption="The release card is generated from data/download and the page's release record"} ``` ![Release card: version, publication date and asset buttons](release-note.webp) {caption="The release card is generated from data/download and the page's release record"} A Markdown `"title"` keeps its own meaning (a hover tooltip) and never becomes the caption. ## Size {#size} `width`/`height` are positive integers overriding the resource's own dimensions: they give a static or remote image a placeholder box so the page does not shift, or display a large image smaller (the browser scales it; the file is unchanged). ```markdown {title="Source"} ![The OINK home page illustration (light)](/images/hero-light.webp) {width="450" height="300" caption="A 900×600 illustration from static/images/ shown at half size"} ``` ![The OINK home page illustration (light)](/images/hero-light.webp) {width="450" height="300" caption="A 900×600 illustration from static/images/ shown at half size"} ## Processed images {#processing} Page resources and global resources can be processed by Hugo at build time: `command` and `options` must both be given, the command is one of `Fit`, `Resize`, `Fill` or `Crop`, and the options are Hugo's image processing string. The rendered `src` is the derivative; with zoom enabled the dialog opens the original. ```markdown {title="Source"} ![shell thumbnail](oink-shell.webp) {command="Fit" options="300x150" caption="Fit 300x150: scaled to fit inside a 300×150 box"} ![the left half of the shell](oink-shell.webp) {command="Fill" options="300x150 Left" caption="Fill 300x150 Left: fills the box, cropped from the left"} ``` ![shell thumbnail](oink-shell.webp) {command="Fit" options="300x150" caption="Fit 300x150: scaled to fit inside a 300×150 box"} ![the left half of the shell](oink-shell.webp) {command="Fill" options="300x150 Left" caption="Fill 300x150 Left: fills the box, cropped from the left"} Static paths, remote URLs and SVG cannot be processed. Writing `command` for one warns and leaves it unprocessed; strict publishing rejects the warning. The options syntax (anchors, quality, format conversion, as in `300x150 webp q80`) is in [Hugo image processing](https://gohugo.io/content-management/image-processing/). ## Linked images {#link} Two forms, for different purposes: - No caption, and the image itself is the link: wrap it in a Markdown link, `[![alt](src)](href)`. - A captioned figure that is clickable as a whole: add `link="…"` to the attribute line (which requires `caption` or `num`). ```markdown {title="Source"} [![Go to the highlights page](oink-shell.webp)](/docs/about/features/) ![Release card](release-note.webp) {caption="Click the image for the releases and downloads guide" link="/docs/write/releases/"} ``` [![Go to the highlights page](oink-shell.webp)](/docs/about/features/) ![Release card](release-note.webp) {caption="Click the image for the releases and downloads guide" link="/docs/write/releases/"} A linked image never zooms. Writing `link=` with no caption warns and drops the link, pointing at `[![…](…)](…)` instead; strict publishing rejects the warning. ## Numbered figures {#numbered} Numbered figures are for books and long manuals: add `num` to the attribute line, with an optional `#id`. The number is a string the author writes (`2-1`, `3.4`) and the theme never counts automatically; the caption gains a localized "Figure 2-1" prefix, and `#id` defaults to `fig-`. Reference it from the prose with an ordinary link `[Figure 2-1](#fig-2-1)` or the `xref` shortcode; a whole-book list of figures is in [Books](/docs/write/book/). ```markdown {title="Source"} ![Release card](release-note.webp) {#fig-release num="2-1" caption="The release card: version, date and assets"} See [Figure 2-1](#fig-release). ``` ![Release card](release-note.webp) {#fig-release num="2-1" caption="The release card: version, date and assets"} See [Figure 2-1](#fig-release). A numbered figure can be a processed image at the same time (`num` plus `command`), and can carry a `link`. ## Zoom {#zoom} Image zoom is off by default. Once the site enables it, block images, figures and gallery images that have alt text become clickable buttons that open the full image in a native `` (Esc closes it, focus returns where it was). This page turns it on in its front matter, so every image above is clickable. ```yaml {title="hugo.yml"} params: ui: image_zoom: true ``` ```yaml {title="One page's front matter: off for this page only"} image_zoom: false ``` Images that never zoom: inline images, decorative images with an empty alt, linked images, and images marked `data-no-zoom`. The runtime loads only when the page really has a candidate; print, Markdown and RSS have no dialog. ```markdown {title="Source: a decorative image does not zoom"} ![](oink-shell.webp) {width="150" height="75"} ``` ![](oink-shell.webp) {width="150" height="75"} ## Light and dark images {#dark-mode} The theme has no parameter for swapping an image by colour scheme. Where two images are needed, give each a `class` and show one per scheme with `[data-bs-theme="dark"]` in the site's CSS: ```markdown {title="Source"} ![Sidebar (light)](oink-shell.webp) {class="only-light"} ![Sidebar (dark)](oink-shell.webp) {class="only-dark"} ``` ```scss {title="assets/scss/_styles_project.scss"} [data-bs-theme="dark"] .only-light, :not([data-bs-theme="dark"]) .only-dark { display: none; } ``` `class` is passed through by the theme untouched, for the site's CSS to use. ## Output {#outputs} | Output | What appears | | --- | --- | | HTML | Inline ``; block ``; with a caption or number, `
` plus `
`; a zoom candidate carries `data-td-image-zoom` | | Print | As HTML, with the zoom controls removed | | Markdown | `![alt](src)` and the attribute line as they stand | | RSS | The image `src` becomes absolute; no zoom | ## Parameter reference {#reference} The attribute line `{…}` (the line immediately after a block image): | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `caption` | plain text | — | Its presence makes a figure; not parsed as Markdown | | `#id` | identifier | `fig-` when `num` is set | `[A-Za-z][A-Za-z0-9_.:-]*`; the anchor and the Book target ID | | `num` | string | — | `[0-9A-Za-z.-]+`; registers a Book figure target and prefixes the caption with "Figure N." | | `width` / `height` | positive integer | the resource's intrinsic size | Overrides the size; static and remote images use it to avoid layout shift | | `command` | enum | — | `Fit`, `Resize`, `Fill`, `Crop`; must accompany `options`; page and global resources only | | `options` | string | — | Hugo image processing options such as `600x300`, `300x150 Left`, `800x webp q80` | | `link` | URL | — | Wraps the figure in a link; requires `caption` or `num`; a linked image does not zoom | | `class` | class list | — | Passed through for the site's CSS | | `data-*` / `aria-*` | string | — | Passed through | {.fields meta="type default"} `style`, `on*`, `alt`, `title`, `src`, and unsupported keys on the attribute line warn and are ignored; strict publishing rejects the warning. Alt, title, and src belong to the Markdown image itself. ## Limits {#limits} - A caption holds no Markdown: every public string parameter is plain text, so rich explanation goes in a paragraph below the image. - `title` is not a caption: the `c` in `![a](b "c")` is a hover tooltip. - Processing applies to resources only: an image in `static/` that needs processing moves to the page bundle or `assets/`. - Remote images are never downloaded at build time. - Zoom has no drag, pan or previous / next; a set of related images uses a [gallery](/docs/components/gallery/). ## Related {#related} - [Gallery](/docs/components/gallery/) — a set of images sharing one zoom dialog - [Books](/docs/write/book/) — the list of figures and `xref` cross-references - [Brand and appearance](/docs/customize/brand/) — where the site logo and favicon go - [Cards](/docs/components/cards/) — images on cards --- Backlinks: - [Components](/docs/components/) - [Asciinema](/docs/components/asciinema/) - [Cards](/docs/components/cards/) - [Draw.io](/docs/components/drawio/) - [FileTree](/docs/components/filetree/) - [Gallery](/docs/components/gallery/) - [Math](/docs/components/math/) - [Mermaid](/docs/components/mermaid/) - [PlantUML](/docs/components/plantuml/) - [Brand and appearance](/docs/customize/brand/) - [Configuration](/docs/customize/config/) - [Print](/docs/customize/print/) - [Books](/docs/write/book/) - [Page parameters](/docs/write/frontmatter/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/components/code/index.md ================ # Code Blocks > A plain Markdown fence plus one attribute line gives you a filename title, exact copy, line numbers, highlighting, wrapping, folding and linkable lines. --- LLMS index: [llms.txt](/llms.txt) --- A code block is an ordinary Markdown fence. Highlighting is done at build time by Chroma, which Hugo embeds; there is no highlighter in the browser. Use it for commands, configuration snippets and source. The `{…}` attributes on the fence's info line decide the title bar, copy behaviour, line numbers and line anchors. Diagram-style fences (`mermaid`, `echarts`, `filetree` and friends) never take this path — each has its own render hook. ## Shortest form {#minimal} ````markdown {title="Source"} ```sql SELECT datname, numbackends FROM pg_stat_database ORDER BY numbackends DESC; ``` ```` ```sql SELECT datname, numbackends FROM pg_stat_database ORDER BY numbackends DESC; ``` A fence with no attributes still gets the full shell and a copy button. Without a title there is no empty bar: the copy button floats at the top right and appears on hover or when focus enters the block, and is always visible on touch devices. The shell does not display the language; the lexer name goes into `data-language` for stylesheets and tests. The language tag is simply Chroma's lexer name. A `diff` fence renders a patch with Chroma's added / removed line styling, no extra component involved: ````markdown {title="Source"} ```diff {title="a change to hugo.yml"} params: ui: - sidebar_menu_compact: true + sidebar_menu_compact: false sidebar_menu_foldable: true ``` ```` ```diff {title="a change to hugo.yml"} params: ui: - sidebar_menu_compact: true + sidebar_menu_compact: false sidebar_menu_foldable: true ``` ## Filename titles {#title} `title` gives the block a visible title bar, usually a filename or a path. It also becomes the block's accessible name. ````markdown {title="Source"} ```yaml {title="hugo.yml"} markup: goldmark: parser: attribute: block: true renderer: unsafe: true ``` ```` ```yaml {title="hugo.yml"} markup: goldmark: parser: attribute: block: true renderer: unsafe: true ``` `filename` is a historical alias of `title`; writing both warns and uses `filename`. Strict publishing rejects the warning. ## Line numbers, start line and highlighting {#line-numbers} `lineNos` takes `inline` (numbers in the same column as the code) or `table` (numbers in their own column, selectable on their own and never copied). `lineNoStart` changes the first displayed number. `hl_lines` marks lines to emphasize, counted from 1 over the source lines inside the fence, independent of `lineNoStart`. ````markdown {title="Source"} ```ini {title="postgresql.conf" lineNos="inline" lineNoStart=120 hl_lines="2 4-5"} shared_buffers = 8GB max_connections = 200 work_mem = 64MB wal_level = replica max_wal_senders = 10 ``` ```` ```ini {title="postgresql.conf" lineNos="inline" lineNoStart=120 hl_lines="2 4-5"} shared_buffers = 8GB max_connections = 200 work_mem = 64MB wal_level = replica max_wal_senders = 10 ``` `lineNos="table"` puts the numbers in a separate column — in both modes the copy button strips them: ````markdown {title="Source"} ```bash {title="deployment in three commands" lineNos="table"} ./configure -c rich ./install.yml pig ext install pg_duckdb ``` ```` ```bash {title="deployment in three commands" lineNos="table"} ./configure -c rich ./install.yml pig ext install pg_duckdb ``` `tabWidth` decides how many spaces a tab expands to and, like `style`, is handed straight to Chroma. This site uses class-based Chroma palettes (one for light, one for dark), so `style` only takes effect when Hugo is switched back to inline style mode. ## Wrapping long lines {#wrap} `wrap=true` changes display only: the source is unchanged and so is the text you copy. Without it, long lines scroll horizontally. ````markdown {title="Source"} ```text {title="config/artifacts.env" wrap=true} ARTIFACT_URL=https://repo.pigsty.io/pkg/infra/v3.6.0/infra-pkg-v3.6.0.el9.x86_64.tgz CHECKSUM=sha256:6d3dce4f7acb18f586469adcb80ab35f3e859f9837786e151cfbc2b3c0f587b2 ``` ```` ```text {title="config/artifacts.env" wrap=true} ARTIFACT_URL=https://repo.pigsty.io/pkg/infra/v3.6.0/infra-pkg-v3.6.0.el9.x86_64.tgz CHECKSUM=sha256:6d3dce4f7acb18f586469adcb80ab35f3e859f9837786e151cfbc2b3c0f587b2 ``` `wrap=true` cannot coexist with table line numbers: the number column and the code column are two table cells, and wrapping puts them out of step. Writing both warns and disables wrapping, suggesting `lineNos="inline"` or dropping the wrap. Strict publishing rejects the warning. ## Folding long code {#collapse} `collapse=N` shows the first N lines with a "show all N lines" button at the bottom. The server emits the complete code; folding is a visual clip applied after the browser measures where line N ends. Without JavaScript, in a screen reader, and in print, the code is complete. ````markdown {title="Source"} ```yaml {title="hugo.yml" collapse=8} baseURL: https://oink.pgsty.com/ title: OINK defaultContentLanguage: en languages: en: languageName: English weight: 1 zh: languageName: 简体中文 weight: 2 params: offline_search: true ui: sidebar_menu_foldable: true ``` ```` ```yaml {title="hugo.yml" collapse=8} baseURL: https://oink.pgsty.com/ title: OINK defaultContentLanguage: en languages: en: languageName: English weight: 1 zh: languageName: 简体中文 weight: 2 params: offline_search: true ui: sidebar_menu_foldable: true ``` When the block is no longer than `collapse`, no button appears. Wrapping and folding work together: folding measures the bottom edge of the Nth source line node, so a wrapped line is never cut in half. ## What gets copied {#copy} By default the whole source is copied. Terminal sessions — the `console` and `shell-session` lexers — copy the commands only: prompted lines survive, the prompts themselves and the output lines are dropped. Copying the block below gives two commands, with no `$` and no output. ````markdown {title="Source"} ```console $ pig ext list duckdb name version category pg_duckdb 1.0.0 OLAP $ pig ext install pg_duckdb INFO installing pg_duckdb ``` ```` ```console $ pig ext list duckdb name version category pg_duckdb 1.0.0 OLAP $ pig ext install pg_duckdb INFO installing pg_duckdb ``` To copy prompts and output too, write `copy="all"`. Using `copy="command"` on an ordinary lexer such as `bash` or `sh` warns and uses `copy="all"`, because those cannot tell prompt, command and output apart. Strict publishing rejects the warning. For multi-line commands, write the continuation prompt (usually `>`) on the continuation lines, or they are treated as output and excluded. When a session-lexer block contains no prompt at all, the copy button reports failure: the icon turns to its error state, an error is logged to the console, and the clipboard is untouched. It never falls back to copying everything. `copy=false` removes the copy button from one block — useful for a counter-example nobody should paste: ````markdown {title="Source"} ```yaml {title="counter-example: the attribute line left its block" copy=false} params: ui: image_zoom: true # wrong: image_zoom is a table, not a boolean ``` ```` ```yaml {title="counter-example: the attribute line left its block" copy=false} params: ui: image_zoom: true # wrong: image_zoom is a table, not a boolean ``` To turn copying off site-wide use `params.ui.code_copy: false`, which overrides whatever a block writes in `copy` (see [Configuration](/docs/customize/config/)). The copy button is icon-only; success and failure swap the icon and announce a localized status. What is copied keeps indentation, blank lines and Unicode, drops line numbers, and ends with exactly one newline. ## Line links and stable IDs {#line-links} Turning "see line 3" into a link takes two steps: give the fence an explicit `id`, then enable `anchorLineNos=true`. The line numbers become anchor links of the form `#-`. ````markdown {title="Source"} ```sql {id="ex-explain" title="explain.sql" lineNos="table" anchorLineNos=true} EXPLAIN (ANALYZE, BUFFERS) SELECT relname, n_live_tup FROM pg_stat_user_tables WHERE n_live_tup > 1000 ORDER BY n_live_tup DESC; ``` Jump to [line 4](#ex-explain-4). ```` ```sql {id="ex-explain" title="explain.sql" lineNos="table" anchorLineNos=true} EXPLAIN (ANALYZE, BUFFERS) SELECT relname, n_live_tup FROM pg_stat_user_tables WHERE n_live_tup > 1000 ORDER BY n_live_tup DESC; ``` Jump to [line 4](#ex-explain-4). Without an `id` the theme still generates one that is unique on the page, but it depends on where the fence sits in the page — insert another fence above it and the ID changes. Only an author-written `id` is a permanent link. IDs must not contain whitespace or control characters, and must not collide with any other viewport, tab, panel, title or line-anchor ID on the page. Invalid or duplicate IDs warn, and strict publishing rejects the warning. ## Numbered examples {#numbered} In a book or a long manual, number the snippets: `num` plus `caption` turns the fence into a Book "example" target that `xref` can reference and that appears in the book-wide list of examples. The number is written by the author — the theme never counts — and `id` defaults to `eg-`. ````markdown {title="Source"} ```sql {num="4-1" caption="Bloat ratio per table" #eg-bloat} SELECT schemaname, relname, n_dead_tup, n_live_tup FROM pg_stat_user_tables WHERE n_dead_tup > n_live_tup * 0.2; ``` See {{< xref eg="4-1" anchor="eg-bloat" >}}. ```` ```sql {num="4-1" caption="Bloat ratio per table" #eg-bloat} SELECT schemaname, relname, n_dead_tup, n_live_tup FROM pg_stat_user_tables WHERE n_dead_tup > n_live_tup * 0.2; ``` See [Example 4-1](#eg-bloat). `num` and `caption` must appear together. A lone caption is ignored and a lone number is dropped with a warning; strict publishing rejects the warning. `num` is mutually exclusive with the tab attribute `tab`. For numbering and indexing figures, tables and equations, see [publishing books](/docs/write/book/). ## A set of fences as tabs {#tabs} Consecutive fences carrying `tab` are assembled into one tab set in the browser. A `group` on the first fence makes the set shareable, synchronized and remembered. ````markdown {title="Source"} ```bash {tab="Homebrew" group="oink-install" value="brew"} brew install hugo ``` ```bash {tab="APT" value="apt"} sudo apt install hugo ``` ```` ```bash {tab="Homebrew" group="oink-install" value="brew"} brew install hugo ``` ```bash {tab="APT" value="apt"} sudo apt install hugo ``` The complete rules — group syntax, URL hash, cross-group synchronization, tabs in running text — are on the [Tabs](/docs/components/tabs/) page. ## Things that bite {#pitfalls} - Showing a shortcode in the docs: a fence does not stop Hugo from parsing, so a `{{< tabs >}}` written inside a code block still executes. To display it verbatim, add a comment marker inside each delimiter — {{</* tabs */>}}, and {{%/* steps */%}} for the percent form. Every shortcode shown on this page is written that way. - Fences inside fences: four backticks outside, three inside — every "Source" block on this page does it. Add another backtick when the inner block has fences of its own. - Attributes go on the info line: a fence's attributes follow the language on the opening line. Only tables and images take their attributes on the line below. Put them on the next line and you get a visible line of braces. - Unknown, unsafe, and theme-reserved attributes warn and are ignored in ordinary preview; the message lists the allowed names. Strict publishing rejects every such warning. - Fences in list items: indent them to line up with the item's content (three spaces after `1.`), or the fence leaves the list. ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | A `
` shell around Chroma's `.highlight`/`.chroma`; copy and fold buttons ship `hidden` and appear once the script confirms it can run | | Print | Complete code; copy, fold and the fade are removed; long blocks may break across pages; the title bar stays | | Markdown | The source fence, `{…}` attributes and all, emitted as written | | RSS | A static code block with no buttons | A page with no copy or fold control never loads `code-block.js`; print, Markdown and RSS never load it. ## Parameter reference {#reference} Inside the `{…}` after the language on the opening line, OINK's own attributes: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `title` | non-empty string | none | The visible title bar (usually a filename) and the accessible name | | `filename` | non-empty string | none | Historical alias of `title`; both together warn and use `filename` | | `copy` | `all` `command` `true` `false` | `command` for session lexers, `all` otherwise | `true` is `all`; `command` is allowed only on `console`/`shell-session` | | `wrap` | boolean | `false` | Visual wrapping, source unchanged; mutually exclusive with table line numbers | | `collapse` | positive integer | none | Lines shown initially; ignored when the block is shorter | | `label` | non-empty string | derived from the title | Accessible name, not displayed; mutually exclusive with `aria-label` | | `id` | non-empty token | generated | Stable block ID and line-anchor prefix; no whitespace | | `tab` | non-empty string | none | Tab label, see [Tabs](/docs/components/tabs/); mutually exclusive with `num` | | `group` | `^[a-z][a-z0-9_-]*$` | none | On the first fence of a set; enables hash / sync / persistence; requires `tab` | | `value` | `^[a-z0-9][a-z0-9_-]*$` | none | Required on every fence of a group, forbidden without one; requires `tab` | | `num` | `[0-9A-Za-z.-]+` | none | Numbered example (Book `eg`); must appear with `caption` | | `caption` | plain text | none | The numbered example's caption; must appear with `num` | | `class` | class list | none | Appended to the `.td-code` root element | | `data-*` / `aria-*` / `role` | string | none | Passed through to the root element | {.fields meta="type default"} `title`, `filename` and `label` already give the block an accessible name and `role="group"`. Any of them together with `aria-label`, `aria-labelledby` or `role` warns and ignores the conflicting attribute; strict publishing rejects the warning. Those three attributes pass through only when the block has neither a title nor a `label`. The same line also takes Chroma options, which the theme hands to Hugo unchanged: | Option | Type | Default | Description | | --- | --- | --- | --- | | `lineNos` | `false` `inline` `table` | `false` | Line-number style; `table` is mutually exclusive with `wrap=true` | | `lineNoStart` | positive integer | `1` | First displayed number; does not affect how `hl_lines` counts | | `hl_lines` | lines and ranges | none | For example `"2 4-5"`, counted over the source lines in the fence | | `anchorLineNos` | boolean | `false` | Line numbers become anchor links prefixed with the block's `id` | | `tabWidth` | positive integer | Hugo's default | Spaces a tab expands to | {.fields meta="type default"} ## Limits {#limits} - No swapping the highlighter: there is no Shiki, no Twoslash, no browser-side highlighting and no runnable playground. For patches use a `diff` fence — Chroma's `.gi`/`.gd` are the added / removed line styles. - `copy="command"` recognizes session lexers only: on any other language it warns and falls back to copying everything; strict publishing rejects the warning. - A generated ID is not a permanent link: write `id` when you intend to share one. - `mermaid`, `math`, `chem`, `markmap`, `plantuml`, `echarts`, `infographic`, `checksums`, `filetree` and `gallery` are not code blocks: each has its own render hook, no shell around it and no copy button. ## Related {#related} - [Tabs](/docs/components/tabs/) — the full rules for assembling adjacent fences - [Include](/docs/components/include/) — pull a real file from the repository in as a code block - [Publishing books](/docs/write/book/) — numbered examples, cross references, the list of examples - [Print](/docs/customize/print/) — what long code looks like on paper --- Backlinks: - [Components](/docs/components/) - [Asciinema](/docs/components/asciinema/) - [ECharts](/docs/components/echarts/) - [FileTree](/docs/components/filetree/) - [Includes](/docs/components/include/) - [Math](/docs/components/math/) - [PlantUML](/docs/components/plantuml/) - [Steps](/docs/components/steps/) - [Tables](/docs/components/table/) - [Tabs](/docs/components/tabs/) - [Print](/docs/customize/print/) - [API reference](/docs/write/openapi/) - [Writing pages](/docs/write/pages/) - [Releases and downloads](/docs/write/releases/) ================ Source: https://oink.pgsty.com/docs/components/tabs/index.md ================ # Tabs > A `{tab=}` attribute on adjacent fences or tables makes a tab set; add a group and it becomes linkable, synchronized and remembered. --- LLMS index: [llms.txt](/llms.txt) --- Tabs put equivalent alternatives side by side: package managers, distributions, YAML / TOML / JSON, an environment variable versus a configuration key. Ordered steps and unrelated content do not belong in tabs — the reader sees only one panel at a time. The native form is a `tab` attribute on adjacent blocks. Reach for the `tabs`/`tab` shortcode only when the panels hold running text: several paragraphs, lists, callouts. Both forms share one runtime, one DOM and the same keyboard behaviour. ## Shortest form {#minimal} Write two fences carrying `tab` back to back, separated by a blank line only. ````markdown {title="Source"} ```bash {tab="Homebrew"} brew install hugo ``` ```bash {tab="Debian / Ubuntu"} sudo apt install hugo ``` ```` ```bash {tab="Homebrew"} brew install hugo ``` ```bash {tab="Debian / Ubuntu"} sudo apt install hugo ``` The server emits two titled code blocks with no panel hidden; after the page loads, the runtime regroups adjacent blocks of the same kind into a tab set. On GitHub, in print, and with JavaScript off, the reader sees two complete blocks one after the other. ## Groups: links, sync and memory {#group} Write `group` on the first block only and the set gains a public URL hash `#-`, in-page synchronization and browser persistence. Every block in a group must carry `value`. ````markdown {title="Source"} ```bash {tab="npm" group="pkgmgr" value="npm"} npm create hugo-site@latest ``` ```bash {tab="pnpm" value="pnpm"} pnpm create hugo-site ``` ```bash {tab="Yarn" value="yarn"} yarn create hugo-site ``` ```` ```bash {tab="npm" group="pkgmgr" value="npm"} npm create hugo-site@latest ``` ```bash {tab="pnpm" value="pnpm"} pnpm create hugo-site ``` ```bash {tab="Yarn" value="yarn"} yarn create hugo-site ``` `value` is the machine value (`^[a-z0-9][a-z0-9_-]*$`), `tab` is the human label; the two are independent. The pnpm panel above answers to `#pkgmgr-pnpm`, and visiting this page with that hash selects it. ## Groups move together {#sync} The set below reuses `group="pkgmgr"`. Switch the package manager above and this one follows; switch it here and the one above follows. The choice is written to `localStorage` under the key `td-tabs:v1:pkgmgr` and still applies to same-group tabs on other pages. ````markdown {title="Source"} ```bash {tab="npm" group="pkgmgr" value="npm"} npm run build ``` ```bash {tab="pnpm" value="pnpm"} pnpm build ``` ```` ```bash {tab="npm" group="pkgmgr" value="npm"} npm run build ``` ```bash {tab="pnpm" value="pnpm"} pnpm build ``` This set has no `yarn` panel. When a value is missing, that set simply stays where it is; a set is never left with nothing selected. The initial selection is decided in this order: URL hash, stored value, the shortcode's `default` or the first block, the first tab. Opening the page with a hash switches the set without overwriting a preference the reader already stored. ## Tables can be tabs too {#tables} The same attributes on a table's attribute line group adjacent tables into a tab set. ```markdown {title="Source"} | Parameter | Default | | --- | --- | | `shared_buffers` | 25% RAM | | `max_connections` | 100 | {tab="PostgreSQL 18" group="pgver" value="pg18"} | Parameter | Default | | --- | --- | | `shared_buffers` | 128MB | | `max_connections` | 100 | {tab="PostgreSQL 13" value="pg13"} ``` | Parameter | Default | | --- | --- | | `shared_buffers` | 25% RAM | | `max_connections` | 100 | {tab="PostgreSQL 18" group="pgver" value="pg18"} | Parameter | Default | | --- | --- | | `shared_buffers` | 128MB | | `max_connections` | 100 | {tab="PostgreSQL 13" value="pg13"} Fences and tables are two block kinds and never merge into one set even when adjacent: a tab set is all fences or all tables. To mix them, use the shortcode form below. ## A label and a filename together {#tab-with-title} A fence can carry both `tab` and `title`: the label goes in the tab bar, the filename title bar stays inside the panel. ````markdown {title="Source"} ```yaml {tab="YAML" title="hugo.yml" group="conffmt" value="yaml"} params: ui: sidebar_menu_foldable: true ``` ```toml {tab="TOML" title="hugo.toml" value="toml"} [params.ui] sidebar_menu_foldable = true ``` ```` ```yaml {tab="YAML" title="hugo.yml" group="conffmt" value="yaml"} params: ui: sidebar_menu_foldable: true ``` ```toml {tab="TOML" title="hugo.toml" value="toml"} [params.ui] sidebar_menu_foldable = true ``` ## A lone block is just a titled block {#single-block} A block needs a neighbour of the same kind to become a tab set. On its own it keeps its title rather than becoming a tab bar with one tab. ````markdown {title="Source"} ```ini {tab="on its own"} listen_addresses = '*' ``` ```` ```ini {tab="on its own"} listen_addresses = '*' ``` Only blank lines may sit between blocks. Three things break a set: running text in between (a paragraph, a heading or a list all count); an HTML comment in between, of which `` is the common one; a later block writing its own `group`, since only the first block of a set may carry it. ## Tabs around running text {#shortcode} When a panel holds paragraphs, lists, callouts, or several blocks, use the `tabs`/`tab` shortcode. The body is full Markdown. `````markdown {title="Source"} {{< tabs group="deploy" default="pages" label="Deployment target" >}} {{< tab label="GitHub Pages" value="pages" >}} The repository ships `.github/workflows/`; a push to `main` builds and publishes. > [!NOTE] > `baseURL` has to be the repository's Pages address. {{< /tab >}} {{< tab label="Cloudflare Pages" value="cloudflare" >}} Connect the repository in the Cloudflare dashboard; the build command is: ```bash hugo --gc --minify ``` {{< /tab >}} {{< /tabs >}} ````` **GitHub Pages** The repository ships `.github/workflows/`; a push to `main` builds and publishes. > [!NOTE] > `baseURL` has to be the repository's Pages address. **Cloudflare Pages** Connect the repository in the Cloudflare dashboard; the build command is: ```bash hugo --gc --minify ``` `default` names the initially selected panel; it must equal a child's `value` and it requires `group`. Without `group`, `value` is forbidden and the theme generates `tab1`, `tab2` and so on — such a set switches locally and touches neither the URL nor storage. The shortcode form is stricter than the attribute form: a mistake is reported at build time instead of in the browser. ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | `
` with `role="tablist"` buttons and panels; every panel is visible until the runtime takes over | | Print | Consecutive titled static sections, no tab bar | | Markdown | The fence form keeps the source fence, `{tab=}` included; the shortcode form emits `**Label**` plus the body | | RSS | Same as print — stacked titled sections | Only a page that uses tabs loads `tabs.js`; print, Markdown and RSS never do. ## Parameter reference {#reference} Attributes on a fence info line or a table attribute line: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `tab` | non-empty string | none | The visible label; on a lone block it is simply that block's title | | `group` | `^[a-z][a-z0-9_-]*$` | none | On the first block of a set; enables hash, in-page sync and persistence; requires `tab` | | `value` | `^[a-z0-9][a-z0-9_-]*$` | none | Required on every block of a group, forbidden without one; requires `tab` | {.fields meta="type default"} The `tabs` shortcode: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `group` | `^[a-z][a-z0-9_-]*$` | none | As above: hash, sync and persistence | | `default` | a child's `value` | the first child | The initially selected panel; requires `group` | | `label` | plain text | localized "Tabs" | Accessible name for the tab bar; not displayed | {.fields meta="type default"} The `tab` shortcode: | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `label` | plain text | yes | The visible label | | `value` | `^[a-z0-9][a-z0-9_-]*$` | when `group` is set | Forbidden without a group, where `tab1`, `tab2` … are generated | {.fields meta="type required"} Behavioural contract: in a group the panel ID is `-`; when the same `group` name appears a second time on one page, later sets get a `-2`, `-3` suffix and the deep-link target stays the first set. Ungrouped sets get theme-generated IDs. The storage key is `td-tabs:v1:`. A click or a key press updates the hash with `replaceState` and writes storage; arriving with a hash only switches. Left and right arrows (RTL-aware) plus Home/End move and activate, and focus stays on the tab. ## Limits {#limits} - Invalid grouping and composition warn during the Hugo build and take a safe fallback: drop an unusable group/value/default, ignore stray content, keep the later duplicate, or render no empty set. Strict publishing rejects every warning, and the message names the source position. - In the attribute form, a run missing a usable `value` loses synchronization and remains a set of local tabs; grouping never silently invents an identity. - Fences and tables never merge into one set. To mix prose with code, use the shortcode form. - Tabs are not a disclosure. To fold away long output use `> [!DETAILS]` (see [Callouts](/docs/components/callout/)). - A `group` name is shared site-wide: a reader who picks pnpm on page A gets pnpm in the same group on page B. That is the point — and it means `group` names should mean something, not be `tabs1`. ## Related {#related} - [Code blocks](/docs/components/code/) — the rest of the fence attributes (title, copy, line numbers, folding) - [Tables](/docs/components/table/) — the rest of the table attribute line - [Callouts](/docs/components/callout/) — for folding rather than juxtaposing - [Steps](/docs/components/steps/) — tabs inside a procedure --- Backlinks: - [Callouts](/docs/components/callout/) - [Cards](/docs/components/cards/) - [Code Blocks](/docs/components/code/) - [FileTree](/docs/components/filetree/) - [Gallery](/docs/components/gallery/) - [Includes](/docs/components/include/) - [Kbd](/docs/components/kbd/) - [Steps](/docs/components/steps/) - [Tables](/docs/components/table/) - [AI-agent support](/docs/customize/agents/) - [Print](/docs/customize/print/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/components/table/index.md ================ # Tables > A plain GFM table plus one attribute line becomes a captioned table, a compatibility matrix, a field list, a numbered table or a tab set; wide tables scroll on their own. --- LLMS index: [llms.txt](/llms.txt) --- A table is an ordinary GFM pipe table. The theme's table render hook wraps every one in a horizontally scrollable region, and the `{…}` attribute line underneath decides which kind of table it is: captioned, a compatibility matrix, a field list, a numbered table, or a tab set. Merged cells, sorting and filtering are out of scope; when you need them, change how the data is presented. ## Shortest form {#minimal} Without an attribute line it is just a table. Alignment still comes from the delimiter row, and header cells are `th scope="col"`. ```markdown {title="Source"} | Component | Port | Purpose | | --- | :---: | --- | | PostgreSQL | 5432 | Database | | Pgbouncer | 6432 | Connection pool | | Patroni | 8008 | High-availability orchestration | ``` | Component | Port | Purpose | | --- | :---: | --- | | PostgreSQL | 5432 | Database | | Pgbouncer | 6432 | Connection pool | | Patroni | 8008 | High-availability orchestration | ## Wide tables scroll themselves {#scroll} A table with too many columns never widens the page; it scrolls inside its own region. That region is focusable: Tab into it and the arrow keys scroll, and its accessible name is the localized "Scrollable table". ```markdown {title="Source"} | Cluster | Role | Version | State | Lag | Connections | Size | Backup | | --- | --- | --- | --- | --- | --- | --- | --- | | pg-meta | primary | 18.1 | running | — | 42 | 12 GB | 2026-08-17 | | pg-test | replica | 18.1 | streaming | 12 ms | 8 | 12 GB | 2026-08-17 | ``` | Cluster | Role | Version | State | Lag | Connections | Size | Backup | | --- | --- | --- | --- | --- | --- | --- | --- | | pg-meta | primary | 18.1 | running | — | 42 | 12 GB | 2026-08-17 | | pg-test | replica | 18.1 | streaming | 12 ms | 8 | 12 GB | 2026-08-17 | ## Captions {#caption} `{caption="…"}` adds a visible ``. It is plain text and it does not number the table. ```markdown {title="Source"} | Item | Value | | --- | --- | | Theme version | v0.8.1 | | Hugo floor | 0.160.1 Extended | | Licence | Apache-2.0 | {caption="Theme facts this site currently builds against"} ``` | Item | Value | | --- | --- | | Theme version | v0.8.1 | | Hugo floor | 0.160.1 Extended | | Licence | Apache-2.0 | {caption="Theme facts this site currently builds against"} ## Compatibility matrices {#matrix} `{.matrix}` is for "row × column = supported or not" tables: the first column becomes a row header (`th scope="row"`), the header row and the first column stay pinned while scrolling, and the remaining cells are centred unless the delimiter row says otherwise. ✅ and ❌ are characters the author writes; the theme does not interpret them. ```markdown {title="Source"} | OS / PG | PG18 | PG17 | PG16 | PG15 | PG14 | | --- | :---: | :---: | :---: | :---: | :---: | | EL 9 | ✅ | ✅ | ✅ | ✅ | ✅ | | EL 8 | ✅ | ✅ | ✅ | ✅ | ✅ | | Debian 13 | ✅ | ✅ | ✅ | ❌ | ❌ | | Ubuntu 24.04 | ✅ | ✅ | ✅ | ✅ | ❌ | {.matrix} ``` | OS / PG | PG18 | PG17 | PG16 | PG15 | PG14 | | --- | :---: | :---: | :---: | :---: | :---: | | EL 9 | ✅ | ✅ | ✅ | ✅ | ✅ | | EL 8 | ✅ | ✅ | ✅ | ✅ | ✅ | | Debian 13 | ✅ | ✅ | ✅ | ❌ | ❌ | | Ubuntu 24.04 | ✅ | ✅ | ✅ | ✅ | ❌ | {.matrix} ## Using the whole canvas {#full-width} `{.full-width}` lets a table exceed the reading column and take the full width the article has. It suits tables with many short columns. ```markdown {title="Source"} | Language | Code | Sidebar | Search | TOC | Print | Status | | --- | --- | --- | --- | --- | --- | --- | | 简体中文 | `zh` | ✅ | ✅ | ✅ | ✅ | Reviewed | | English | `en` | ✅ | ✅ | ✅ | ✅ | Reviewed | {.full-width} ``` | Language | Code | Sidebar | Search | TOC | Print | Status | | --- | --- | --- | --- | --- | --- | --- | | 简体中文 | `zh` | ✅ | ✅ | ✅ | ✅ | Reviewed | | English | `en` | ✅ | ✅ | ✅ | ✅ | Reviewed | {.full-width} ## Field lists {#fields} `{.fields}` turns a table into a definition list: the first column is the name, the last is the description, and the columns in between are metadata. It is the shape for configuration keys, command flags and API fields; the full syntax is on the [Fields](/docs/components/fields/) page. ```markdown {title="Source"} | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `offline_search` | boolean | `false` | Build the local search index | | `page_width` | string | `normal` | Width of the reading column | {.fields meta="type default"} ``` | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `offline_search` | boolean | `false` | Build the local search index | | `page_width` | string | `normal` | Width of the reading column | {.fields meta="type default"} ## Numbered tables {#numbered} In a book or a long manual, number the tables: `num` plus an optional `#id` and `caption`. The table is wrapped in a `
` labelled with a localized "Table N." and registered as a Book target, so `xref` can reference it and it appears in the book-wide list of tables. The number is written by the author — the theme never counts — and `id` defaults to `tbl-`. ```markdown {title="Source"} | Isolation level | Dirty read | Non-repeatable read | Phantom read | | --- | --- | --- | --- | | Read committed | no | yes | yes | | Repeatable read | no | no | yes | | Serializable | no | no | no | {#tbl-iso num="9-1" caption="Anomalies each PostgreSQL isolation level permits"} See {{< xref tbl="9-1" anchor="tbl-iso" >}}. ``` | Isolation level | Dirty read | Non-repeatable read | Phantom read | | --- | --- | --- | --- | | Read committed | no | yes | yes | | Repeatable read | no | no | yes | | Serializable | no | no | no | {#tbl-iso num="9-1" caption="Anomalies each PostgreSQL isolation level permits"} See [Table 9-1](#tbl-iso). ## Tables as tabs {#tabs} Adjacent tables carrying `{tab="…"}` form a tab set under the same rules as adjacent fences: `group` on the first table enables hash, sync and persistence, and every table after it needs `value`. The complete rules are on the [Tabs](/docs/components/tabs/) page. ```markdown {title="Source"} | Directory | Contents | | --- | --- | | `content/` | Pages | | `data/` | Landing and release data | {tab="Content" group="repo-layout" value="content"} | Directory | Contents | | --- | --- | | `assets/` | SCSS and image resources | | `static/` | Files copied verbatim | {tab="Assets" value="assets"} ``` | Directory | Contents | | --- | --- | | `content/` | Pages | | `data/` | Landing and release data | {tab="Content" group="repo-layout" value="content"} | Directory | Contents | | --- | --- | | `assets/` | SCSS and image resources | | `static/` | Files copied verbatim | {tab="Assets" value="assets"} ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | A focusable `
` around the ``; matrix and full-width are modifier classes on that wrapper | | Print | The complete table laid out to the page width; the wrapper stays but is marked `td-table-scroll--static` and is no longer a focusable viewport | | Markdown | The source table and its attribute line, emitted as written | | RSS | The complete static table | Tables load no script. ## Parameter reference {#reference} The attribute line on the row below the table: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `.full-width` | marker | none | Exceed the reading column and use the article canvas | | `.matrix` | marker | none | First column as row header, header and first column pinned, other cells centred | | `.fields` | marker | none | Render as a definition list, see [Fields](/docs/components/fields/) | | `caption` | plain text | none | Visible table caption; on `.fields` it labels the list | | `meta` | role list | none | Names the meaning of the middle `.fields` columns: `type` `required` `default` `-`; requires `.fields` | | `#id` | identifier | `tbl-` when `num` is set | `[A-Za-z][A-Za-z0-9_.:-]*`; lands on the `
`, or on the `
` for a numbered table | | `num` | string | none | `[0-9A-Za-z.-]+`; registers a Book table target and prefixes the caption with "Table N." | | `tab` / `group` / `value` | see [Tabs](/docs/components/tabs/) | none | Adjacent tables become a tab set | | `class` | class list | none | Left on the `
` for site CSS | | `data-*` / `aria-*` | string | none | Passed through | {.fields meta="type default"} `style`, `on*`, and other keys warn and are ignored; strict publishing rejects the warning. ## Limits {#limits} - Mutual exclusions: `.fields` cannot combine with `.matrix`, `.full-width` or `num`; `num` and `tab` are exclusive; `group`/`value` require `tab`; `meta` requires `.fields`. - The attribute line must touch the table: leave a blank line and it becomes a visible line of braces. Markdown formatters like to move it — wrap it in `` / ``. - No merged cells, no sorting, no filtering: what a GFM pipe table can express is all there is. Split a complex table with a merged header into two tables, or turn it into a matrix. - Block content does not fit in a cell: multi-paragraph descriptions, lists and fences need the `fields`/`field` shortcode. - `.matrix` centring is CSS: an explicit alignment in the delimiter row wins. ## Related {#related} - [Fields](/docs/components/fields/) — everything `{.fields}` can do - [Tabs](/docs/components/tabs/) — adjacent tables as a tab set - [Publishing books](/docs/write/book/) — numbered tables, cross references, the list of tables - [Code blocks](/docs/components/code/) — where attributes go on the info line instead of the next line --- Backlinks: - [ECharts](/docs/components/echarts/) - [Fields](/docs/components/fields/) - [Tabs](/docs/components/tabs/) - [Print](/docs/customize/print/) - [Books](/docs/write/book/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/components/fields/index.md ================ # Fields > A plain table plus `{.fields}` documents configuration keys, command flags and API fields — name, type, default and description each in place, readable on a narrow screen, every entry individually linkable. --- LLMS index: [llms.txt](/llms.txt) --- Fields render "a list of named values with metadata and a description" as a responsive definition list: the name gets its own line, type / required / default sit beside it as small chips, the description starts on the next line, and every entry carries its own anchor. Use it for configuration keys, command flags and API fields. When readers need to compare many rows across the same columns, keep a plain table; when the content is a sequence of actions, use steps. There are two spellings: a plain table plus `{.fields}` (the default choice), and the `fields`/`field` shortcode, for when a description needs several paragraphs, a list or a code block. Both render the same entries. ## Shortest form {#minimal} A pipe table with at least two columns and `{.fields}` on the next line. The first column is the name, the last is the description, and every column in between is metadata labelled with its own header text. ```markdown {title="Source"} | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `offline_search` | boolean | `false` | Build the local search index and enable the command palette | | `offline_search_max_results` | integer | `10` | Maximum number of search results | | `page_width` | string | `normal` | Reading column width: `narrow` `normal` `wide` | {.fields} ``` | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `offline_search` | boolean | `false` | Build the local search index and enable the command palette | | `offline_search_max_results` | integer | `10` | Maximum number of search results | | `page_width` | string | `normal` | Reading column width: `narrow` `normal` `wide` | {.fields} Metadata here shows as "Header: value". The theme infers nothing from the header — `Type` is only a label. The next section turns those into standard chips. Cells accept inline Markdown (code, emphasis, links) and empty middle cells are omitted. ## Semantic columns with `meta=` {#meta} `meta` says, in order, what each middle column means: `type`, `required`, `default`, or `-` to keep the header as a plain label. With it, the table form renders the same chips as the shortcode form. ```markdown {title="Source"} | Parameter | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `baseURL` | string | yes | | Site address, subpath included | | `title` | string | yes | | Site name, shown in the navbar and the tab | | `defaultContentLanguage` | string | | `en` | Default language; decides which language unprefixed paths belong to | {.fields meta="type required default"} ``` | Parameter | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `baseURL` | string | yes | | Site address, subpath included | | `title` | string | yes | | Site name, shown in the navbar and the tab | | `defaultContentLanguage` | string | | `en` | Default language; decides which language unprefixed paths belong to | {.fields meta="type required default"} The rules: - `meta` should name a role for every middle column — exactly the column count minus two. Too many or too few warns and ignores `meta`; strict publishing rejects the warning. - A `required` column is "non-empty means true": "yes", "是" or "✔" all read the same, and the rendered chip is the untranslated `required`. An empty cell shows nothing. - `type` and `default` cells with no inline markup of their own are wrapped in code formatting, matching the shortcode form. - The three semantic chips always display in the order `type`, `required`, `default`, whatever order the columns are in; `-` columns follow, in column order. `-` mixes with semantic roles, which is how you keep one custom label: ```markdown {title="Source"} | Environment variable | Type | Scope | Description | | --- | --- | --- | --- | | `HUGO_MODULE_WORKSPACE` | string | build | Points at `go.work` so the theme resolves from a local checkout | | `HUGO_ENV` | string | build | Set to `production` to enable minification and fingerprinting | {.fields meta="type -"} ``` | Environment variable | Type | Scope | Description | | --- | --- | --- | --- | | `HUGO_MODULE_WORKSPACE` | string | build | Points at `go.work` so the theme resolves from a local checkout | | `HUGO_ENV` | string | build | Set to `production` to enable minification and fingerprinting | {.fields meta="type -"} ## Labels and container IDs {#caption-id} `caption` gives the whole list a visible label, which is also its accessible name; `id` names the outer container so it can be linked to or styled. ```markdown {title="Source"} | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `enable` | boolean | `false` | Turn image zoom on | | `selector` | string | `.td-content` | Root selector scanned for candidate images | {.fields caption="params.ui.image_zoom" id="zoom-params" meta="type default"} ``` | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `enable` | boolean | `false` | Turn image zoom on | | `selector` | string | `.td-content` | Root selector scanned for candidate images | {.fields caption="params.ui.image_zoom" id="zoom-params" meta="type default"} ## Every entry is linkable {#anchors} Each entry gets an anchor of the form `field-`, and a self-link icon appears beside the name on hover. `page_width` in the first table above is [#field-page_width](#field-page_width) — a link you can send on its own when answering a question. Duplicate names on one page get `-2`, `-3` suffixes, the same rule Goldmark uses for duplicate headings. Anchors are generated in HTML only: print and RSS assemble many pages into one document, where in-page anchors would collide. ## The shortcode form {#shortcode} When the description needs several paragraphs, a list or a code block, a table cell cannot hold it. Use `fields`/`field`: ````markdown {title="Source"} {{< fields label="Common pig flags" >}} {{< field name="--config" type="path" required=true >}} Path to the configuration file. Relative paths resolve against the working directory. When `PIG_CONFIG` is also set, the command-line flag wins. {{< /field >}} {{< field name="--log-level" type="string" default="info" >}} Log level, from low to high: - `debug`: print every remote call - `info`: the default - `error`: output only on failure {{< /field >}} {{< field name="--dry-run" type="boolean" default=false >}} Print what would happen and change nothing: ```bash pig ext install pg_duckdb --dry-run ``` {{< /field >}} {{< /fields >}} ```` **Common pig flags** - `--config` — `path`; required Path to the configuration file. Relative paths resolve against the working directory. When `PIG_CONFIG` is also set, the command-line flag wins. - `--log-level` — `string`; default: `info` Log level, from low to high: - `debug`: print every remote call - `info`: the default - `error`: output only on failure - `--dry-run` — `boolean`; default: `false` Print what would happen and change nothing: ```bash pig ext install pg_duckdb --dry-run ``` `required=true` and `default=false` are booleans and take no quotes. `default` accepts any scalar: `default=0` and `default=""` both display faithfully (the empty string shows as `""`), and omitting `default` omits the chip. Every `field` needs a non-empty body and must be a direct child of `fields`. ## Which form to use {#which} | Situation | Use | | --- | --- | | One-sentence descriptions that fit in a table cell | table + `{.fields}` | | Descriptions with paragraphs, lists or code blocks | the `fields`/`field` shortcode | | Readers comparing many rows across the same columns | a plain table, not a field list | | Content that is a sequence of actions | [Steps](/docs/components/steps/) | The table form stays a readable table on GitHub, and OINK's Markdown output keeps it as a table. That is why it is the default. ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | `
` around a semantic `
`; entries carry `#field-` anchors and self-links | | Print | The complete definition list, without entry anchors | | Markdown | The table form keeps the source table; the shortcode form emits a bulleted list of "`name` — type; required; default: value" plus the indented description | | RSS | The complete static `
`, without entry anchors | No script is loaded. ## Parameter reference {#reference} The table attribute line, on the row below the table: | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `.fields` | marker | none | Required; renders the table as a field list | | `meta` | role list | none | Space-separated `type` `required` `default` `-`; one per middle column; semantic roles cannot repeat | | `caption` | plain text | none | Visible label and the list's accessible name | | `id` | identifier | none | ID of the outer container | | `class` | class list | none | Passed through for site CSS | | `data-*` / `aria-*` | string | none | Passed through | {.fields meta="type default"} The `fields` shortcode: | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `label` | non-empty string | no | Visible label; the same thing the table's `caption` does | | `id` | identifier | no | Container ID; no whitespace, quotes, `<`, `>` or `&` | | `class` / `data-*` / `aria-*` | string | no | The same policy as the table attribute line | {.fields meta="type required"} The `field` shortcode: | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `name` | non-empty string | yes | The field name | | `type` | non-empty string | no | Type label such as `boolean`, `string[]`, `duration` | | `required` | boolean | no | `true` shows the untranslated `required` chip; defaults to `false` | | `default` | scalar | no | String / boolean / integer / float; `false`, `0` and `""` all display | {.fields meta="type required"} ## Limits {#limits} - The first column must be non-empty and unique within one table; a duplicate or empty name warns and skips that row, and strict publishing rejects the warning. - `.fields` cannot combine with `.matrix`, `.full-width` or `num`, and `meta` cannot appear on a table without `.fields`. - Block content does not fit in a table cell: paragraphs, lists and fences need the shortcode form. - `required` and `default` are untranslated API vocabulary and stay in English in every language. They are contract words, not interface copy. - No `kind`, `since`, `deprecated`, `location`, per-field links or nested structures, and nothing parses TypeScript or an OpenAPI schema at build time. ## Related {#related} - [Tables](/docs/components/table/) — the rest of the attribute line and the exclusion rules - [Configuration](/docs/customize/config/) — the full site parameter table, itself a field list - [Front matter](/docs/write/frontmatter/) — the full front matter table - [Steps](/docs/components/steps/) — ordered actions do not belong in a field list --- Backlinks: - [Cards](/docs/components/cards/) - [Tables](/docs/components/table/) - [AI-agent support](/docs/customize/agents/) - [Print](/docs/customize/print/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/components/steps/index.md ================ # Steps > An ordered list plus `{.steps}` becomes a numbered procedure with dots and a connecting rule; switch to the steps shortcode when each step needs a heading in the table of contents. --- LLMS index: [llms.txt](/llms.txt) --- Steps are an ordered list with numbered dots and a rule running through them: a plain ordered list plus a `{.steps}` marker line. The dots and the rule are drawn in CSS and no script is loaded. Use it for procedures that have an order. Parallel items with no order belong in a plain list or in cards. There are two spellings: an ordered list plus `{.steps}` (the default choice), and the `{{% steps %}}` shortcode, for when each step needs its own heading and those headings belong in the table of contents. ## Shortest form {#minimal} Write `1.` for every item and let Markdown do the counting. Inserting, deleting and reordering steps then needs no renumbering, and the content indent is always three spaces. ```markdown {title="Source"} 1. Install Hugo Extended 1. Clone OINK Starter 1. Start the local preview {.steps} ``` 1. Install Hugo Extended 1. Clone OINK Starter 1. Start the local preview {.steps} `{.steps}` must touch the last line of the list; leave a blank line and it turns into a visible line of braces. ## What goes in a step {#blocks} A list item takes any block content: paragraphs, fenced code, callouts, tables, nested lists, images. Indent it to the item's content column — three spaces. ````markdown {title="Source"} 1. Clone OINK Starter; it is the small project template for the theme. ```bash git clone https://github.com/pgsty/oink-starter my-docs cd my-docs ``` 1. Start the local server. ```bash hugo server ``` > [!NOTE] > The first build fetches the theme through the Go module proxy, which needs > Go on the machine. 1. Replace three things and it is your site. | Where | Replace with | | --- | --- | | `title` in `hugo.yml` | your site name | | `baseURL` in `hugo.yml` | your domain | | `content/` | your content | {.steps} ```` 1. Clone OINK Starter; it is the small project template for the theme. ```bash git clone https://github.com/pgsty/oink-starter my-docs cd my-docs ``` 1. Start the local server. ```bash hugo server ``` > [!NOTE] > The first build fetches the theme through the Go module proxy, which needs > Go on the machine. 1. Replace three things and it is your site. | Where | Replace with | | --- | --- | | `title` in `hugo.yml` | your site name | | `baseURL` in `hugo.yml` | your domain | | `content/` | your content | {.steps} Shortcodes in `{{< … >}}` form — tabs, cards, badges — work inside a list item too. The `{{% … %}}` form does not; see [Limits](#limits). ## Splitting one step per platform {#tabs-in-steps} When one step differs per platform, write the `{tab=}` fences side by side inside that list item and they still assemble into a tab set. `````markdown {title="Source"} 1. Install Hugo Extended. 1. Install the dependencies: ```bash {tab="EL / RHEL" group="stepdemo" value="rpm"} sudo dnf install golang git ``` ```bash {tab="Debian / Ubuntu" value="deb"} sudo apt install golang-go git ``` 1. Run `hugo server` to preview. {.steps} ````` 1. Install Hugo Extended. 1. Install the dependencies: ```bash {tab="EL / RHEL" group="stepdemo" value="rpm"} sudo dnf install golang git ``` ```bash {tab="Debian / Ubuntu" value="deb"} sudo apt install golang-go git ``` 1. Run `hugo server` to preview. {.steps} ## Continuing the numbering {#start} When prose interrupts a procedure, write the first item of the next group with its real number. Markdown emits `start` and the numbering continues from there (up to 40). ```markdown {title="Source"} 4. Configure `baseURL` and the deployment workflow. 1. Push to `main` and wait for GitHub Actions to finish. {.steps} ``` 4. Configure `baseURL` and the deployment workflow. 1. Push to `main` and wait for GitHub Actions to finish. {.steps} ## Steps with headings {#shortcode} When the procedure is long and each step deserves a heading that can be linked to and collected by the table of contents, use `{{% steps %}}`: its body is page-level Markdown, every direct child heading is one step, and the body is not indented. The three headings below appear in this page's table of contents. ```markdown {title="Source"} {{% steps %}} ### Install the toolchain {#install-toolchain} You need Hugo Extended ≥ 0.160.1 and Go. ### Run the server {#run-server} {{< tabs group="oink-os" default="macos" >}} {{< tab label="macOS" value="macos" >}} `brew install hugo go` {{< /tab >}} {{< tab label="Debian" value="debian" >}} `sudo apt install hugo golang-go` {{< /tab >}} {{< /tabs >}} ### Publish {#publish} Push to `main`; the workflow the repository ships builds and publishes. {{% /steps %}} ``` ### Install the toolchain {#install-toolchain} You need Hugo Extended ≥ 0.160.1 and Go. ### Run the server {#run-server} **macOS** `brew install hugo go` **Debian** `sudo apt install hugo golang-go` ### Publish {#publish} Push to `main`; the workflow the repository ships builds and publishes. This is the theme's only `{{% … %}}` shortcode. The percent form hands its body to Goldmark as page-level Markdown, which is the only way its headings can reach the table of contents and the only way container shortcodes such as `tabs`, `cards` and `fields` can live inside it. The price is that it cannot nest inside a list item or inside another percent container. Keep the headings of one procedure at one level, and never nest one `steps` inside another. ## Which form to use {#which} | Situation | Use | | --- | --- | | A step is a sentence or two plus a command | ordered list + `{.steps}` | | Each step needs a heading, a link and a place in the TOC | `{{% steps %}}` | | A step must contain a `tabs`, `cards` or `fields` container | `{{% steps %}}` | | The procedure itself has to nest inside another list item | ordered list + `{.steps}` | ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | The native form is `
    ` with numbers and rule drawn in CSS; the shortcode form is `
    ` plus headings | | Print | Numbers and content unchanged, the rule stays | | Markdown | The source as written: an ordered list plus `{.steps}`, or headings plus bodies | | RSS | A static list or titled sections | No script; with JavaScript off nothing changes. ## Parameter reference {#reference} Neither form takes parameters — only conventions: | Spelling | Where | Description | | --- | --- | --- | | `{.steps}` | line below the ordered list | Required; has no effect on an unordered list | | `1.` | every item | Let Markdown count; the content indent is always three spaces | | `4.` (first item) | first item | Emits `
      ` and continues from 4; supported for 2–40 | | `{{% steps %}}` | around a set of headings | Direct child headings (`##`–`######`) are the steps; the body is not indented | {.fields meta="-"} ## Limits {#limits} - No `{{% … %}}` inside a list item: the multi-line output of a percent shortcode truncates the list. To put a container in a step, switch the whole procedure to the shortcode form. - `{{% steps %}}` cannot go inside a list item, nor inside another percent container. - The marker must touch the list: no blank line between the list and `{.steps}`. Wrap it in `` / `` when a formatter like Prettier is in play. - `{.steps}` applies to ordered lists only: on a `-` list there are no numbers. - Steps do not fold and do not track progress: no "done" state, no expanding or collapsing. ## Related {#related} - [Tabs](/docs/components/tabs/) — commands split per platform - [Callouts](/docs/components/callout/) — prerequisites and warnings inside a step - [Code blocks](/docs/components/code/) — the commands in a step - [Cards](/docs/components/cards/) — "what next" once the procedure is done --- Backlinks: - [Oink v0.2.0](/blog/release/0.2.0/) - [Asciinema](/docs/components/asciinema/) - [Callouts](/docs/components/callout/) - [Cards](/docs/components/cards/) - [Fields](/docs/components/fields/) - [Infographic](/docs/components/infographic/) - [Kbd](/docs/components/kbd/) - [Mermaid](/docs/components/mermaid/) - [Tabs](/docs/components/tabs/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/components/cards/index.md ================ # Cards > A link list plus `{.cards}` lays out a grid of navigation cards; switch to the shortcode when you need icons, badges or images. --- LLMS index: [llms.txt](/llms.txt) --- Cards are a set of parallel links: each card is a linked title plus a sentence, and the grid adapts to the container width. They suit section landing pages, "what to read next", and a handful of parallel entry points. They do not suit running prose (use paragraphs) or a wall of images (use a [gallery](/docs/components/gallery/)). ## Shortest form {#minimal} A link list with `{.cards}` is a card grid. The link is the title; whatever follows ` — ` is the description. ```markdown {title="Source"} - [Get started](/docs/start/) — Clone this documentation site, delete what you do not need, replace the site details with your own. - [Authoring](/docs/write/) — How pages are organized and which front matter keys exist. - [Customization](/docs/customize/) — Navigation, search, branding, languages. {.cards} ``` - [Get started](/docs/start/) — Clone this documentation site, delete what you do not need, replace the site details with your own. - [Authoring](/docs/write/) — How pages are organized and which front matter keys exist. - [Customization](/docs/customize/) — Navigation, search, branding, languages. {.cards} The whole card is the click target, not just the title text. There is no `columns` parameter: the column count follows the container width and collapses to one on a narrow screen. ## Title-only cards {#title-only} The description is optional. One link per line, `{.cards}` at the end. ```markdown {title="Source"} - [Callouts](/docs/components/callout/) - [Tabs](/docs/components/tabs/) - [Steps](/docs/components/steps/) - [Fields](/docs/components/fields/) {.cards} ``` - [Callouts](/docs/components/callout/) - [Tabs](/docs/components/tabs/) - [Steps](/docs/components/steps/) - [Fields](/docs/components/fields/) {.cards} ## Loose lists and longer descriptions {#loose} When a sentence is not enough, switch to a loose list: the link is its own paragraph, the description another, with a blank line between items. The title takes its own line and the description sits under it. `{.cards}` still has to touch the last paragraph — **no blank line** in between. ```markdown {title="Source"} - [Front matter](/docs/write/frontmatter/) Every page parameter is defined here exactly once: type, default, accepted values, and the page that explains it. - [Configuration](/docs/customize/config/) Site parameters grouped by feature, each row linking back to the guide that explains it. {.cards} ``` - [Front matter](/docs/write/frontmatter/) Every page parameter is defined here exactly once: type, default, accepted values, and the page that explains it. - [Configuration](/docs/customize/config/) Site parameters grouped by feature, each row linking back to the guide that explains it. {.cards} ## Icons and badges {#icon-badge} A link list has no icons, badges, images or multi-paragraph descriptions; those need the `cards` / `card` shortcode. `icon` is exactly one Font Awesome class pair and `badge` is plain text. ```markdown {title="Source"} {{< cards >}} {{< card title="Get started" link="/docs/start/" icon="fa-solid fa-rocket" badge="start here" >}} Use OINK Starter and establish a local preview before customizing. {{< /card >}} {{< card title="Release and download pages" link="/docs/write/releases/" icon="fa-solid fa-box-open" badge="v0.5" >}} A `release` fact record, an asset table and checksums — all generated locally. {{< /card >}} {{< card title="Keyboard navigation" link="/docs/customize/keyboard/" icon="fa-solid fa-keyboard" >}} Site-wide shortcuts and focus order. {{< /card >}} {{< /cards >}} ``` - [Get started](/docs/start/) (start here) — Use OINK Starter and establish a local preview before customizing. - [Release and download pages](/docs/write/releases/) (v0\.5) — A `release` fact record, an asset table and checksums — all generated locally. - [Keyboard navigation](/docs/customize/keyboard/) — Site-wide shortcuts and focus order. An icon that is not one valid Font Awesome class pair warns and is dropped in ordinary preview; strict publishing rejects the warning. ## Markdown bodies {#markdown-body} A `card` body renders as page-level Markdown: inline code, emphasis, links, lists. Parameters such as `title` and `badge` are plain text and are not parsed as Markdown. ```markdown {title="Source"} {{< cards >}} {{< card title="Hugo Module" icon="fa-brands fa-golang" >}} `hugo mod get github.com/pgsty/oink`. The recommended way; upgrading is one version line. {{< /card >}} {{< card title="Git submodule" icon="fa-solid fa-code-branch" >}} No Go installation needed: - `git submodule add` - the theme lands in `themes/oink` {{< /card >}} {{< /cards >}} ``` - **Hugo Module** — `hugo mod get github.com/pgsty/oink`. The recommended way; upgrading is one version line. - **Git submodule** — No Go installation needed: - `git submodule add` - the theme lands in `themes/oink` A card without `link` renders as a bold title and produces no link. ## Cards with images {#image} `image` resolves in the same order as `![alt](src)`: page resource → global resource in `assets/` → static path `/images/…` → remote URL. Local resources carry their intrinsic size so nothing shifts while loading. `image` needs one source of alternative text: `image_alt="…"` for an informative image or `decorative=true` for a decorative one. Writing both warns and keeps the alt text; writing neither warns and renders the image decorative. Strict publishing rejects either warning. ```markdown {title="Source"} {{< cards >}} {{< card title="The OINK shell" link="/docs/about/features/" image="/images/oink.webp" image_alt="An OINK documentation page: sidebar, article and table of contents" >}} Sidebar, article, table of contents — each can be turned off on its own. {{< /card >}} {{< card title="Release notes" link="/docs/write/releases/" image="/images/releasenote.webp" decorative=true >}} A decorative cover: `decorative=true` emits an empty alt and screen readers skip it. {{< /card >}} {{< /cards >}} ``` - [The OINK shell](/docs/about/features/) — Sidebar, article, table of contents — each can be turned off on its own. - [Release notes](/docs/write/releases/) — A decorative cover: `decorative=true` emits an empty alt and screen readers skip it. Card images do not take part in [image zoom](/docs/components/image/#zoom) — the whole card is already a link. ## Automatic cards on section pages {#section-index} A section landing page (`_index.md`) needs no hand-written card list: the theme reads each child page's `title`, `description` and `icon` and generates the cards. This site turns it on globally in `hugo.yml`: ```yaml {title="hugo.yml"} params: ui: section_index: cards # list | cards ``` One section can override it in its own front matter, or push the choice down a whole subtree with `cascade`: ```yaml {title="content/docs/customize/_index.md"} section_index: list ``` Automatic and hand-written cards share the `td-content-card` styling; only the data source differs. Do not hand-write a list of child pages on a section page — it drifts out of step with the sidebar. Hand-write cards only when the set is not this section's children (external links mixed in, cross-section recommendations). The keys are defined in [Configuration](/docs/customize/config/). ## Which form to use {#forms} | What you want | Which form | | --- | --- | | A grid of links with one-sentence descriptions | `{.cards}` link list | | Icons, badges, images | `cards` / `card` shortcode | | Lists, code or several paragraphs in the description | `cards` / `card` shortcode | | A card with no link | `cards` / `card` shortcode | | This section's child pages | nothing at all — `section_index: cards` | A link list is still a link list on GitHub; a shortcode is not. Use the native form whenever it is enough. ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | Native form: `
        `. Shortcode form: `
        ` with one `
        ` each. Both are pure CSS grids and load no script | | Print | The native form stacks; the shortcode form collapses to two columns; in both, a card avoids breaking across pages | | Markdown | The native form keeps the link list; the shortcode form emits `- [Title](link) (badge) — description` | | RSS | The same markup as HTML — a readable list of links without site CSS | ## Parameter reference {#reference} The native form: | Element | Type | Default | Description | | --- | --- | --- | --- | | `{.cards}` | list attribute line | — | On the line **after** an unordered list; unordered lists only | | First link in an item | Markdown link | — | The card title and the whole card's click target | | Everything else | Markdown | — | The description: after ` — ` in a tight list, its own paragraph in a loose one | {.fields meta="type default"} `card` parameters (`cards` itself takes none): | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `title` | plain text | — | Required, non-empty. The card title | | `link` | URL | — | Site path, relative path, `http(s):`, `mailto:`; external links get `rel="noopener"` | | `icon` | Font Awesome class pair | — | For example `fa-solid fa-rocket`; a malformed value warns and is dropped | | `badge` | plain text | — | A small label beside the title | | `image` | image source | — | Page resource / global resource / static path / remote URL | | `image_alt` | plain text | — | With `image`, exactly one of this and `decorative` | | `decorative` | boolean | `false` | `true` marks a decorative image and emits an empty alt | | Body | Markdown | — | The card description | {.fields meta="type default"} There is no `cols`, `columns`, `accent`, `desc` or `color` parameter. Unknown parameters warn and are ignored in ordinary preview; strict publishing rejects the warning. ## Limits {#limits} - `{.cards}` recognizes unordered lists only: on an ordered list it does nothing. - `{.cards}` must touch the list: a blank line in between, or indenting it into a list item, drops the marker silently — the build succeeds and the list stays a list. Check that line first when the output is not a card grid. - A `card` lives only inside `cards`: alone, or inside another shortcode, it warns and is skipped; strict publishing rejects the warning. - The column count is not configurable: the grid adapts to the container. Only automatic section cards take a count, through `params.ui.section_index_columns`. - Cards are not for long text: when a description runs past two lines, use a paragraph or a [callout](/docs/components/callout/). ## Related {#related} - [Fields](/docs/components/fields/) — also has a native form and a shortcode form - [Galleries](/docs/components/gallery/) — a grid of images - [Badges](/docs/components/badge/) — inline status labels - [Organizing content](/docs/write/organize/) — sections, weights and landing pages - [Configuration](/docs/customize/config/) — `section_index` and friends --- Backlinks: - [Badge](/docs/components/badge/) - [Gallery](/docs/components/gallery/) - [Images](/docs/components/image/) - [Infographic](/docs/components/infographic/) - [Steps](/docs/components/steps/) - [Writing pages](/docs/write/pages/) ================ Source: https://oink.pgsty.com/docs/components/filetree/index.md ================ # FileTree > A `filetree` fence draws an annotated directory structure — aligned comment column, per-entry icons, collapsible directories, a draggable split. --- LLMS index: [llms.txt](/llms.txt) --- A file tree is a `filetree` fence whose body is the listing itself: indentation is depth, a trailing `/` marks a directory, and everything after `#` is a comment. Use it to explain the part of a directory structure that concerns the reader, one annotation at a time. When the reader has to copy the listing verbatim, use an ordinary code block. ## Shortest form {#minimal} ````markdown {title="Source"} ```filetree - content/ - _index.md - docs/ - blog/ - hugo.yml - go.mod ``` ```` ```filetree - content/ - _index.md - docs/ - blog/ - hugo.yml - go.mod ``` Bullets (`-`, `*`, `+`) may be omitted; the result is the same. An entry with children is a directory. Without children, a trailing `/` tells the theme it is one. ## Adding comments {#comments} Everything after the first whitespace-preceded `#` on a line is a comment, rendered as an aligned right-hand column. Comments are plain text, so Markdown inside them shows literally; for a literal hash write `\#`. ````markdown {title="Source"} ```filetree - content/ # every page, both languages in one directory - docs/ # the documentation tree you are reading - blog/ # release notes and articles - assets/scss/ # the site's own SCSS, overriding theme variables - layouts/ # site-level template overrides, the fewer the better - static/images/ # images that need no build-time processing - hugo.yml # site configuration: languages, menus, params.ui ``` ```` ```filetree - content/ # every page, both languages in one directory - docs/ # the documentation tree you are reading - blog/ # release notes and articles - assets/scss/ # the site's own SCSS, overriding theme variables - layouts/ # site-level template overrides, the fewer the better - static/images/ # images that need no build-time processing - hugo.yml # site configuration: languages, menus, params.ui ``` Where the comment column starts is computed at build time from the widest row, so every `#` begins at the same column whether or not the source lines up. The comment column takes at most the right half of the panel and at least three tenths. The dashed rule between them is a splitter you can drag, or focus with Tab and move with the arrow keys (Home / End go to the extremes). Overlong names and comments are truncated with an ellipsis inside their own column, and hovering shows the full text through `title`. The splitter is the file tree's only JavaScript, and only a tree **with comments** loads it. ````markdown {title="Source"} ```filetree {title="truncation in both columns"} - runbooks/ - a-deliberately-long-runbook-filename-for-a-failover-drill.md # an equally overlong comment, kept on one line so it has to be clipped inside the comment column - restart.md # short ``` ```` ```filetree {title="truncation in both columns"} - runbooks/ - a-deliberately-long-runbook-filename-for-a-failover-drill.md # an equally overlong comment, kept on one line so it has to be clipped inside the comment column - restart.md # short ``` ## Title bars {#title} The fence attribute `{title="…"}` renders a title bar above the tree; without it there is none. ````markdown {title="Source"} ```filetree {title="the oink.pgsty.com repository root"} - content/ # pages - assets/ # resources that take part in the build - data/ # data for the home page, landings and downloads - layouts/ # template overrides - static/ # files copied verbatim - tests/ # Playwright and node --test - hugo.yml - go.mod # the theme, imported as a Hugo Module - Makefile # make d / make b / make c ``` ```` ```filetree {title="the oink.pgsty.com repository root"} - content/ # pages - assets/ # resources that take part in the build - data/ # data for the home page, landings and downloads - layouts/ # template overrides - static/ # files copied verbatim - tests/ # Playwright and node --test - hugo.yml - go.mod # the theme, imported as a Hugo Module - Makefile # make d / make b / make c ``` ## Indentation and depth {#indent} Depth comes from indentation. Two spaces, four spaces, or tabs (counted as four columns) all work and need not be consistent within one tree, as long as every level you return to has been opened before. Output from the `tree` command can be pasted whole, root line and summary line included — the summary is dropped. ````markdown {title="Source"} ```filetree content/docs ├── about │ ├── _index.md │ └── features.md ├── components │ ├── filetree.md │ └── image │ └── index.md └── _index.md 3 directories, 5 files ``` ```` ```filetree content/docs ├── about │ ├── _index.md │ └── features.md ├── components │ ├── filetree.md │ └── image │ └── index.md └── _index.md 3 directories, 5 files ``` Returning to an indentation level that was never opened warns and skips that line; the message carries the line number inside the fence, and strict publishing rejects it. ## Folding and explicit types {#dir-file} A directory with children is open by default; `{open=false}` starts it closed. Directories render as native `
        `, so they are keyboard-operable without JavaScript. `open` is valid on directories only. An entry with no children whose name does not end in `/` is treated as a file; `{type=dir}` overrides that, and `{type=file}` the other way. ````markdown {title="Source"} ```filetree {title="the content directory"} - content/ - docs/ # the documentation tree - components/ # 22 component pages {open=false} - callout.md - filetree.md - image/ # page bundle: body + images {type=dir} - customize/ # site-level configuration {open=false} - config.md - blog/ - release.md ``` ```` ```filetree {title="the content directory"} - content/ - docs/ # the documentation tree - components/ # 22 component pages {open=false} - callout.md - filetree.md - image/ # page bundle: body + images {type=dir} - customize/ # site-level configuration {open=false} - config.md - blog/ - release.md ``` ## Icons and tones {#icon-tone} Icons are inferred from the name: directories get a folder icon that follows the open state; files are matched first by full filename (`LICENSE`, `Makefile`, `go.mod`, `package.json`, `.gitignore` …), then by extension (`md yml toml json sh py go js sql css png svg pdf zip` …), and otherwise get a generic file icon. `{icon=…}` overrides it and takes exactly one Font Awesome class pair. `{tone=…}` colours the icon, using the same vocabulary as [badges](/docs/components/badge/): `neutral` `info` `success` `warning` `danger`. ````markdown {title="Source"} ```filetree {title="deployment layout: permissions and what matters"} - /etc/pigsty/ # 0755 root:root · configuration root {icon="fa-solid fa-server" tone=info} - pigsty.yml # 0644 root:root · cluster inventory - ca/ # 0700 root:root · self-signed CA, never commit {icon="fa-solid fa-lock" tone=danger open=false} - ca.key # 0600 root:root - /var/lib/pgsql/18/data/ # 0700 postgres:postgres · data directory {tone=warning} - postgresql.conf # 0600 postgres:postgres - /usr/bin/pig # 0755 root:root · command-line tool {icon="fa-solid fa-terminal" tone=success} ``` ```` ```filetree {title="deployment layout: permissions and what matters"} - /etc/pigsty/ # 0755 root:root · configuration root {icon="fa-solid fa-server" tone=info} - pigsty.yml # 0644 root:root · cluster inventory - ca/ # 0700 root:root · self-signed CA, never commit {icon="fa-solid fa-lock" tone=danger open=false} - ca.key # 0600 root:root - /var/lib/pgsql/18/data/ # 0700 postgres:postgres · data directory {tone=warning} - postgresql.conf # 0600 postgres:postgres - /usr/bin/pig # 0755 root:root · command-line tool {icon="fa-solid fa-terminal" tone=success} ``` `tone` colours the icon only, never the text. Colour is a supplement; the meaning belongs in the name or the comment. ## Linked entries {#link} Write an entry name as `[name](link)` to make it a link. Site paths, relative paths and `http(s):` all work, under the same URL validation as every other component. ````markdown {title="Source"} ```filetree {title="this site's component pages"} - content/docs/ - [callout.md](/docs/components/callout/) # callouts - [filetree.md](/docs/components/filetree/) # this page - [gallery.md](/docs/components/gallery/) # galleries - image/ # page bundle - [index.md](/docs/components/image/) # images - [hugo.yml](https://github.com/pgsty/oink/blob/main/tests/site/hugo.yaml) # fixture configuration on GitHub ``` ```` ```filetree {title="this site's component pages"} - content/docs/ - [callout.md](/docs/components/callout/) # callouts - [filetree.md](/docs/components/filetree/) # this page - [gallery.md](/docs/components/gallery/) # galleries - image/ # page bundle - [index.md](/docs/components/image/) # images - [hugo.yml](https://github.com/pgsty/oink/blob/main/tests/site/hugo.yaml) # fixture configuration on GitHub ``` ## One tree per platform {#tabs} A fence carrying `tab=` (and `group=` / `value=`) becomes one panel of a [tab set](/docs/components/tabs/) and can sit alongside code fences. ````markdown {title="Source"} ```filetree {tab="Linux" group="platform" value="linux"} - /etc/pigsty/ # configuration - /var/lib/pgsql/ # data - /usr/bin/pig # executable ``` ```filetree {tab="macOS" value="macos"} - ~/Library/Application Support/pigsty/ # configuration - /opt/homebrew/bin/pig # executable ``` ```` ```filetree {tab="Linux" group="platform" value="linux"} - /etc/pigsty/ # configuration - /var/lib/pgsql/ # data - /usr/bin/pig # executable ``` ```filetree {tab="macOS" value="macos"} - ~/Library/Application Support/pigsty/ # configuration - /opt/homebrew/bin/pig # executable ``` ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | `
        `, an optional title bar, directories as native `
        `; a tree with comments also gets the draggable splitter, its only runtime | | Print | The same tree, fully expanded, no splitter, comments wrapped instead of truncated | | Markdown | The `filetree` fence, emitted as written | | RSS | The fence source inside a `
        ` |
        
        Below the `sm` breakpoint the layout collapses to a single column: comments move
        under the name, stop being truncated, and the splitter is hidden. A tree without
        comments is single-column and loads no script at all.
        
        ## Parameter reference {#reference}
        
        Fence attributes, after ```` ```filetree ````:
        
        | Attribute | Type | Default | Description |
        | --- | --- | --- | --- |
        | `title` | plain text | — | Title bar above the tree; omitted when absent; must not be empty |
        | `tab` | plain text | — | Makes this tree one panel of a tab set |
        | `group` / `value` | string | — | Tab group and sync value; must appear with `tab` |
        | `class` | class list | — | Passed through for site CSS |
        {.fields meta="type default"}
        
        Entry attributes, in the `{…}` at the end of a line:
        
        | Attribute | Type | Default | Description |
        | --- | --- | --- | --- |
        | `icon` | Font Awesome class pair | matched by name / extension | For example `fa-solid fa-lock`; a malformed value warns and uses the default icon |
        | `tone` | enum | `neutral` | `neutral` `info` `success` `warning` `danger`; colours the icon only |
        | `open` | boolean | `true` | Directories only; `false` starts it closed |
        | `type` | enum | inferred | `dir` or `file`, overriding the inference |
        {.fields meta="type default"}
        
        The line syntax itself:
        
        | Element | Description |
        | --- | --- |
        | Indentation | Two spaces / four spaces / tabs / the `│ ├── └──` drawing from `tree` |
        | `- name` | The bullet is optional; `-`, `*` and `+` are equivalent |
        | `name/` | A trailing slash marks a directory; the name renders as written, slash kept |
        | `[name](url)` | A linked entry |
        | `# comment` | Everything after the first whitespace-preceded `#`; `\#` is a literal hash |
        | `N directories, M files` | The `tree` summary line, dropped automatically |
        {.fields}
        
        Unknown attributes and values, `open` on a file, malformed `{…}`, and a dedent
        to an unopened level all warn and take a safe fallback or skip the bad line.
        The message names the line; strict publishing rejects the warning.
        
        ## Limits {#limits}
        
        - The `filetree` fence is the only form: there is no `{.filetree}` list marker
          and no shortcode.
        - Names and comments are plain text: `**bold**` shows literally, so the fence
          source reads correctly anywhere.
        - Nothing is read from disk: the tree is static content you write or paste, and
          it does not follow the repository.
        - No search, no multi-select, no copy-the-whole-tree: when the reader has to
          copy it verbatim, use a code block.
        - The split position is not persisted: after a reload it returns to the width
          computed at build time.
        
        ## Related {#related}
        
        - [Code blocks](/docs/components/code/) — listings meant to be copied verbatim
        - [Tabs](/docs/components/tabs/) — one tree per platform, side by side
        - [Badges](/docs/components/badge/) — `tone` uses the same vocabulary
        - [Organizing content](/docs/write/organize/) — how a real content directory is laid out
        
        ---
        
        Backlinks:
        
        - [Badge](/docs/components/badge/)
        - [Gallery](/docs/components/gallery/)
        - [Markmap](/docs/components/markmap/)
        - [Print](/docs/customize/print/)
        - [Writing pages](/docs/write/pages/)
        
        
        ================
        Source: https://oink.pgsty.com/docs/components/math/index.md
        ================
        
        # Math
        
        > Inline and display mathematics with KaTeX, rendered at build time — the reader downloads no script.
        
        ---
        
        LLMS index: [llms.txt](/llms.txt)
        
        ---
        
        Mathematics is rendered by KaTeX at build time into HTML + MathML. A page with
        formulas gains one local KaTeX stylesheet and nothing else — no JavaScript, no
        request to a remote maths service. Inline formulas are `\(…\)`, display
        formulas are `$$…$$` or `\[…\]`, and there are `math` and `chem` fences. For
        TikZ drawings or macro packages KaTeX does not support, use a pre-rendered
        [image](/docs/components/image/).
        
        ## Shortest form {#minimal}
        
        An inline formula sits inside a sentence, with the surrounding spaces and
        punctuation outside the delimiters.
        
        ```markdown {title="Source"}
        The shared buffer hit ratio is \(\mathrm{hit} = \frac{H}{H + R}\), where \(H\) is `blks_hit` and \(R\) is `blks_read`.
        ```
        
        The shared buffer hit ratio is \(\mathrm{hit} = \frac{H}{H + R}\), where \(H\) is `blks_hit` and \(R\) is `blks_read`.
        
        ## Display formulas {#display}
        
        A formula in its own paragraph goes between `$$`, centred and set larger.
        `\[…\]` is equivalent.
        
        ```markdown {title="Source"}
        A B-tree with fan-out \(f\) over \(N\) keys has height:
        
        $$
        h = \left\lceil \log_{f} N \right\rceil
        $$
        ```
        
        A B-tree with fan-out \(f\) over \(N\) keys has height:
        
        $$
        h = \left\lceil \log_{f} N \right\rceil
        $$
        
        A formula too long for one line scrolls horizontally inside the reading column
        rather than widening the layout; in print it stays static.
        
        ## The `math` fence {#math-fence}
        
        The `math` fence is another way to write a display formula, and it does not
        depend on the site's passthrough configuration. On GitHub the source is an
        ordinary code block.
        
        ````markdown {title="Source"}
        ```math
        N_{\text{conn}} = \lambda \cdot \bar{t}_{\text{resp}}
        ```
        ````
        
        ```math
        N_{\text{conn}} = \lambda \cdot \bar{t}_{\text{resp}}
        ```
        
        That is Little's law applied to a connection pool: in steady state, the
        concurrency you need is the arrival rate times the mean response time. A pool is
        usually far smaller than the number of clients.
        
        ## Chemistry and units {#chem}
        
        The `chem` fence uses KaTeX's mhchem extension, and its body is written
        `\ce{…}`. The same extension typesets physical units.
        
        ````markdown {title="Source"}
        ```chem
        \ce{CO2 + H2O <=> H2CO3 <=> H+ + HCO3^-}
        ```
        ````
        
        ```chem
        \ce{CO2 + H2O <=> H2CO3 <=> H+ + HCO3^-}
        ```
        
        For the syntax see the [mhchem manual](https://mhchem.github.io/MathJax-mhchem/).
        
        ## Numbered equations {#numbered}
        
        An attribute line under a display formula makes it a numbered equation. `num` is
        a string the author writes (`3-1`, `5.3`) — the theme never counts — and `#id`
        defaults to `eq-`. The number shows to the right of the formula with a
        localized "Equation" prefix.
        
        ```markdown {title="Source"}
        $$
        \text{WAL}_{\text{day}} \approx \text{TPS} \times \bar{s}_{\text{record}} \times 86400
        $$
        {#eq-wal num="3-1" caption="Estimating daily WAL volume"}
        
        See [Equation 3-1](#eq-wal): multiply by the retention period for the floor on archive disk size.
        ```
        
        $$
        \text{WAL}_{\text{day}} \approx \text{TPS} \times \bar{s}_{\text{record}} \times 86400
        $$
        {#eq-wal num="3-1" caption="Estimating daily WAL volume"}
        
        See [Equation 3-1](#eq-wal): multiply by the retention period for the floor on
        archive disk size.
        
        `caption` (plain text) is optional. `#id` and `caption` must appear with `num` —
        there is no half-numbered equation. Incomplete or duplicate targets warn and
        drop the unusable part or keep the first; strict publishing rejects the warning.
        
        ## Cross references {#xref}
        
        The prose can reference a numbered equation with an ordinary link, as the
        previous section does. For a cross-page reference, or when the "Equation N"
        label should be filled in automatically, use `xref`:
        
        ```markdown {title="Source"}
        Capacity planning starts from {{< xref eq="3-1" anchor="eq-wal" />}}.
        ```
        
        Capacity planning starts from [Equation 3-1](#eq-wal).
        
        `xref` may appear before its target; forward references are legal. For a
        book-wide list of equations and the `book-equations` index, see
        [publishing books](/docs/write/book/).
        
        ## The `eq` shortcode {#eq-shortcode}
        
        `eq` exists for sites that cannot enable passthrough; its body goes to the same
        KaTeX renderer. Without parameters it is a display formula that registers no
        number; with `num` it is equivalent to the attribute-line form above.
        
        ```markdown {title="Source"}
        {{< eq >}}\sigma_{\text{idx}} = \frac{\text{rows}_{\text{matched}}}{\text{rows}_{\text{total}}}{{< /eq >}}
        
        {{< eq num="3-2" caption="Where a sequential scan and an index scan cost the same" >}}
        c_{\text{seq}} \cdot P = c_{\text{rand}} \cdot \sigma \cdot T
        {{< /eq >}}
        ```
        
        $$
        \sigma_{\text{idx}} = \frac{\text{rows}_{\text{matched}}}{\text{rows}_{\text{total}}}
        $$
        
        **Equation 3-2.** Where a sequential scan and an index scan cost the same
        
        $$
        c_{\text{seq}} \cdot P = c_{\text{rand}} \cdot \sigma \cdot T
        $$
        
        This site has passthrough on, so day-to-day writing uses `$$`. `eq` is for
        migrated manuscripts and for sites that cannot change `hugo.yml`.
        
        ## Site prerequisites {#config}
        
        The `math` and `chem` fences need no configuration. The `$$`, `\[…\]` and
        `\(…\)` delimiters depend on Goldmark's passthrough extension. Hugo does not
        merge a theme's `markup` configuration, so this block has to live in the site's
        own configuration file. This site uses:
        
        ```yaml {title="hugo.yml"}
        markup:
          goldmark:
            parser:
              attribute:
                block: true # numbered equations need the attribute line
            extensions:
              passthrough:
                enable: true
                delimiters:
                  block: [['\[', '\]'], ['$$', '$$']]
                  inline: [['\(', '\)']]
        ```
        
        Every key is defined in
        [Configuration](/docs/customize/config/). Delimiters must
        not collide with the prose: a single `$` is deliberately not configured, so a
        price like "$5" is never read as mathematics.
        
        ## Output {#outputs}
        
        | Output | Shape |
        | --- | --- |
        | HTML | KaTeX HTML + MathML rendered at build time; this page also loads a local `katex.min.css`, which pages without formulas never load |
        | Print | Same as HTML, static, long formulas do not scroll |
        | Markdown | The source as written: `$$` blocks with their attribute line, `math` / `chem` fences, `\(…\)`; the `eq` shortcode emits `**Equation 3-2.** caption` plus a `$$` block |
        | RSS | The same static text as Markdown |
        
        No form loads JavaScript.
        
        ## Parameter reference {#reference}
        
        Four spellings:
        
        | Spelling | Placement | Description |
        | --- | --- | --- |
        | `\(…\)` | inline | Governed by the site's passthrough configuration; takes no attributes |
        | `$$…$$` / `\[…\]` | display | As above; may be followed by an attribute line to become numbered |
        | ```` ```math ```` | display fence | Independent of passthrough; takes no attributes |
        | ```` ```chem ```` | display fence | As above, with `\ce{…}` in the body |
        {.fields}
        
        The attribute line `{…}` under a display formula:
        
        | Parameter | Type | Default | Description |
        | --- | --- | --- | --- |
        | `num` | string | — | `[0-9A-Za-z.-]+`; registers a numbered equation and shows "Equation N" at the right |
        | `#id` | identifier | `eq-` | `[A-Za-z][A-Za-z0-9_.:-]*`; the anchor and cross-reference target |
        | `caption` | plain text | — | Caption after the number; requires `num` |
        {.fields meta="type default"}
        
        The `eq` shortcode:
        
        | Parameter | Type | Default | Description |
        | --- | --- | --- | --- |
        | `num` | string | — | As above; without it the formula is an unnumbered display formula |
        | `id` | identifier | `eq-` | Requires `num` |
        | `caption` | plain text | — | Requires `num` |
        | `class` | class list | — | Requires `num`; passed through for site CSS |
        | Body | TeX | — | Required, non-empty |
        {.fields meta="type default"}
        
        Broken TeX warns and leaves the expression as written in ordinary preview. The
        message carries KaTeX's detail and the source position; strict publishing
        rejects the warning.
        
        ## Limits {#limits}
        
        - Delimiters are a site decision: whether `$$`, `\[…\]` and `\(…\)` render
          depends solely on the passthrough extension in the site's `markup.goldmark`.
          The theme does not read a `math: true` front matter key, and without the
          configuration `$$` shows literally. The `math` fence and `eq` route around it.
        - Only `$$` blocks and `eq` can be numbered: the `math` fence takes no attribute
          line, so switch spelling when you need a number.
        - Numbers are hand-written: the theme neither counts nor renumbers, so
          reordering chapters means editing `num`.
        - Inline formulas take no attributes: the attribute line applies to display
          formulas only.
        - `caption` is plain text: Markdown inside it is not parsed.
        
        ## Related {#related}
        
        - [Code blocks](/docs/components/code/) — fence attributes and numbered examples
        - [Images](/docs/components/image/) — figures use the same `{#id num=}` numbering
        - [Publishing books](/docs/write/book/) — lists of equations and cross-page references
        - [Configuration](/docs/customize/config/) — the `markup.goldmark` keys
        
        ---
        
        Backlinks:
        
        - [Troubleshooting](/docs/admin/troubleshooting/)
        - [Upgrade](/docs/admin/upgrade/)
        - [Components](/docs/components/)
        - [Print](/docs/customize/print/)
        - [From scratch](/docs/start/from-scratch/)
        - [Books](/docs/write/book/)
        - [Writing pages](/docs/write/pages/)
        
        
        ================
        Source: https://oink.pgsty.com/docs/components/mermaid/index.md
        ================
        
        # Mermaid
        
        > A `mermaid` fence turns text into flowcharts, sequence diagrams, Gantt charts, class diagrams and state diagrams — rendered locally, theme-aware, diff-friendly.
        
        ---
        
        LLMS index: [llms.txt](/llms.txt)
        
        ---
        
        A `mermaid` fence renders text as a flowchart, sequence diagram, Gantt chart,
        class diagram, ER diagram or state diagram. The diagram exists as source: it goes
        into Git, it reviews as a diff, and search finds it. Rendering happens in the
        reader's browser with the Mermaid copy the theme ships — no external service is
        contacted. Diagrams that need pixel-level control belong in an SVG, used as an
        [image](/docs/components/image/).
        
        ## Shortest form {#minimal}
        
        ````markdown {title="Source"}
        ```mermaid
        flowchart LR
          content["content/"] --> Hugo
          config["hugo.yml"] --> Hugo
          theme["OINK theme"] --> Hugo
          Hugo --> site["public/"]
        ```
        ````
        
        ```mermaid
        flowchart LR
          content["content/"] --> Hugo
          config["hugo.yml"] --> Hugo
          theme["OINK theme"] --> Hugo
          Hugo --> site["public/"]
        ```
        
        The fence language is `mermaid` and there is no other switch. Only when the
        theme sees such a fence does it add the Mermaid runtime to that page, and ten
        diagrams on one page still load it once.
        
        ## Sequence diagrams {#sequence}
        
        `sequenceDiagram` describes messages between participants over time, which suits
        request paths and load order.
        
        ````markdown {title="Source"}
        ```mermaid
        sequenceDiagram
          autonumber
          participant Reader as Reader's browser
          participant CDN as Static hosting
          participant JS as Page script bundle
          Reader->>CDN: GET /docs/components/mermaid/
          CDN-->>Reader: HTML (a figure plus the fence source)
          Reader->>CDN: GET this page's bundle
          CDN-->>Reader: mermaid.min.js
          JS->>JS: render the fence source into SVG
          Note over JS: runtimes the page never used are not downloaded
        ```
        ````
        
        ```mermaid
        sequenceDiagram
          autonumber
          participant Reader as Reader's browser
          participant CDN as Static hosting
          participant JS as Page script bundle
          Reader->>CDN: GET /docs/components/mermaid/
          CDN-->>Reader: HTML (a figure plus the fence source)
          Reader->>CDN: GET this page's bundle
          CDN-->>Reader: mermaid.min.js
          JS->>JS: render the fence source into SVG
          Note over JS: runtimes the page never used are not downloaded
        ```
        
        ## Gantt charts {#gantt}
        
        `gantt` draws intervals. Below is the five-year community support window of each
        PostgreSQL major version, counted from its release date; `1825d` is five years.
        
        ````markdown {title="Source"}
        ```mermaid
        gantt
          title Five-year community support per PostgreSQL major version
          dateFormat YYYY-MM-DD
          axisFormat %Y
          section PG 15
          released 2022-10-13 :2022-10-13, 1825d
          section PG 16
          released 2023-09-14 :2023-09-14, 1825d
          section PG 17
          released 2024-09-26 :2024-09-26, 1825d
          section PG 18
          released 2025-09-25 :active, 2025-09-25, 1825d
        ```
        ````
        
        ```mermaid
        gantt
          title Five-year community support per PostgreSQL major version
          dateFormat YYYY-MM-DD
          axisFormat %Y
          section PG 15
          released 2022-10-13 :2022-10-13, 1825d
          section PG 16
          released 2023-09-14 :2023-09-14, 1825d
          section PG 17
          released 2024-09-26 :2024-09-26, 1825d
          section PG 18
          released 2025-09-25 :active, 2025-09-25, 1825d
        ```
        
        ## Class and ER diagrams {#class-and-er}
        
        `classDiagram` draws types and relationships, `erDiagram` entities and
        cardinality. Both are common ways to explain a data model.
        
        ````markdown {title="Source"}
        ```mermaid
        classDiagram
          class Page {
            +string Title
            +string Description
            +int Weight
            +Content()
            +OutputFormats()
          }
          class Resource {
            +string Name
            +string RelPermalink
            +Resize(spec)
          }
          class OutputFormat {
            +string Name
            +string MediaType
          }
          Page "1" --> "0..*" Resource : page bundle resources
          Page "1" --> "1..*" OutputFormat : html / print / markdown / rss
        ```
        ````
        
        ```mermaid
        classDiagram
          class Page {
            +string Title
            +string Description
            +int Weight
            +Content()
            +OutputFormats()
          }
          class Resource {
            +string Name
            +string RelPermalink
            +Resize(spec)
          }
          class OutputFormat {
            +string Name
            +string MediaType
          }
          Page "1" --> "0..*" Resource : page bundle resources
          Page "1" --> "1..*" OutputFormat : html / print / markdown / rss
        ```
        
        ````markdown {title="Source"}
        ```mermaid
        erDiagram
          pg_database ||--o{ pg_namespace : "contains schemas"
          pg_namespace ||--o{ pg_class : "contains relations"
          pg_class ||--o{ pg_attribute : "has columns"
          pg_class ||--o{ pg_index : "is indexed by"
          pg_class {
            oid oid PK
            name relname
            char relkind
          }
          pg_attribute {
            oid attrelid FK
            name attname
            smallint attnum
          }
        ```
        ````
        
        ```mermaid
        erDiagram
          pg_database ||--o{ pg_namespace : "contains schemas"
          pg_namespace ||--o{ pg_class : "contains relations"
          pg_class ||--o{ pg_attribute : "has columns"
          pg_class ||--o{ pg_index : "is indexed by"
          pg_class {
            oid oid PK
            name relname
            char relkind
          }
          pg_attribute {
            oid attrelid FK
            name attname
            smallint attnum
          }
        ```
        
        ## State diagrams {#state}
        
        `stateDiagram-v2` draws states and the conditions between them. Below are the
        five states an OINK release passes through. They are not interchangeable, and a
        green local build is none of them.
        
        ````markdown {title="Source"}
        ```mermaid
        stateDiagram-v2
          [*] --> SourceComplete
          SourceComplete --> Validated : theme checks + site suite green
          Validated --> Published : an immutable signed vX.Y.Z tag is pushed
          Published --> Documented : the site's go.mod pins that tag
          Documented --> Deployed : the production build goes live
          Deployed --> [*]
          Published --> SourceComplete : a problem means a new patch version; tags never move
        ```
        ````
        
        ```mermaid
        stateDiagram-v2
          [*] --> SourceComplete
          SourceComplete --> Validated : theme checks + site suite green
          Validated --> Published : an immutable signed vX.Y.Z tag is pushed
          Published --> Documented : the site's go.mod pins that tag
          Documented --> Deployed : the production build goes live
          Deployed --> [*]
          Published --> SourceComplete : a problem means a new patch version; tags never move
        ```
        
        ## Per-diagram title and configuration {#per-diagram-config}
        
        The top of a fence body may carry Mermaid's own YAML header — this is not Hugo
        front matter. `title` gives the diagram a title and `config` overrides Mermaid
        configuration for this diagram alone. A diagram that hard-codes `config.theme`
        no longer follows the site's colour scheme.
        
        ````markdown {title="Source"}
        ```mermaid
        ---
        title: Only the runtimes a page used are bundled
        config:
          flowchart:
            curve: linear
        ---
        flowchart TD
          Page --> Which{which components?}
          Which -->|Mermaid fence| M[mermaid.min.js]
          Which -->|ECharts fence| E[echarts.min.js]
          Which -->|none| B[base bundle only]
        ```
        ````
        
        ```mermaid
        ---
        title: Only the runtimes a page used are bundled
        config:
          flowchart:
            curve: linear
        ---
        flowchart TD
          Page --> Which{which components?}
          Which -->|Mermaid fence| M[mermaid.min.js]
          Which -->|ECharts fence| E[echarts.min.js]
          Which -->|none| B[base bundle only]
        ```
        
        ## Light and dark {#dark-mode}
        
        The theme reads the current colour scheme when the page initializes: in dark
        mode it uses Mermaid's `dark` theme, in light mode the theme the site
        configured. Switching the colour scheme redraws the diagrams in place — the
        page is not reloaded, and each diagram holds its height while it is redrawn,
        so nothing on the page moves under you.
        
        Site-wide defaults go in `hugo.yml` with lowercase keys; the theme matches them
        back to Mermaid's own casing:
        
        ```yaml {title="hugo.yml"}
        params:
          mermaid:
            theme: neutral
            flowchart:
              diagrampadding: 6
        ```
        
        The full key table is in
        [Configuration](/docs/customize/config/); for accepted
        values see the
        [Mermaid configuration reference](https://mermaid.js.org/config/schema-docs/config.html).
        
        ## Inside tabs and steps {#compose}
        
        A `mermaid` fence has no `tab` attribute — adjacent-fence tabs apply to ordinary
        code fences only. To compare two diagrams side by side, use the `tabs`
        shortcode.
        
        ````markdown {title="Source"}
        {{< tabs >}}
        {{< tab label="By data flow" >}}
        ```mermaid
        flowchart LR
          Markdown --> Goldmark --> RenderHooks --> HTML
        ```
        {{< /tab >}}
        {{< tab label="By output format" >}}
        ```mermaid
        flowchart LR
          Page --> HTML
          Page --> Print
          Page --> Markdown
          Page --> RSS
        ```
        {{< /tab >}}
        {{< /tabs >}}
        ````
        
        **By data flow**
        
        ```mermaid
        flowchart LR
          Markdown --> Goldmark --> RenderHooks --> HTML
        ```
        
        **By output format**
        
        ```mermaid
        flowchart LR
          Page --> HTML
          Page --> Print
          Page --> Markdown
          Page --> RSS
        ```
        
        Each step inside `{{% steps %}}` is page-level Markdown and can hold a
        `mermaid` fence; see [Steps](/docs/components/steps/).
        
        ## Output {#outputs}
        
        | Output | Shape |
        | --- | --- |
        | HTML | A `figure` holding an empty stage and the fence source as JSON; the page's Mermaid runtime draws the SVG into it |
        | Print | The source inside `
        `, static — no runtime runs there |
        | Markdown | The `mermaid` fence and its source, kept as written |
        | RSS | The source inside `
        ` — subscribers see text |
        
        ## Parameter reference {#reference}
        
        Fence attributes: none. A `mermaid` fence reads no attribute line; writing
        `{height=…}` or `{class=…}` neither works nor errors. Size follows the diagram
        itself and the container width, and the diagram is centred in it.
        
        Site parameters (`hugo.yml`):
        
        | Parameter | Type | Default | Description |
        | --- | --- | --- | --- |
        | `params.mermaid` | map | unset | The whole map is passed to Mermaid's `initialize()`; write keys in lowercase and the theme matches them back to Mermaid's casing |
        | `params.mermaid.theme` | string | Mermaid's default | The light-mode theme; dark mode forces `dark` |
        {.fields meta="type default"}
        
        Per-diagram configuration goes in the YAML header at the top of the fence body
        (`title`, `config`). That is Mermaid syntax, not a theme parameter.
        
        ## Enlarging a diagram {#zoom}
        
        A diagram is centred in the column, and Mermaid scales anything wider than the
        column down to fit — a wide sequence diagram can land near a third of its own
        size on a phone. Hovering a diagram (or reaching it with the keyboard) reveals
        a control in its corner that opens the diagram on its own: rendered again at
        full size, panned by dragging, zoomed with the wheel, a pinch, or the `+` and
        `-` keys, and reset with `0`. `Esc` closes it. A diagram that would have to
        shrink past half size to fit opens at 1:1 at its starting corner instead of as
        a thumbnail, and zooming back out always reaches the whole diagram however
        large it is. Nothing is downloaded for this and there is no switch to set: the
        viewer ships with the fence.
        
        ## Limits {#limits}
        
        - Diagrams cannot be numbered: Mermaid emits inline SVG, not an ``, so
          `{#id num=}` numbering does not apply. Export to an image when you need a
          number and use the [image](/docs/components/image/) numbering.
        - Fence attributes do nothing: control width inside the diagram (flowchart
          direction, class-diagram layout) or with CSS. There is no alignment
          attribute — a diagram is always centred.
        - Syntax errors show up only in the browser: Hugo does not parse Mermaid, so a
          broken diagram renders an alert carrying the parse error and its own source,
          while the build still passes. Check in a browser before publishing.
        - RSS, Markdown and Print carry the source, not the picture: put the conclusion
          in the prose, not only in the diagram.
        
        ## Related {#related}
        
        - [PlantUML](/docs/components/plantuml/) — more complete UML, at the price of a rendering server
        - [Markmap](/docs/components/markmap/) — outline-shaped hierarchies
        - [ECharts](/docs/components/echarts/) — charts with numbers in them
        - [Images](/docs/components/image/) — hand-drawn SVG and numbering
        
        ---
        
        Backlinks:
        
        - [Components](/docs/components/)
        - [Draw.io](/docs/components/drawio/)
        - [ECharts](/docs/components/echarts/)
        - [Infographic](/docs/components/infographic/)
        - [Markmap](/docs/components/markmap/)
        - [PlantUML](/docs/components/plantuml/)
        - [Print](/docs/customize/print/)
        - [Writing pages](/docs/write/pages/)
        
        
        ================
        Source: https://oink.pgsty.com/docs/components/plantuml/index.md
        ================
        
        # PlantUML
        
        > A `plantuml` fence writes sequence, class, component, activity and use-case diagrams; rendering requires a PlantUML server you configure yourself.
        
        ---
        
        LLMS index: [llms.txt](/llms.txt)
        
        ---
        
        A `plantuml` fence holds PlantUML source. The browser compresses and encodes it,
        appends it to the URL of a PlantUML server, and gets an SVG back. It suits
        sequence, class, component, activity and use-case diagrams that need the full
        expressiveness of UML. Rendering depends on that server: the theme ships no
        default endpoint. `enable: true` without `svg_image_url` warns and leaves
        PlantUML off in ordinary preview; strict publishing rejects the warning. With
        no server available, use [Mermaid](/docs/components/mermaid/) instead.
        
        > [!WARNING] This page shows source only, not rendered diagrams
        > PlantUML has to reach a server you run, and this site assumes no endpoint on
        > the reader's behalf. In the current theme version the `plantuml` fence also
        > double-escapes `<`, `>`, `&` and `"`, so source with arrows or quotes comes
        > back from the endpoint as a `Syntax Error?` image (see [Limits](#limits)).
        > Every snippet below is correct PlantUML in itself.
        
        > [!IMPORTANT] Diagrams leave the reader's browser
        > The encoded diagram source is sent to the endpoint you configure. Never put
        > passwords, internal hostnames or customer names in a PlantUML fence. Internal
        > sites should run their own endpoint, or use a pre-rendered
        > [image](/docs/components/image/).
        
        ## Shortest form {#minimal}
        
        Sequence diagrams are the most common kind: `participant` declares a
        participant, `->` is a synchronous message, `-->` a return.
        
        ````markdown {title="Source"}
        ```plantuml
        @startuml
        actor Reader
        participant Browser
        participant Endpoint as Server
        Reader -> Browser : open the page
        Browser -> Server : GET /plantuml/svg/{compressed source}
        Server --> Browser : SVG
        Browser -> Browser : replace the fence with an img element
        @enduml
        ```
        ````
        
        That draws four lanes and four messages: the reader opens the page, the browser
        requests the endpoint with the encoded source, the endpoint returns SVG, and the
        runtime swaps the fence for an image.
        
        ## Class diagrams {#class}
        
        `class` lists members and `"1" -- "0..*"` gives a relationship its cardinality —
        the usual way to explain a data model.
        
        ````markdown {title="Source"}
        ```plantuml
        @startuml
        class Publication {
          + pubname : name
          + puballtables : bool
          + pubinsert / pubupdate / pubdelete : bool
        }
        class Subscription {
          + subname : name
          + subconninfo : text
          + subslotname : name
        }
        class ReplicationSlot {
          + slot_name : name
          + plugin : name
          + confirmed_flush_lsn : pg_lsn
        }
        Publication "1" -- "0..*" Subscription : subscribed by
        Subscription "1" -- "1" ReplicationSlot : bound to
        @enduml
        ```
        ````
        
        Three boxes with their fields and two annotated connectors: one publication can
        serve many subscriptions, and every subscription binds one replication slot.
        
        ## Component diagrams {#component}
        
        `package` groups deployment units, `[component]` is a box, and `-->` is the
        direction of a dependency.
        
        ````markdown {title="Source"}
        ```plantuml
        @startuml
        package "Monitoring node" {
          [Grafana] as grafana
          [Prometheus] as prom
          [Alertmanager] as alert
        }
        package "Database node" {
          [node_exporter] as node
          [pg_exporter] as pgexp
          [PostgreSQL] as pg
        }
        pg --> pgexp : query the statistics views
        node --> prom : /metrics
        pgexp --> prom : /metrics
        prom --> alert : rule fired
        grafana --> prom : PromQL
        @enduml
        ```
        ````
        
        Two dashed boxes with three components each, and five labelled arrows tracing
        the collection path.
        
        ## Activity diagrams {#activity}
        
        `start` / `stop` with `if … then … else … endif` draws a branching procedure.
        This kind contains no arrow characters, so it is the one kind that renders
        correctly in the current version.
        
        ````markdown {title="Source"}
        ```plantuml
        @startuml
        start
        :write content/docs/**/*.md;
        :add the translated peer, copying the rendered heading IDs;
        if (hugo --panicOnWarning passes?) then (yes)
          :npm test;
        else (no)
          :fix using the file and line in the error;
          stop
        endif
        if (tests green?) then (yes)
          :open the PR;
          stop
        else (no)
          :back to editing;
          stop
        endif
        @enduml
        ```
        ````
        
        One vertical flow line, two diamonds each branching yes / no, four end points.
        
        ## Use-case diagrams {#usecase}
        
        `actor` is a stick figure, `(use case)` an ellipse, and `rectangle` draws the
        system boundary — a good fit for a "who is this for" section.
        
        ````markdown {title="Source"}
        ```plantuml
        @startuml
        left to right direction
        actor Reader as reader
        actor Author as author
        actor Maintainer as maintainer
        rectangle "Documentation site" {
          reader --> (full-text search)
          reader --> (switch language)
          reader --> (export the print view)
          author --> (add a page)
          author --> (preview locally)
          maintainer --> (upgrade the theme)
          maintainer --> (publish)
        }
        @enduml
        ```
        ````
        
        Three figures on the left, one box with seven ellipses on the right, and
        connectors saying who can do what.
        
        ## Colours in dark mode {#dark-mode}
        
        The server knows nothing about the site's colour scheme, so the SVG comes back
        on a fixed white ground. `skinparam backgroundColor transparent` removes it and
        the diagram sits on the page background. With neutral lines and text it reads in
        both modes.
        
        ````markdown {title="Source"}
        ```plantuml
        @startuml
        skinparam backgroundColor transparent
        skinparam defaultFontName sans-serif
        skinparam ArrowColor #7C7C7C
        skinparam ActivityBorderColor #7C7C7C
        skinparam ActivityBackgroundColor #B0BEC522
        start
        :hugo mod get -u github.com/pgsty/oink;
        :hugo --gc --minify;
        :upload public/;
        stop
        @enduml
        ```
        ````
        
        PlantUML's `!theme` directive (`!theme plain`, for instance) also works. Themes
        come from the server, so a self-hosted endpoint has to have them installed.
        
        ## The rendering server {#server}
        
        The fence itself has no switch; whether it renders depends on the site
        configuration:
        
        ```yaml {title="hugo.yml"}
        params:
          plantuml:
            enable: true
            svg_image_url: https://plantuml.internal.example/plantuml/svg/
            svg: false
        ```
        
        - `enable: true` without `svg_image_url` warns and stays off with
          `params.plantuml.enable requires an explicit params.plantuml.svg_image_url`.
          Strict publishing rejects the warning. The theme never picks a public service.
        - To self-host, the official image
          [`plantuml/plantuml-server`](https://github.com/plantuml/plantuml-server)
          works; point `svg_image_url` at its `/svg/` path and **keep the trailing
          slash** — the encoded source is appended to it.
        - The endpoint's CORS policy and the site's CSP `img-src` (plus `connect-src`
          when `svg: true`) must both allow it; use an absolute URL on a subpath
          deployment.
        
        These keys are defined in
        [Configuration](/docs/customize/config/).
        
        ## Output {#outputs}
        
        | Output | Shape |
        | --- | --- |
        | HTML | The source is emitted as `
        `; once enabled, the runtime replaces it with an `` (with `svg: true`, an ``) |
        | Print | Same as HTML: the print view loads the runtime and requests the endpoint too |
        | Markdown | The `plantuml` fence and its source, kept as written |
        | RSS | The fence source only — subscribers see text |
        
        When the feature is off, or the runtime has not loaded, what stays on the page
        is a readable source block, never a broken-image icon.
        
        ## Parameter reference {#reference}
        
        Fence attributes: none. A `plantuml` fence reads no attribute line and does not
        go through OINK's code-block shell, so `title`, `copy` and the line-number
        options from [Code blocks](/docs/components/code/) have no effect here.
        
        Site parameters (`hugo.yml`):
        
        | Parameter | Type | Default | Description |
        | --- | --- | --- | --- |
        | `params.plantuml.enable` | bool | `false` | With it off, the fence stays a code block and no runtime loads |
        | `params.plantuml.svg_image_url` | string | none | The rendering endpoint; the encoded source is appended to it. Required when `enable: true`, otherwise PlantUML warns and stays off |
        | `params.plantuml.svg` | bool | `false` | `false` inserts ``; `true` inserts `` and loads an external SVG loader, putting the SVG in the DOM where CSS can reach it |
        {.fields meta="type default"}
        
        The theme reads those three keys and nothing else.
        
        ## Limits {#limits}
        
        - `<`, `>`, `&` and `"` are double-escaped: the current theme version escapes the
          fence content once too often, leaving literal `-->` and `"` in the page
          and returning a `Syntax Error?` image from the endpoint. Diagrams with arrows
          (sequence, component, use case, state) therefore do not render today; activity
          diagrams, which contain none of those characters, do. Until it is fixed, use
          [Mermaid](/docs/components/mermaid/) or a pre-rendered
          [image](/docs/components/image/).
        - A server is mandatory: the theme provides no default endpoint and assumes
          none.
        - Diagram source leaves the browser: keep anything confidential out of a
          PlantUML fence.
        - No colour-scheme awareness: the server does not know the reader's mode, so
          `skinparam` is the only lever.
        - No numbering, no zoom: the `` the runtime inserts does not pass through
          the image render hook, so `{#id num=}` and image zoom do not apply.
        
        ## Related {#related}
        
        - [Mermaid](/docs/components/mermaid/) — no server, follows the colour scheme, the everyday choice
        - [Draw.io](/docs/components/drawio/) — the other integration that needs a server of your own
        - [Images](/docs/components/image/) — pre-rendered SVG: numberable, zoomable, no external dependency
        - [Configuration](/docs/customize/config/) — the full definition of `params.plantuml.*`
        
        ---
        
        Backlinks:
        
        - [Deploy](/docs/admin/deploy/)
        - [Draw.io](/docs/components/drawio/)
        - [Mermaid](/docs/components/mermaid/)
        - [Configuration](/docs/customize/config/)
        - [Print](/docs/customize/print/)
        
        
        ================
        Source: https://oink.pgsty.com/docs/components/markmap/index.md
        ================
        
        # Markmap
        
        > A `markmap` fence turns a Markdown outline into an expandable, zoomable mind map — and the source stays a readable outline.
        
        ---
        
        LLMS index: [llms.txt](/llms.txt)
        
        ---
        
        The body of a `markmap` fence is a plain Markdown outline: headings and lists
        give the hierarchy, and the browser draws it as a tree you can expand and
        collapse. It suits showing "what this section covers" at one glance. For flows
        with direction and conditions, use [Mermaid](/docs/components/mermaid/).
        
        ## Shortest form {#minimal}
        
        ````markdown {title="Source"}
        ```markmap
        # OINK
        ## Local-first
        - every runtime ships with the theme
        - no CDN involved
        ## Markdown-native
        - components are fences and attribute lines
        - usable without writing a shortcode
        ## Four output states
        - HTML
        - print
        - Markdown
        - RSS
        ```
        ````
        
        ```markmap
        # OINK
        ## Local-first
        - every runtime ships with the theme
        - no CDN involved
        ## Markdown-native
        - components are fences and attribute lines
        - usable without writing a shortcode
        ## Four output states
        - HTML
        - print
        - Markdown
        - RSS
        ```
        
        The first-level heading is the root; other headings and list items hang under it
        by indentation. Click the dot on a node to fold or unfold that branch, scroll to
        zoom, drag to pan. The toolbar at the bottom right offers zoom, fit-to-window
        and download-as-SVG.
        
        ## Depth {#depth}
        
        Deeper levels are set smaller and the canvas lays itself out. Below are the six
        sections of this theme's documentation site and their page counts.
        
        ````markdown {title="Source"}
        ```markmap
        # OINK documentation
        ## Introduction (4 pages)
        ### What it is
        ### Feature tour
        ### Showcase
        ### Licences
        ## Get started (4 pages)
        ### Choose a path
        ### OINK Starter
        ### Repository tour
        ### From scratch
        ## Authoring (8 pages)
        ### Organizing content
        ### Writing pages
        ### Front matter
        ### Blog
        ### Books
        ### Releases and downloads
        ### OpenAPI
        ## Components (22 pages)
        ### Callouts / tabs / steps / cards
        ### Images / galleries / tables / fields
        ### Diagrams: Mermaid / PlantUML / Markmap / ECharts
        ## Customization (15 pages)
        ### Branding / navigation / search / languages
        ### Landing / versions / taxonomies / print
        ## Operations (7 pages)
        ### Preview / deploy / upgrade
        ### Comments / analytics / troubleshooting
        ```
        ````
        
        ```markmap
        # OINK documentation
        ## Introduction (4 pages)
        ### What it is
        ### Feature tour
        ### Showcase
        ### Licences
        ## Get started (4 pages)
        ### Choose a path
        ### OINK Starter
        ### Repository tour
        ### From scratch
        ## Authoring (8 pages)
        ### Organizing content
        ### Writing pages
        ### Front matter
        ### Blog
        ### Books
        ### Releases and downloads
        ### OpenAPI
        ## Components (22 pages)
        ### Callouts / tabs / steps / cards
        ### Images / galleries / tables / fields
        ### Diagrams: Mermaid / PlantUML / Markmap / ECharts
        ## Customization (15 pages)
        ### Branding / navigation / search / languages
        ### Landing / versions / taxonomies / print
        ## Operations (7 pages)
        ### Preview / deploy / upgrade
        ### Comments / analytics / troubleshooting
        ```
        
        ## Links, code and emphasis {#inline-markdown}
        
        Nodes take inline Markdown: links are clickable, inline code is monospaced, bold
        and italic behave as usual.
        
        ````markdown {title="Source"}
        ```markmap
        # Everyday commands
        ## Preview
        - `hugo server` — open [localhost:1313](http://localhost:1313/)
        - `hugo server -D` — **including drafts**
        ## Build
        - `hugo --printPathWarnings --panicOnWarning`
        - `hugo --gc --minify` — for publishing
        ## Theme
        - `hugo mod get -u github.com/pgsty/oink`
        - [theme repository](https://github.com/pgsty/oink)
        - [site source](https://github.com/pgsty/oink.pgsty.com)
        ```
        ````
        
        ```markmap
        # Everyday commands
        ## Preview
        - `hugo server` — open [localhost:1313](http://localhost:1313/)
        - `hugo server -D` — **including drafts**
        ## Build
        - `hugo --printPathWarnings --panicOnWarning`
        - `hugo --gc --minify` — for publishing
        ## Theme
        - `hugo mod get -u github.com/pgsty/oink`
        - [theme repository](https://github.com/pgsty/oink)
        - [site source](https://github.com/pgsty/oink.pgsty.com)
        ```
        
        ## Mathematics in nodes {#math}
        
        The Markmap runtime carries a local KaTeX, so `$…$` inside a node renders as a
        formula.
        
        ````markdown {title="Source"}
        ```markmap
        # PostgreSQL metrics worth watching
        ## Cache hit ratio
        - $\frac{blks\_hit}{blks\_hit + blks\_read}$
        - below 0.99, look at shared_buffers
        ## Replication lag
        - $lsn_{primary} - lsn_{replica}$
        ## Transaction throughput
        - $TPS = \frac{\Delta xact\_commit}{\Delta t}$
        ```
        ````
        
        ```markmap
        # PostgreSQL metrics worth watching
        ## Cache hit ratio
        - $\frac{blks\_hit}{blks\_hit + blks\_read}$
        - below 0.99, look at shared_buffers
        ## Replication lag
        - $lsn_{primary} - lsn_{replica}$
        ## Transaction throughput
        - $TPS = \frac{\Delta xact\_commit}{\Delta t}$
        ```
        
        ## Controlling the initial depth {#options}
        
        The top of a fence body may carry Markmap's own YAML header — not Hugo front
        matter. `initialExpandLevel` expands only the first few levels and leaves the
        rest for the reader; `colorFreezeLevel` says from which level a branch keeps one
        colour.
        
        ````markdown {title="Source"}
        ```markmap
        ---
        markmap:
          initialExpandLevel: 2
          colorFreezeLevel: 2
        ---
        
        # Check scripts in the theme repository
        ## Source-level contracts
        ### check-i18n.py
        ### check-taxonomy.py
        ### check-font-tokens.py
        ## Output-level checks
        ### check-output.py
        ### check-goldens.py
        ### check-code-blocks.py
        ### check-content-primitives.py
        ### check-media-primitives.py
        ## Browser runtimes
        ### node --test tests/js/**/*.test.js
        ```
        ````
        
        ```markmap
        ---
        markmap:
          initialExpandLevel: 2
          colorFreezeLevel: 2
        ---
        
        # Check scripts in the theme repository
        ## Source-level contracts
        ### check-i18n.py
        ### check-taxonomy.py
        ### check-font-tokens.py
        ## Output-level checks
        ### check-output.py
        ### check-goldens.py
        ### check-code-blocks.py
        ### check-content-primitives.py
        ### check-media-primitives.py
        ## Browser runtimes
        ### node --test tests/js/**/*.test.js
        ```
        
        ## Folded into a disclosure {#in-details}
        
        Every map is a fixed 300 pixels tall, so three in a row eat a lot of page. Fold
        a panoramic one into `> [!DETAILS]` and let the reader open it. Every line
        inside the disclosure starts with `>`, fences included.
        
        ````markdown {title="Source"}
        > [!DETAILS] What the theme repository looks like
        > ```markmap
        > # pgsty/oink
        > ## layouts/
        > - baseof.html and the per-type shells
        > - _partials/shell/
        > - _markup/ render hooks
        > - _shortcodes/
        > ## assets/
        > - scss/ tokens and component styles
        > - js/ browser runtimes
        > - third_party/ libraries shipped with the theme
        > ## i18n/
        > - 32 locale files with identical keys
        > ## docs/
        > - maintainer contracts
        > ```
        ````
        
        > [!DETAILS] What the theme repository looks like
        > ```markmap
        > # pgsty/oink
        > ## layouts/
        > - baseof.html and the per-type shells
        > - _partials/shell/
        > - _markup/ render hooks
        > - _shortcodes/
        > ## assets/
        > - scss/ tokens and component styles
        > - js/ browser runtimes
        > - third_party/ libraries shipped with the theme
        > ## i18n/
        > - 32 locale files with identical keys
        > ## docs/
        > - maintainer contracts
        > ```
        
        ## Output {#outputs}
        
        | Output | Shape |
        | --- | --- |
        | HTML | `
        ` first; the runtime replaces it with `
        ` and draws the SVG | | Print | Same as HTML: the print view loads the runtime too | | Markdown | The `markmap` fence and its outline, kept as written | | RSS | The outline source only — a readable outline for subscribers | The outline is the content: wherever JavaScript does not reach, the full hierarchy is still legible. ## Parameter reference {#reference} Fence attributes: none. A `markmap` fence reads no attribute line; the height is fixed by the theme at 300px (`.markmap > svg`) and the width fills the reading column. Site parameters (`hugo.yml`): | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `params.markmap` | bool | `false` | With it off, the fence stays a code block and no runtime loads | {.fields meta="type default"} The key is defined in [Configuration](/docs/customize/config/). Per-map behaviour goes in the `markmap:` YAML header at the top of the fence body (`initialExpandLevel`, `colorFreezeLevel`, `maxWidth` …), which is Markmap syntax; the accepted keys are in the [Markmap documentation](https://markmap.js.org/docs/json-options). ## Limits {#limits} - The output is an inline SVG fixed at 300px tall: one `.markmap > svg` rule decides it and the fence cannot change it. When a map has too many levels, use `initialExpandLevel` or split it in two. Inline SVG also means `{#id num=}` numbering and image zoom do not apply. - No colour-scheme awareness: link colours come from Markmap's own palette, so check contrast in both modes. - Without `params.markmap` it is only a code block: sites that do not use the component load no runtime. - "Download SVG" in the toolbar is a browser action and exports a snapshot of the current expansion state. - Avoid `<`, `>`, `&` and `"` in the outline: the current theme version double-escapes them and nodes show literal `>` or `"`. Write links as `[text](URL)` rather than as autolinks in angle brackets. ## Related {#related} - [Mermaid](/docs/components/mermaid/) — diagrams with direction and conditions - [File trees](/docs/components/filetree/) — more precise for directory structure - [Callouts](/docs/components/callout/) — everything `[!DETAILS]` can do - [Configuration](/docs/customize/config/) — `params.markmap` --- Backlinks: - [Components](/docs/components/) - [Mermaid](/docs/components/mermaid/) - [Configuration](/docs/customize/config/) - [Print](/docs/customize/print/) ================ Source: https://oink.pgsty.com/docs/components/drawio/index.md ================ # Draw.io > Put a `.drawio.svg` that carries an editable copy on the page as an ordinary image; hovering gives the reader a button that opens the Draw.io editor. --- LLMS index: [llms.txt](/llms.txt) --- The Draw.io integration has neither a fence nor a shortcode — it uses plain Markdown images. Tick "Include a copy of my diagram" when exporting from Draw.io and the SVG or PNG carries an `mxfile` copy inside it; the theme's runtime spots that copy and adds an edit button to the image. It suits diagrams readers are meant to take away and change. A diagram that is only there to be looked at is an ordinary [image](/docs/components/image/). ## Shortest form {#minimal} The syntax is the plain image syntax. The filename does not matter; `.drawio.svg` is only a convention. ```markdown {title="Source"} ![The Hugo build pipeline: content goes through Hugo and out as public](pipeline.drawio.svg) {width="620" height="140"} ``` ![The Hugo build pipeline: content goes through Hugo and out as public](pipeline.drawio.svg) {width="620" height="140"} An export that carries an `mxfile` copy is wrapped in a `.drawio` container. Hover it and a pencil button appears at the bottom right; clicking lays a full-screen iframe over the page and loads the editor the site configured. ## How the copy is detected {#detection} The runtime looks at one thing: whether the file's contents contain `mxfile`. The filename is irrelevant. A hand-drawn SVG written exactly the same way — a block image with the same attribute line — carries no copy, so it gets no button. ```markdown {title="Source"} ![The three columns of the documentation shell: sidebar, article, table of contents](plain-shell.svg) {width="620" height="140"} ``` ![The three columns of the documentation shell: sidebar, article, table of contents](plain-shell.svg) {width="620" height="140"} ## With a caption {#caption} Draw.io images go through the ordinary image render hook, so every [image](/docs/components/image/) attribute still applies. Add `caption` for a captioned figure; the edit button still appears on the image. ```markdown {title="Source"} ![The Hugo build pipeline](pipeline.drawio.svg) {caption="Content, configuration and theme templates flow into Hugo and out as public/" width="620" height="140"} ``` ![The Hugo build pipeline](pipeline.drawio.svg) {caption="Content, configuration and theme templates flow into Hugo and out as public/" width="620" height="140"} ## As a numbered figure {#numbered} Add `{#id num=…}` for a cross-referenceable numbered figure, which `xref` can reach and which appears in the list of figures like any other. ```markdown {title="Source"} ![The Hugo build pipeline](pipeline.drawio.svg) {#fig_pipeline num="1-1" caption="From content to a static site" width="620" height="140"} ``` ![The Hugo build pipeline](pipeline.drawio.svg) {#fig_pipeline num="1-1" caption="From content to a static site" width="620" height="140"} The complete numbering and cross-reference rules are in [publishing books](/docs/write/book/). ## SVG or PNG {#svg-or-png} Both are recognized. A Draw.io PNG export can carry the same copy in a text chunk, and the runtime's test is identical. ```markdown {title="Source"} ![The Hugo build pipeline (PNG export)](pipeline.drawio.png) {width="620" height="140"} ``` ![The Hugo build pipeline (PNG export)](pipeline.drawio.png) {width="620" height="140"} Prefer SVG in documentation: it scales without loss, its text is real text (searchable, readable by screen readers) and its diffs are legible. Use PNG when the diagram is very complex or the target platform cannot take SVG. Only PNG can go through Hugo's image processing; operations on SVG warn and leave the source unchanged, and strict builds reject the warning. ## What the button does {#editing} Three things, in order. ### Lay an overlay over the page {#editing-overlay} A full-screen `div.drawioframe` is inserted holding an iframe whose address is the configured `drawio_server` plus a fixed query string (`embed=1&ui=atlas&proto=json&saveAndEdit=1&noSaveBtn=1`). ### Hand the diagram to the editor {#editing-load} Once the editor is ready, the runtime sends this image's contents — the `mxfile` copy included — into the iframe as a data URL. That step does not go through your server. ### Save and write back {#editing-save} Saving in the editor makes it export in the original format, SVG or PNG, and the browser downloads it under the same name. The runtime never writes to the repository: overwrite the file in `content/` with what you downloaded and commit it yourself. The edit button is there so a reader can take the diagram away and change it. It is not online editing of the site. ## The editor address {#server} ```yaml {title="hugo.yml"} params: drawio: enable: true drawio_server: https://drawio.internal.example/ ``` - `enable: true` without `drawio_server` warns and disables editing; strict builds fail on that warning. The theme does not pick a public service. - When editing has to stay inside the organization, deploy a [self-hosted editor](https://github.com/jgraph/docker-drawio) and point at it. - The public endpoint `https://embed.diagrams.net/` works, and the reader's diagram then travels to a third-party page. Both keys are defined in [Configuration](/docs/customize/config/). ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | A plain `` or `
        `; once enabled, the runtime wraps an image that carries a copy in `
        ` and adds the button | | Print | The image prints as usual; the button is hidden except on hover, so it never reaches paper | | Markdown | Plain Markdown image syntax | | RSS | A plain `` with an absolute URL and no button | The image itself exists in all four states; the edit button is an increment on top. ## Parameter reference {#reference} There are no fence or shortcode parameters of its own. The image attribute line is the one from [Images](/docs/components/image/): `caption`, `width`, `height`, `link`, `#id`, `num`, `command`, `options`. Site parameters (`hugo.yml`): | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `params.drawio.enable` | bool | `false` | With it off no script loads and an image is just an image | | `params.drawio.drawio_server` | string | none | The editor address; required when `enable: true` | {.fields meta="type default"} ## Limits {#limits} - The runtime loads only when rendered page content contains `.svg` or `.png` candidates. It groups matching images by URL, then reads each URL once to look for `mxfile`. - Forget to tick "Include a copy of my diagram" on export and the image is just an image, with no button. - Editing needs the editor and never writes back: offline, the images display fine and the button does nothing; saving is a browser download, and replacing the file and committing it are manual. - The button appears on hover only: touch devices have no hover, so readers may not find it. Do not present editability as a headline feature. - Colours do not follow the colour scheme: an exported SVG has fixed colours. Set fills to `none` and use neutral greys for lines and text and it reads in both modes. ## Related {#related} - [Images](/docs/components/image/) — captions, numbering, sizing and zoom in full - [PlantUML](/docs/components/plantuml/) — the other integration that needs a server - [Mermaid](/docs/components/mermaid/) — diagrams from text with no server at all - [Configuration](/docs/customize/config/) — the full definition of `params.drawio.*` --- Backlinks: - [PlantUML](/docs/components/plantuml/) - [Configuration](/docs/customize/config/) ================ Source: https://oink.pgsty.com/docs/components/echarts/index.md ================ # ECharts > Write ECharts options as YAML or JSON in an `echarts` fence; Hugo validates them at build time and the browser draws a theme-aware chart with the local ECharts. --- LLMS index: [llms.txt](/llms.txt) --- The body of an `echarts` fence is an ECharts option object in YAML or JSON — not code. Use it for quantitative charts that need axes, series and a legend. For relationships and flows use [Mermaid](/docs/components/mermaid/); for order and hierarchy use [Infographic](/docs/components/infographic/). Hugo parses the options at build time; invalid input warns and leaves its source readable in an ordinary preview, while strict publishing rejects the warning. The browser draws with the ECharts copy the theme ships, and only a page that uses it loads the runtime. ## Shortest form {#minimal} A bar chart needs three parts: `xAxis`, `yAxis`, `series`. Below is how many pages each of the six documentation sections has. ````markdown {title="Source"} ```echarts {height="320px"} tooltip: trigger: axis xAxis: type: category data: [Introduction, Get started, Authoring, Components, Customization, Operations] yAxis: type: value name: pages series: - name: pages type: bar data: [4, 4, 8, 22, 15, 7] ``` ```` ```echarts {height="320px"} tooltip: trigger: axis xAxis: type: category data: [Introduction, Get started, Authoring, Components, Customization, Operations] yAxis: type: value name: pages series: - name: pages type: bar data: [4, 4, 8, 22, 15, 7] ``` Both formats are accepted; YAML needs no quotes or commas and is shorter to write. Broken indentation, or a body that parses to an array instead of a map, warns on that line and renders the source instead of a blank chart. Strict publishing rejects the warning. ## Multiple line series {#line} `series` is an array, so another entry is another line, and `legend` lets the reader hide one. Below are the release years of PostgreSQL major versions and the end-of-support years implied by the community's five-year policy. ````markdown {title="Source"} ```echarts {height="360px"} tooltip: trigger: axis legend: data: [Released, End of support] grid: left: 56 right: 24 top: 48 bottom: 40 xAxis: type: category name: major version data: ["9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18"] yAxis: type: value min: 2015 max: 2031 name: year series: - name: Released type: line smooth: false data: [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025] - name: End of support type: line lineStyle: type: dashed data: [2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030] ``` ```` ```echarts {height="360px"} tooltip: trigger: axis legend: data: [Released, End of support] grid: left: 56 right: 24 top: 48 bottom: 40 xAxis: type: category name: major version data: ["9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18"] yAxis: type: value min: 2015 max: 2031 name: year series: - name: Released type: line smooth: false data: [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025] - name: End of support type: line lineStyle: type: dashed data: [2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030] ``` Quote the version numbers: unquoted `10` is a number in YAML and so is `9.6`, but as category-axis labels they have to be strings. ## Pie and doughnut charts {#pie} Give `radius` two values for a doughnut. Below is how OINK's 29 shortcodes break down by purpose. ````markdown {title="Source"} ```echarts {height="340px"} tooltip: trigger: item formatter: "{b}: {c} ({d}%)" legend: bottom: 0 series: - type: pie radius: [42%, 70%] itemStyle: borderRadius: 6 borderWidth: 2 label: formatter: "{b} {c}" data: - { value: 14, name: Core components } - { value: 10, name: Book numbering and indexes } - { value: 3, name: Releases and downloads } - { value: 2, name: OpenAPI } ``` ```` ```echarts {height="340px"} tooltip: trigger: item formatter: "{b}: {c} ({d}%)" legend: bottom: 0 series: - type: pie radius: [42%, 70%] itemStyle: borderRadius: 6 borderWidth: 2 label: formatter: "{b} {c}" data: - { value: 14, name: Core components } - { value: 10, name: Book numbering and indexes } - { value: 3, name: Releases and downloads } - { value: 2, name: OpenAPI } ``` `{b}`, `{c}` and `{d}` are ECharts template placeholders — name, value, percentage. Writing them in a string is enough; no function is needed. ## Height and full width {#size} `height` defaults to `400px` and accepts `px rem em vh vw %`. `full=true` drops the reading-column limit so the chart fills the content area, which suits charts with many points or long labels. ````markdown {title="Source"} ```echarts {height="260px" full=true} tooltip: trigger: axis grid: left: 40 right: 16 top: 24 bottom: 32 xAxis: type: category data: [i18n, taxonomy, font tokens, content contracts, navigation, runtime, sidebar icons, search, actions, palette, params, reading, release assets, download, landing, book, migrations, keyboard, shell, output, goldens] yAxis: type: value name: scripts series: - type: bar data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ``` ```` ```echarts {height="260px" full=true} tooltip: trigger: axis grid: left: 40 right: 16 top: 24 bottom: 32 xAxis: type: category data: [i18n, taxonomy, font tokens, content contracts, navigation, runtime, sidebar icons, search, actions, palette, params, reading, release assets, download, landing, book, migrations, keyboard, shell, output, goldens] yAxis: type: value name: scripts series: - type: bar data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ``` An invalid height (`360`, `36pt`) warns and uses the default in ordinary preview; strict publishing rejects the warning. ## Light and dark {#theme} Without `theme`, a chart initializes in the reader's current colour scheme and redraws in place when that changes — no page reload. It resizes automatically when its container does. Switch this page to dark and the ground and text of every chart above change with it. A fixed `theme` pins the colours in both modes: ````markdown {title="Source"} ```echarts {height="240px" theme="dark"} xAxis: type: category data: [HTML, Print, Markdown, RSS] yAxis: type: value series: - type: bar data: [1, 1, 1, 1] ``` ```` ```echarts {height="240px" theme="dark"} xAxis: type: category data: [HTML, Print, Markdown, RSS] yAxis: type: value series: - type: bar data: [1, 1, 1, 1] ``` `dark` is the only theme built into the runtime; any other ECharts theme has to be registered with `echarts.registerTheme()` before it can be named here. Without a branding requirement, leave `theme` out and let the chart follow the site. ## Callbacks with `$fn:` {#callbacks} A fence is data and cannot carry JavaScript. When an option needs a function — a tooltip formatter, a data-driven colour — write the string `"$fn:name"` in the options and register that name on `window.OinkEchartsFunctions`: ````markdown {title="Source"} ```echarts {height="300px"} tooltip: trigger: axis formatter: "$fn:pageShare" xAxis: type: category data: [Introduction, Get started, Authoring, Components, Customization, Operations] yAxis: type: value series: - type: bar data: [4, 4, 8, 22, 15, 7] ``` ```` ```echarts {height="300px"} tooltip: trigger: axis formatter: "$fn:pageShare" xAxis: type: category data: [Introduction, Get started, Authoring, Components, Customization, Operations] yAxis: type: value series: - type: bar data: [4, 4, 8, 22, 15, 7] ``` Hover any bar and the tooltip is the sentence that function builds. An unregistered name resolves to `undefined`, the chart is drawn as if the option were not set, and neither the build nor the runtime complains. Keep the script next to the fence so they change together. That script is site code and deserves code review. Formatting a string template (`{b}`, `{c}`, `{d}`) can express does not need a function. ## Where the data lives {#data} A fence body is a literal. Hugo does not expand shortcodes, front matter variables or files under `data/` inside it — the numbers are written in the fence. The cost is that data cannot be shared; the benefit is that the chart and its data go into Git together and a diff shows which number moved. Do not draw data that changes often (version matrices, asset lists). Use a [table](/docs/components/table/) or the `data/`-driven components on a [release page](/docs/write/releases/). ## Output {#outputs} | Output | Shape | | --- | --- | | HTML | A canvas container inside `
        ` plus an `application/json` options block; the local ECharts draws it | | Print | No chart; the fence source inside `
        ` |
        | Markdown | The `echarts` fence and its option source, kept as written |
        | RSS | Same as print — source only |
        
        Whatever the chart shows, say it in the prose too: print and RSS have no chart.
        
        ## Parameter reference {#reference}
        
        The fence attribute line (```` ```echarts {…} ````):
        
        | Parameter | Type | Default | Description |
        | --- | --- | --- | --- |
        | `height` | CSS length | `400px` | A non-negative number plus `px` `rem` `em` `vh` `vw` `%`; anything else warns and uses the default |
        | `theme` | string | unset | Pin an ECharts theme and stop following the site's colour scheme; only `dark` is built in |
        | `full` | bool | `false` | `true` drops the reading-column limit and fills the content area |
        | `class` | space-separated classes | — | Passed through to the container for site CSS |
        {.fields meta="type default"}
        
        `style`, `on*`, and unknown attributes warn and are ignored. A fence body that
        does not parse to a YAML/JSON map warns and renders as source. Strict publishing
        rejects all these warnings. The option keys themselves are ECharts', documented in the
        [official option manual](https://echarts.apache.org/en/option.html).
        
        There is no site-level parameter: ECharts needs no switch in `hugo.yml` and
        loads only where it is used.
        
        ## Limits {#limits}
        
        - No JavaScript in the fence: bridge through `$fn:` when a function is needed,
          and remember an unregistered name resolves to `undefined` with no error.
        - The fence reads no external data: `data/`, front matter and shortcodes are all
          out of reach; the numbers live in the fence.
        - Print and RSS carry the source only, so the conclusion belongs in the prose.
        - YAML type coercion: `10`, `9.6`, `on` and `yes` on a category axis become
          numbers or booleans and need quotes.
        - Colour is not the only distinction: in a multi-series chart vary line style or
          marker shape too, and check legend contrast in both colour schemes.
        
        ## Related {#related}
        
        - [Infographic](/docs/components/infographic/) — structure and order, not statistics
        - [Tables](/docs/components/table/) — for few values that must be read exactly
        - [Mermaid](/docs/components/mermaid/) — relationship and flow diagrams
        - [Code blocks](/docs/components/code/) — the general rules for fence attribute lines
        
        ---
        
        Backlinks:
        
        - [Deploy](/docs/admin/deploy/)
        - [Infographic](/docs/components/infographic/)
        - [Mermaid](/docs/components/mermaid/)
        - [Print](/docs/customize/print/)
        
        
        ================
        Source: https://oink.pgsty.com/docs/components/infographic/index.md
        ================
        
        # Infographic
        
        > An `infographic` fence picks an AntV template and renders a title plus a list of items as a flow, timeline, funnel, grid or hierarchy.
        
        ---
        
        LLMS index: [llms.txt](/llms.txt)
        
        ---
        
        An `infographic` fence picks an AntV template and renders "a title plus a list
        of items" as an infographic. Use it for structure: order, hierarchy, comparison.
        When you need axes and numeric precision use
        [ECharts](/docs/components/echarts/); when you need a flow with conditional
        branches use [Mermaid](/docs/components/mermaid/). The fence body is data, and
        it stays readable text on GitHub.
        
        ## Shortest form {#minimal}
        
        The first line is `infographic