Files
human-experience-filter-art/deploy/cloudflare

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.

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):

.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

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=<hash>), 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://<host>/ so config.js matches.

3. Pages — deploy + domain + redirect

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

# 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/<some>/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.