Versioned Sites
hugo-styles and hugo-styles-template now ship with version-aware deployment support.
The default behavior is intentionally small:
- the default branch is published at the site root and labelled
Latest - plain
hugo serverstill serves only the current checkout - archived branches or tags are only added when you opt into them in
hugo.toml
This keeps local authoring simple while still letting the GitHub Pages build publish multiple refs under stable URLs.
The config model
Both repositories include this block by default:
[params.versioning]
enable = true
defaultBranch = "main"
menuName = "Versions"
menuIdentifier = "versions"
menuWeight = 70
[params.versioning.latest]
enable = true
label = "Latest"
[params.versioning.branches]
refs = []
patterns = []
all = false
[params.versioning.tags]
refs = []
patterns = []
all = falseWhat this does by default
With the default config:
- the GitHub Pages workflow builds the configured default branch as the site root
- the navbar gets a
Versionsdropdown with one entry:Latest - no extra branch or tag builds are published
The generated output uses these URL patterns:
https://<account>.github.io/<repo>/forLatesthttps://<account>.github.io/<repo>/versions/<name>/for extra branches or tags
Add specific branches or tags
To publish named refs explicitly:
[params.versioning.branches]
refs = ["release-2026"]
[params.versioning.tags]
refs = ["v1.0.0", "v1.1.0"]These appear in the dropdown as:
Latestrelease-2026v1.0.0v1.1.0
If Latest already points at the default branch, that branch is not duplicated under its raw branch name.
Match tags or branches with wildcards
Pattern matching uses shell-style globs:
[params.versioning.tags]
patterns = ["v*"]
[params.versioning.branches]
patterns = ["release/*"]Typical uses:
v*for release tagsrelease/*for maintenance branchesdocs-*for documentation branches
Publish all branches or all tags
If you want broad discovery instead of an explicit list:
[params.versioning.branches]
all = true
[params.versioning.tags]
all = trueYou can combine all = true with patterns or refs, but in most cases one approach is enough.
Disable Latest
If you only want named version entries in the dropdown:
[params.versioning.latest]
enable = falseThe root site still has to exist for GitHub Pages, so the default branch continues to build at the site root.
This option only hides the Latest menu entry.
Local development
For day-to-day authoring, keep using:
hugo serverThat serves only the current checkout. It does not try to build other tags or branches locally.
If you want the full deployment artifact on your machine, run the same helper the GitHub Pages workflow uses:
python3 scripts/build-versioned-site.pyYou can override the production base URL while testing:
python3 scripts/build-versioned-site.py --base-url http://localhost:8000/Then serve public/ with any static file server.
For local rendered-site link checking, build a validation artifact with a root base URL and run lychee
against the generated HTML:
python3 scripts/build-versioned-site.py --base-url / --destination .cache/linkcheck-site --no-minify
lychee --cache --config lychee.toml --no-progress --root-dir .cache/linkcheck-site '.cache/linkcheck-site/**/*.html'Using --base-url / keeps the generated version menu and internal links portable on disk, which makes local
link checking work even for GitHub Pages project sites that deploy under /<repo>/.
For repositories created from hugo-styles-template, that helper is committed in the lesson repository
and refreshed from the pinned hugo-styles module version by the template’s update workflow and local
./scripts/sync-template-files.sh helper.
How the build works
The helper script:
- reads the effective Hugo config via
hugo config --format json - resolves the configured branches and tags from Git
- creates temporary Git worktrees instead of repeatedly checking out the current directory
- generates a small per-build config file with the correct
Versionsmenu entries - runs Hugo once for the root site and once for each extra version
This stays close to Hugo’s built-in behavior:
- Hugo still handles routing, menus,
baseURL, and output paths - the custom logic is limited to ref discovery and menu generation
Current limitation
The shared build helper supports one site root per repository via Hugo’s normal --source option.
It does not currently support a different source_dir per ref like Hextra’s own script does for its historical docs and exampleSite layouts.
That tradeoff is deliberate:
- it keeps the lesson workflow simpler
- it matches the current
hugo-stylesand template repository layouts - it avoids adding extra per-version config unless a real downstream use case needs it
If you need to publish older refs from a different site subdirectory, extend the helper at that point instead of carrying that complexity by default.