Skip to content

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

Return to the regular view of this page.

Components

Every component available for writing, one page each, examples from the simplest upwards, with the parameter table at the end.

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

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

Components depend on three Goldmark settings. OINK Starter provides them already configured; copy the snippet when starting from scratch:

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 <p>
  • 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 <p>, 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.

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 Separate prerequisites, warnings and asides from the prose > [!NOTE] native none
Images Captions, sizing, zoom, numbering and build-time processing ![alt](oink.webp) native site switch
Code Blocks Highlighting, titles, copy, folding, linkable lines ```sh fence per page
Tabs One thing, several platforms or languages attribute {tab="Linux"} native + shortcode per page
Tables Plain tables plus full-width, matrix, caption and numbering {.full-width} native none
Fields Parameter lists with type / required / default chips {.fields meta="type default"} native + shortcode none
Steps A procedure with an order {.steps} native + shortcode none
Cards A set of parallel destinations {.cards} native + shortcode none
FileTree Directory structure with an aligned comment column ```filetree fence per page
Math KaTeX inline and display formulas $$ … $$ native per page
Mermaid Flowcharts, sequence diagrams, Gantt charts ```mermaid fence per page
PlantUML UML diagrams; needs a rendering server ```plantuml fence site switch
Markmap A Markdown outline becomes a mind map ```markmap fence site switch
Draw.io Diagrams that stay editable; needs a server ![alt](arch.drawio.svg) native site switch
ECharts Declarative statistical charts ```echarts fence per page
Infographic AntV infographics ```infographic fence per page
Gallery A set of images sharing one zoom dialog ```gallery fence site switch
Badge Inline status markers {{</* badge text="Beta" */>}} shortcode none
Kbd Key names and chords {{</* kbd "Ctrl" "K" */>}} shortcode none
Includes Pull in files, print site parameters, drop build-time notes {{</* include file="parts/x.md" */>}} shortcode none
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.

1 - Callouts

Write notes, warnings and collapsible asides — with colour, icon and title — as > [!NOTE] blockquotes, no shortcode involved.

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

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

The first five match GitHub; the other five are semantic types OINK adds. Every type has a default icon and accent colour.

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.

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

Text after the marker on the same line becomes the title and accepts inline Markdown — code, bold, links.

Source
> [!WARNING] Rewrites `public/`
> Check that `baseURL` points at the production domain before a production
> build, or every absolute link will be wrong.
Rewrites public/

Check that baseURL points at the production domain before a production build, or every absolute link will be wrong.

Body content

The body is page-level Markdown: lists, fenced code, tables, images, nested callouts. Every line starts with >, fences included.

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 <http://localhost:1313/>
>
> | Port | Purpose |
> | --- | --- |
> | 1313 | Hugo development server |
Three commands to a live preview
  1. Clone: git clone https://github.com/pgsty/oink-starter my-docs
  2. Enter the directory and preview:
    cd my-docs && hugo server
  3. Open http://localhost:1313/
Port Purpose
1313 Hugo development server

Collapsing

A - after the type starts the callout closed, a + starts it open. Both render as a native <details>; no JavaScript is loaded. Use them for full command output, alternatives, background — anything that need not be visible by default.

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

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] 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.

Source
> [!DETAILS] Full `hugo version` output
> ```text
> hugo v0.165.0+extended+withdeploy darwin/arm64
> ```
Full hugo version output
hugo v0.165.0+extended+withdeploy darwin/arm64

Custom 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.

Source
> [!TIP] PostgreSQL 18 is supported
> Pigsty v4 installs PostgreSQL 18 by default.
{icon="fa-solid fa-database"}
PostgreSQL 18 is supported

Pigsty v4 installs PostgreSQL 18 by default.

Nesting

Callouts nest (one more > per level) and can sit inside list items or steps. One level of nesting is plenty.

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`.
Back up before upgrading

A theme version bump can change how a page renders.

How to back up

git tag pre-upgrade is enough — rolling back is git checkout pre-upgrade.

Unknown types and common slips

An unknown type name neither fails the build nor loses content: the block renders as an ordinary blockquote with the [!TYPE] marker still visible.

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 <!-- prettier-ignore-start --> / <!-- prettier-ignore-end -->.
  • 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

Output Shape
HTML Static types are <div class="td-callout" role="note">; collapsible types are a native <details> + <summary>
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

The marker line > [!TYPE]± Title:

TYPE , enum , default
NOTE TIP IMPORTANT WARNING CAUTION SUCCESS DANGER QUESTION EXAMPLE QUOTE DETAILS; case-insensitive; an unknown value renders as a plain blockquote
± , - / + / none , defaultnone
- collapses closed, + collapses open; bare DETAILS is closed
Title , inline Markdown , defaultthe localized type name
On the same line as the marker

The attribute line {…}, immediately after the blockquote:

icon , Font Awesome class pair , defaultthe type’s icon
For example fa-solid fa-database; DETAILS has no default icon
class , space-separated classes , default
Passed through verbatim for site CSS

style, on*, and other keys warn and are ignored; strict publishing rejects the warning.

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); every line of the blockquote starts with > and lines up with the list item’s indent.
  • Steps — callouts inside a procedure
  • Tabs — the same note split per platform
  • Writing pages — when to use a callout and when to use prose

2 - Images

Plain Markdown image syntax plus one attribute line gives you captions, sizing, zoom, links, numbering and Hugo image processing.

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

Source
![The OINK documentation shell: sidebar, article and table of contents](oink-shell.webp)
The OINK documentation shell: sidebar, article and table of contents

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

An image inside a line of text is an inline image, rendered as one <img> and unable to carry attributes; an image standing alone as its own paragraph is a block image and can carry an attribute line.

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 sits inside a sentence — an inline image.

shell thumbnail

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). Without it, Goldmark wraps a standalone image in <p> and the attribute line is treated as prose.

Captions

An attribute line with caption="…" renders the image as a <figure> plus a <figcaption>. A caption is plain text and is not parsed as Markdown.

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

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).

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)
A 900×600 illustration from static/images/ shown at half size

Processed images

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.

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
Fit 300x150: scaled to fit inside a 300×150 box
the left half of the shell
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.

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).
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

Release card
Click the image for the releases and downloads guide

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 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-<num>. 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.

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
Figure 2-1 The release card: version, date and assets

See Figure 2-1.

A numbered figure can be a processed image at the same time (num plus command), and can carry a link.

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 <dialog> (Esc closes it, focus returns where it was). This page turns it on in its front matter, so every image above is clickable.

hugo.yml
params:
  ui:
    image_zoom: true
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.

Source: a decorative image does not zoom
![](oink-shell.webp)
{width="150" height="75"}

Light and dark images

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:

Source
![Sidebar (light)](oink-shell.webp)
{class="only-light"}

![Sidebar (dark)](oink-shell.webp)
{class="only-dark"}
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

Output What appears
HTML Inline <img>; block <img class="td-image">; with a caption or number, <figure class="td-figure"> plus <figcaption>; 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

The attribute line {…} (the line immediately after a block image):

caption , plain text , default
Its presence makes a figure; not parsed as Markdown
#id , identifier , defaultfig-<num> when num is set
[A-Za-z][A-Za-z0-9_.:-]*; the anchor and the Book target ID
num , string , default
[0-9A-Za-z.-]+; registers a Book figure target and prefixes the caption with “Figure N.”
width / height , positive integer , defaultthe resource’s intrinsic size
Overrides the size; static and remote images use it to avoid layout shift
command , enum , default
Fit, Resize, Fill, Crop; must accompany options; page and global resources only
options , string , default
Hugo image processing options such as 600x300, 300x150 Left, 800x webp q80
link , URL , default
Wraps the figure in a link; requires caption or num; a linked image does not zoom
class , class list , default
Passed through for the site’s CSS
data-* / aria-* , string , default
Passed through

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

  • 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.
  • Gallery — a set of images sharing one zoom dialog
  • Books — the list of figures and xref cross-references
  • Brand and appearance — where the site logo and favicon go
  • Cards — images on cards

3 - 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.

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

Source
```sql
SELECT datname, numbackends FROM pg_stat_database ORDER BY numbackends DESC;
```
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:

Source
```diff {title="a change to hugo.yml"}
 params:
   ui:
-    sidebar_menu_compact: true
+    sidebar_menu_compact: false
     sidebar_menu_foldable: true
```
a change to hugo.yml
 params:
   ui:
-    sidebar_menu_compact: true
+    sidebar_menu_compact: false
     sidebar_menu_foldable: true

Filename titles

title gives the block a visible title bar, usually a filename or a path. It also becomes the block’s accessible name.

Source
```yaml {title="hugo.yml"}
markup:
  goldmark:
    parser:
      attribute:
        block: true
    renderer:
      unsafe: true
```
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

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.

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
```
postgresql.conf
120shared_buffers = 8GB
121max_connections = 200
122work_mem = 64MB
123wal_level = replica
124max_wal_senders = 10

lineNos="table" puts the numbers in a separate column — in both modes the copy button strips them:

Source
```bash {title="deployment in three commands" lineNos="table"}
./configure -c rich
./install.yml
pig ext install pg_duckdb
```
deployment in three commands
1
2
3
./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=true changes display only: the source is unchanged and so is the text you copy. Without it, long lines scroll horizontally.

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
```
config/artifacts.env
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=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.

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
```
hugo.yml
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

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.

