scripts/build-static-demo.py
Rama main · réplica de lectura del repositorio original.
#!/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/<key>.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 = """<script async src="https://www.googletagmanager.com/gtag/js?id={id}"></script>
<script>window.dataLayer=window.dataLayer||[];function gtag(){{dataLayer.push(arguments);}}gtag('js',new Date());gtag('config','{id}');</script>
<script>document.addEventListener('click',function(e){{var el=e.target.closest&&e.target.closest('.cta,.btn,.vbtn,a[href]');if(!el)return;if(el.matches('.vbtn')){{gtag('event','demo_verify_click',{{transport_type:'beacon'}});}}else if(el.matches('.cta,.btn')){{gtag('event','demo_run_click',{{transport_type:'beacon',cta_location:el.matches('.cta')?'hero':'panel'}});}}else{{var h=el.getAttribute('href')||'';if(el.matches('.arena-btn')){{gtag('event','arena_interest_click',{{transport_type:'beacon'}});}}else if(/github\\.com\\/5uper0\\/parley\\/discussions/.test(h)){{gtag('event','discussions_click',{{transport_type:'beacon'}});}}else if(/github\\.com\\/5uper0\\/parley/.test(h)){{gtag('event','github_click',{{transport_type:'beacon'}});}}else if(/linkedin\\.com/.test(h)){{gtag('event','linkedin_click',{{transport_type:'beacon'}});}}}}}});</script>"""
# 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 <head> 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 = "<head>"
assert marker in html, "index.html has no <head> 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("<head>", "<head>\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()