Recipe to create various documentation screenshots
Subsections of Development
Contributing
Code Quality
A new release can happen at any time from the main branch of the GitHub project without further acknowledgment. This makes it necessary that, every pushed set of changesets into the main branch must be self-contained and correct, resulting in a releasable version.
Stay simple for the user by focusing on the mantra “convention over configuration”.
At installation the site should work reasonable without (m)any configuration.
Stay close to the Hugo way.
Don’t use npm or any preprocessing, our contributors may not be front-end developers.
Document new features in the docs. This also contains entries to the What’s new page.
Don’t break existing features if you don’t have to.
Remove reported issue from the browser’s console.
Check for unnecessary whitespace and correct indention of your resulting HTML.
The test suite and the tooling that drives it. Nothing here is shipped to users.
The rule for deciding where something belongs is a single question: does somebody installing the theme need this file? If not, it belongs in the infra repository - unless it can only act from the theme repository. GitHub runs a workflow only in the repository holding it, and a git hook only fires on the checkout it sits in, so those stay put, along with the actions those workflows call.
Where to report
Open issues in the theme repository, even when they concern the tests or the tooling.
Issues, milestones and releases are all tracked there, and a release is cut from a milestone in that repository. A second tracker would split that history apart.
Why the Split
For a Hugo theme the repository is the distributed artifact. Anything committed to it is downloaded by every user, so a test suite and a screenshot generator would be dead weight for every consumer.
The docs and exampleSite directory stay in the theme repository despite not being needed to run the theme to make the theme self-contained and help to quickly set up a test installation.
Setting Up
There are two setups. Pick the smaller one unless you need what the larger one adds - most contributions never do.
The Simple Setup
One repository and Hugo. Nothing else to install.
git clone https://github.com/McShelby/hugo-theme-relearn.git
cd hugo-theme-relearn/docs
hugo server
That serves the documentation site, built with the theme itself, so your changes show up as you save. Swap docs for exampleSite to work against the simpler starting-point site instead.
This is enough for most changes. If that is what you came to do, stop here.
Note
You cannot run the test suite in this setup, so your change gets verified by CI rather than by you. Take the second setup if you want the answer before pushing.
The Full Setup
Add the infra repository as a sibling of the theme. This is what you need to run the test suite, regenerate the screenshots, or change the CI workflows.
hugo-theme-relearn/
hugo-theme-relearn-infra/
cd hugo-theme-relearn-infra
npm ci
npm test
The tooling finds the theme by looking at the RELEARN_THEME_DIR environment variable, then a sibling directory named hugo-theme-relearn, then the parent directory. The theme is never copied into the infra repository, so the tests always run against a real checkout.
To point the tooling at a checkout somewhere else:
RELEARN_THEME_DIR=/path/to/hugo-theme-relearn npm test
Git Hooks
Optional, and independent of which setup you chose - the hooks live in the theme repository, in the .githooks root folder. Documentation for each hook is contained in each file.
The post-commit hook updates the version number on each commit, which is what makes a build from main distinguishable from a release when debugging user reports. Nothing depends on you having it.
#!/bin/sh
python3 .githooks/post-commit.py
Working Across Both Repositories
Only this repository triggers test runs; the infra repository triggers nothing.
A change spanning both - a theme change that alters what the tests expect - takes the same branch name in each, and the infra branch is pushed first. The single run the push here then starts sees both halves. The other way round it pairs against infra main and can pass while testing only half the change; nothing detects that, so the order is the safeguard.
A change to the suite alone - a runner refactor, a regenerated baseline - never reaches this repository, so nothing triggers. Start a run from the Actions tab and name the infra branch in infra_ref.
Each workflow is described on the page for the thing it does: the test suite and releases.
Testing
The test suite lives in the infra repository and runs against a theme checkout. See Developing for how the two are wired together.
Requirements
Node.js at the version .nvmrc pins. Install it through a version manager - nvm, or nvm-windows - rather than as a system package, so the version can follow the project rather than the machine. nvm use in the infra checkout reads that file, and so does CI, so the two cannot drift.
The pin is not arbitrary. Before v26.8.1, Node’s fs.rmSync silently removed nothing on Windows when a path contained a non-ASCII character, which made “replace this directory” quietly mean “merge into it” - and a suite whose whole job is comparing directories cannot live with that.
Hugo at least the minimum the theme declares in its theme.toml. The plain edition is enough, as the theme uses no Sass. Install it the same way, through hvm, which keeps several versions side by side - the suite can then build against any of them, including the declared minimum, rather than only the one on your PATH.
Running the Tests
Check both repositories out side by side, then:
cd hugo-theme-relearn-infra
npm ci
npm test
The parts run cheapest first and stop at the first failure, so a stale declaration fails at once rather than after a full round of Hugo builds. The whole suite is quick enough to run on every change.
The checks are tests/checks.js and cover what the build layer cannot see: the runner’s own configuration handling, the dependency declaration - that it matches the vendored tree, and that every shipped component carries a license - and the properties the SBOM promises its readers, that it renders identically twice and that its serial number is recomputable from the document and moves with the contents. The SBOM comparison is npm run sbom, a narrower question: whether the committed sbom.cdx.json is still what that declaration renders.
Each part also runs alone, and those with something to write back do it under an :update name:
Command
Runs
npm test
everything
npm run test:update
everything, rewriting whatever has something to write back
npm run format
the formatting check
npm run format:update
the formatting check, rewriting what it flags
npm run checks
the runner checks
npm run sbom
the SBOM comparison
npm run sbom:update
the SBOM comparison, rewriting sbom.cdx.json
npm run golden
the cases
npm run golden:update
the cases, rewriting the expected output
tests/golden.js is the actual runner behind golden. Calling it directly takes the same flags and skips the rest, which is what you want while iterating on one case:
npm test -- --build=<name>
node tests/golden.js --build=<name>
Flags reach the runner directly; through npm they have to follow a -- separator first, and land on the last command in the chain - so the first form still runs every part before the cases, and only then narrows. The examples below use the runner, being the shorter of the two.
The Vocabulary
A run is more than a list of sites, hence these words:
Term
Meaning
Site
content plus the configuration it needs to be itself
Axis
a dimension of configuration, each of whose values is a config directory
Case
what to build, and how deeply to check it
Build
one Hugo invocation - one site, one configuration
Result
one output tree, compared as a whole
Most cases are one site, one configuration, one result. A case that varies an axis produces a result per combination. A case whose builds only mean something as a pair - a versioned site, or the docs and the exampleSite as GitHub Pages serves them - produces several builds sharing one result.
Cases live in tests/cases/<name>/case.toml in the infra repository, and reading them is the quickest way to see what the suite covers.
Shaping a Run
The parameters, all optional:
Parameter
Selects
Default
--build
which builds to run
all of them
--hugo
which Hugo to build with
each site’s own, see below
--update
rewrite the stored output instead of comparing against it
--build matches a path prefix, so naming a case runs everything in it and naming a combination runs the one. A small case is cheap enough to run on every save while working on one thing.
The accepted names are the ones a run prints. To see them without waiting for a full run, ask for something that does not exist and the runner lists them:
node tests/golden.js --build=?
A sequence is the exception: its builds share one tree, so it is named as a whole and a prefix reaching inside it is rejected. A build lifted out of a sequence proves nothing, which is what makes it a sequence.
--hugo - build with a particular version
Left out, each site is built with the version an interactive shell would use in its own directory: the one its .hvm file names, or the hugo on your PATH when there is none. A pin therefore applies to the site it sits beside, and a run can legitimately span several versions. Each site a pin applies to says so in the output, so a result never looks like it came from a version it did not.
Passing --hugo overrides every pin and holds the whole run to one version:
min is whatever the theme declares in its theme.toml, and latest the newest release. Anything not already installed is fetched for you.
Use min before pushing something that might rely on a newer Hugo feature, and latest to see a coming Hugo release before it reaches your users.
--update - rewrite the expected output
When a change legitimately alters what the theme produces, record the new output as the expectation:
npm run golden:update
node tests/golden.js --build=<name> --update
A full regeneration also prunes: a stored result no case produces any more is deleted rather than left behind. A filtered run does not, having no way to know whether a result it did not build still exists.
Commit the regenerated output together with the change that caused it, never as a commit of its own - otherwise the next person cannot tell which change produced which output.
Warning
The resulting diff is the test result. Read it before committing. An unreviewed regeneration turns the suite from a safety net into a rubber stamp.
If the change spans both repositories, give both branches the same name; see Developing.
Reading a Failure
Every result is checked in layers, and the one that fails tells you what kind of problem you have.
Layer
Asserts
A failure usually means
Build
the build exits cleanly, with no unexpected WARN or ERROR
a template error, or a Hugo deprecation
File set
exactly the expected files were generated
output formats, permalinks or a renamed page
Content
every file is what was stored, byte for byte bar line endings and the checkout path
either a regression, or a change you meant to make
Layers are cumulative, and a case declares how deep to go with layer. It defaults to content, so a case opts down rather than up and always says why - the theme’s own sites stop at the file set, because a content baseline over 2000 files would churn on every prose edit and be read by nobody.
A pinned older Hugo reduces every case to the build layer, since Hugo legitimately changes what it emits between releases and a baseline holds for the version that produced it. The run says when that happened, so a build check never reads as a content check.
Adding a Case
First decide whether you need a new site at all. If an existing one already renders the thing you changed, extending its content is enough - add a page, regenerate, review the diff.
A New Site
Everything lives in the infra repository.
Create tests/sites/<name>/ with a config/_default/ and content/. Keep the configuration about the site - a title, output formats, content wiring. Nothing about reproducibility belongs there; that is what naming the testing environment does.
Write the least content that demonstrates your case. Sites are meant to stay small - a readable diff is the whole point, and one needing hundreds of pages is testing the wrong thing.
Add tests/cases/<name>/case.toml:
site="<name>"environment="testing"
Generate its expected output, and read it:
node tests/golden.js --build=<name> --update
Look at tests/expected/<name>/. This is the moment the case is worth something or not: if the output does not show the behaviour you set out to pin, it will not catch a regression in it either.
Commit the site, the case and the expected output together.
Varying a Configuration
Some behaviour only differs by configuration - URL generation being the standing example, where relative, absolute and ugly URLs are genuinely different paths through the theme.
That is what an axis is for. Each value is a config directory under tests/axes/<axis>/<value>/, and a case lists the values it wants:
One content set, one result per mode, compared separately. Adding a further mode is a directory and one more name. An axis with a single value still applies - it just does not branch the tree, so nothing is nested that carries no information.
Builds That Belong Together
Some results are not one Hugo build. The published GitHub Pages site is the docs with the exampleSite beneath it. Neither half says anything alone.
Such a case spells the sequence out, and its builds share one output tree:
dest says where in the shared tree a build writes. The builds run in the order written, and the result is compared once, as a whole.
Accepting a Known Warning
Any WARN or ERROR fails a build unless it is listed in a baseline. These are consulted and their entries unioned:
File
Holds
tests/warnings.txt
theme-wide, mostly Hugo deprecations
tests/sites/<site>/warnings.txt
what a site’s own content provokes
tests/cases/<case>/warnings.txt
what a configuration provokes
A site’s file is checked against the build of that site, so what the docs provoke applies wherever the docs are built. Each entry is a substring; a warning containing it is accepted.
These baselines record outstanding work, not noise to be silenced. Adding an entry means consciously accepting a defect, so delete it as soon as the underlying issue is fixed and let a regression fail the suite again.
Building a Site Outside the Suite
A site that is not a case - a user’s site attached to an issue, say - can still be built the way the suite would build it. Install the tool once as a global command:
cd hugo-theme-relearn-infra
npm install -g .
The global command links to the checkout, so it always runs the current state of the infra repository. Then run it from the site’s directory, the one holding content/:
test-hugo
test-hugo min
test-hugo 0.150.0
The argument selects the Hugo version through hvm, defaulting to latest. The theme’s testing configuration is layered onto the site’s own, so the output is deterministic, and the result lands in a public.<theme version>+hugo.<hugo version> directory inside the site. Beside the generated files it holds metrics.log, Hugo’s log with template metrics, and dir.log, a list of every generated file - so two runs against different theme or Hugo versions diff directly.
Nothing is compared or stored; reading the difference is up to you.
Continuous Integration
This repository runs the suite on every branch and every pull request, and nightly against the latest Hugo release - which is how a Hugo change that breaks the theme is found in CI rather than in an issue report. The infra repository runs nothing; one run tests the pair, and this is where it happens.
That is why a change spanning both repositories is pushed to infra first, then here, and why a change to the suite alone has to be started by hand: see Developing.
The suite never releases, deploys or publishes anything.
Maintaining
Semver
This project tries to follow the semver policy - although not followed 100% in the past.
Usually an entry of Breaking on the What’s new page causes a new major release number.
All other entries on the What’s new page will increase the minor release number.
Releases resulting in a new major or minor number are called main release.
Releases containing bugfixes only, are only increasing the patch release number. Those releases don’t result in announcements on the What’s new page.
Entries on the What’s new page are checked and enforced during the version-release GitHub Action.
Managing Issues
Issues are categorized and managed by assigning labels to it.
Once working on an issue, assign it to a fitting maintainer.
When done, close the ticket. Once an issue is closed, it needs to be assigned to next release milestone.
A once released ticket is not allowed to be reopened and rereleased in a different milestone. This would cause the changelog to be changed even for the milestone the issue was previously released in. Instead write a new ticket.
Managing Pull Requests
If a PR is merged and closed it needs an accompanied issue assigned to. If there is no issue for a PR, the maintainer needs to create one.
You can assign multiple PRs to one issue as long as they belong together.
Usually set the same labels and milestone for the PR as for the accompanied issue.
Labels
Kind
An issue that results in changesets must have exactly one of the following labels. This needs to be assigned latest before release.
Label
Description
Changelog section
documentation
Improvements or additions to documentation
-
discussion
This issue was converted to a discussion
-
task
Maintenance work
Maintenance
feature
New feature or request
Features
bug
Something isn’t working
Fixes
Impact
If the issue would cause a new main release due to semver semantics it needs one of the according labels and the matching badge on the What’s new page.
Label
Description
change
Introduces changes with existing installations
breaking
Introduces breaking changes with existing installations
Declination
If an issue does not result in changesets but is closed anyways, it must have exactly one of the following labels.
Label
Description
duplicate
This issue or pull request already exists
invalid
This doesn’t seem right
support
Request for achieving a special goal
unresolved
No progress on this issue
update
A change in behavior after updat
wontchange
This will not be worked on
Halt
You can assign one further label out of the following list to signal readers that development on an open issue is currently halted for different reasons.
Label
Description
blocked
Depends on other issue to be fixed first
idea
A valuable idea that’s currently not worked on
undecided
No decision was made yet
helpwanted
Great idea, send in a PR
needsfeedback
Further information is needed
3rd-Party
If the issue is not caused by a programming error in the themes own code, you can label the causing program or library.
Label
Description
asciidoc
This is a topic related to processing of AsciiDoc
browser
This is a topic related to the browser but not the theme
device
This is a topic related to a certain device
hugo
This is a topic related to Hugo itself but not the theme
mermaid
This is a topic related to Mermaid itself but not the theme
SBOM
Every third-party resource the project uses is declared in docs/data/relearn/thirdparty.toml. That file is the single source of truth behind the credits page, the SBOM at sbom.cdx.json, and several checks in the test suite.
When you vendor an update, change its version in the same commit that changes the files. A version is knowable at the moment of vendoring and at no other time. Some of these bundles carry no version string at all, and the ones that do disagree about where to put it. Nothing can check it, which is why every component carries a digest of its files as well: a component that changed on disk without changing its version still shows up in the diff of sbom.cdx.json.
A font is the exception that can be asked. It declares its own version inside the file, where subsetting and conversion both leave it, so npm run fontversion in the infra repository reads it back out of what you vendored rather than out of the page you downloaded from.
The test suite fails when the declaration and the vendored tree disagree, in either direction: a directory holding files that no component claims, or a component claiming paths that no longer exist. Add a new dependency to the declaration or the build stays red.
npm test also fails when the committed sbom.cdx.json is not what the declaration currently renders, so regenerating is a step of the change rather than an afterthought.
cd ../hugo-theme-relearn-infra
npm ci
npm run sbom:update
The release workflow regenerates the file too, after it has stamped the new version - the document’s own version and timestamp come from layouts/partials/version.txt and CHANGELOG.md, which hold the values of the release being made only from that point on. Until then those two fields keep naming the previous release, while the components track whatever is vendored.
Making Releases
A release is based on a milestone named like the release itself - just the version number, eg: 1.2.3. It’s in the maintainers responsibility to check semver semantics of the milestone’s name prior to release and change it if necessary.
Making releases is automated by the version-release GitHub workflow. It requires the version number of the milestone that should be released. The release will be created from the main branch of the repository.
Treat released milestones as immutable. Don’t rerelease an already released milestone. An already released milestone may already been consumed by your users.
Automation Steps
During execution of the workflow a few things are checked. If a check fails the action fails, resulting in no new release. You can correct the errors afterwards and rerun the action.
The following checks will be enforced
the test suite passes against both the minimum supported and the latest Hugo release, which includes the dependency declaration matching the vendored tree and sbom.cdx.json matching the declaration
the milestone exists
there is at least one closed issue assigned to the milestone
all assigned issues for this milestone are closed
if it’s a main release, there must be an accompanying releasenotes file present in the repo at introduction/releasenotes/<major>/<minor>.en.md
After a successful run of the action
the changelog at introduction/changelog/<major>/<minor>/<patch>.<lang>.md is created for english and piratish, including missing generic upper level files
the CHANGELOG.md is updated
the releasenotes at introduction/releasenotes/<major>/<minor>.en.md are updated, including release version and release date
missing generic upper level files for english and piratish are created
the version number for the <meta generator> is updated
the repository is tagged with the version number (eg. 1.2.3), the main version number (eg. 1.2.x) and the major version number (eg. 1.x)
a new entry in the GitHub release list with the according changelog will be created
sbom.cdx.json is attached to that release, so it can be downloaded by version
a signed build provenance attestation is issued for the sbom.cdx.json at that tag, unless the signing service is unavailable - a release is never held up for it
the version number for the <meta generator> is updated to a temporary and committed (this helps to determine if users are running directly on the main branch or are using releases)
a new milestone for the next patch release is created (this can later be renamed to a main release if necessary)
Rehearsing on a Branch
The workflow only performs an actual release when it runs on main. Started on any other branch it runs the parts that are safe to repeat - the test suite and the documentation build - and skips every step that changes something outside the run. The milestone input is ignored there, and only required on main.
In both cases the built site is uploaded as a workflow artifact named <workflow>-<run number>-<run id> and kept for 30 days, so you can download the result of a run and inspect it before releasing for real. It is found at the bottom of the run’s summary page in the Actions tab.
Screenshooting
Sometimes screenshots need to be redone. This page explains how to create the different screenshots, tools and settings
Common
Creation:
Use English translation
Empty search
Remove history checkmarks but leave it on the page thats used for the screenshot
After resize of the page into the required resolution, reload the page to have all scrollbars in default loading position
Demo Screenshot
Content:
A meaningful full-screen screenshot of an interesting page.
The content should be:
timeless: not showing any dates or often edited content
interesting: show a bunch of interesting elements like headings, code, etc
balanced: no cluttering with overpresent elements or coloring
From original template resize to 3000 x 1500 offset y: -330, scale to 1280 x 640 and save as images/hero.png
Previewimages/hero.png:
Shortcode Feature Images
The feature images for the shortcodes are generated automatically via a Node.js script.
It lives in the infra repository inside of the tools/screenshots directory. All following commands need to be executed from the root of that repository.
To recreate the screenshots
install Node.js according to their installation guide
check out the infra repository next to the theme, as described in Developing
run npm ci
run npm run screenshots
The script serves the documentation itself on port 3132, captures the page of every directory below docs/content/shortcodes and writes the result back into docs/content/shortcodes/<shortcode>/featured.png of the resolved theme checkout. To capture against a server you are already running instead, pass its address:
npm run screenshots -- --base=http://localhost:1313
To capture only some of the pages, name their directories, separated by commas:
npm run screenshots -- --page=tabs,tree
Run this locally and commit the resulting images with the change that made them stale. The regenerated files land in your theme checkout, so they show up in git status alongside everything else.