Skip to content

Static sites

zensical-pages and hugo-pages both build a static site and deploy it to GitLab Pages, and both split that into the same two-job shape: a build job that produces a public/ artifact, and a pages job that deploys it. This very docs site is built by zensical-pages — its self-include lives in this repo's own root .gitlab-ci.yml.

The shared shape, and the one thing that makes it work

Both jobs in each component share the exact same changes filter, and that's load-bearing, not incidental: pages needs: the build job's artifact, so if the filters ever diverged, a push that skipped the build (nothing relevant changed) but somehow still matched the deploy filter would try to deploy an artifact that was never produced — a hard failure. With matching filters, a no-content push to the default branch correctly skips both jobs: nothing changed, so nothing rebuilds and nothing redeploys.

zensical-build (unlike hugo-build) doesn't run on tag pipelines at all — this repo, and consumers like it, use tags for release publishing, not for a docs redeploy.

hugo-pages is the deliberate exception to the schedule rule

Every other component in this repo carries a leading schedule → never guard, so a Renovate pipeline schedule runs renovate-self and nothing else — see change-detection and the schedule-scoping decision this predates. hugo-pages breaks that rule on purpose, in both its hugo-build and pages jobs: the schedule rule sits before the change-gated rules, unfiltered, so a nightly schedule always rebuilds and redeploys regardless of whether the diff would have matched.

The reason is Hugo-specific: HUGO_ENV=production excludes future-dated, non-draft content from the build until its date actually arrives. A post scheduled for tomorrow simply isn't in today's build — there's no code change that "happens" when the clock ticks over, so change-detection has nothing to detect. A nightly pipeline schedule is the only mechanism that makes those posts go live on time. The schedule isn't a fallback here — for Hugo sites with time-bound content, it is the feature.

The MR-time build gate mr_gate restores

Before hugo-pages existed, the project's hand-rolled Hugo sites (blog, dusthole, shutterandstove) each carried a near-identical pages job with no MR-time build gate at all — a broken Hugo build only surfaced after it had already landed on the default branch. hugo-build's default behaviour (mr_gate: true) fixes that: it runs in merge-request pipelines too, so a broken template or a malformed shortcode fails the MR, not the deploy.

mr_gate: false opts back into the old, looser model — hugo-build then runs only where it deploys (on deploy_branch and on schedule), with no MR/branch build at all. That's the right shape for a project audited locally before every push rather than gated in CI — a deliberate trade some consumers make, not a defect.

zensical-pages bootstraps its toolchain at build time; hugo-pages doesn't need to

zensical-build pip-installs Zensical from the consumer's python_lock file at job runtime, inside infra-tools (which provides Python but not Zensical itself). hugo-build, by contrast, runs on a pinned hugomods/hugo image that already bundles Hugo (the -git variant also bundles git, needed for .GitInfo/.Lastmod) — no per-job install step. The Zensical approach costs a small amount of job time; it also means a docs-site upgrade is a lockfile bump, not a component image bump. Neither is strictly better — they reflect how each tool is typically distributed and versioned upstream.

See also