Writing pages
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.
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:
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 ###:
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/#prerequisitesand/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:
Use lowercase English with hyphens, unique within the page. This site’s translation audit compares the heading IDs rendered by the English and Chinese pages and fails on a mismatch.
Writing links
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/),  |
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
grepto replace them site-wide after a restructure; - When moving a page, add
aliasesfor the old path and update internal links to the new route — do not let an alias carry navigation indefinitely; - Use
reffor 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  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:
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.
- Install Hugo Extended, 0.160.1 at the oldest:
- Clone the documentation site and preview it:
Tip
Add
-Dto 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:
- 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 arefwhose target is missing all fail here naming the file and the line; the theme never degrades silently. --printPathWarningsreports two pages resolving to the same output path, which turns up most often in multilingual sites or after changingpermalinks.
Then confirm three things in the browser:
- The page is in the sidebar, in the position
weightimplies; - The right-hand outline lists the
##headings you wrote, and clicking one puts an English anchor in the URL; - The English and Chinese versions of the same heading share an anchor (this site audits that with
node scripts/check-doc-translations.mjs --public public).
Related
- Organizing content — how the directory structure decides the sidebar
- Page parameters — the full front matter table
- Components — each component’s syntax and parameters
- Languages — paired bilingual files and fallback for untranslated pages
- Local preview — the
hugo serverswitches worth knowing