Source
```console
$ pig ext list duckdb
name       version  category
pg_duckdb  1.0.0    OLAP
$ pig ext install pg_duckdb
INFO installing pg_duckdb
```
$ 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:

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
```
counter-example: the attribute line left its block
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). 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.

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 #<id>-<line>.

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).
explain.sql
1
2
3
4
5
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.

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

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-<num>.

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" >}}.
Example 4-1 Bloat ratio per table
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.

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.

A set of fences as 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.

Source
```bash {tab="Homebrew" group="oink-install" value="brew"}
brew install hugo
```
```bash {tab="APT" value="apt"}
sudo apt install hugo
```
Homebrew
brew install hugo
APT
sudo apt install hugo

The complete rules — group syntax, URL hash, cross-group synchronization, tabs in running text — are on the Tabs page.

Things that bite

  • 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

Output Shape
HTML A <div class="td-code"> 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

Inside the {…} after the language on the opening line, OINK’s own attributes:

title , non-empty string , defaultnone
The visible title bar (usually a filename) and the accessible name
filename , non-empty string , defaultnone
Historical alias of title; both together warn and use filename
copy , all command true false , defaultcommand for session lexers, all otherwise
true is all; command is allowed only on console/shell-session
wrap , boolean , defaultfalse
Visual wrapping, source unchanged; mutually exclusive with table line numbers
collapse , positive integer , defaultnone
Lines shown initially; ignored when the block is shorter
label , non-empty string , defaultderived from the title
Accessible name, not displayed; mutually exclusive with aria-label
id , non-empty token , defaultgenerated
Stable block ID and line-anchor prefix; no whitespace
tab , non-empty string , defaultnone
Tab label, see Tabs; mutually exclusive with num
group , ^[a-z][a-z0-9_-]*$ , defaultnone
On the first fence of a set; enables hash / sync / persistence; requires tab
value , ^[a-z0-9][a-z0-9_-]*$ , defaultnone
Required on every fence of a group, forbidden without one; requires tab
num , [0-9A-Za-z.-]+ , defaultnone
Numbered example (Book eg); must appear with caption
caption , plain text , defaultnone
The numbered example’s caption; must appear with num
class , class list , defaultnone
Appended to the .td-code root element
data-* / aria-* / role , string , defaultnone
Passed through to the root element

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:

lineNos , false inline table , defaultfalse
Line-number style; table is mutually exclusive with wrap=true
lineNoStart , positive integer , default1
First displayed number; does not affect how hl_lines counts
hl_lines , lines and ranges , defaultnone
For example "2 4-5", counted over the source lines in the fence
anchorLineNos , boolean , defaultfalse
Line numbers become anchor links prefixed with the block’s id
tabWidth , positive integer , defaultHugo’s default
Spaces a tab expands to

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.
  • Tabs — the full rules for assembling adjacent fences
  • Include — pull a real file from the repository in as a code block
  • Publishing books — numbered examples, cross references, the list of examples
  • Print — what long code looks like on paper

4 - Tabs

A {tab=} attribute on adjacent fences or tables makes a tab set; add a group and it becomes linkable, synchronized and remembered.

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

Write two fences carrying tab back to back, separated by a blank line only.

Source
```bash {tab="Homebrew"}
brew install hugo
```
```bash {tab="Debian / Ubuntu"}
sudo apt install hugo
```
Homebrew
brew install hugo
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

Write group on the first block only and the set gains a public URL hash #<group>-<value>, in-page synchronization and browser persistence. Every block in a group must carry value.

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
```
npm
npm create hugo-site@latest
pnpm
pnpm create hugo-site
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

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.

Source
```bash {tab="npm" group="pkgmgr" value="npm"}
npm run build
```
```bash {tab="pnpm" value="pnpm"}
pnpm build
```
npm
npm run build
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

The same attributes on a table’s attribute line group adjacent tables into a tab set.

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"}
PostgreSQL 18
Parameter Default
shared_buffers 25% RAM
max_connections 100
PostgreSQL 13
Parameter Default
shared_buffers 128MB
max_connections 100

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

A fence can carry both tab and title: the label goes in the tab bar, the filename title bar stays inside the panel.

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
hugo.yml
params:
  ui:
    sidebar_menu_foldable: true
TOML
hugo.toml
[params.ui]
sidebar_menu_foldable = true

A lone block is just a titled 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.

Source
```ini {tab="on its own"}
listen_addresses = '*'
```
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 <!-- prettier-ignore-end --> 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

When a panel holds paragraphs, lists, callouts, or several blocks, use the tabs/tab shortcode. The body is full Markdown.

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

The repository ships .github/workflows/; a push to main builds and publishes.

Note

baseURL has to be the repository’s Pages address.

Connect the repository in the Cloudflare dashboard; the build command is:

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

Output Shape
HTML <div class="td-tabs"> 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

Attributes on a fence info line or a table attribute line:

tab , non-empty string , defaultnone
The visible label; on a lone block it is simply that block’s title
group , ^[a-z][a-z0-9_-]*$ , defaultnone
On the first block of a set; enables hash, in-page sync and persistence; requires tab
value , ^[a-z0-9][a-z0-9_-]*$ , defaultnone
Required on every block of a group, forbidden without one; requires tab

The tabs shortcode:

group , ^[a-z][a-z0-9_-]*$ , defaultnone
As above: hash, sync and persistence
default , a child’s value , defaultthe first child
The initially selected panel; requires group
label , plain text , defaultlocalized “Tabs”
Accessible name for the tab bar; not displayed

The tab shortcode:

label , plain text , required
The visible label
value , ^[a-z0-9][a-z0-9_-]*$ , required
Forbidden without a group, where tab1, tab2 … are generated

Behavioural contract: in a group the panel ID is <group>-<value>; 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:<group>. 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

  • 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).
  • 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.
  • Code blocks — the rest of the fence attributes (title, copy, line numbers, folding)
  • Tables — the rest of the table attribute line
  • Callouts — for folding rather than juxtaposing
  • Steps — tabs inside a procedure

5 - 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.

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

Without an attribute line it is just a table. Alignment still comes from the delimiter row, and header cells are th scope="col".

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

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”.

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="…"} adds a visible <caption>. It is plain text and it does not number the table.

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"}
Theme facts this site currently builds against
Item Value
Theme version v0.8.1
Hugo floor 0.160.1 Extended
Licence Apache-2.0

Compatibility matrices

