Skip to content

Books

Turn a directory tree into a book with type: book: chapter numbering, numbered figures and tables, cross-references, generated indexes and whole-book print.

A book is a content tree of type: book: the directory decides chapter order, front matter decides chapter numbers, and figures, tables, equations and examples each carry a hand-written number and a stable anchor. Cross-references resolve in all four outputs, and the book’s root page can generate a whole-book print HTML.

Two prerequisites: the site’s markup.goldmark has attribute lines and passthrough enabled (see Components); and params.ui.shell_types still contains book (the theme default includes it).

A book’s directory

The book root is an ordinary Hugo section, chapters are its subdirectories, and sections are the pages inside a chapter. There is no second chapter list: the sidebar, the pager and the generated contents all read this one tree.

content/handbook/, one book

  • content/handbook/
    • _index.mdbook home: type: book + cascade, holding book-toc and the indexes
    • ch01/
      • _index.mdchapter 1 front page: book_number: 1
      • install.mdsection 1.x
      • bootstrap.md
    • ch02/
      • _index.mdchapter 2: numbered with book_number, optionally marked draft
      • replication.md
      • failover.md
    • appendix.mdan unnumbered appendix, still in the sidebar and the reading order

Chapter numbers are written by hand: book_number displays exactly what you write, and the theme never numbers by directory order. The num on a figure, table, equation or example works the same way — a string the author controls (2-1, 5.3 and A-2 are all valid), not an index computed at render time. Rearranging the tree therefore never shifts a number that has already been printed.

The book home and chapter pages

The book root declares the type, cascades it to descendants, and explicitly requests the print output. That aggregate is expensive to build, so the theme does not turn it on for a consuming site:

content/handbook/_index.md
---
title: The PostgreSQL operations handbook
type: book
book_number: B
cascade:
  type: book
outputs: [HTML, print, markdown]
---

A book that is a section maps to Hugo’s section output kind; home applies only when the book sits at the site root:

hugo.yml
outputs:
  section: [HTML, print, markdown]
params:
  ui:
    sidebar_headings: 3     # project an h2–h3 heading tree under the current entry
    book_draft_banner: true # draft chapters get a localized banner above the body

A chapter page needs only its number and its order:

content/handbook/ch02/_index.md
---
title: Replication and failover
book_number: 2
book_status: draft
weight: 20
---

book_number appears before the page title, in the sidebar and in the generated contents. book_status: draft is a visible editorial label and does not change Hugo’s publication state: a draft chapter builds and publishes as usual.

sidebar_headings accepts false, true (h2 only) or a maximum level from 2 to 4. Give every heading that will be referenced an explicit ID, such as ## Synchronous replication {#sync-replication}: a generated slug is fine for navigation and unfit as a long-lived reference target.

The full key definitions are in Configuration and Page parameters.

Numbering: the native form

Each of the four numbered kinds has a native form: one Markdown block followed immediately by an attribute line. On that line num= is the number, #id is the anchor, and caption= is a plain-text caption.

Figures

An attribute line follows the image block. Omitting #id defaults it to fig-<num>.

