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
That checks the runner itself, then builds every case, and takes well under a minute.
npm test is a wrapper around tests/run.js, which is the actual runner. Either form works - these two are the same command:
npm test -- --build=<name>
node tests/run.js --build=<name>
Flags reach the runner directly; through npm they have to follow a -- separator first. The examples below use the runner, being the shorter of the two.
The Vocabulary
Five words, because a run is not simply a list of sites any more.
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
Three 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 takes about a second, which 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/run.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:
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 three 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/run.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, three results, 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. A versioned site is two, each configured to know about the other; the published GitHub Pages site is the docs with the exampleSite beneath it. Neither half says anything alone.
Those spell the sequence out, and 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. Three 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.
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
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
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 updated files are committed
the milestone is closed
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
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 check, tagging, committing, publishing the GitHub release and deploying to GitHub Pages. 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 each page and writes the result back into docs/content/<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
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.