{.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.

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

Using the whole canvas

{.full-width} lets a table exceed the reading column and take the full width the article has. It suits tables with many short columns.

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

Field lists

{.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 page.

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"}
offline_search , boolean , defaultfalse
Build the local search index
page_width , string , defaultnormal
Width of the reading column

Numbered tables

In a book or a long manual, number the tables: num plus an optional #id and caption. The table is wrapped in a <figure> 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-<num>.

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
Table 9-1 Anomalies each PostgreSQL isolation level permits

See Table 9-1.

Tables as 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 page.

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"}
Content
Directory Contents
content/ Pages
data/ Landing and release data
Assets
Directory Contents
assets/ SCSS and image resources
static/ Files copied verbatim

Output

Output Shape
HTML A focusable <div class="td-table-scroll"> around the <table>; 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

The attribute line on the row below the table:

.full-width , marker , defaultnone
Exceed the reading column and use the article canvas
.matrix , marker , defaultnone
First column as row header, header and first column pinned, other cells centred
.fields , marker , defaultnone
Render as a definition list, see Fields
caption , plain text , defaultnone
Visible table caption; on .fields it labels the list
meta , role list , defaultnone
Names the meaning of the middle .fields columns: type required default -; requires .fields
#id , identifier , defaulttbl-<num> when num is set
[A-Za-z][A-Za-z0-9_.:-]*; lands on the <table>, or on the <figure> for a numbered table
num , string , defaultnone
[0-9A-Za-z.-]+; registers a Book table target and prefixes the caption with “Table N.”
tab / group / value , see Tabs , defaultnone
Adjacent tables become a tab set
class , class list , defaultnone
Left on the <table> for site CSS
data-* / aria-* , string , defaultnone
Passed through

style, on*, and other keys warn and are ignored; strict publishing rejects the warning.

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 <!-- prettier-ignore-start --> / <!-- prettier-ignore-end -->.
  • 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.
  • Fields — everything {.fields} can do
  • Tabs — adjacent tables as a tab set
  • Publishing books — numbered tables, cross references, the list of tables
  • Code blocks — where attributes go on the info line instead of the next line

6 - 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.

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

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.

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}
offline_search , Typeboolean , Defaultfalse
Build the local search index and enable the command palette
offline_search_max_results , Typeinteger , Default10
Maximum number of search results
page_width , Typestring , Defaultnormal
Reading column width: narrow normal wide

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

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"}
baseURL , string , required
Site address, subpath included
title , string , required
Site name, shown in the navbar and the tab
defaultContentLanguage , string , defaulten
Default language; decides which language unprefixed paths belong to

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:

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 -"}
HUGO_MODULE_WORKSPACE , string , Scopebuild
Points at go.work so the theme resolves from a local checkout
HUGO_ENV , string , Scopebuild
Set to production to enable minification and fingerprinting

Labels and container IDs

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.

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

params.ui.image_zoom

enable , boolean , defaultfalse
Turn image zoom on
selector , string , default.td-content
Root selector scanned for candidate images

Every entry is linkable

Each entry gets an anchor of the form field-<name>, and a self-link icon appears beside the name on hover. page_width in the first table above is #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

When the description needs several paragraphs, a list or a code block, a table cell cannot hold it. Use fields/field:

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

Log level, from low to high:

  • debug: print every remote call
  • info: the default
  • error: output only on failure
--dry-run , boolean , defaultfalse

Print what would happen and change nothing:

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

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

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

Output Shape
HTML <div class="td-fields"> around a semantic <dl>; entries carry #field-<name> 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 <dl>, without entry anchors

No script is loaded.

Parameter reference

The table attribute line, on the row below the table:

.fields , marker , defaultnone
Required; renders the table as a field list
meta , role list , defaultnone
Space-separated type required default -; one per middle column; semantic roles cannot repeat
caption , plain text , defaultnone
Visible label and the list’s accessible name
id , identifier , defaultnone
ID of the outer container
class , class list , defaultnone
Passed through for site CSS
data-* / aria-* , string , defaultnone
Passed through

The fields shortcode:

label , non-empty string , required
Visible label; the same thing the table’s caption does
id , identifier , required
Container ID; no whitespace, quotes, <, > or &
class / data-* / aria-* , string , required
The same policy as the table attribute line

The field shortcode:

name , non-empty string , required
The field name
type , non-empty string , required
Type label such as boolean, string[], duration
required , boolean , required
true shows the untranslated required chip; defaults to false
default , scalar , required
String / boolean / integer / float; false, 0 and "" all display

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.
  • Tables — the rest of the attribute line and the exclusion rules
  • Configuration — the full site parameter table, itself a field list
  • Front matter — the full front matter table
  • Steps — ordered actions do not belong in a field list

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

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

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.

Source
1. Install Hugo Extended
1. Clone OINK Starter
1. Start the local preview
{.steps}
  1. Install Hugo Extended
  2. Clone OINK Starter
  3. Start the local preview

{.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

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.

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.

    git clone https://github.com/pgsty/oink-starter my-docs
    cd my-docs
  2. Start the local server.

    hugo server
    Note

    The first build fetches the theme through the Go module proxy, which needs Go on the machine.

  3. 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

Shortcodes in {{< … >}} form — tabs, cards, badges — work inside a list item too. The {{% … %}} form does not; see Limits.

Splitting one step per platform

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.

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.

  2. Install the dependencies:

    EL / RHEL
    sudo dnf install golang git
    Debian / Ubuntu
    sudo apt install golang-go git
  3. Run hugo server to preview.

Continuing the numbering

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).

Source
4. Configure `baseURL` and the deployment workflow.
1. Push to `main` and wait for GitHub Actions to finish.
{.steps}
  1. Configure baseURL and the deployment workflow.
  2. Push to main and wait for GitHub Actions to finish.

Steps with headings

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.

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

You need Hugo Extended ≥ 0.160.1 and Go.

Run the server

brew install hugo go

sudo apt install hugo golang-go

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

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

Output Shape
HTML The native form is <ol class="steps"> with numbers and rule drawn in CSS; the shortcode form is <div class="td-steps"> 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

Neither form takes parameters — only conventions:

{.steps} , Whereline below the ordered list
Required; has no effect on an unordered list
1. , Whereevery item
Let Markdown count; the content indent is always three spaces
4. (first item) , Wherefirst item
Emits <ol start="4"> and continues from 4; supported for 2–40
{{% steps %}} , Wherearound a set of headings
Direct child headings (########) are the steps; the body is not indented

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 <!-- prettier-ignore-start --> / <!-- prettier-ignore-end --> 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.
  • Tabs — commands split per platform
  • Callouts — prerequisites and warnings inside a step
  • Code blocks — the commands in a step
  • Cards — “what next” once the procedure is done

8 - Cards

A link list plus {.cards} lays out a grid of navigation cards; switch to the shortcode when you need icons, badges or images.

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).

Shortest form

A link list with {.cards} is a card grid. The link is the title; whatever follows is the description.

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 — Clone this documentation site, delete what you do not need, replace the site details with your own.
  • Authoring — How pages are organized and which front matter keys exist.
  • Customization — Navigation, search, branding, languages.

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

The description is optional. One link per line, {.cards} at the end.

Source
- [Callouts](/docs/components/callout/)
- [Tabs](/docs/components/tabs/)
- [Steps](/docs/components/steps/)
- [Fields](/docs/components/fields/)
{.cards}

Loose lists and longer descriptions

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.

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

    Every page parameter is defined here exactly once: type, default, accepted values, and the page that explains it.

  • Configuration

    Site parameters grouped by feature, each row linking back to the guide that explains it.

Icons and badges

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.

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 startedstart here

Use OINK Starter and establish a local preview before customizing.

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

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.

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

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 >}}
An OINK documentation page: sidebar, article and table of contents

Sidebar, article, table of contents — each can be turned off on its own.

A decorative cover: decorative=true emits an empty alt and screen readers skip it.

Card images do not take part in image zoom — the whole card is already a link.

Automatic cards on section pages

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:

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:

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.

Which form to use

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

Output Shape
HTML Native form: <ul class="cards">. Shortcode form: <div class="td-content-cards"> with one <article class="td-content-card"> 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

The native form:

{.cards} , list attribute line , default
On the line after an unordered list; unordered lists only
First link in an item , Markdown link , default
The card title and the whole card’s click target
Everything else , Markdown , default
The description: after in a tight list, its own paragraph in a loose one

card parameters (cards itself takes none):

