Skip to content
8.4. Documentation

8.4. Documentation

In one glance

  • You will: Break a quoted source region on purpose, watch the docs check refuse, and put it back green.
  • You need: mise run install finished.
  • Time: about 18 minutes, hands-on.

Why this course quotes source by reference, not by copy

A source-backed include is a shortcode naming a repository path, a marked region inside that file, and a language. Hugo resolves it at build time, so the page shows the code the repository holds, not a copy of it. Prose has no compiler: rename a function and every page quoting it keeps rendering the old signature with nothing going red, until a reader copies a snippet that will not compile and stops trusting the document. Includes turn that decay into a build error.

This page triggers that error on purpose, then works back through the three controls and the page frame that keep the build strict. Take the include on 8.2. Releases, which quotes a named region of the release workflow, and misspell the region name — release-dispatch-autority. Then ask the checker:

tools/bin/conventions docs
content/8. Community/8.2. Releases.md: line 41: snippet region "release-dispatch-autority" needs exactly one start and end marker in .github/workflows/release.yml
content/8. Community/8.2. Releases.md: source-backed workflow example must include ".github/workflows/release.yml:release-dispatch-authority"

Exit code 1, two findings, both naming the page, the line, and the source file that failed to supply the region. Your line number will be wherever that include sits when you try it. Fix the spelling and the command prints nothing and exits 0, which is what a passing run looks like here.

The two findings are not duplicates. The first is generic: no region by that name. The second knows this page is the course’s one worked example of a quoted workflow and refuses to let it disappear. A page can quote source badly in more than one way, so the checks are specific.

The include shortcode carries those three arguments and nothing else:

{{< include path="agents/go/model/model.go" region="build-model" lang="go" >}}

The source carries one --8<-- [start:build-model] marker and one matching end marker. Hugo reads the mounted asset, extracts the region, dedents it so a block nested inside a function renders standalone, and highlights it.

Seven conditions fail the build instead of rendering a block that looks like quoted source but is not: a missing file, a missing region, a duplicate marker, an empty region, reversed markers, an absolute path, or a parent traversal. The shortcode must also stand alone outside a fenced block, because it emits its own. hugo.toml mounts agents/go, agents/data, evals, tools, infra, scripts, and .github under assets/source/, which is why the live preview rebuilds when quoted code changes.

How three independent controls make the docs build strict

mise run check:docs
[check:docs] $ tools/bin/conventions docs
[check:docs] $ snapshot="$(mktemp -d)"
Start building sites …
[check:docs] $ tools/bin/conventions rendered site
Finished in 16.77s

That is the task’s own command echo, trimmed of Hugo’s version banner and its page-count table. Three controls had to hold for it. build:docs passes --panicOnWarning, so a Hugo warning is a failure rather than a line of scrollback. hugo.toml sets reference-link errors to ERROR, so a relref — the shortcode that resolves a link to another page by path — stops the build when that page does not exist instead of rendering a dead link. And the navigation checker rejects any course page missing from data/nav.yaml, whose order is hand-maintained because reading order is not file order, so a new page cannot quietly exist outside the learning path. No single switch replaces all three; removing one lets a distinct kind of breakage through.

The run has two halves, visible in the output. Before Hugo starts, conventions docs parses the source pages: front matter, page frame, includes, exercise contracts, admonitions, diagram alternatives, and the contracts that pin prose to manifests. After Hugo renders into a throwaway directory, conventions rendered derives the expected routes from the source slugs and the Hugo configuration, then reads the rendered site for uniqueness, hierarchy, trailing slashes, canonical and Open Graph URLs, the sitemap, the search index, navigation links, and edit links. Today’s routes come from the slugs, never from a manifest — a manifest of the current site would be one more thing to forget to update. The one manifest that does exist, data/released-urls.json, records only the routes the course published in the past, which no slug can derive because those pages no longer exist under those names.

A rename that would have broken a link now breaks a build instead — before anyone reads the page.

What the page frame requires, and how slugs become URLs

Each page declares a title and a one-sentence description, and every non-home page declares one lowercase kebab-case slug. The body opens with an “In one glance” block carrying You will, You need, and a Time line naming one page kind, and closes with a checkpoint heading. Teaching pages close with “What you can do now”, chapter indexes with “What this chapter proved”, and pure lookup pages such as troubleshooting and glossary with “How to use this page later”. Those three spellings are the whole list the checker accepts. Every other H2 states a claim rather than asking a question; one interrogative heading per page is allowed, for the tension the page actually resolves.

