Build default app-path → /human-machine-strata; deploy.sh APP_PATH default too. _write_redirects now emits a permanent redirect /human-experience-simulator/* → /human-machine-strata/:splat so old links still land. Media (R2 bucket) + Pages project names are unchanged (not in the benstull.art URL). Updated the build test (asserts the legacy redirect) + static-build e2e path + README paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.7 KiB
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-machine-strata, 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.
Repeatable deploy (after the one-time setup below)
Once the bucket/CORS/custom-domains + wrangler login exist (one-time, §1–3), every
later deploy is one command (deploy/cloudflare/deploy.sh, or the Make targets):
make deploy # frontend only: build + Pages deploy (fast — most deploys)
make deploy-media # also re-sync all media to R2 (~2 min, parallel) — when media changed
./deploy/cloudflare/deploy.sh --media-only # only the R2 media sync, skip Pages
The script rebuilds dist/, (optionally) parallel-uploads dist-media/ to R2, and
deploys Pages — printing the live URL. It checks wrangler auth first and fails
loudly. Override defaults via env: BUCKET, PROJECT, MEDIA_BASE, APP_PATH,
BRANCH, JOBS. The sections below document the one-time setup the script assumes.
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-machine-strata/(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-machine-strata/; 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-machine-strata/. 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.