title , plain text , default
Required, non-empty. The card title
link , URL , default
Site path, relative path, http(s):, mailto:; external links get rel="noopener"
icon , Font Awesome class pair , default
For example fa-solid fa-rocket; a malformed value warns and is dropped
badge , plain text , default
A small label beside the title
image , image source , default
Page resource / global resource / static path / remote URL
image_alt , plain text , default
With image, exactly one of this and decorative
decorative , boolean , defaultfalse
true marks a decorative image and emits an empty alt
Body , Markdown , default
The card description

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

  • {.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.

9 - FileTree

A filetree fence draws an annotated directory structure — aligned comment column, per-entry icons, collapsible directories, a draggable split.

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

Source
```filetree
- content/
  - _index.md
  - docs/
  - blog/
- hugo.yml
- go.mod
```
  • 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

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 \#.

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
```
  • 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.ymlsite 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.

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

truncation in both columns

  • runbooks/
    • a-deliberately-long-runbook-filename-for-a-failover-drill.mdan equally overlong comment, kept on one line so it has to be clipped inside the comment column
    • restart.mdshort

Title bars

The fence attribute {title="…"} renders a title bar above the tree; without it there is none.

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

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.modthe theme, imported as a Hugo Module
  • Makefilemake d / make b / make c

Indentation and depth

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.

Source
```filetree
content/docs
├── about
│   ├── _index.md
│   └── features.md
├── components
│   ├── filetree.md
│   └── image
│       └── index.md
└── _index.md

3 directories, 5 files
```
  • content/docs
    • about
      • _index.md
      • features.md
    • components
      • filetree.md
      • image
        • index.md
    • _index.md

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

A directory with children is open by default; {open=false} starts it closed. Directories render as native <details>, 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.

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

the content directory

  • content/
    • docs/the documentation tree
      • components/22 component pages
        • callout.md
        • filetree.md
        • image/page bundle: body + images
      • customize/site-level configuration
        • config.md
    • blog/
      • release.md

Icons and tones

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: neutral info success warning danger.

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

deployment layout: permissions and what matters

  • /etc/pigsty/0755 root:root · configuration root
    • pigsty.yml0644 root:root · cluster inventory
    • ca/0700 root:root · self-signed CA, never commit
      • ca.key0600 root:root
  • /var/lib/pgsql/18/data/0700 postgres:postgres · data directory
    • postgresql.conf0600 postgres:postgres
  • /usr/bin/pig0755 root:root · command-line tool

tone colours the icon only, never the text. Colour is a supplement; the meaning belongs in the name or the comment.

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.

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

this site's component pages

One tree per platform

A fence carrying tab= (and group= / value=) becomes one panel of a tab set and can sit alongside code fences.

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
```
Linux
  • /etc/pigsty/configuration
  • /var/lib/pgsql/data
  • /usr/bin/pigexecutable
macOS
  • ~/Library/Application Support/pigsty/configuration
  • /opt/homebrew/bin/pigexecutable

Output

Output Shape
HTML <div class="td-filetree">, an optional title bar, directories as native <details>; 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 <pre>

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

Fence attributes, after ```filetree:

title , plain text , default
Title bar above the tree; omitted when absent; must not be empty
tab , plain text , default
Makes this tree one panel of a tab set
group / value , string , default
Tab group and sync value; must appear with tab
class , class list , default
Passed through for site CSS

Entry attributes, in the {…} at the end of a line:

icon , Font Awesome class pair , defaultmatched by name / extension
For example fa-solid fa-lock; a malformed value warns and uses the default icon
tone , enum , defaultneutral
neutral info success warning danger; colours the icon only
open , boolean , defaulttrue
Directories only; false starts it closed
type , enum , defaultinferred
dir or file, overriding the inference

The line syntax itself:

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

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

  • 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.
  • Code blocks — listings meant to be copied verbatim
  • Tabs — one tree per platform, side by side
  • Badgestone uses the same vocabulary
  • Organizing content — how a real content directory is laid out

10 - Math

Inline and display mathematics with KaTeX, rendered at build time — the reader downloads no script.

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.

Shortest form

An inline formula sits inside a sentence, with the surrounding spaces and punctuation outside the delimiters.

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 hit=HH+R\mathrm{hit} = \frac{H}{H + R}, where HH is blks_hit and RR is blks_read.

Display formulas

A formula in its own paragraph goes between $$, centred and set larger. \[…\] is equivalent.

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 ff over NN keys has height:

h=logfN 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

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.

Source
```math
N_{\text{conn}} = \lambda \cdot \bar{t}_{\text{resp}}
```
Nconn=λtˉrespN_{\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

The chem fence uses KaTeX’s mhchem extension, and its body is written \ce{…}. The same extension typesets physical units.

Source
```chem
\ce{CO2 + H2O <=> H2CO3 <=> H+ + HCO3^-}
```
COX2+HX2OHX2COX3HX++HCOX3X\ce{CO2 + H2O <=> H2CO3 <=> H+ + HCO3^-}

For the syntax see the mhchem manual.

Numbered equations

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-<num>. The number shows to the right of the formula with a localized “Equation” prefix.

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.
WALdayTPS×sˉrecord×86400 \text{WAL}_{\text{day}} \approx \text{TPS} \times \bar{s}_{\text{record}} \times 86400
Equation 3-1 Estimating daily WAL volume

See Equation 3-1: 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

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:

Source
Capacity planning starts from {{< xref eq="3-1" anchor="eq-wal" />}}.

Capacity planning starts from Equation 3-1.

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.

The 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.

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 >}}
σidx=rowsmatchedrowstotal\sigma_{\text{idx}} = \frac{\text{rows}_{\text{matched}}}{\text{rows}_{\text{total}}}
cseqP=crandσTc_{\text{seq}} \cdot P = c_{\text{rand}} \cdot \sigma \cdot T
Equation 3-2 Where a sequential scan and an index scan cost the same

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

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:

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. Delimiters must not collide with the prose: a single $ is deliberately not configured, so a price like “$5” is never read as mathematics.

Output

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

Four spellings:

\(…\) , Placementinline
Governed by the site’s passthrough configuration; takes no attributes
$$…$$ / \[…\] , Placementdisplay
As above; may be followed by an attribute line to become numbered
```math , Placementdisplay fence
Independent of passthrough; takes no attributes
```chem , Placementdisplay fence
As above, with \ce{…} in the body

The attribute line {…} under a display formula:

num , string , default
[0-9A-Za-z.-]+; registers a numbered equation and shows “Equation N” at the right
#id , identifier , defaulteq-<num>
[A-Za-z][A-Za-z0-9_.:-]*; the anchor and cross-reference target
caption , plain text , default
Caption after the number; requires num

The eq shortcode:

num , string , default
As above; without it the formula is an unnumbered display formula
id , identifier , defaulteq-<num>
Requires num
caption , plain text , default
Requires num
class , class list , default
Requires num; passed through for site CSS
Body , TeX , default
Required, non-empty

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

  • 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.

11 - Mermaid

A mermaid fence turns text into flowcharts, sequence diagrams, Gantt charts, class diagrams and state diagrams — rendered locally, theme-aware, diff-friendly.

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.

Shortest form

Source
```mermaid
flowchart LR
  content["content/"] --> Hugo
  config["hugo.yml"] --> Hugo
  theme["OINK theme"] --> Hugo
  Hugo --> site["public/"]
```
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

sequenceDiagram describes messages between participants over time, which suits request paths and load order.

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
```
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 draws intervals. Below is the five-year community support window of each PostgreSQL major version, counted from its release date; 1825d is five years.

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

classDiagram draws types and relationships, erDiagram entities and cardinality. Both are common ways to explain a data model.

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

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.

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

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.

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]
```
---
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

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:

hugo.yml
params:
  mermaid:
    theme: neutral
    flowchart:
      diagrampadding: 6

The full key table is in Configuration; for accepted values see the Mermaid configuration reference.

Inside tabs and steps

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.

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 >}}
flowchart LR
  Markdown --> Goldmark --> RenderHooks --> HTML
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.

Output

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 <pre class="td-mermaid-source">, static — no runtime runs there
Markdown The mermaid fence and its source, kept as written
RSS The source inside <pre class="td-mermaid-source"> — subscribers see text

Parameter 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):

params.mermaid , map , defaultunset
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 , defaultMermaid’s default
The light-mode theme; dark mode forces dark

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

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

  • Diagrams cannot be numbered: Mermaid emits inline SVG, not an <img>, so {#id num=} numbering does not apply. Export to an image when you need a number and use the 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.
  • PlantUML — more complete UML, at the price of a rendering server
  • Markmap — outline-shaped hierarchies
  • ECharts — charts with numbers in them
  • Images — hand-drawn SVG and numbering

12 - PlantUML

A plantuml fence writes sequence, class, component, activity and use-case diagrams; rendering requires a PlantUML server you configure yourself.

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 instead.

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). Every snippet below is correct PlantUML in itself.

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.

Shortest form

Sequence diagrams are the most common kind: participant declares a participant, -> is a synchronous message, --> a return.

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 lists members and "1" -- "0..*" gives a relationship its cardinality — the usual way to explain a data model.

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

package groups deployment units, [component] is a box, and --> is the direction of a dependency.

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

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.

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

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.

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

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.

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

The fence itself has no switch; whether it renders depends on the site configuration:

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

Output

Output Shape
HTML The source is emitted as <pre><code class="language-plantuml">; once enabled, the runtime replaces it with an <img> (with svg: true, an <svg data-src>)
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

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 have no effect here.

Site parameters (hugo.yml):

params.plantuml.enable , bool , defaultfalse
With it off, the fence stays a code block and no runtime loads
params.plantuml.svg_image_url , string , defaultnone
The rendering endpoint; the encoded source is appended to it. Required when enable: true, otherwise PlantUML warns and stays off
params.plantuml.svg , bool , defaultfalse
false inserts <img src>; true inserts <svg data-src> and loads an external SVG loader, putting the SVG in the DOM where CSS can reach it

The theme reads those three keys and nothing else.

Limits

  • <, >, & and " are double-escaped: the current theme version escapes the fence content once too often, leaving literal --&gt; and &#34; 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 or a pre-rendered 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 <img> the runtime inserts does not pass through the image render hook, so {#id num=} and image zoom do not apply.
  • Mermaid — no server, follows the colour scheme, the everyday choice
  • Draw.io — the other integration that needs a server of your own
  • Images — pre-rendered SVG: numberable, zoomable, no external dependency
  • Configuration — the full definition of params.plantuml.*

13 - Markmap

A markmap fence turns a Markdown outline into an expandable, zoomable mind map — and the source stays a readable outline.

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.

Shortest form

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
```
# 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

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.

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
```
# 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

Nodes take inline Markdown: links are clickable, inline code is monospaced, bold and italic behave as usual.

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)
```
# 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

The Markmap runtime carries a local KaTeX, so $…$ inside a node renders as a formula.

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}$
```
# 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

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.

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

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.

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
> ```
What the theme repository looks like
# 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

Output Shape
HTML <pre><code class="language-markmap"> first; the runtime replaces it with <div class="markmap"> 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

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):

params.markmap , bool , defaultfalse
With it off, the fence stays a code block and no runtime loads

The key is defined in Configuration. 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.

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 &gt; or &#34;. Write links as [text](URL) rather than as autolinks in angle brackets.

14 - 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.

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.

Shortest form

The syntax is the plain image syntax. The filename does not matter; .drawio.svg is only a convention.

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

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

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.

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

With a caption

Draw.io images go through the ordinary image render hook, so every image attribute still applies. Add caption for a captioned figure; the edit button still appears on the image.

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
Content, configuration and theme templates flow into Hugo and out as public/

As a numbered figure

Add {#id num=…} for a cross-referenceable numbered figure, which xref can reach and which appears in the list of figures like any other.

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
Figure 1-1 From content to a static site

The complete numbering and cross-reference rules are in publishing books.

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.

Source
![The Hugo build pipeline (PNG export)](pipeline.drawio.png)
{width="620" height="140"}
The Hugo build pipeline (PNG export)

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

Three things, in order.

Lay an overlay over the page

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

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

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

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

Output

Output Shape
HTML A plain <img> or <figure>; once enabled, the runtime wraps an image that carries a copy in <div class="drawio"> 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 <img> 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

There are no fence or shortcode parameters of its own. The image attribute line is the one from Images: caption, width, height, link, #id, num, command, options.

Site parameters (hugo.yml):

params.drawio.enable , bool , defaultfalse
With it off no script loads and an image is just an image
params.drawio.drawio_server , string , defaultnone
The editor address; required when enable: true

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.
  • Images — captions, numbering, sizing and zoom in full
  • PlantUML — the other integration that needs a server
  • Mermaid — diagrams from text with no server at all
  • Configuration — the full definition of params.drawio.*

15 - 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.

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; for order and hierarchy use 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

A bar chart needs three parts: xAxis, yAxis, series. Below is how many pages each of the six documentation sections has.

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]
```
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

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.

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]
```
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

Give radius two values for a doughnut. Below is how OINK’s 29 shortcodes break down by purpose.

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

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.

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]
```
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

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:

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]
```
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:

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:

Source
<script>
  window.OinkEchartsFunctions = window.OinkEchartsFunctions || {};
  window.OinkEchartsFunctions.pageShare = function (params) {
    var p = params[0];
    return p.name + ': ' + p.value + ' pages, ' + Math.round((p.value / 60) * 100) + '% of the site';
  };
</script>

```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]
```
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

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 or the data/-driven components on a release page.

Output

Output Shape
HTML A canvas container inside <div class="td-echarts"> plus an application/json options block; the local ECharts draws it
Print No chart; the fence source inside <pre class="td-echarts-source">
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

