diff --git a/Dockerfile b/Dockerfile
index 582c882..c14fec0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,7 +9,11 @@ FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4
WORKDIR /src
# Install build dependencies first so the layer caches until the
-# scripts change (script/bootstrap installs git, make, hugo, node/npm).
+# scripts change (script/bootstrap installs git, make, go, hugo,
+# node/npm). Hugo is not an apk package here: script/bootstrap builds
+# the exact pinned version with `go install`, hash-verified against
+# sum.golang.org, so the published artifact does not depend on whatever
+# hugo this base image's repos happen to serve.
COPY script/ script/
RUN script/bootstrap
diff --git a/README.md b/README.md
index 1d0610f..b377288 100644
--- a/README.md
+++ b/README.md
@@ -46,8 +46,10 @@ standard: normalized scripts in `script/` are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call them. We
provide:
-- `script/bootstrap` — install all build dependencies (git, make, hugo,
- node/npm) idempotently
+- `script/bootstrap` — install all build dependencies (git, make, go, hugo,
+ node/npm) idempotently. Hugo is pinned to an exact version and installed with
+ `go install`, which verifies it against `sum.golang.org`; the version is the
+ `HUGO_VERSION` constant at the top of the script
- `script/setup` — prepare a fresh clone: run `script/bootstrap` and install the
git pre-commit hook
- `script/test` — the correctness check: a clean `hugo --minify` production
diff --git a/TODO.md b/TODO.md
index 081abba..807288f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -15,7 +15,9 @@ pre-1.0
No git tags. The site is live and now has the scripts-to-rule-them-all scaffold
(`Makefile`, `script/`, `Dockerfile`, `check.yml`); still missing `LICENSE` and
policy files. Every external reference in the repo is now pinned by
-cryptographic hash (or, for the wrangler CLI install, an exact version).
+cryptographic hash (or, for the wrangler CLI install, an exact version), and the
+Hugo that builds the published site is a deliberate pinned version rather than
+whatever the base image's package repo serves.
# Next Step
@@ -25,6 +27,29 @@ Update `README.md` accordingly.
# Completed Steps
+- 2026-08-09: replaced `hugo.toml`'s deprecated `languageCode` key with `locale`
+ (closes #18). Hugo deprecated `languageCode` in v0.158.0, so the Hugo pinned
+ in the preceding commit warns about it; left alone it would become a third
+ routinely-ignored warning, and a latent breakage when the key is removed.
+ Deliberately sequenced **after** the Hugo version move and in the same branch:
+ under the apk hugo 0.139.0 that CI ran until now, `locale` is an unknown key
+ that is silently ignored, which downgrades the generated RSS from
+ `en-us` to `en` with no warning and
+ exit 0. Verified on hugo v0.164.0 that the RSS `` still reads
+ `en-us`, the `lang` attribute is unchanged, and `public/` is byte-identical to
+ the preceding commit's output
+- 2026-08-09: installed Hugo at a deliberate, hash-verified version instead of
+ taking whatever alpine ships (closes #26). `script/bootstrap` no longer does
+ `pkg_install hugo`; it installs `github.com/gohugoio/hugo@v0.164.0` with
+ `go install`, which verifies the module against `sum.golang.org`. The version
+ is a commented constant, as is the Go toolchain (`go1.26.5`) — hugo v0.164.0
+ requires go >= 1.26.0 and alpine 3.21 ships go 1.23.9 with
+ `GOTOOLCHAIN=local`, so a bare `go install` refuses to run. `CGO_ENABLED=0` is
+ deliberate: standard Hugo, not extended, because this site has no SCSS, no
+ `resources.ToCSS`, no PostCSS and no image processing. This moves the build
+ off apk's hugo 0.139.0, about two years behind, onto the current stable.
+ Rendered output across the whole `public/` tree is unchanged except the
+ `meta name=generator` version string
- 2026-08-09: made `script/check` run `script/lint` (closes #9). It previously
ran only `fmt-check` then `test`, so `script/lint` executed nowhere — not in
`make check`, not in the pre-commit hook, and not in CI, even though the
diff --git a/hugo.toml b/hugo.toml
index a58301b..5a43c1b 100644
--- a/hugo.toml
+++ b/hugo.toml
@@ -1,5 +1,5 @@
baseURL = 'https://lora.vegas/'
-languageCode = 'en-us'
+locale = 'en-us'
title = 'LoRa Vegas — Las Vegas Meshtastic Community'
theme = 'loravega'
diff --git a/script/bootstrap b/script/bootstrap
index cd4ca97..926f4c5 100755
--- a/script/bootstrap
+++ b/script/bootstrap
@@ -2,9 +2,10 @@
# script/bootstrap: install all dependencies needed to build and develop
# this Hugo site, idempotently. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes NOTHING is present (not git,
-# make, hugo, or node). Installs hugo (the site build) and node/npm
-# (prettier, used to format the repo's own markdown docs). Every install
-# is guarded by a check so already-installed tools are skipped.
+# make, go, hugo, or node). Installs hugo (the site build, at the exact
+# version pinned below) and node/npm (prettier, used to format the
+# repo's own markdown docs). Every install is guarded by a check so
+# already-installed tools are skipped.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -12,6 +13,50 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
PKGMGR=""
SUDO=""
+# --- Hugo -------------------------------------------------------------
+#
+# Hugo produces the published artifact, so its version is a property of
+# the site's output, not of the build environment. It is therefore
+# pinned here rather than taken from whatever the distro serves: before
+# this, alpine 3.21's apk supplied hugo 0.139.0 -- a version chosen by
+# nobody, roughly two years behind upstream, and liable to change
+# silently whenever the base image digest moves.
+#
+# `go install` is the hash-verified mechanism: Go checks the module
+# against the sum.golang.org checksum database. That is the mechanism
+# REPO_POLICIES.md already names for Go, and it needs no hand-maintained
+# sha256. The other tools this script installs stay on the package
+# manager, which #19 settled is fine for build-time conveniences.
+#
+# hugo v0.164.0, 2026-07-06
+HUGO_VERSION="v0.164.0"
+
+# hugo v0.164.0's go.mod requires go >= 1.26.0, and alpine 3.21's `go`
+# package is 1.23.9 built with GOTOOLCHAIN=local, so a bare `go install`
+# refuses to run at all. Naming the toolchain explicitly makes Go fetch
+# it through the module proxy and verify it against sum.golang.org like
+# any other module, so the chain stays hash-verified end to end -- and
+# the Go version that compiles hugo becomes deliberate too, instead of
+# being inherited from whatever the base image happens to ship.
+# go1.26.5, 2026-08-09
+HUGO_GOTOOLCHAIN="go1.26.5"
+
+# Standard hugo, not hugo extended: CGO_ENABLED=0 is deliberate.
+# Verified that this site uses nothing extended provides -- there are no
+# .scss/.sass files, no resources.ToCSS, no PostCSS, and no image
+# processing (.Resize/.Fill/.Fit/images.* are all absent). The CSS is
+# plain and inlined by `readFile` in baseof.html. The `+extended` on the
+# apk build this replaces was incidental, not a requirement, so do not
+# assume a future change needs it without rechecking the above.
+HUGO_CGO_ENABLED="0"
+
+# Where the hugo binary lands. It has to be on the default PATH of a
+# *fresh* shell, not just of this script: the Dockerfile's `RUN make
+# check` and deploy.yml's `script/test` step each start their own shell
+# and would never see a GOPATH bin directory. Overridable so an
+# unprivileged install can point somewhere writable.
+HUGO_BIN_DIR="${HUGO_BIN_DIR:-/usr/local/bin}"
+
detect_pkgmgr() {
[ -n "$PKGMGR" ] && return 0
if command -v nix-env >/dev/null 2>&1; then
@@ -28,13 +73,17 @@ detect_pkgmgr() {
fi
if [ "$PKGMGR" = "apt" ]; then
export DEBIAN_FRONTEND=noninteractive
- if [ "$(id -u)" != "0" ]; then
- SUDO="sudo"
- fi
+ detect_sudo
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
fi
}
+detect_sudo() {
+ if [ -z "$SUDO" ] && [ "$(id -u)" != "0" ]; then
+ SUDO="sudo"
+ fi
+}
+
# pkg_install
pkg_install() {
detect_pkgmgr
@@ -50,6 +99,56 @@ missing() {
! command -v "$1" >/dev/null 2>&1
}
+# True when the hugo already on PATH is the pinned version. Unlike the
+# other tools, mere presence is not good enough here: an older hugo has
+# to be replaced, not accepted, or the pin means nothing.
+hugo_pinned() {
+ command -v hugo >/dev/null 2>&1 || return 1
+ # `hugo version` prints e.g. "hugo v0.164.0 linux/amd64 ..." for a
+ # `go install` build, or "hugo v0.164.0-ce2470e+extended ..." for an
+ # official release binary. Compare only the vX.Y.Z part: a build of
+ # the same version that happens to be extended renders this site
+ # identically (see HUGO_CGO_ENABLED above), so there is no reason to
+ # overwrite a developer's existing matching install.
+ have="$(hugo version 2>/dev/null | awk '{print $2}' | sed 's/[-+].*//')"
+ [ "$have" = "$HUGO_VERSION" ]
+}
+
+install_hugo() {
+ detect_sudo
+
+ # The Go toolchain is a build-time convenience like git and make, so
+ # it comes from the package manager; the thing that must be
+ # deliberate is what it builds, which HUGO_VERSION and
+ # HUGO_GOTOOLCHAIN pin.
+ if missing go; then pkg_install go golang-go go go; fi
+
+ # Build as the invoking user into a scratch GOBIN, then place the
+ # binary with `install`. Running the whole `go install` under sudo
+ # would work but would populate root's module cache instead of the
+ # user's, which is needlessly slow and surprising on a workstation.
+ gobin="$(mktemp -d)"
+ CGO_ENABLED="$HUGO_CGO_ENABLED" \
+ GOTOOLCHAIN="$HUGO_GOTOOLCHAIN" \
+ GOBIN="$gobin" \
+ go install "github.com/gohugoio/hugo@${HUGO_VERSION}"
+ $SUDO install -d "$HUGO_BIN_DIR"
+ $SUDO install -m 0755 "$gobin/hugo" "$HUGO_BIN_DIR/hugo"
+ rm -rf "$gobin"
+
+ # Drop any cached PATH lookup of the hugo we just replaced, so the
+ # check below tests the new binary and not the old one.
+ hash -r 2>/dev/null || true
+
+ # Fail loudly rather than let a later build run on a shadowing hugo
+ # from somewhere earlier in PATH.
+ if ! hugo_pinned; then
+ echo "bootstrap: installed $HUGO_VERSION into $HUGO_BIN_DIR but" \
+ "'hugo' on PATH is still $(hugo version 2>/dev/null || echo absent)" >&2
+ exit 1
+ fi
+}
+
main() {
cd "$ROOT"
@@ -63,8 +162,8 @@ main() {
git submodule update --init --recursive
fi
- # Site build.
- if missing hugo; then pkg_install hugo hugo hugo hugo; fi
+ # Site build. Pinned and hash-verified -- see the HUGO_* constants.
+ if ! hugo_pinned; then install_hugo; fi
# node/npm provide prettier (via npx) for formatting the docs.
if missing node; then pkg_install nodejs nodejs node nodejs; fi