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 <span data-build-time> 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 <span class="footer-build-time" data-build-time>...</span> 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 <noreply@anthropic.com>
This commit is contained in:
parent
154b47a4cb
commit
fad8719045
6
Makefile
6
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 <span data-build-time>
|
||||
# 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); \
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<span class="footer-license"><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" rel="license">CC BY-NC-SA 4.0</a> · <a href="https://git.levineuwirth.org/neuwirth/levineuwirth.org">MIT</a> · <a href="/memento-mori.html" class="footer-mm">MM</a></span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<a href="/build/" class="footer-build-link" aria-label="Build telemetry">build</a> $build-time$
|
||||
<a href="/build/" class="footer-build-link" aria-label="Build telemetry">build</a> <span class="footer-build-time" data-build-time>$build-time$</span>
|
||||
· <a href="$url$.sig" class="footer-sig-link" aria-label="PGP signature for this page" title="Ed25519 signing subkey C9A42A6F AD444FBE 566FD738 531BDC1C C2707066 · public key at /gpg/pubkey.asc">sig</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -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'(<span class="footer-build-time" data-build-time>)[^<]*(</span>)'
|
||||
)
|
||||
|
||||
|
||||
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"))
|
||||
Loading…
Reference in New Issue