Skip to content

Writing pages

Creating a documentation page — where the file goes, what the front matter says, why heading anchors are written by hand, how links and images work, and what appears at the end of a page on its own.

This page covers writing a documentation page end to end: where the file goes, the front matter, heading anchors, links, images, drafts, and the page end. It assumes the site already builds locally; if it does not yet, start with Quick start.

Creating a page

A page is a Markdown file under content/, and its URL follows its position there: content/docs/install.md is published as /docs/install/. The Chinese translation is a .zh.md file of the same name in the same directory, sharing one logical path with the English page.

A page with no attached resources is a single file. When a page carries images, cast files or example configuration, make it a directory instead, name the page itself index.md, and put the resources beside it — Hugo calls this a page bundle:

the two page shapes inside content/

  • content/
    • docs/
      • _index.mdsection index, English
      • _index.zh.mdsection index, Chinese
      • install.mdsingle-file page → /docs/install/
      • install.zh.mdits Chinese translation
      • anatomy/page bundle → /docs/anatomy/
        • index.md
        • index.zh.md
        • shell.webppage resource, shared by both languages

hugo new content docs/install.md generates an empty file with front matter from an archetype — see the Hugo documentation — and writing the file by hand works just as well.

Important

When a Chinese page has no English counterpart, Hugo does not hand it resources that carry no language suffix. In that case the resource filename needs the .zh. infix (shell.zh.webp) while the body still writes shell.webp.

The front matter you need

Between the two --- lines at the top of the file is YAML front matter. Four keys belong on every page:

content/docs/install.md
---
title: Install Pigsty       # page heading, browser title, search result title
linkTitle: Install          # short name in the sidebar and breadcrumbs; falls back to title
description: Get a working PostgreSQL cluster onto a clean EL 9 machine.
weight: 20                  # ordering among siblings; use multiples of 10 to leave room
---

Let description say in one sentence what the page lets the reader accomplish. It appears on the section index cards, in search results and on social cards. weight decides the sidebar order, and only equal weights fall back to alphabetical order.

The remaining keys are optional — icon, draft, search weight, comment switch, page shell and so on. The full table is in Page parameters.

Heading levels and stable anchors

Start sections at ## in the body and leave # to title. The theme already renders the page heading, so another # in the body produces two top-level headings. The outline in the right column starts at ##, and how deep it goes is decided by Hugo’s markup.tableOfContents#### on this site.

