#!/usr/bin/env python3 """Pre-render the money-shot demo as a fully STATIC site (no server, no Docker). The demo API is deterministic over a fixed whitelist of recipes, so every `/api/run?recipe=X` response can be baked to a static JSON file. This emits a static tree that deploys to Vercel / Cloudflare Pages / any static host with zero backend: build/ index.html # the demo page, fetches rewritten to static paths api/recipes.json # = list_recipes() api/run/.json # = run_recipe(path) for each whitelisted recipe Usage: scripts/build-static-demo.py [out_dir] (default: build/static-demo) """ import json import os import shutil import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from examples.demo.server import RECIPES, HERE, run_recipe, list_recipes, _recipe_path # noqa: E402 OUT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("build/static-demo") # Analytics is injected at build time rather than committed into index.html, so the published # demo source carries no tracking tag and a contributor building locally is not silently # instrumented. Opt in with PARLEY_GA4_ID; unset means no analytics. GA4_ID = os.environ.get("PARLEY_GA4_ID", "") GA4_SNIPPET = """ """ # The `.arena-btn` check runs BEFORE the discussions-href regex on purpose: the Arena fake-door # CTA links into /discussions too, and arena_interest_click must fire INSTEAD OF the generic # discussions_click so the interest signal is attributable to that CTA alone (a footer # Discussions click still logs discussions_click as before). # Static files copied verbatim into the deploy root alongside index.html. _ASSETS = Path(HERE).parent.parent / "docs" / "brand" / "assets" STATIC_COPIES = { # robots.txt/sitemap.xml/privacy.html are deliberately NOT copied here — all three are # root-only (privacy.html was a demo-local drifted duplicate of the canonical # landing/src/pages/privacy.astro; the merged site is the only real deploy target, and its # root copies of robots.txt/sitemap.xml already cover /demo/ — a second copy under # /demo/sitemap.xml only rotted, e.g. proofcard URLs listed without the /demo/ prefix they # actually live at). _ASSETS / "og-card.png": "og-card.png", _ASSETS / "og-card-demo.png": "og-card-demo.png", # index.html links these; manifest.json additionally needs icon-192/512. _ASSETS / "favicon" / "favicon.svg": "favicon.svg", _ASSETS / "favicon" / "favicon-16.png": "favicon-16.png", _ASSETS / "favicon" / "favicon-32.png": "favicon-32.png", _ASSETS / "favicon" / "apple-touch-icon.png": "apple-touch-icon.png", _ASSETS / "favicon" / "icon-192.png": "icon-192.png", _ASSETS / "favicon" / "icon-512.png": "icon-512.png", _ASSETS / "manifest.json": "manifest.json", # llms.txt is deliberately NOT copied here: it's a root-only discovery convention (like # robots.txt/sitemap.xml), added directly at the merged site's root by build-site.sh — a # copy under /demo/llms.txt would be dead weight nothing looks for. # Orphan-but-live URLs: nothing on the site links these, but they are served today and # the proof cards were built to be shared, so a deploy that dropped them would 404 links # already in the wild. Cloudflare Pages replaces the whole tree, so absent means deleted. Path(HERE, "proofcard_p2p.html"): "proofcard_p2p.html", Path(HERE, "proofcard_dao.html"): "proofcard_dao.html", Path(HERE, "proofcard_estate_real.html"): "proofcard_estate_real.html", Path(HERE, "proofcard_partnership.html"): "proofcard_partnership.html", _ASSETS / "proofcard.png": "proofcard.png", _ASSETS / "parley-money-shot.gif": "parley-money-shot.gif", _ASSETS.parent / "built-by-a-fleet.html": "built-by-a-fleet.html", } def main(): out = OUT.resolve() (out / "api" / "run").mkdir(parents=True, exist_ok=True) (out / "api" / "recipes.json").write_text( json.dumps(list_recipes(), ensure_ascii=False), encoding="utf-8") for key in RECIPES: result = run_recipe(_recipe_path(key)) (out / "api" / "run" / f"{key}.json").write_text( json.dumps(result, ensure_ascii=False), encoding="utf-8") # Rewrite the two fetches from server routes to static files. html = Path(HERE, "index.html").read_text(encoding="utf-8") html = html.replace("fetch('/api/recipes')", "fetch('/api/recipes.json')") html = html.replace( "fetch('/api/run?recipe='+encodeURIComponent(key))", "fetch('/api/run/'+encodeURIComponent(key)+'.json')") if GA4_ID: marker = "" assert marker in html, "index.html has no to inject analytics into" html = html.replace(marker, marker + "\n" + GA4_SNIPPET.format(id=GA4_ID), 1) (out / "index.html").write_text(html, encoding="utf-8") sys.path.insert(0, str(HERE)) # proofcard.py imports its sibling `server` from proofcard import replay_html replays = [ ("committee", "recipe_committee.json", "en"), ("herencia-hermanos", "recipe_herencia_hermanos.json", "es"), ] for slug, recipe, lang in replays: page = replay_html(run_recipe(str(Path(HERE, recipe))), f"/demo/replay/{slug}/", recipe, lang) if GA4_ID: page = page.replace("", "\n" + GA4_SNIPPET.format(id=GA4_ID), 1) (out / "replay" / slug).mkdir(parents=True, exist_ok=True) (out / "replay" / slug / "index.html").write_text(page, encoding="utf-8") for src, name in STATIC_COPIES.items(): if src.exists(): shutil.copy2(src, out / name) else: print(f" ! skipped missing static file: {src}") files = sorted(p.relative_to(out).as_posix() for p in out.rglob("*") if p.is_file()) print(f"✓ static demo → {out}") for f in files: print(f" {f}") if __name__ == "__main__": main()