Routes come from those slugs, not from the filesystem: the source tree keeps readable numbered directories while the front matter owns the reviewed route segment. A chapter section carries a slug like 2-agents, a page carries one like 2-1-first-agent, and the permalink rules in hugo.toml combine :sectionslugs with :slug to produce /2-agents/2-1-first-agent/ without repeating the chapter path in every file.

Never add a front-matter url. Hugo gives it precedence, so it would silently shadow the reviewed slug, which is why the conventions checker rejects it. Home stays at / with no manufactured slug. When a slug does change, the navigation entry and every inbound link change in the same commit, because a slug is a promise to whoever already bookmarked it.

Authority for each documentation concern sits in exactly one place. Configuration and source mounts live in hugo.toml, the theme version in the root go.mod and go.sum, the hand-ordered path in data/nav.yaml, and the pages themselves in content/.

The rest is presentation: layouts/_shortcodes/include.html and layouts/_partials/include/ extract quoted regions, assets/css/custom.css and the admonition and collapsible shortcodes style them, assets/js/search-a11y.js owns search accessibility, assets/json/search-data.json owns the search route index, and assets/js/vendor/versions.json pins vendored assets.

Preview the site, check accessibility, and date every measurement

Use one terminal for the preview. It runs the authoring contracts once before Hugo starts, so a page that would fail the docs check never renders as if it passed:

mise run serve

The check runs at startup only; live reload does not repeat it, so keep a second terminal for the rest:

mise run check:docs
mise run check:links
mise run check:accessibility
mise run build:docs

Inspect each changed page at desktop and narrow widths: headings, tables, code overflow, focus order, link names, admonitions, and diagram descriptions. Every Mermaid diagram needs adjacent **Diagram in words:** prose describing actors, relationships, and sequence without relying on colour. There is no exemption list: a hash allowlist used to carry diagrams that predated the rule, and it was deleted once the last of them gained prose.

Two kinds of claim age differently, and swapping them is the most common documentation bug in this repository. A policy is owned by source: the 80% per-package coverage floor lives in the test task in agents/go/mise.toml and evals/mise.toml, and quoting it is quoting a contract. A measurement — a percentage, a test count, an image size, a timing — is a local reading that goes stale, so it travels with the command that produced it and the date it was taken. That is why the timing in the capture above sits under its command, not in prose.

Publication is a three-step contract, and every step is in this repository. build:docs writes site/; check:docs re-reads that output and fails on a page that rendered wrong; then .github/workflows/docs.yml uploads site/ and its deploy job publishes it to GitHub Pages at the hostname in static/CNAME. Exactly one job may hold that authority — checkPagesDeployment fails the build if a second one appears or if the only one disappears in a refactor, because a site that silently keeps serving its last good build is the failure mode you do not notice.

The part that outlives any one build is the URL. This course published 76 addresses under a different site generator, and Hugo’s permalinks share none of them. data/released-urls.json records every one of those routes against the page that answers it today, each page declares its historical addresses as Hugo aliases, and checkReleasedRoutes fails the build if a route loses its claimant or stops rendering. A page rename now breaks CI instead of breaking a reader’s bookmark.

Your turn: misspell an include region and watch check:docs fail

Predict before you run it. When a quoted region name no longer exists, does the build render the page without the code block, render it with an empty block, or refuse to build at all — and which of those would you want the day before a release?

  • Mode: temporary experiment.
  • Goal: see a documentation check name a page, a line, and a source file for a failure that has no compiler behind it.
  • Files to touch: only content/8. Community/8.2. Releases.md, restored at the end.
  • Preflight: git diff --quiet -- 'content/8. Community/8.2. Releases.md' must exit 0, and tools/bin/conventions docs must currently pass.
  • Steps: in that page’s include, change region="release-dispatch-authority" to region="release-dispatch-autority", run tools/bin/conventions docs, then run mise run build:docs and compare which failure each command reports first.
  • Gate that proves completion: tools/bin/conventions docs exits non-zero and names the page, the line, and .github/workflows/release.yml; you can say in one sentence why the second finding is more specific than the first.
  • Final state: run git restore -- 'content/8. Community/8.2. Releases.md', then confirm tools/bin/conventions docs and mise run build:docs are both green again.

A documentation change is finished when the source contract, the affected pages, the include regions, and the rendered output all agree, and when you quoted the smallest exact excerpt and explained the decision around it.

What you can do now

  • You can say what a misspelled region= draws: two findings naming page, line, and source file.
  • You can explain the three independent controls that make this build strict, and what each one alone would miss.
  • You can predict a page’s public URL from its front-matter slug, and say why a front-matter url is forbidden.
  • You can tell a source-owned policy from a dated measurement, and write each so the next reader can re-derive it.

Return to 8. Community, or to 8.7. Capstone to replace the reference domain with your own.