From fad871904546c709854e752d650ca1ed686b2879 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sat, 23 May 2026 12:05:28 -0400 Subject: [PATCH] Stamp the site-wide build time post-render Hakyll caches per-page outputs, so pages whose dependencies have not changed are not recompiled and the rendered $build-time$ in their footer goes stale relative to a fresh build. The right granularity for "last built at" is site-wide rather than per-page; wrapping the footer timestamp in and rewriting it after Hakyll lets every page reflect the current build without paying recompilation cost. * tools/stamp-build-time.py walks _site/**/*.html after Hakyll runs and rewrites each element wrapped in [data-build-time] to the same format Contexts.hs:buildTimeField emits, so a fresh render and the post-pass agree. * templates/partials/footer.html wraps $build-time$ in ... so the sweep has a stable selector. * Makefile invokes the sweep between embed.py and compress-assets so the .gz/.br sidecars include the fresh stamp. Co-Authored-By: Claude Opus 4.7 --- Makefile | 6 +++ templates/partials/footer.html | 2 +- tools/stamp-build-time.py | 77 ++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100755 tools/stamp-build-time.py diff --git a/Makefile b/Makefile index 3144573..174f586 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,12 @@ build: else \ echo "Embedding skipped: run 'uv sync' to enable similar-links (build continues)"; \ fi + # Site-wide footer timestamp: rewrite every + # in _site/**/*.html so cached (un-recompiled) pages don't show a + # stale per-page build time. See tools/stamp-build-time.py for the + # full rationale. Must run before compress-assets so the .gz/.br + # sidecars include the fresh stamp. + @python3 tools/stamp-build-time.py _site @./tools/compress-assets.sh _site > IGNORE.txt @BUILD_END=$$(date +%s); \ diff --git a/templates/partials/footer.html b/templates/partials/footer.html index cc2ebd4..85a7a29 100644 --- a/templates/partials/footer.html +++ b/templates/partials/footer.html @@ -7,7 +7,7 @@ CC BY-NC-SA 4.0 · MIT · MM diff --git a/tools/stamp-build-time.py b/tools/stamp-build-time.py new file mode 100755 index 0000000..be8c9f5 --- /dev/null +++ b/tools/stamp-build-time.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Post-build sweep: stamp the site-wide build time into every footer. + +Why this exists +--------------- +build/Contexts.hs binds $build-time$ to getCurrentTime at item-compile +time, but Hakyll caches outputs. Pages whose dependencies have not +changed are not recompiled, so the previously-rendered timestamp stays +on disk and the footer drifts per page. We want one site-wide +"last built at" stamp, so this script walks _site/**/*.html after +Hakyll runs and rewrites the contents of every wrapped element. + +Format must match build/Contexts.hs:buildTimeField exactly so a fresh +build (where Hakyll renders the timestamp itself) and the sweep agree. +""" +from __future__ import annotations + +import os +import re +import sys +from datetime import datetime, timezone + + +def ordinal_suffix(day: int) -> str: + if 11 <= day <= 13: + return "th" + return {1: "st", 2: "nd", 3: "rd"}.get(day % 10, "th") + + +def format_now() -> str: + now = datetime.now(timezone.utc) + return ( + f"{now.strftime('%A, %B')} " + f"{now.day}{ordinal_suffix(now.day)}, " + f"{now.strftime('%Y %H:%M:%S')}" + ) + + +PATTERN = re.compile( + rb'()[^<]*()' +) + + +def stamp_file(path: str, replacement_bytes: bytes) -> bool: + with open(path, "rb") as f: + data = f.read() + new_data, count = PATTERN.subn( + lambda m: m.group(1) + replacement_bytes + m.group(2), + data, + ) + if count and new_data != data: + with open(path, "wb") as f: + f.write(new_data) + return True + return False + + +def main(root: str) -> int: + if not os.path.isdir(root): + print(f"stamp-build-time: {root} not found", file=sys.stderr) + return 1 + timestamp = format_now().encode("utf-8") + rewritten = 0 + scanned = 0 + for dirpath, _, files in os.walk(root): + for name in files: + if not name.endswith(".html"): + continue + scanned += 1 + if stamp_file(os.path.join(dirpath, name), timestamp): + rewritten += 1 + print(f"stamp-build-time: rewrote {rewritten}/{scanned} HTML files") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "_site"))