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 \
|
else \
|
||||||
echo "Embedding skipped: run 'uv sync' to enable similar-links (build continues)"; \
|
echo "Embedding skipped: run 'uv sync' to enable similar-links (build continues)"; \
|
||||||
fi
|
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
|
@./tools/compress-assets.sh _site
|
||||||
> IGNORE.txt
|
> IGNORE.txt
|
||||||
@BUILD_END=$$(date +%s); \
|
@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>
|
<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>
|
||||||
<div class="footer-right">
|
<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>
|
· <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>
|
</div>
|
||||||
</footer>
|
</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