The fence attribute line (```echarts {…}):

height , CSS length , default400px
A non-negative number plus px rem em vh vw %; anything else warns and uses the default
theme , string , defaultunset
Pin an ECharts theme and stop following the site’s colour scheme; only dark is built in
full , bool , defaultfalse
true drops the reading-column limit and fills the content area
class , space-separated classes , default
Passed through to the container for site CSS

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.

There is no site-level parameter: ECharts needs no switch in hugo.yml and loads only where it is used.

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.
  • Infographic — structure and order, not statistics
  • Tables — for few values that must be read exactly
  • Mermaid — relationship and flow diagrams
  • Code blocks — the general rules for fence attribute lines

16 - 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.

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; when you need a flow with conditional branches use Mermaid. The fence body is data, and it stays readable text on GitHub.

Shortest form

The first line is infographic <template>, followed by a data block: title is the title and every entry under items needs at least a label.

Source
```infographic
infographic list-row-simple-horizontal-arrow
data
  title Three steps in one documentation change
  items
    - label Write
      desc Start with the source language
    - label Check
      desc Zero build warnings, every example really rendered
    - label Ship
      desc Add the translated peer, open the PR
```
infographic list-row-simple-horizontal-arrow
data
  title Three steps in one documentation change
  items
    - label Write
      desc Start with the source language
    - label Check
      desc Zero build warnings, every example really rendered
    - label Ship
      desc Add the translated peer, open the PR

Indentation decides the structure, two spaces per level. Keep labels short and put the explanation in desc.

Timelines

The sequence-timeline-* family lays the items out on a time axis, with label as the point in time and desc as the event.

Source
```infographic {height="420px"}
infographic sequence-timeline-simple
data
  title The last five PostgreSQL major versions
  items
    - label 2021
      desc 14: another round of parallel query and logical replication work
    - label 2022
      desc 15: the MERGE statement
    - label 2023
      desc 16: logical replication from a standby
    - label 2024
      desc 17: incremental backup and JSON_TABLE
    - label 2025
      desc 18: the asynchronous IO subsystem
```
infographic sequence-timeline-simple
data
  title The last five PostgreSQL major versions
  items
    - label 2021
      desc 14: another round of parallel query and logical replication work
    - label 2022
      desc 15: the MERGE statement
    - label 2023
      desc 16: logical replication from a standby
    - label 2024
      desc 17: incremental backup and JSON_TABLE
    - label 2025
      desc 18: the asynchronous IO subsystem

Funnels

sequence-funnel-simple draws stages that narrow. Below are the theme’s five release states: they are not interchangeable, and only the last one is live.

Source
```infographic {height="420px"}
infographic sequence-funnel-simple
data
  title The five states a theme release passes through
  items
    - label Source complete
      desc The code is written, and that is all
    - label Validated
      desc Theme checks and the site suite are green
    - label Published
      desc An immutable signed tag, resolvable through the Go proxy
    - label Documented
      desc The documentation site pins that tag
    - label Deployed
      desc Production runs this version
```
infographic sequence-funnel-simple
data
  title The five states a theme release passes through
  items
    - label Source complete
      desc The code is written, and that is all
    - label Validated
      desc Theme checks and the site suite are green
    - label Published
      desc An immutable signed tag, resolvable through the Go proxy
    - label Documented
      desc The documentation site pins that tag
    - label Deployed
      desc Production runs this version

Grid cards

When items have no order between them, list-grid-* arranges them in a grid rather than a queue.

Source
```infographic {height="380px"}
infographic list-grid-compact-card
data
  title One page, four outputs
  desc Every content component has to produce something usable in all four
  items
    - label HTML
      desc Interactive, runtimes loaded on demand
    - label Print
      desc Disclosures expanded, zoom and copy removed
    - label Markdown
      desc Plain text, compared byte for byte against goldens
    - label RSS
      desc Static, from the same source as print
```
infographic list-grid-compact-card
data
  title One page, four outputs
  desc Every content component has to produce something usable in all four
  items
    - label HTML
      desc Interactive, runtimes loaded on demand
    - label Print
      desc Disclosures expanded, zoom and copy removed
    - label Markdown
      desc Plain text, compared byte for byte against goldens
    - label RSS
      desc Static, from the same source as print

Items with values

Add value to an item and templates that express proportion — pies, doughnuts, progress — will use it.

Source
```infographic {height="400px"}
infographic chart-pie-donut-plain-text
data
  title How the 29 shortcodes break down
  items
    - label Core components
      value 14
    - label Book numbering and indexes
      value 10
    - label Releases and downloads
      value 3
    - label OpenAPI
      value 2
```
infographic chart-pie-donut-plain-text
data
  title How the 29 shortcodes break down
  items
    - label Core components
      value 14
    - label Book numbering and indexes
      value 10
    - label Releases and downloads
      value 3
    - label OpenAPI
      value 2

Hierarchy and hand-drawn style

Items can nest through children, and hierarchy-mindmap-* draws two levels of structure. A top-level theme block changes the whole look; type takes light, dark or hand-drawn.

Source
```infographic {height="320px"}
infographic hierarchy-mindmap-level-gradient-compact-card
theme
  type hand-drawn
data
  root
    label Theme repository
    children
      - label layouts
        desc templates
        children
          - label _markup
            desc render hooks
          - label _partials
            desc shell and helpers
      - label assets
        desc resources
        children
          - label scss
            desc tokens and component styles
          - label js
            desc browser runtimes
          - label third_party
            desc libraries shipped with the theme
```
infographic hierarchy-mindmap-level-gradient-compact-card
theme
  type hand-drawn
data
  root
    label Theme repository
    children
      - label layouts
        desc templates
        children
          - label _markup
            desc render hooks
          - label _partials
            desc shell and helpers
      - label assets
        desc resources
        children
          - label scss
            desc tokens and component styles
          - label js
            desc browser runtimes
          - label third_party
            desc libraries shipped with the theme

theme belongs to the DSL, not to the fence attributes, and it does not follow the site’s colour scheme: a diagram with type dark stays dark on a light page. Check contrast in both modes.

Picking a template

Template names are structure-variant, and one structure has several visual variants. The common families:

Structure prefix What it expresses Example
list-row-* list-column-* Items in a row or a column list-row-simple-horizontal-arrow
list-grid-* A grid, no order between items list-grid-compact-card list-grid-badge-card
list-pyramid-* sequence-funnel-* Narrowing stages sequence-funnel-simple
sequence-timeline-* sequence-roadmap-vertical-* Timelines and roadmaps sequence-timeline-simple
sequence-steps-* sequence-snake-steps-* Ordered steps sequence-steps-simple
compare-binary-horizontal-* compare-quadrant-* Binary comparison and quadrants compare-binary-horizontal-simple-vs
hierarchy-mindmap-* hierarchy-structure-* Hierarchy, with children hierarchy-mindmap-level-gradient-compact-card
chart-pie-* chart-bar-* chart-column-* Illustrative charts, with value chart-pie-donut-plain-text
relation-network-* relation-dagre-flow Networks and flows, with relations relation-dagre-flow

Choose the smallest form that makes the relationship clear. The full gallery is at AntV Infographic, and the template names match the version shipped with the theme.

Output

Output Shape
HTML A canvas container inside <div class="td-infographic"> plus the DSL; the local AntV runtime draws the SVG
Print No diagram; the DSL source inside <pre class="td-infographic-source">
Markdown The infographic fence and its DSL, kept as written
RSS Same as print — source only

Whatever the diagram says, say it in the prose too: print and RSS carry the DSL and nothing else.

Parameter reference

The fence attribute line (```infographic {…}):

height , auto or a CSS length , defaultauto
A non-negative number plus px rem em vh vw %; anything else warns and uses auto
full , bool , defaultfalse
true drops the reading-column limit
class , space-separated classes , default
Passed through to the container

style, on*, and unknown attributes warn and are ignored. An empty DSL body warns and renders nothing. Strict publishing rejects all these warnings.

The DSL’s top-level keys (AntV’s, not the theme’s):

infographic / template
The template name, on the first line
data
title, desc, items (or sequences, compares, nodes, values, relations, root, depending on the structure), order
theme
type (light / dark / hand-drawn), palette, colorPrimary, stylize
width / height
Canvas size at the DSL level; usually left to the fence’s height
design
Per-part tuning; rarely needed

Each entry under items accepts label, desc, value, icon, children, group and id. The DSL is defined by the AntV Infographic documentation; the version shipped with the theme and its checksum are recorded in the theme’s VENDOR.json.

Limits

  • A wrong template name does not fail the build: Hugo checks the fence attributes only, the DSL is parsed by the browser runtime, and a missing template shows a line of error text in the container. Check the page after changing a template name.
  • No colour-scheme awareness: theme lives in the DSL, so check contrast in both modes.
  • Print and RSS carry the DSL only, so the conclusion belongs in the prose.
  • SVG is not a semantic structure: the order a screen reader gets is not necessarily the visual order. Prefer headings, lists and tables when they can say it.
  • Keep labels short: long text is truncated or squeezed on a narrow screen, so check at phone width after editing.
  • ECharts — when you need axes and exact numbers
  • Steps — when the reader has to follow the procedure
  • Cards — a grid of clickable entry points
  • Mermaid — flows with branches and conditions

17 - 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.

A gallery arranges related images in a responsive grid, one image per line inside the fence. It suits several views of one thing: a few screenshots, a few states, a few colour schemes. A single image is an image, and images with no order or comparison between them do not belong in one gallery.

Shortest form

One image per line, written as Markdown’s ![alt](src).

Source
```gallery
![OINK's default documentation shell](/images/oink.webp)
![The classic Docsy layout upstream](/images/docsy.webp)
```

Alternative text is mandatory: it is the item’s title, the only text a screen reader gets, and what decides whether the image can zoom. There is no column parameter — the grid adapts to the container and drops columns on a narrow screen.

Descriptions

Start a description with # after the image and it appears underneath. Descriptions are plain text, so Markdown inside them shows literally; for a literal hash write \#.

Source
```gallery
![The three-column layout of an OINK page](/images/oink.webp) # The default shell: sidebar, article, table of contents
![The classic Docsy documentation layout](/images/docsy.webp) # Docsy upstream — the content model is the same lineage
![A release notes page](/images/releasenote.webp) # Release pages are generated from facts in data/download, offline
```

Descriptions need not be the same length: the grid aligns to the tallest item and a wrapped description does not disturb its neighbours. The image is parsed first, so a # inside the alt text or the path needs no escaping.

{link=…} at the end of a line turns that item into a link. Site paths, relative paths and http(s): all work.

Source
```gallery
![OINK's default documentation shell](/images/oink.webp) # Opens the Images component page {link=/docs/components/image/}
![A release notes page](/images/releasenote.webp) # Opens "Releases and downloads" {link=/docs/write/releases/}
```

A linked item does not zoom, because clicking already means something else. Both kinds can share one gallery: linked items open a page, the rest open the full image.