Source
![The OINK release notes page](/images/releasenote.webp)
{#book-release-note num="2-1" caption="The release notes page is also the single source of release facts." width=600 height=300}
The OINK release notes page
Figure 2-1 The release notes page is also the single source of release facts.

The native figure form requires the site to set markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false; otherwise the attribute line attaches to the paragraph and is ignored. The alternative text comes from the Markdown image itself and is never replaced by the caption.

Tables

An attribute line follows a pipe table, and the default ID is tbl-<num>.

Source
| Isolation level | Dirty read | Non-repeatable read | Phantom read |
| --- | --- | --- | --- |
| Read Committed | Not possible | Possible | Possible |
| Repeatable Read | Not possible | Not possible | Possible |
| Serializable | Not possible | Not possible | Not possible |
{#tbl-2-1 num="2-1" caption="Anomalies permitted at each PostgreSQL isolation level."}
Isolation level Dirty read Non-repeatable read Phantom read
Read Committed Not possible Possible Possible
Repeatable Read Not possible Not possible Possible
Serializable Not possible Not possible Not possible
Table 2-1 Anomalies permitted at each PostgreSQL isolation level.

Equations

An attribute line follows a $$ block, and the default ID is eq-<num>. The number and caption sit on one non-wrapping line to the right of the formula, so a long caption squeezes the formula column until it becomes a horizontally scrolling region. Keep an equation caption short.

Source
$$
A = \frac{\mathrm{MTBF}}{\mathrm{MTBF} + \mathrm{MTTR}}
$$
{#eq-2-1 num="2-1" caption="Availability from MTBF and MTTR."}
A=MTBFMTBF+MTTR A = \frac{\mathrm{MTBF}}{\mathrm{MTBF} + \mathrm{MTTR}}
Equation 2-1 Availability from MTBF and MTTR.

The native form depends on the site enabling Goldmark passthrough. Without it, use the eq shortcode below, which goes through local server-side KaTeX.

Examples

A code fence with num= and caption= is a numbered example, and the default ID is eg-<num>. An #id written on the fence names the enclosing <figure> — the reference target — rather than the code block itself. The caption is required: writing only num or only caption fails the build. A numbered example renders as one framed unit: the caption is the frame’s header and the body sits inside it, and a body that is exactly one code block sits flush against the frame instead of drawing a second border.

Source
```sql {num="2-1" caption="Daily write volume on the primary." #eg-2-1}
SELECT date_trunc('day', ts) AS day, count(*)
FROM pg_stat_statements_history
GROUP BY 1 ORDER BY 1 DESC LIMIT 7;
```
Example 2-1 Daily write volume on the primary.
SELECT date_trunc('day', ts) AS day, count(*)
FROM pg_stat_statements_history
GROUP BY 1 ORDER BY 1 DESC LIMIT 7;

Numbering: the shortcode form

The four shortcodes fig, tbl, eq and eg render a <figure> identical to the native form, register into the same target table, and sort by source position. Use them only where the native form cannot reach: an image that needs an outbound link, several tables under one number, a site without passthrough, or an example body made of several fences and prose.

fig takes src= (it also accepts inner Markdown content, and the two are mutually exclusive) and additionally supports link, alt, width, height, class, and the migration alias title:

Source
{{< fig num="2-2" src="/images/docsy.webp" alt="The default Docsy shell"
    caption="OINK's upstream: the Docsy content model is still underneath." width="600" height="300" />}}
The default Docsy shell
Figure 2-2 OINK's upstream: the Docsy content model is still underneath.

tbl wraps the label, the table, the caption and the anchor in one semantic figure:

Source
{{< tbl num="2-2" caption="How a numbered component appears in each of the four outputs." >}}
| Output | Label | Anchor |
| --- | --- | --- |
| HTML | Visible | Stable |
| Print | Visible | Stable |
{{< /tbl >}}
Output Label Anchor
HTML Visible Stable
Print Visible Stable
Table 2-2 How a numbered component appears in each of the four outputs.

eq hands its content to local server-side KaTeX, so it does not depend on passthrough:

Source
{{< eq num="2-2" caption="Connection pool saturation." >}}U = \frac{\lambda}{\mu \cdot c}{{< /eq >}}
U=λμcU = \frac{\lambda}{\mu \cdot c}
Equation 2-2 Connection pool saturation.

A bare {{< eq >}} with no parameters is the unnumbered display-maths escape hatch: it registers no target, cannot be reached by xref, and does not appear in the equation index.

eg is a wrapping shortcode whose body renders under the page’s Markdown policy, usually holding one or more fences:

Source
{{< eg num="2-2" caption="Bringing up a new replica with pg_basebackup." >}}
```bash
pg_basebackup -h primary -U replicator -D /pg/data -Fp -Xs -P -R
```
{{< /eg >}}
Example 2-2 Bringing up a new replica with pg_basebackup.
pg_basebackup -h primary -U replicator -D /pg/data -Fp -Xs -P -R

IDs must be unique within a page, and within one kind a number maps to exactly one ID. A duplicate fails the build, and the error names the line that claimed it first.

Footnotes cannot appear in a shortcode body

Hugo renders a shortcode body as its own Goldmark document, and footnotes are page-level. A [^label] inside the body of tbl, eg, fig, card, tab, field or include fails the build, naming the file, the line and the label. With the definition on the page, the reference would print literally as [^label]; with the definition in the body, it would build a second footnote list whose fn:N ids collide with the page’s own. Neither belongs in published output.

A table or code block that needs footnotes uses the native form instead: a table, image or fence carrying {num=… caption=…} keeps its content in the page document, where a footnote numbers, links and backlinks like any other. The rendered figure is the same either way, so this is usually a one-line change. Footnote-shaped text in code — a [^0-9] character class in a listing, or a code span — is left alone.

Cross-references

A target on the same page can be reached with a plain Markdown link: Table 2-1 points at the isolation table above. The cost is that the label and the number are hand-written, so changing a number means finding them yourself.

xref composes the label, the number and the anchor in one place, and works across pages and languages:

Source
See {{< xref fig="2-2" />}} and {{< xref eg="2-1" />}};
with an explicit anchor: {{< xref fig="2-1" anchor="book-release-note" />}}.

See Figure 2-2 and Example 2-1; with an explicit anchor: Figure 2-1.

The rules:

  • At most one kind key (fig, tbl, eq, eg). The kind supplies the localized label (Figure / Table / Equation / Example) and derives the default anchor <kind>-<num>.
  • anchor= overrides the derived anchor, for a target that wrote an explicit #id.
  • page= references another page through Hugo’s page lookup in the current language, so the source never hard-codes a /zh/ prefix.
  • Without a kind, both anchor= and inner link text are required: {{< xref page="../ch01/install" anchor="sync-replication" >}}synchronous replication{{< /xref >}}.
  • A reference may precede its target: nothing reads the registry at render time, so forward references are valid.

A plain cross-page Markdown link is still a site URL inside the whole-book print. A reference that must also jump within the aggregate document is written as an xref.

Indexes: contents and lists of figures

Five index shortcodes walk the same book tree, triggering descendant content and aggregating what it registered. They usually sit on the book home (_index.md) or on a dedicated “list of figures” page.

content/handbook/_index.md
{{< book-toc depth=3 >}}

## List of figures {#lof}
{{< book-figures >}}

## List of tables {#lot}
{{< book-tables >}}

## List of equations {#loe}
{{< book-equations >}}

## List of examples {#lox}
{{< book-examples >}}

These five appear here as source only. They walk down from the navigation root the current page belongs to, so placing one in an ordinary documentation tree would list the whole docs tree as a book. For the real effect, read Write Beautiful Docs and inspect its content/book/_index.md source.

  • book-toc takes a depth of 1 to 3: 1 lists chapters, 2 adds nested sections, 3 also projects each page’s heading tree. drafts=false filters book_status: draft rows out of this generated list only, and does not affect publication.
  • book-figures, book-tables, book-equations and book-examples take no parameters. Each lists one kind, with entries like “Figure 2-1 — caption” linked to the stable ID.
  • In whole-book print, all of these links become in-document fragments.

Sequential reading and drafts

The pager is on by default for the docs, book and blog types, and its order is a pre-order walk of the sidebar tree: a section index first, then its children by weight. Turn a whole type off with params.ui.pager_types, and a single page off with pager: false.

hugo.yml
params:
  ui:
    pager_types: [docs, book]

Entries hidden with toc_hide, manual_link link-only placeholders and sidebar_divider rows never become pager destinations.

Besides the “draft” label in the sidebar, a draft chapter can carry a banner above its body:

hugo.yml
params:
  ui:
    book_draft_banner: true

The banner appears only on pages that are both type: book and book_status: draft, and its wording comes from the localization key book_draft_notice.

Printing the whole book

Once the book root has the print output, it generates a cover, a local table of contents, the root page’s body and every descendant chapter in visible reading order, all inside one HTML document. Pages with no_print: true, link-only nodes, divider rows and hidden placeholders never become chapters.

Inside the aggregate, the IDs of numbered components are preserved byte for byte. Markdown heading IDs within a page are prefixed with their source page to avoid collisions when several chapters share an anchor such as summary, and the generated heading links are rewritten to match. The output is print-oriented HTML; PDF and EPUB are the site’s own business.

The switches themselves, and per-chapter print, are covered in Print.

Migrating an existing manuscript

An existing manuscript usually expresses figure and table numbering with the site’s own figure shortcode, bold pseudo-captions, and bare links to #fig_*. The theme repository ships a migration script that rewrites those legacy forms into fig, tbl and xref while preserving the public anchors already published. Pin the site to a released OINK version that includes the Book components first, then migrate the content.

Dry run: diff and report only, no files changed
python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme \
  --root /path/to/your-book \
  --report /tmp/book-migrate.json > /tmp/book-migrate.diff

Four profiles cover the legacy conventions of three real manuscripts (DDIA contributes one each for v1 and v2), and each recognizes only the forms actually observed in them:

--profile Legacy form it recognizes
tpme A pseudo-h6 caption beside an image, a caption beside a table, and bare /en/...#fragment links
ddia-v2 The site’s own figure shortcode, classified by number into figure / table / code example
ddia-v1 A bare image with an adjacent bold numbered caption, with the ID derived from the image filename
pg-internal A bold or italic “Figure N” caption in Chinese or English next to an image, and a numbered table caption next to a table
--profile
Required; one of the four values above
--root
Required; the consuming repository’s root
--path
Restricts to a file or directory under --root; repeatable. The default scans the whole content tree
--write
Applies the rewrite. The default is a dry run that writes nothing
--no-diff
Suppresses the diff while keeping the summary and the report
--report
Writes the machine-readable JSON report

The diff goes to standard output, the summary to standard error, and the report carries files_scanned, files_changed, counts, skipped and idempotent. The script rewrites only targets it can determine uniquely: where the number is unclear, the caption is not unique, or the marker form is unrecognized, the text is left as it stands and recorded in skipped for a human. Bold text, inline code and formulas inside a legacy caption degrade to plain text, because a Book caption is plain text by contract.

After reviewing the diff, apply it on a dedicated branch and run a second pass to confirm idempotency:

Apply, then verify idempotency
python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme --root /path/to/your-book --write \
  --report /tmp/book-migrate-written.json

python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme --root /path/to/your-book --no-diff \
  --report /tmp/book-migrate-second.json

The second report should read files_changed: 0, an empty counts and idempotent: true; the script signals idempotency with exit code 0.

The profiles recognize only the legacy forms actually observed in those three manuscripts. Where a manuscript’s conventions fall outside the four, the script does not apply and the rewrite is manual, following Numbering: the native form. The theme repository’s bin/check-book-migrations.py covers all four profiles with a dry-run and an idempotency check.

Verify

  1. The build is warning-free: hugo --printPathWarnings --panicOnWarning. A malformed number, a duplicate ID and a missing caption all fail here.
  2. The page should show a localized label such as “Figure 2-1”, clickable xref links, and anchors that land correctly.
  3. Compare the chapter order across all four places: sidebar, pager, book-toc and whole-book print.
  4. Check the Markdown output: curl -s http://localhost:1313/handbook/ch02/index.md. The shortcode form should degrade to **Figure 2-2.** caption plus the original body, and the native form should keep its source block and attribute line as they are.
  5. Run the anchor check from the theme repository against the build output:
python3 ~/pgsty/oink/bin/check-book.py --site-public public

It verifies that every reference’s target anchor exists, that kind and number agree, that page-local IDs are unique, and that a numbered image has alternative text worthy of its caption.

Book shortcode parameters

num , string , default
Required (except for the bare eq form). Matches [0-9A-Za-z.-]+ and must be quoted
id , string , defaultfig-<num> / tbl-<num> / eq-<num> / eg-<num>
Matches [A-Za-z][A-Za-z0-9_.:-]* and is preserved byte for byte
caption , plain text , defaultempty
Required for eg; optional for fig, tbl and eq. Not Markdown
class , class token , default
Appended to the <figure>; requires num
src , image path , default
fig only. Mutually exclusive with inner content, and follows the shared image resolution order
title , plain text , default
fig only. A migration alias for caption, mutually exclusive with it

xref:

fig tbl eq eg , number string , default
At most one. Supplies the localized label and derives the anchor
anchor , ID , defaultderived from kind and number
Required when no kind is given, together with inner link text
page , page reference , defaultcurrent page
Resolved through page lookup in the current language; a missing page fails the build

book-toc:

depth , integer 1–3 , default2
1 chapters / 2 with nested sections / 3 with the heading tree
drafts , boolean , defaulttrue
false filters draft chapters out of the generated list

book-figures, book-tables, book-equations and book-examples take no parameters.

Limits

  • There is no automatic numbering. Chapter, figure and table numbers are all written by hand; changing one is a deliberate edit, not a side effect of a build.
  • The attribute line must touch its block, with no blank line between. An attribute line a tool like Prettier has moved fails silently, and the figure degrades to a plain image.
  • book_kind and book_part are metadata keys the contract acknowledges but the current templates do not render. The ones with a visible effect are book_number and book_status.
  • The index shortcodes trigger descendant content rendering, which noticeably lengthens the build on a very large tree. The same reason is why whole-book print has to be requested explicitly.
  • A footnote reference cannot appear in a shortcode body; the build fails and names the native form to use instead — see Numbering: the shortcode form.
  • The theme stops at print HTML: pagination, font embedding, index compilation and PDF / EPUB packaging are outside the contract.
  • Organizing content — how the tree becomes the sidebar and the reading order
  • Images — captions, sizing, zoom and image processing
  • Tables — table attribute lines and full-width tables
  • Math — KaTeX and passthrough configuration
  • Print — per-chapter and whole-book print