Files
msos/tools/build_site.py
T
popovskikandClaude Opus 4.8 954f186761
Deploy to Production on Develop Push / deploy (push) Successful in 20s
build: content-hash cache-busting for local CSS/JS (?v=hash)
Stops deploys being served stale from browser cache — the recurring "changes
do not show up until a hard refresh" problem.

- New tools/cache_bust.py stamps a global content hash onto every local
  <link>/<script> CSS/JS ref AND the @import lines in css/main.css (so partial
  edits bust too). External (https://) refs are untouched. Deterministic and
  idempotent: same content -> same stamp, any asset edit -> new stamp sitewide.
- Wired as the final step of tools/build_site.py so every build/deploy stamps.
- Initial stamp applied across 259 pages + main.css (v=64a53dd6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-27 18:14:44 +02:00

90 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""
build_site.py -- one command to build the whole site from content/.
Implements U4 of docs/plans/2026-08-08-001-feat-no-code-cms-plan.md. Runs the
existing tools in dependency order so a single content edit propagates to every
derived page. This is what CI runs before deploying (U5).
Order matters:
1. build_content content/ front-matter + body -> detail pages
2. build_listings detail pages -> News hub + homepage "Latest news"
3. seo_inject inject SEO block + rebuild sitemap.xml / robots.txt
4. build_botlinks regenerate /links-for-bots/ + llms.txt from the pages
5. build_search_index rebuild the offline assistant index
Each step is a separate process (build_search_index chdir's at import, and this
keeps a failing step from corrupting later ones). Any non-zero exit aborts the
build so a broken step never reaches deploy.
Usage:
python tools/build_site.py # full build (writes pages)
python tools/build_site.py --images # also optimise images -> WebP first
python tools/build_site.py --check # non-destructive: verify pages match content/
"""
import argparse
import os
import subprocess
import sys
import time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# (description, argv relative to ROOT)
BUILD_STEPS = [
("Render detail pages from content/", ["tools/build_content.py", "--write"]),
("Rebuild News hub + homepage listings", ["tools/build_listings.py"]),
("Inject SEO + rebuild sitemap/robots", ["tools/seo_inject.py", "--all", "--sitemap"]),
("Rebuild bot/AI discovery files", ["tools/build_botlinks.py"]),
("Rebuild offline assistant search index", ["tools/build_search_index.py"]),
# Last: stamp a content-hash ?v= on local CSS/JS refs so deploys aren't
# served stale from cache. Must run after every step that writes pages.
("Cache-bust CSS/JS references", ["tools/cache_bust.py"]),
]
IMAGES_STEP = ("Optimise images to WebP + update refs", ["optimize_images.py"])
CHECK_STEP = ("Verify rendered pages match content/", ["tools/build_content.py", "--check"])
def _run(desc, argv):
print(f"\n>>> {desc}\n $ python {' '.join(argv)}")
env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
t0 = time.time()
result = subprocess.run([sys.executable, *argv], cwd=ROOT, env=env)
dt = time.time() - t0
if result.returncode != 0:
raise SystemExit(
f"\n!!! Step failed ({result.returncode}): {desc}\n"
f" Build aborted — nothing downstream ran, so no partial output is deployed.")
print(f" done ({dt:.1f}s)")
def build(images=False):
steps = list(BUILD_STEPS)
if images:
steps.insert(1, IMAGES_STEP) # after pages exist, before refs are consumed
print(f"Building site from content/ ({len(steps)} steps)")
t0 = time.time()
for desc, argv in steps:
_run(desc, argv)
print(f"\nBuild complete in {time.time() - t0:.1f}s. "
f"Preview locally with: python -m http.server 8000 (see docs/local-preview.md)")
def check():
print("Verifying the site matches content/ (no files written)...")
_run(*CHECK_STEP)
if __name__ == "__main__":
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--images", action="store_true",
help="also run optimize_images.py (WebP conversion + ref rewrite)")
ap.add_argument("--check", action="store_true",
help="verify rendered pages match content/ without writing")
args = ap.parse_args()
if args.check:
check()
else:
build(images=args.images)