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>
Extends the Phase-1 monogram mark system to every long-form content
type (essays, blog posts, poems, fiction, music) and introduces a
coverage audit so gaps are visible.
* build/Marks.hs gains hasMonogram (predicate), monogramSvgFieldFor +
hasMonogramFieldFor (for explicit-path callers like the /build/ and
/stats/ pages). Contexts.hs exports hasMonogramField as a siteCtx
boolean so templates can conditionally render the slot without
emitting an empty <div>.
* essay.html, blog-post.html, reading.html: hoist the frontmatter
block out of <main id="markdownBody"> so the monogram + epistemic
marks render as wrapper chrome rather than indexable prose; left
+ right mark slots are now unconditional (CSS handles the empty
state) so the layout is grid-stable across pieces.
* templates/partials/item-card.html: optional monogram chip on cards
(item-card--has-monogram modifier), gated on $has-monogram$ so
monogram-less pieces stay flush.
* build/Stats.hs grows a "Marks coverage" telemetry section: per-type
pieces / monogram / epistemic-figure counts + a coverage rollup,
rendered between epistemic and output on /build/.
* tools/audit-marks.py: coverage report (ASCII table) walking
content/**/*.md, plus a pre-commit hook at
tools/hooks/pre-commit-marks.sh that runs the same scan against
newly-staged .md files. New `make audit-marks` runs the report
manually; the hook gates commits.
* static/css/marks.css: layout for the new frontmatter slots and the
item-card monogram chip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Preserve external works the site cites against link rot, host them at
permanent /archive/<slug>/ URLs in site chrome, and treat them as
first-class citizens of the backlinks and similar-pages indexes.
Curated, not crawled: the author adds one line to archive/manifest.yaml
and the build fetches, hashes, snapshots, and indexes the work.
* archive/manifest.yaml + tools/archive.py (fetch / refresh / wayback /
check / gc) — PDFs downloaded directly, HTML pages snapshotted with a
vendored monolith (tools/bin/monolith @ 2.10.1) into a single
self-contained file with the archive CSP and a noarchive robots meta
injected. Per-entry PROVENANCE.json committed; gitignored .txt
sidecars regenerated from the artifact's SHA-256.
* build/Archive.hs + build/ArchiveIndex.hs + build/Filters/Archive.hs
— Hakyll rules for /archive/ and /archive/<slug>/, a body Pandoc
filter that appends an archive affordance to live citations and
flips dead ones to the local copy on archive.py check's asymmetric
hysteresis (rotted needs 3 fails over >= 14 days; one ok recovers).
* build/Backlinks.hs — keeps archived external URLs through pass 1 and
canonicalises them to /archive/<slug>/ in pass 2, producing a
"Referenced by" section grouped by the fragment each citation
targets. build/Stats.hs gains a "Link archive" telemetry block on
/build/ (count, total size, median age, by-status / by-quality /
by-visibility, orphans).
* Integrity: archive.py fetch and build/Archive.hs (via sha256sum)
both re-hash every committed artifact, so a tampered file halts the
build even with cabal invoked directly or no .venv present. refresh
refuses to replace an uncommitted prior snapshot and rolls back
atomically on any exit path. removed.yaml is honoured by fetch,
wayback, and check using canonical-form (tracking-stripped,
arXiv-canonicalised) comparison.
* visibility: private keeps an entry in-repo but undeployed.
nginx/archive.conf emits X-Robots-Tag: noindex, noarchive for raw
artifacts that cannot carry meta directives.
The full design, phase plan (1-5), and three refinement passes live
in ARCHIVE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A VPS_PATH containing whitespace or shell metacharacters would split
on the unquoted expansion and hand rsync extra arguments. The
existing VPS_PATH guard rejects obviously dangerous parents (/srv,
/var, etc.) but does not catch this. Quoting fails closed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>