Versioning
Tag-based versioning with an automatic pipeline, version pickers and outdated banners.
The model
You only ever write one docs tree, living under a directory named after your working-docs token (the snippets below use __mydocs__, this site uses __vcurrent__):
content/docs/
├── _index.md
└── __mydocs__/
├── _index.md ← title: "__mydocs__" (the literal token)
└── en/ … ← you write here, alwaysReleasing means pushing a git tag (v1.0.0). Nothing is copied in the repository. Git remembers what the token directory looked like at every tag.
At build time, the release pipeline assembles the full site:
tag v1.0.0 ──┐
tag v1.1.0 ──┼─→ content/docs/v1_0_0/… ← extracted from each tag,
tag v2.0.0 ──┘ v1_1_0/… the token dir renamed to the tag,
v2_0_0/… token links and title replaced
└─→ static/versions.json ← generated: newest tag = latestOne Hugo build then renders everything. Pickers, search scopes, hreflang and the outdated-version banner work across the assembled versions automatically.
Choosing the token
Releasing is a literal string replacement, so the token must be a string that will never occur naturally in your content. Avoid real words like “current”. A sentinel like __mydocs__ or __vnext__ works well. Two hard rules:
- It is configured in exactly one place: the last directory of
--docs-pathpassed to the pipeline script. The theme itself never knows it. - Capitals in the token require
disablePathToLower = trueinhugo.toml, otherwise Hugo lowercases the URLs while your links keep the capitals.
The pipeline replaces the token in three places:
- Link paths in every Markdown file:
/docs/__mydocs__/…becomes/docs/v1_2_0/… - The token directory’s own
_index.md: every bare__mydocs__becomes the tag name (v1.2.0). That is why itstitleshould be the literal token, so each released version’s card and breadcrumb show its tag. site.json: links into the token dir (landing CTAs, menu entries) are pointed at the latest version, since the token dir itself is gone after assembly.
No other file’s prose is ever touched.
The pipeline
The theme ships the script (ci/scripts/build-docs.sh). It takes named flags, so there is no config file:
build-docs.sh --docs-path content/docs/__mydocs__ [options]| Flag | Meaning |
|---|---|
--docs-path |
Where your working docs live (must contain /content/). The last directory is your token. Required. |
--source-path |
Where to read the docs from git, if your authored content lives outside content/. Defaults to --docs-path. |
--from-tag / --until-tag |
Tag range to build. Omit for the first tag up to the latest. |
--skip-tags "A B" |
Yanked or broken releases to exclude (space- or comma-separated). |
--tag-pattern |
Which tags to select (default v*). Widen it to include a non-v tag, such as a rolling dev. |
--max-versions N |
Keep only the newest N versions. Omit for all. |
--latest-tag |
Pin a specific tag as the latest version, ordered first. Without it the newest tag by version sort is latest. |
--latest-label |
Display name for the pinned latest (e.g. latest), instead of the tag-derived label. |
--prefix |
URL path prefix for the generated versions.json links, for sites served from a sub-path. |
CI usage:
pages:
rules: [{ if: $CI_COMMIT_TAG }]
variables: { GIT_DEPTH: 0 }
script:
- sh ci/scripts/build-docs.sh --docs-path content/docs/__mydocs__ --max-versions 10
- hugo --environment production --baseURL "${CI_PAGES_URL}/"With no tags the script leaves the working tree untouched. Running hugo server while writing simply shows the token directory.
Writing rules this enables
- Internal links always use the fixed prefix
docs/__mydocs__/<lang>/…. The pipeline rewrites it per version. - Version names come from tags (
v1.2.0becomes directoryv1_2_0, displayed as “v1.2.0”). You never name version directories yourself. - Fixing an old release: branch from its tag, fix, tag again (
v1.0.1). Retire the bad tag via--skip-tags.
The version manifest
/versions.json always lives beside the site. The theme fetches it host-relative. It drives the pickers, search scoping and the outdated banner, and is generated by the pipeline. The minimal copy committed under static/ only serves local development.
The outdated-version banner
Readers of any non-latest version get a banner linking to the same page in the latest version. To preview it locally: hand-edit static/versions.json, add a fake newer version with "latest": true, restart hugo server.
Failure behavior
If versions.json cannot be fetched, the pickers hide, a toast informs the reader and everything else keeps working. Search falls back to unscoped results.