Write an explicit English anchor {#id} on every ## and ###:

Source
## Prerequisites {#prerequisites}

### Disk and memory {#disk-and-memory}

There are two reasons:

  • Cross-language alignment. Hugo derives an ID from the heading text, so a Chinese heading yields a Chinese ID: /docs/install/#prerequisites and /zh/docs/install/#前提条件 point at the same semantic place through two different anchors, which no translation audit can compare. Give the translated heading the English page’s ID and both sides share one fragment.
  • Link stability. Heading text changes as wording is revised, and a public link should not break with it. An explicit ID is a public route once published; when a rename is needed, leave an empty anchor for the old ID:
Source: leaving a target behind for the old anchor
## Getting started <a id="get-started"></a> {#quickstart}

Use lowercase English with hyphens, unique within the page. This site’s translation audit compares the heading IDs rendered by the English and Chinese pages and fails on a mismatch.

Three forms, for different purposes:

Form Example When to use it
Absolute site path [Configuration](/docs/customize/config/) The default. It points at a published route, is easy to audit and replace site-wide, and survives source files moving
Relative path [another page](../organize/), ![diagram](shell.webp) Resources inside the same page bundle, or a neighbouring page that should deliberately follow the source directory
The ref / relref shortcode [Configuration]({{< ref "/docs/configure/overview" >}}) When the target’s existence must be checked at build time; a missing target fails the build instead of leaving a dead link

All three carry a trailing slash and point at directory-style routes (/docs/write/pages/), matching Hugo’s default permalinks.

The theme has no link render hook: links go to Goldmark untouched. External links get no automatic target="_blank"; write HTML where a new tab is needed, or handle it in the site’s own layouts/_markup/render-link.html.

Plain Markdown links are not checked for existence. So:

  • Prefer absolute paths for internal links, and grep to replace them site-wide after a restructure;
  • When moving a page, add aliases for the old path and update internal links to the new route — do not let an alias carry navigation indefinitely;
  • Use ref for a target you are unsure of, and let the build check it for you.

In a bilingual site, link to the logical page (/docs/write/pages/) rather than to a .zh.md filename, and keep fragment IDs language-neutral.

Where images go

A page’s own screenshots go in its page bundle, images shared by several pages go in assets/images/, and large files that need no processing go in static/. All three are written ![alt text](source) in the source, and an attribute line controls caption, size, zoom and numbering — see Images.

Drafts and publishing

A page with draft: true never reaches the build output:

front matter
---
title: Migration guide, not yet final
draft: true
---

Preview with hugo server -D to show drafts (-D is --buildDrafts). A page whose date is in the future is excluded too; -F shows those. A production build uses neither switch, and plain hugo publishes only finished content.

OINK’s Markdown extensions at a glance

The body is standard Markdown (Goldmark) plus the native forms below. Each is ordinary Markdown syntax with one attribute line, and each stays readable as source on GitHub:

Component Shortest syntax Page
Callouts > [!NOTE] on the first line of a blockquote Callouts
Tabs Two adjacent fences each carrying {tab="Homebrew"} Tabs
Steps An ordered list followed by a {.steps} line Steps
Cards A list of links followed by a {.cards} line Cards
Field lists A table followed by {.fields meta="type default"} Fields
Table extras A table followed by {.matrix} or {caption="…"} Tables
Code blocks {title="hugo.yml" copy=false} on the fence info line Code Blocks
Images A standalone image followed by {caption="…" width="600"} Images
File trees A filetree fence, one - name/ # comment per line FileTree
Mathematics A math fence, or display maths wrapped in $$ Math
Diagrams A mermaid fence (also plantuml, markmap, echarts) Mermaid

The few remaining components — badges, keys, file includes, terminal recordings, the Book figure and table family — are shortcodes, with syntax and parameters in Components.

A combined example: code fences and a callout inside steps.

Source
1. Install Hugo Extended, 0.160.1 at the oldest:
   ```bash
   brew install hugo
   ```
1. Clone the documentation site and preview it:
   ```bash
   git clone https://github.com/pgsty/oink.pgsty.com my-docs
   cd my-docs && hugo server
   ```
   > [!TIP]
   > Add `-D` to preview drafts as well.
{.steps}
  1. Install Hugo Extended, 0.160.1 at the oldest:
    brew install hugo
  2. Clone the documentation site and preview it:
    git clone https://github.com/pgsty/oink.pgsty.com my-docs
    cd my-docs && hugo server
    Tip

    Add -D to preview drafts as well.

What appears at the end of a page

Four blocks are generated by the theme in a fixed order, and none is written in the body:

Position What it is Default Where to configure
1 Feedback: the two “Was this page helpful?” buttons Off Repository links and page info
2 Last modified: the time and the most recent commit subject, linked to GitHub On when Git information is available Repository links and page info
3 Pager: previous and next, in sidebar tree order On for docs / book / blog Navigation and menus
4 Comments: giscus When configured and enabled Comments

The action menu beside the title (copy Markdown, edit this page, view history, open an issue, print) is automatic too, and is configured in the same place, Repository links and page info.

To turn one of them off for a single page, use front matter: feedback: false, annotation: false, pager: false, comments: false. The keys are described in Page parameters.

Verify

After writing a page, run a strict build:

hugo --printPathWarnings --panicOnWarning
  • The output must end with Total in … and no ERROR and no WARN. A disallowed key on an attribute line, an invalid component parameter, or a ref whose target is missing all fail here naming the file and the line; the theme never degrades silently.
  • --printPathWarnings reports two pages resolving to the same output path, which turns up most often in multilingual sites or after changing permalinks.

Then confirm three things in the browser:

  1. The page is in the sidebar, in the position weight implies;
  2. The right-hand outline lists the ## headings you wrote, and clicking one puts an English anchor in the URL;
  3. The English and Chinese versions of the same heading share an anchor (this site audits that with node scripts/check-doc-translations.mjs --public public).