Real bucket name, wrangler 'cors set --file' (+ wrangler-format CORS file), per-file r2 object put upload (no bulk sync exists), CLI custom-domain bind via zone-id, Pages domain is dashboard-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 + bakedclips.json/ring.json/media-versions.json+ generatedconfig.js) and a rootdist/_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
Bucket name: human-experience-simulator-media. The default wrangler login
OAuth token already has R2 + Pages + zone scopes (no separate API token needed).
wrangler r2 bucket create human-experience-simulator-media
# CORS — load-bearing (the WebGL dream shader taints without it). The wrangler CLI
# wants its own schema, NOT the S3-style array; use the *.wrangler.json file:
wrangler r2 bucket cors set human-experience-simulator-media --file deploy/cloudflare/r2-cors.wrangler.json
# (deploy/cloudflare/r2-cors.json is the equivalent S3-style policy, for the
# dashboard / S3 API; r2-cors.wrangler.json is the same rule in wrangler's
# `{ "rules": [ { "allowed": {...} } ] }` form.)
# Custom domain — wrangler CAN do this (no dashboard needed); needs the zone id.
ZONE=$(curl -s -H "Authorization: Bearer $(grep '^oauth_token' ~/.wrangler/config/default.toml | cut -d'"' -f2)" \
"https://api.cloudflare.com/client/v4/zones?name=benstull.art" | python3 -c "import sys,json;print(json.load(sys.stdin)['result'][0]['id'])")
wrangler r2 bucket domain add human-experience-simulator-media \
--domain static.benstull.art --zone-id "$ZONE" --min-tls 1.2
Media upload — there is NO wrangler r2 bucket sync. Upload per-file with
wrangler r2 object put ... --remote, setting content-type + immutable cache. The
URLs are content-hash-versioned (?v=<hash>), so immutable is safe — a re-baked
clip gets a new hash → new URL → cache busts. ~604 files / ~2.3 GB, sequential:
WR=$(command -v wrangler); BUCKET=human-experience-simulator-media
find dist-media -type f | while read -r f; do
key="${f#dist-media/}"
case "${f##*.}" in mp4) ct=video/mp4;; mp3) ct=audio/mpeg;; json) ct=application/json;; *) ct=application/octet-stream;; esac
"$WR" r2 object put "$BUCKET/$key" --file "$f" --remote \
--content-type "$ct" --cache-control "public, max-age=31536000, immutable"
done
(If you have rclone + an R2 S3 API token, rclone copy dist-media <remote>:bucket
is much faster than the loop.)
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 --production-branch main
wrangler pages deploy dist --project-name human-experience-simulator --branch main
Then in the dashboard (Workers & Pages → project → Custom domains) — there is no wrangler CLI for Pages custom domains:
- Bind
benstull.artto the project. (Allow a few minutes — a fresh bind serves522until the cert/routing provisions.) - The app serves at
benstull.art/human-experience-simulator/; the shippeddist/_redirectssends/and the no-slash form to it.
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.