Add tools/build_site.py: runs the full pipeline in dependency order
(build_content -> build_listings -> seo_inject --all --sitemap ->
build_botlinks -> build_search_index), each as a subprocess so a failing
step aborts the build before anything downstream runs (no partial
deploy). --check verifies non-destructively; --images also runs
optimize_images. This is what CI will run in U5.
Fix a build_content idempotence bug found via U4: the hero and
article-header regions were emitted with leading indentation while the
regex left the template's pre-tag whitespace in place, so indentation
compounded (+16 spaces) on every rebuild. First line is now unindented;
the full build is verified idempotent (identical git tree across two
runs). Added a fixpoint test (re-rendering a page's own output is
byte-identical) so this can't regress.
docs/local-preview.md: build + serve on :8000 in all three languages.
tools/README.md: document build_site + build_content.
26 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tools/build_content.py: renders every content/ entry into its blog /
news / event detail page by self-templating the page's own chrome and
replacing only content-derived regions (title, article header, hero,
body, and — for new pages only — the scrollspy).
Faithful reproduction, proven against the live site:
- body_format: html fragments injected verbatim, so guide-boxes, inline
figures and event galleries render unchanged.
- scrollspy sidebars are preserved (labels are hand-curated, shorter than
the H2s); only generated when a page has none.
- <title> suffix derived per page, so EN/SI " - MSOS", MK " - МСОС", and
suffix-less event titles all reproduce exactly.
- hero /images/… rewritten to the page's ../../../ depth.
- ASCII-only console output (no cp1252 crash on Windows/CI).
tools/tests/test_build_content.py: 8 characterization tests; the core one
renders all 51 pages and asserts each is semantically equivalent
(whitespace/attr-order-insensitive) to the current page — the R5
no-regression guarantee.
Does NOT overwrite the committed pages; whether generated pages are
committed vs built fresh in CI is decided in U4/U5. Run:
python tools/build_content.py --check # verify equivalence
python tools/build_content.py --write # emit pages (for U4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>