Where images come from

Sources resolve exactly as for a plain image: page resource (a file next to the page in its bundle) → global resource in assets/ → static path /images/… → remote URL. A local resource carries its intrinsic size, so the page does not shift while loading; a remote image is neither downloaded at build time nor measured.

Source
```gallery
![OINK documentation overview (global resource)](images/content-primitives/oink.webp) # Under assets/images/…, eligible for build-time processing
![The light home page (static path)](/images/hero-light.webp) # Under static/images/…, published as is
```

An unresolved page/global resource is retained as a static path, just like an explicit static path; the theme does not check static or remote existence.

Decorative images and zoom

Empty alternative text marks a decorative image: no title, skipped by screen readers, and never a zoom candidate.

Image zoom is a site-level switch and is off by default. This page turns it on in its front matter, so every image above that has alt text and no link opens full size (Esc closes it and focus returns where it was).

this page's front matter
image_zoom: true
Source: one decorative image, one ordinary one
```gallery
![](/images/docsy.webp) # Decorative, never zooms
![The Pigsty release notes page](/images/releasenote.webp) # Has alt text, so it opens
```

A gallery has no zoom runtime of its own; it reuses the one dialog the page shares. With no zoomable image on the page, that runtime is never loaded. The details are in Images · Zoom.

Classes and tabs

class can go on the whole fence (after the language) or on one item (at the end of its line). The theme does not interpret it and passes it through for site CSS. A fence carrying tab= (with group= / value=) becomes one panel of a tab set.

Source
```gallery {tab="OINK" group="shell" value="oink"}
![OINK's default documentation shell](/images/oink.webp) # Sidebar, article, table of contents
```
```gallery {tab="Docsy" value="docsy"}
![The classic Docsy layout upstream](/images/docsy.webp) # The same content-model lineage
```
OINK
Docsy

Output

Output Shape
HTML <ul class="td-gallery"> with one <li> per item; eligible images carry data-td-image-zoom; everything is lazy-loaded
Print The same images stacked, without zoom markers
Markdown The gallery fence, emitted as written
RSS The same static stack as print

Galleries load no JavaScript of their own.

Parameter reference

