Chapter 5

Development

This chapter contains information only needed for development and maintaining the theme.

  • Contributing

    What to know if you want to contribute

  • Developing

    How to set up, and how the theme and its tooling are split

  • Testing

    How to run and extend the automated test suite

  • Maintaining

    What to know as a maintainer

  • Screenshooting

    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.

Conventional Commits

Write commit messages in the conventional commit format.

Following is an incomplete list of some of the used conventional commit types. Be creative.

Common Feature Structure Shortcodes
build a11y favicon attachments
browser archetypes search badge
chore alias menu button
docs generator history children
shortcodes i18n scrollbar expand
theme mobile nav icon
print toc include
rss clipboard math
variant syntaxhighlight mermaid
boxes notice
openapi
piratify
siteparam
tabs

Developing

The theme is developed across two repositories.

Repository Contents
hugo-theme-relearn The theme itself, and the workflows, release actions and git hooks that act on it.
hugo-theme-relearn-infra 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 compare

--build - run part of the suite

node tests/run.js --build=minimal
node tests/run.js --build=url-permutations
node tests/run.js --build=url-permutations/urls-relative

--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:

node tests/run.js --hugo=min
node tests/run.js --hugo=latest
node tests/run.js --hugo=v0.150.0

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:

node tests/run.js --update
node tests/run.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 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.

  1. 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.

  2. 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.

  3. Add tests/cases/<name>/case.toml:

    site        = "<name>"
    environment = "testing"
  4. Generate its expected output, and read it:

    node tests/run.js --build=<name> --update
  5. 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.

  6. 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:

site        = "url-permutations"
environment = "testing"

[axes]
  urls = ["relative", "absolute", "ugly"]

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:

[[builds]]
  site        = "versioning-current"
  environment = "testing"

[[builds]]
  site        = "versioning-archived"
  environment = "testing"
  dest        = "0.666"

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 official documentation is built and deployed
  • 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
  • aligned: aligned outlines

Used by:

Page URL: Screenshot Link

Creation:

  • save as images/screenshot.png
  • from original screenshot, scale to 900 x 600 and save as images/tn.png

Remarks:

The locations are mandatory due to Hugo’s theme site builder.

Preview images/screenshot.png:

Screenshot Screenshot

Preview images/tn.png:

tn tn

Hero Image

Content:

Show the Demo Screenshot page on different devices and different themes. Composition of the different device screenshots into a template.

The content should be:

  • consistent: always use the same page for all devices
  • pleasing: use a delightful background

Used by:

Page URL: Hero Image Link

Creation:

Preview images/hero.png:

Hero Hero

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.