diff --git a/deploy/cloudflare/README.md b/deploy/cloudflare/README.md new file mode 100644 index 0000000..1ba295f --- /dev/null +++ b/deploy/cloudflare/README.md @@ -0,0 +1,89 @@ +# 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. diff --git a/deploy/cloudflare/r2-cors.json b/deploy/cloudflare/r2-cors.json new file mode 100644 index 0000000..60834b5 --- /dev/null +++ b/deploy/cloudflare/r2-cors.json @@ -0,0 +1,9 @@ +[ + { + "AllowedOrigins": ["https://benstull.art"], + "AllowedMethods": ["GET", "HEAD"], + "AllowedHeaders": ["Range", "Content-Type"], + "ExposeHeaders": ["Content-Length", "Content-Range", "ETag", "Accept-Ranges"], + "MaxAgeSeconds": 86400 + } +]