The line syntax ![alt](src) [# description] [{key=value …}]:

![alt](src) , Requiredyes
Must start the line. alt is the item’s title; empty means decorative
src , Requiredyes
Page resource / global resource / static path / remote URL
# description , Requiredno
Plain text under the image; \# is a literal hash; must not be empty
{link=…} , Requiredno
Makes the item a link, and therefore not zoomable
{class=…} , Requiredno
Adds a site CSS class to that item

Fence attributes:

tab , plain text , default
Makes this gallery one panel of a tab set
group / value , string , default
Tab group and sync value; must appear with tab
class , class list , default
Passed through for site CSS

There is no columns, caption or title attribute. A malformed line or attribute warns, drops only the invalid part or line, and names the line number inside the fence. Strict publishing rejects the warning.

Limits

  • The fence is the only form: there is no {.gallery} list marker and no shortcode. The cost is that the source does not render as images on GitHub; the benefit is that four-state output and zoom eligibility are guaranteed by the theme.
  • Columns cannot be set and images are not cropped to one aspect ratio: the grid follows the viewport and images keep their own proportions.
  • No slideshow, no carousel, no previous / next: the zoom dialog shows one image at a time.
  • Remote images are not downloaded: there is no network request at build time, so a remote image’s size is unknown until the browser loads it and the layout may shift.
  • Descriptions are not Markdown: put rich text in a paragraph under the gallery.
  • Images — single images, captions, numbering, the zoom switch
  • Cards — a grid of links with images
  • Tabs — one gallery per platform or theme
  • File trees — the same line syntax family

18 - Badge

Put a semantic status label next to a feature name, a version or a table cell — five tones, no custom colours.

A badge is an inline status label that sits right after a name: Beta, deprecated, v0.5, needs a server. It suits a status of one or two words. The author picks a semantic tone and the theme picks the colour, with contrast guaranteed in light and dark. When the status needs an explanation, a procedure or a deadline, use prose or a callout.

Shortest form

Source
{{< badge text="Beta" tone="warning" >}}
Beta

text is the only required parameter and must be a non-empty string.

Five tones

These five values, and no custom colours.

Source
{{< badge text="Default" >}}
{{< badge text="Info" tone="info" >}}
{{< badge text="Supported" tone="success" >}}
{{< badge text="Experimental" tone="warning" >}}
{{< badge text="Deprecated" tone="danger" >}}

Default Info Supported Experimental Deprecated

Without tone the badge is neutral. Any other value warns and uses neutral in ordinary preview; the warning names the source location and fails a strict publishing build.

Inside a sentence

A badge is an inline element that follows a name; it never takes its own line.

Source
With `params.ui.image_zoom` {{< badge text="off by default" tone="neutral" >}} enabled,
block images that have alt text open full size. PlantUML {{< badge text="needs a server" tone="warning" >}}
and Draw.io {{< badge text="needs a server" tone="warning" >}} warn and remain off when no
endpoint is configured, rather than reaching for a public service.

With params.ui.image_zoom off by default enabled, block images that have alt text open full size. PlantUML needs a server and Draw.io needs a server warn and remain off when no endpoint is configured, rather than reaching for a public service.

Next to a heading

Never put a shortcode in a heading. Hugo builds the table of contents before it expands shortcodes, so the badge renders correctly on the heading while the table of contents is left with an internal Hugo placeholder. Put the status in the first paragraph under the heading instead:

Source
### OpenAPI pages {#openapi-example}

{{< badge text="new in 0.5" tone="success" >}} This section covers…

OpenAPI pages

new in 0.5 The badge sits just under the heading, the table of contents stays clean, and sharing the anchor link does not drag the badge text along.

In table cells

Badges make a comparison table easier to scan than a column of “yes” and “no”.

Source
| Component | Form | Status |
| --- | --- | --- |
| Callouts | `> [!NOTE]` | {{< badge text="stable" tone="success" >}} |
| Galleries | ` ```gallery ` fence | {{< badge text="stable" tone="success" >}} |
| PlantUML | ` ```plantuml ` fence | {{< badge text="needs a server" tone="warning" >}} |
| The `image` shortcode | — | {{< badge text="removed" tone="danger" >}} |
Component Form Status
Callouts > [!NOTE] stable
Galleries ```gallery fence stable
PlantUML ```plantuml fence needs a server
The image shortcode removed

In lists and steps

Source
1. Install Hugo Extended {{< badge text="≥ 0.160.1" tone="info" >}}
1. Create a site from OINK Starter and change `baseURL` in `hugo.yaml`
1. `hugo server` to preview {{< badge text="port 1313" tone="neutral" >}}
{.steps}
  1. Install Hugo Extended ≥ 0.160.1
  2. Create a site from OINK Starter and change baseURL in hugo.yaml
  3. hugo server to preview port 1313

On cards

A card has its own badge parameter — plain text, fixed to the right of the title — and the card body can hold badge shortcodes.

Source
{{< cards >}}
{{< card title="Hugo Module" icon="fa-brands fa-golang" badge="recommended" >}}
One `hugo mod get` and you are done {{< badge text="needs Go" tone="info" >}}
{{< /card >}}
{{< card title="Offline archive" icon="fa-solid fa-box-archive" >}}
Builds on a machine with no network {{< badge text="manual upgrades" tone="warning" >}}
{{< /card >}}
{{< /cards >}}
Hugo Modulerecommended

One hugo mod get and you are done needs Go

Offline archive

Builds on a machine with no network manual upgrades

With link the badge becomes an <a>: site paths, relative paths, http(s): and mailto: all work.

Source
Current version {{< badge text="v0.5" tone="info" link="/blog/" >}};
for the upgrade steps see {{< badge text="Upgrading" tone="neutral" link="/docs/admin/upgrade/" >}}.

Current version v0.5; for the upgrade steps see Upgrading.

An illegal link warns and is dropped, leaving a plain badge in ordinary preview; strict publishing rejects the warning.

Output

Output Shape
HTML <span class="td-badge td-badge--<tone>">, or <a class="td-badge …"> when linked
Print Same as HTML, a static inline element
Markdown **Beta**, or [**Beta**](/…) when linked
RSS Same as print

No JavaScript. A badge is not a live region, so adding one does not announce anything to a screen reader.

Parameter reference

text , plain text , default
Required, non-empty. What the reader sees
tone , enum , defaultneutral
neutral info success warning danger
link , URL , default
Turns the badge into a link

Named parameters only. There is no icon, class, color, outline or size parameter. Invalid input warns and takes the safe result: unknown parameters are ignored, empty text renders nothing, bad tone becomes neutral, and an unsafe link is dropped. Strict publishing rejects every such warning.

Limits

  • Colour is not the meaning: tone supplements the text, which has to say it. {{< badge text="🔴" >}} tells a screen reader nothing.
  • No icon parameter: when you need an icon, use cards or a callout.
  • Keep the text short: a badge follows a name without wrapping, so anything longer than a few words belongs in the prose.
  • No more than three in one place: a row of badges drowns out the name it qualifies.
  • Badges exist only as a shortcode — there is no native Markdown form — and in a plain Markdown reader they degrade to bold text.
  • Cardscard has a badge parameter of its own
  • File treestone uses the same vocabulary
  • Keys — the other inline shortcode
  • Callouts — when the status needs explaining

19 - 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.

Keys separate what the reader has to press from the prose. Use it for shortcuts and chords: one positional parameter per key, and the theme draws the caps, adds the separators, and gives screen readers a readable sequence. Command names, flags and text to type are inline code — they are not physical keys.

Shortest form

Source
Press {{< kbd "Ctrl" "K" >}} to open the command palette.

Press Ctrl with K to open the command palette.

Parameters must be quoted, one key per positional parameter. Missing, empty, or named parameters warn and render no invalid key in ordinary preview; strict publishing rejects the warning.

A single key

One parameter is one key, and symbol keys are written as they are.

Source
{{< kbd "Escape" >}} closes a dialog;
{{< kbd "/" >}} jumps to search;
{{< kbd "t" >}} toggles light and dark;
{{< kbd "l" >}} cycles through languages.

Escape closes a dialog; / jumps to search; t toggles light and dark; l cycles through languages.

Chords

Several parameters render in order with + between them. That plus sign is hidden from assistive technology, which hears a localized connector instead.

Source
{{< kbd "⌘" "Shift" "P" >}} and {{< kbd "Ctrl" "Shift" "P" >}} are the same action.
For a literal plus, treat it as a key of its own: {{< kbd "Ctrl" "+" >}} zooms the page in.

with Shift with P and Ctrl with Shift with P are the same action. For a literal plus, treat it as a key of its own: Ctrl with + zooms the page in.

Platform differences

Write the label printed on the reader’s keyboard: on macOS, Ctrl on Windows and Linux. Never merge two platforms into one sequence — a spelling like Ctrl/⌘ cannot be read aloud correctly. Say which platform in the sentence, or split into tabs.

Source
On macOS press {{< kbd "⌘" "K" >}}; on Windows and Linux, {{< kbd "Ctrl" "K" >}}.

On macOS press with K; on Windows and Linux, Ctrl with K.

Shortcut tables

A cheatsheet is where keys most often live. Here are some of the global keys this site honours:

Source
| Key | Action |
| --- | --- |
| {{< kbd "Ctrl" "K" >}} | Open the command palette ({{< kbd "⌘" "K" >}} on macOS) |
| {{< kbd "/" >}} | The palette's full search state |
| {{< kbd "t" >}} | Toggle light and dark |
| {{< kbd "q" >}} / {{< kbd "e" >}} | Previous / next page |
| {{< kbd "w" >}} {{< kbd "s" >}} {{< kbd "a" >}} {{< kbd "d" >}} | Move, collapse and expand in the sidebar tree |
| {{< kbd "Escape" >}} | Leave the sidebar tree for the article |
Key Action
Ctrl with K Open the command palette ( with K on macOS)
/ The palette’s full search state
t Toggle light and dark
q / e Previous / next page
w s a d Move, collapse and expand in the sidebar tree
Escape Leave the sidebar tree for the article

The complete list of site-wide shortcuts is in keyboard navigation.

In steps

Source
1. Press {{< kbd "Ctrl" "K" >}} to open the command palette
1. Type `>` for the command-only state, or type a keyword to search
1. Select with {{< kbd "↑" >}} {{< kbd "↓" >}} and press {{< kbd "Enter" >}} to go
1. {{< kbd "Escape" >}} closes it and focus returns where it was
{.steps}
  1. Press Ctrl with K to open the command palette
  2. Type > for the command-only state, or type a keyword to search
  3. Select with and press Enter to go
  4. Escape closes it and focus returns where it was

Raw <kbd> tags

A raw <kbd> tag in Markdown gets the same styling, and GitHub renders it too. The difference is that the separators and the accessible sequence are then yours to maintain: either spelling works for a single key, but use the shortcode for chords.

Source
Press <kbd>F5</kbd> to reload; in an editor, <kbd>Ctrl</kbd>+<kbd>S</kbd> saves.

Press F5 to reload; in an editor, Ctrl+S saves.

Output

Output Shape
HTML <span class="td-kbd-sequence"> around one <kbd> per key; the visible + is hidden from screen readers, which get a localized connector
Print Same as HTML, static
Markdown Plain text: Ctrl + K, ⌘ + Shift + P
RSS Same as print

Without CSS or JavaScript the instruction is still readable.

Parameter reference

Positional 1..n , string , default
At least one; each must be non-empty and quoted; order is display order

Positional parameters only. There is no separator, label, platform, class or size: Hugo does not allow positional and named parameters in one shortcode call.

Limits

  • One sequence is one set of keys pressed together: press-A-then-B is two kbd calls and a sentence — press Escape, then Enter.
  • No platform detection: the page never swaps Ctrl for based on the visitor’s operating system.
  • No key mapping or recording: menu paths, gestures and gamepads are out of scope.
  • Missing quotes fail the build: Ctrl in {{< kbd Ctrl K >}} is not a string parameter.
  • Do not use it for commands: hugo server is inline code; Ctrl is a key.

20 - Includes

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.

Three shortcodes, one job each: include puts another file’s contents into this page, param prints a page or site parameter, and comment discards a passage. They are for fragments reused across pages and constants scattered over many: one set of install steps that appears on three pages is an include, a version number that appears on dozens is a param, and either way you edit one place. Content that appears on one page belongs on that page.

Shortest form

include takes one required parameter, file:

Source
{{< include file="parts/install-oink.md" >}}

The file it pulls in is ordinary Markdown living under assets/:

assets/parts/install-oink.md
Installing OINK into an existing Hugo site takes three commands:

```sh
hugo mod init github.com/you/your-site
hugo mod get github.com/pgsty/oink
hugo server
```

> [!NOTE]
> `hugo mod get` needs Go on the machine; an offline archive or a submodule does not.

The current release is {{< param version >}}.

The result is what you would get by writing it here: the code block has its copy button and the callout is a callout.

Installing OINK into an existing Hugo site takes three commands:

hugo mod init github.com/you/your-site
hugo mod get github.com/pgsty/oink
hugo server
Note

hugo mod get needs Go on the machine; an offline archive or a submodule does not.

The current release is v1.0.0.

The file that gets included is not a page of its own: it is absent from the sidebar, it takes no part in translation pairing, and it has no URL.

Where the file comes from

file resolves in this order, first match wins:

Order Looked up as Written as
1 A page resource — a file in this page’s bundle file="config.yaml"
2 A global resource under assets/ file="snippets/dsn.txt"
3 A file under content/: a leading / is the content root, otherwise relative to the page’s directory file="notes/caveat.md", file="/shared/notice.md"

Missing in all three, or containing .., the include warns and emits nothing. Strict publishing rejects the warning: include reads from content/ and assets/ and nowhere else.

A Markdown fragment is read as source, so write the file’s real name on disk. One trap belongs to step 1 alone: Hugo attaches a language-suffixed page resource such as notice.zh.md under its stripped name, so asking a bundle for notice.md hands include already-rendered HTML instead of the source, and <div class="td-code"> turns up in the Markdown output. Under assets/ and content/ the name you write is the file you get. Non-Markdown files (.yaml, .sh, .txt) never have this distinction.

Each language of this page includes its own fragment: English pulls assets/parts/install-oink.md, Chinese pulls assets/parts/install-oink.zh.md. Keeping them under assets/ rather than in the page bundle is what lets both languages fetch the source under the name they write.

Including code files

code=true renders the file as a code block, and lang= sets the highlighting language. Point it at a real file in the repository and the documentation cannot drift from it.

Source
{{< include file="parts/module.yml" code=true lang="yaml" >}}
module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: 0.160.1

Code blocks and fences share one pipeline: highlighting, line numbers and the copy button all work. Fence attributes (title=, collapse, hl_lines=) cannot be passed through; when you need them, write the content as an ordinary code block.

What a fragment can contain

A fragment is page-level Markdown rendered in the current page’s context: callouts, tables, lists, images, steps and shortcodes all work. The last line of the fragment above — “The current release is v0.8.1” — is its {{< param version >}} expanded on this page.

When two pages include one fragment, each renders it separately and each generates its own heading anchors and code-block IDs. They do not collide.

What makes a good fragment

Install commands, connection strings, support matrices, legal notices: content that changes, and that must change everywhere at once. Content that appears on one page belongs on that page.

Printing a site parameter

param prints one parameter: this page’s front matter first, then the site configuration — Hugo’s .Param rule.

Source
This site publishes {{< param version >}}, copyright from {{< param copyright.from_year >}},
and this page's front matter says `pigsty_pg_major: 18`, which reads back as {{< param pigsty_pg_major >}}.

This site publishes v1.0.0, copyright from 2026, and this page’s front matter says pigsty_pg_major: 18, which reads back as 18.

Nested keys join with ., so copyright.from_year reads params.copyright.from_year. A parameter that does not exist, or whose value is a map or list rather than a scalar, warns and prints nothing; strict publishing rejects the warning.

Parameters inside commands, tables and links

param emits escaped plain text, so it can sit in a code fence, a table cell or a link target. A version number in an install command is the obvious case:

Source
```sh
hugo mod get github.com/pgsty/oink@{{< param tdVersion.latest >}}
```

| Item | Value |
| --- | --- |
| Current version | {{< param version >}} |
| Hugo floor | {{< param hugoMinVersion >}} |

[Release notes](https://github.com/pgsty/oink/releases/tag/{{< param tdVersion.latest >}})
hugo mod get github.com/pgsty/oink@v1.0.0
Item Value
Current version v1.0.0
Hugo floor 0.160.1

Release notes

Where site parameters are defined and which exist is in Configuration; page parameters are in front matter.

Notes deleted at build time

A comment body appears in none of the four outputs — HTML, print, Markdown, RSS. An HTML comment is different: it stays in the page source and reaches llms.txt.

Source
Since PostgreSQL 18, `pg_stat_io` breaks out WAL statistics.

{{< comment >}}
TODO: after v0.5 ships, bump the version above to 19 and add a pg_stat_io screenshot.
This text reaches no output at all, llms.txt included.
{{< /comment >}}

Verify the dashboards on a test database before upgrading.

Since PostgreSQL 18, pg_stat_io breaks out WAL statistics.

Verify the dashboards on a test database before upgrading.

There is a comment between those two paragraphs, and viewing the page source will not find it.

Output

Output include (Markdown) include code=true param comment
HTML The fragment renders as normal content Highlighted code block + copy button Escaped plain text nothing
Print As HTML As HTML, without the copy button As HTML nothing
Markdown The fragment’s source, as written A source fence The value itself nothing
RSS As HTML As HTML As HTML nothing

In Markdown output a fragment is source rather than HTML, and shortcodes inside it stay as {{< param version >}}. That is consistent with “Markdown output keeps the source”; it is not a missed render. None of the three shortcodes loads a script.

Parameter reference

include (named parameters only):

file , path (required) , default
Resolution order in Where the file comes from; a .., missing file, or empty value warns and emits nothing
code , boolean , defaultfalse
true renders as a code block; quoted code="true" warns and includes ordinary content
lang , string , default
Code language; without code=true it warns and is ignored

Any other parameter name warns and is ignored, with the file and line in the message; strict publishing rejects the warning.

param (one positional parameter):

parameter name , string (required) , default
Nested keys join with .; page front matter first, then site params; missing or non-scalar values warn and print nothing

comment takes no parameters. It is used in pairs, and everything between {{< comment >}} and {{< /comment >}} is discarded.

Limits

  • include is not a template: you cannot pass variables to a fragment, include conditionally, or give the included code block fence attributes (title=, collapse). For per-platform variants, write two fragments and use tabs.
  • Fragment languages are yours to maintain: include does no language fallback and takes the exact path you write. Share one fragment across languages — this page’s Chinese translation includes the same English file — or write one per language and point each page at its own.
  • param prints scalars only: structured data — version matrices, download lists — belongs in data/ and is rendered by the matching component.
  • comment is not “unpublish for now”: the content is discarded on every build. To take a whole page down temporarily, use draft: true.
  • Do not use include to build an index page: a page that pulls in ten fragments is a page where the reader wanted ten links.
  • Code blocks — every fence attribute, and the pipeline include code=true reuses
  • Tabs — per-platform or per-language fragments
  • Configuration — the site parameters param can reach
  • Front matter — page parameters, which win over site configuration

21 - 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.

asciinema renders a .cast recording as a terminal player on the page. It suits command-line walkthroughs: the text in the terminal is still text, it can be selected and copied, and the near-two-minute install excerpt on this page is about 110 KB. Graphical interfaces belong in screenshots or video — this component plays terminal recordings only. The player and its styles ship with the theme, nothing is downloaded at build time, no CDN is contacted at runtime, and the runtime loads only on a page that uses it, and only in its HTML output.

Shortest form

file is the only required parameter:

Source
{{< asciinema file="images/install.cast" >}}

images/install.cast — /images/install.cast

The recording is a single-node Pigsty install on a Debian machine in a 120×36 terminal, trimmed to the first minute and 54 seconds. The file lives at static/images/install.cast on this site, so the path is written from the site root. A file under assets/ is written as a relative path: the theme looks in resources first and falls back to treating the value as a site-root path. Without title, the window title shows the value of file.

Window title and theme

title sets the window title, theme the colours:

Source
{{< asciinema file="images/install.cast" title="Pigsty single-node install" theme="dracula" >}}

Pigsty single-node install — /images/install.cast

theme defaults to auto: it follows the site’s colour scheme, td-light in light and td-dark in dark, remounting in place when the reader switches. To pin a terminal palette, the values are the player’s own asciinema, dracula, gruvbox-dark, monokai, nord, seti, solarized-dark, solarized-light, tango, plus the theme’s td-light / td-dark. A pinned theme stops following the colour scheme, and solarized-light on a dark site does not have workable contrast. The terminal font needs no setting: the player uses the site’s code font, the one the code blocks use.

Speed, start point and poster

Three parameters control where a long recording starts: speed sets the rate, startAt skips the opening, poster decides the frame shown before playback.

Source
{{< asciinema file="images/install.cast" title="From 60 seconds in, at double speed"
  speed="2" startAt="60" poster="npt:1:30" >}}

From 60 seconds in, at double speed — /images/install.cast

speed and startAt are numbers (seconds) and poster uses the player’s npt: notation for a point in time, so npt:1:30 is one minute thirty. The player above rests on the frame at 90 seconds and starts playing from 60.

idleTimeLimit compresses silent stretches to at most N seconds. This recording was already compressed while recording (idle_time_limit: 0.5 in the .cast header), so it does not need it. Only files recorded without an idle limit do.

Size and fit

The player scales to the container width by default (fit="width"), and the terminal’s rows and columns come from the .cast header. cols / rows override that:

Source
{{< asciinema file="images/install.cast" title="Only 16 rows tall" rows="16" >}}

Only 16 rows tall — /images/install.cast

A size smaller than the recording clips it — the one above shows 16 of the 36 rows. cols / rows exist to correct a wrong size in the recording’s header; they are not a layout tool. To make the player shorter, record again in a smaller terminal.

fit takes four values: width (the default, scale to width), height (to height), both (fit both axes) and none (no scaling — a wide terminal overflows).

Looping and preloading

loop replays at the end, and preload fetches the .cast when the page loads so pressing play does not wait:

Source
{{< asciinema file="images/install.cast" title="Looping: the first minute after login"
  startAt="0" speed="3" loop="true" preload="true" >}}

Looping: the first minute after login — /images/install.cast

autoplay="true" starts playback as the page opens. It is not recommended: a “reduce motion” preference only disables the transitions on the player’s controls, it does not stop autoplay. When you really need it, pair it with loop, keep the clip very short, and put only one on a page.

Inside steps

Put the recording next to the step: the text says what to do, the recording shows what it looks like.

Source
1. Install the dependencies and fetch the installer:

   ```sh
   curl -fsSL https://repo.pigsty.io/get | bash
   ```

2. Run the install; here are the first two minutes:

   {{< asciinema file="images/install.cast" title="pig install" speed="4" >}}

3. Open `http://<node address>:3000` and sign in to Grafana with `admin / pigsty`.
{.steps}
  1. Install the dependencies and fetch the installer:

    curl -fsSL https://repo.pigsty.io/get | bash
  2. Run the install; here are the first two minutes:

    pig install — /images/install.cast

  3. Open http://<node address>:3000 and sign in to Grafana with admin / pigsty.

A page can hold several players, and the script and styles load once.

Recording a cast file

The theme only plays. Record with asciinemaasciinema rec --idle-time-limit=2 --cols=100 --rows=28 install.cast — and check it locally with asciinema play install.cast.

  • Keep the terminal under 100 columns so it stays readable on a narrow screen, and clear before you start.
  • Clear secrets first: a .cast is plain text and every character in the recording is greppable. Check before committing.
  • Put the file in static/images/ or in the page bundle and commit it. Do not reference a .cast URL on someone else’s site.

Output

Output Shape
HTML A <div class="td-asciinema"> window frame plus the player; the player CSS/JS and the runtime load on demand, once per page, and only in this output
Print A labelled static link showing the recording’s address; no player, no runtime
Markdown A plain Markdown link, [title](/images/install.cast) — no component markup, no configuration block
RSS The same plain link

A recording must never be the only source of information. Write the key commands and the key output beside it in text or a code block: offline readers, whatever consumes llms.txt, and anyone printing the page get the link and your prose, not the terminal session.

Parameter reference

file , path (required) , default
Named, or the first positional parameter; looked up as a global resource first, then as a site-root path; a full URL with a scheme is passed through unchanged
title , plain text , defaultthe value of file
The window title
theme , enum , defaultauto
auto follows the site’s colour scheme; or td-light td-dark asciinema dracula gruvbox-dark monokai nord seti solarized-dark solarized-light tango
fit , enum , defaultwidth
width height both none; anything else warns and uses width
cols / rows , integer , defaultfrom the .cast header
Override the terminal size; smaller than the recording clips it
speed , number , default1
Playback rate
startAt , number (seconds) , default0
Where playback starts
idleTimeLimit , number (seconds) , defaultfrom the .cast header
Longest a silent stretch plays for
poster , string , default
The frame shown before playback, npt:mm:ss
autoplay , "true" / omitted , defaultoff
Play as the page opens; not recommended
loop , "true" / omitted , defaultoff
Replay at the end
preload , "true" / omitted , defaultoff
Fetch the .cast when the page loads
pauseOnMarkers , "true" / omitted , defaultoff
Pause at chapter markers
markers , time:label,time:label , default
Chapter markers; see the limits — the labels do not reach the player today

The boolean-ish parameters compare against the text true: loop="true" and loop=true both enable, anything else disables. Everything else warns and carries on: an illegal fit uses width, a non-numeric speed uses 1, a non-numeric startAt uses 0, and a cols, rows, idleTimeLimit or marker time that is not a number is ignored. None of them stops an ordinary build, and every one of them fails a publishing gate built with --panicOnWarning.

Limits

  • markers labels are lost: the theme flattens the time:label list into a one-dimensional array, and the player accepts only pairs, so the timeline ends up with unlabelled markers. A marker whose time is not a number warns and is skipped. When you need chapters, write a list beside the recording.
  • The player needs JavaScript: with scripts disabled in the browser, only the window frame remains. Print, Markdown and RSS carry a link instead — see Output.
  • Recordings are not searchable: the site index covers page text, so a command that only appears in a recording cannot be found.
  • Do not reference a remote .cast: http and https addresses are accepted, and the page then depends on someone else’s site. Any other scheme, a protocol-relative //host, or an empty value warns and the component renders nothing.
  • Keep each clip short: few people finish a recording longer than five or six minutes. Split a long procedure into several short ones, each with its own text.
  • Code blocks — the key commands and output, copyable
  • Steps — the recording beside the step it belongs to
  • Images — static screenshots: recordings for terminals, screenshots for graphical interfaces
  • Include — when the same commands appear on several pages