0015 Docs toolchain: Hugo with Hextra, a Diátaxis docs/ folder and generated reference
Status: Accepted. The owner said yes at Gate 1 on 15 October 2026.
The evidence is in the research note on Go project structure, sections 2.7 and 3.
Context
The goal asks for:
- user docs split by Diátaxis (tutorials, how-to guides, reference, explanation), apart from the contributor docs;
- reference docs generated from the code: the CLI and man pages, the config, the keymap, the agent table and the shell completions;
- a site with search, dark and light themes, and versioned docs per release;
- a static build that CI attaches as an artifact, and GitHub Pages once the repo is public;
- link, Markdown, spelling and plain-English checks in CI.
Contributors need Go, just and golangci-lint today, so the toolchain should fit a Go repo and add as few runtimes as it can.
Two facts from October 2026 shape the choice. Material for MkDocs has been in maintenance mode since 5 November 2025, and MkDocs has had no stable release since August 2024. And GoReleaser, the release tool we already use, moved its own site from Material for MkDocs to Hugo with the Hextra theme on 22 March 2026.
Options
- Hugo with the Hextra theme. One Go binary. FlexSearch and a light, dark and system toggle are built in. We build the versions ourselves.
- Zensical. The successor to Material for MkDocs, on Python 3.11 or later. It is at 0.0.70, and versioning works only through a transitional fork of mike.
- MkDocs with Material and mike. Mature, but in maintenance mode.
- Docusaurus. Built-in versioning, but it needs Node and React, and local search needs a plugin.
- Starlight. Pagefind search is built in. It needs Node, and versioning is an early-stage plugin.
- VitePress or mdBook. Neither has built-in versioning.
- No site. Markdown on GitHub only, with
docs/anddocs-master/as lazygit does.
Decision
Option 1, set up like this:
Content stays as plain Markdown in
docs/, readable on GitHub, intutorials/,how-to/,reference/andexplanation/. Contributor docs go indocs/contributing/. Each page has atitlein its front matter.The site lives in
site/:hugo.yaml, and a nestedgo.modthat pins Hextra, so it stays out of the main module. It mounts../docsas content. The just recipe sets the site title frominternal/brand, so the config never spells the name. Relative image links needuglyURLs: trueor an image render hook. The spike showed that Hugo’s default URLs break them.Reference is generated by
cmd/docgen(0014) throughjust docs, and is never edited by hand:- Markdown pages for each command, from cobra’s
docpackage; - the config, keymap and agent pages, from the code.
Man pages and completions are made into a build folder and in GoReleaser’s
before.hooks; they are not committed. CI runsjust docsand fails on any diff.- Markdown pages for each command, from cobra’s
Versions. At each release, CI builds that version’s site and attaches it to the release as a tarball. Once the repo is public, a Pages job puts the newest release at
/, each release at/vX.Y/, and main at/dev/.CI builds the site on every push and uploads it as an artifact. It runs lychee for links, markdownlint-cli2 for Markdown, and typos with
locale = "en-gb"for spelling. Vale checks plain English, as warnings at first.Versions are pinned: Hugo in CI and in the setup recipe, and Hextra in
site/go.mod, which Dependabot keeps up to date.
The reasons:
- Hugo adds one binary and no Node or Python. Most contributors never need it, because CI builds the site.
- Search and both themes come built in.
- Versions are immutable once built.
- It is the same choice as GoReleaser, on the same stack.
A spike with Hugo 0.168.0 and Hextra 0.13.0 confirmed the search index, the theme toggle, relative .md links from a mounted docs/ folder, and a title set from the environment.
Consequences
- Good, because the docs stay Markdown in the repo, and the generator can be swapped by changing
site/only. - Good, because generated reference and the stale-docs check make it hard for the docs to drift from the code.
- Good, because old versions are never rebuilt with newer tools, so they cannot break.
- Bad, because versioning is our own script, not a feature of the tool, and the version menu must be kept up to date.
- Bad, because Hextra is at 0.x, and one maintainer writes most of it. We pin it and keep the content portable.
- Bad, because previewing the site needs Hugo’s extended edition, and the Markdown lint needs Node in CI.
- Bad, because every page needs
titlefront matter, which GitHub shows as a small table at the top of the file. - Revisit when Zensical reaches 1.0 with native versioning.