# Deploying to benstull.art — Cloudflare Pages + R2 Publishes the simulator as a fully-static site: the frontend on **Cloudflare Pages** (`benstull.art`, app at `/human-experience-simulator`, apex redirects to it) and the video/audio on **R2** behind `static.benstull.art`. No origin server. Design: `docs/superpowers/specs/2026-06-30-cloudflare-static-publish-design.md`. Plan: `docs/superpowers/plans/2026-06-30-cloudflare-static-publish.md`. This project is **exempt from the flotilla/PPE/§9 pipeline** (operator-set) — it deploys directly to Cloudflare. Safety/hygiene rules still hold. ## Prerequisites (operator gestures — need Cloudflare auth) `wrangler` is not installed in the dev box; the deploy runs from a machine with Cloudflare credentials. ```bash npm i -g wrangler wrangler login ``` `benstull.art` is already a Cloudflare zone (no nameserver migration). ## 1. Build the artifacts From the repo root (run as a MODULE — `tools/http.py` shadows stdlib `http` if run as a script): ```bash .venv/bin/python -m tools.build_static ``` Produces: - `dist/` — Pages output: `dist/human-experience-simulator/` (frontend + baked `clips.json`/`ring.json`/`media-versions.json` + generated `config.js`) and a root `dist/_redirects` (apex → app path, and no-slash → slashed). - `dist-media/` — R2 upload tree, ~2.3 GB / 604 files, manifest-referenced only (clip bases + 154 transition morphs + 5 per-scale audio). No master/mezzanine sources. ## 2. R2 — bucket, CORS, custom domain, media ```bash wrangler r2 bucket create hef-media wrangler r2 bucket cors put hef-media --rules deploy/cloudflare/r2-cors.json wrangler r2 bucket sync ./dist-media r2://hef-media ``` Then in the dashboard (R2 → `hef-media` → Settings): - **Custom domain:** add `static.benstull.art` (this is the `mediaBase` baked into `config.js`). - **CORS is load-bearing** — the WebGL dream shader taints without it. The policy in `r2-cors.json` allows `https://benstull.art` + `Range` (video scrubbing). - **Caching:** the media URLs are content-hash-versioned (`?v=`), so set the objects `Cache-Control: public, max-age=31536000, immutable` (a re-baked clip gets a new hash → new URL → cache busts cleanly). If you change the media base, rebuild with `--media-base https:///` so `config.js` matches. ## 3. Pages — deploy + domain + redirect ```bash wrangler pages project create human-experience-simulator wrangler pages deploy dist --project-name human-experience-simulator ``` Then in the dashboard (Pages → project → Custom domains): - Bind **`benstull.art`** to the project. - The app serves at `benstull.art/human-experience-simulator/`; the generated `dist/_redirects` sends `/` and the no-slash form to it. (Alternatively, a zone-level **Redirect Rule** `benstull.art/` → `/human-experience-simulator` is the authoritative equivalent if you prefer not to ship `_redirects`.) ## 4. Verify before/after going live ```bash # CORS + Range on the media origin (after step 2) curl -sI -H "Origin: https://benstull.art" -H "Range: bytes=0-1" \ https://static.benstull.art//base.mp4 | grep -iE "access-control-allow-origin|content-range|accept-ranges" ``` Open `https://benstull.art/` → should redirect to `https://benstull.art/human-experience-simulator/`. Turn the **Feel** knob to max and confirm the dream effect renders (no `SecurityError` in the console — that would mean CORS is misconfigured and the GL texture is tainted). The local static-build E2E (`simulator/e2e/tests/static-build.spec.ts`) exercises this same cross-origin path before you ship.