# Deploying Garden Street Games

Live at **https://gardenstreetgames.com** and **https://www.gardenstreetgames.com**.

This is a zero-dependency static site: plain HTML/CSS/JS, no build step, no `npm`,
no framework. This doc covers local preview, how the live site is actually wired,
how to ship an update, and the one Godot constraint that shapes all of it.

---

## Prerequisites on this Mac

`git` and `python3` are **Xcode Command Line Tools stubs** — running them prints a
note about missing developer tools and exits. There is no Node, no npm and no
Homebrew, and none are wanted (see `CLAUDE.md`). That rules out `wrangler`.

What works out of the box: **Ruby** (with WEBrick) and **Perl**. Everything below
uses those.

To get real `git` — and with it any version control on this project, which it
currently has none of:

```
xcode-select --install
```

---

## Local preview

From the project root:

```
ruby -run -e httpd . -p 8000
```

Then open <http://localhost:8000>. Stop it with `Ctrl+C`.

Every page uses relative paths, so it **must** be served over HTTP. Opening
`index.html` from Finder (`file://`) breaks the game embeds, because Godot loads
its `.wasm`/`.pck` via `fetch()`, which browsers block on `file://`.

No special server configuration is needed for anything, including the compressed
Godot wasm — that is handled in the page, on purpose, so local and production
behave identically. See **The 25 MiB cap** below.

---

## How the live site is wired

`gardenstreetgames.com` is served by a **Cloudflare Worker with static assets**
named `old-cherry-5132` — not Cloudflare Pages. It was created by dashboard
upload, has **no Git connection and no build step**, and its versions live under
Workers & Pages → old-cherry-5132 → Deployments.

- Both `gardenstreetgames.com` and `www.gardenstreetgames.com` are **Custom
  Domains** on that Worker. Adding `www` is what fixed the site being
  unreachable from phones, which autocomplete to `www.` — before that, `www`
  was NXDOMAIN with no record of any kind.
- **Always Use HTTPS** is on, so `http://` 301s to `https://` on both hostnames.
- Version history keeps the last 100 versions and supports **one-click
  rollback**. That is the safety net for a bad deploy — use it rather than
  scrambling to fix forward.

---

## Shipping an update

**1. Build the bundle.** Copy the site to a staging folder outside the repo:

```
rm -rf ~/Documents/Projects/gsg-deploy
mkdir -p ~/Documents/Projects/gsg-deploy
cd ~/Documents/Projects/GardenStreetGames
tar --exclude='.DS_Store' --exclude='.git' --exclude='.gitignore' \
    --exclude='./server' -cf - . \
  | (cd ~/Documents/Projects/gsg-deploy && tar -xf -)
```

`server/` is excluded on purpose: it is source for the leaderboard API
Worker, which Cloudflare runs separately, not part of the static site. See
`docs/LEADERBOARD.md`.

**2. Check nothing is over the cap.** This must come back empty:

```
find ~/Documents/Projects/gsg-deploy -type f -size +25M
```

**3. Upload.** Workers & Pages → **old-cherry-5132** → **New deployment** → drag
the **`gsg-deploy` folder itself** onto the drop zone → **Deploy**.

It must be a *folder* drag. Not a zip, not a multi-file selection — the uploader
reads directory structure, and a flat file list lands everything at the root and
breaks every relative path on the site.

**4. Verify.** Load the site, then load a game page specifically. A deploy can
look perfect while the game hangs, because the wasm is the fragile part.

### Why step 3 is a human

Claude prepares the bundle but cannot perform the upload. Every route to
automate it is closed:

- **`wrangler`** needs Node, which is deliberately not on this machine.
- **The Cloudflare API** needs an API token, which nobody has minted. If you
  want unattended deploys, minting a scoped token is the sane path — ask.
- **Driving the dashboard uploader** needs a real folder selection with
  directory structure intact. Synthesising one requires reaching a local server
  from a public HTTPS page, which means defeating Chrome's Private Network
  Access protection. That is out of bounds regardless of intent.

---

## The 25 MiB cap — the constraint that shapes the Godot pipeline

Cloudflare refuses any single asset over **25 MiB**, on both Workers static
assets and Pages. A Godot 4.7 web export is a **~38 MB `.wasm`**. It cannot be
uploaded raw, and an oversized file rejects the **entire deploy**, not just that
file.

### What does NOT work

**Storing the file gzipped and declaring it in `_headers`.** Workers static
assets **strips `Content-Encoding` from `_headers`** and then applies its own
compression on top. Verified against the live site:

- `Accept-Encoding: identity` → the stored gzip bytes, with **no**
  `Content-Encoding` header at all.
- A normal browser request → **double-compressed**. Decoding one layer yields
  `1f 8b` (gzip) where `00 61 73 6d` (wasm) should be.

The browser peels off one layer, is left holding gzip, and Godot hangs forever
on load **with nothing in the console**. Do not try this again.

### What works

**Ship the gzip and decompress in the page.** The repo stores
`games/<slug>/index.wasm.gz` (9.6 MB for Pioneering) and no `index.wasm` at all.
A shim at the top of `games/<slug>/index.html`, above `<script src="index.js">`,
patches `window.fetch`: a request for `index.wasm` is rewritten to
`index.wasm.gz`, piped through `DecompressionStream('gzip')`, and returned as a
fresh `application/wasm` response, which Godot can still stream-compile.

This works because `fetch()` transparently undoes whatever the host did to the
transfer encoding, so the shim always sees exactly the bytes that were stored —
regardless of what Cloudflare layers on top. It needs no `_headers`, no host
configuration, and behaves identically under `ruby -run -e httpd` and in
production.

To repeat it for a new game, see `games/README.md` step 2.

`DecompressionStream` needs Chrome 80+, Safari 16.4+ or Firefox 113+. The shim
logs `[pioneering]`-style errors if it is unavailable.

---

## Other hosts

Nothing about the site is Cloudflare-specific except the 25 MiB workaround. If
it ever moves:

- **GitHub Pages** — 100 MB per file, so the raw `.wasm` drops in untouched and
  the gzip shim becomes unnecessary. Cannot set custom response headers at all,
  which is fine for single-threaded exports. Needs working `git`.
- **Netlify** — drag-deploy or Git-connected, build command empty, publish
  directory `.`. Supports `_headers` properly.

In both cases Cloudflare can keep the domain and just point DNS at the host.

---

## If a threaded Godot export ever ships

Single-threaded is the standing rule (`games/README.md`), so this should stay
hypothetical. If it doesn't, the whole page tree needs
`Cross-Origin-Opener-Policy: same-origin` and
`Cross-Origin-Embedder-Policy: require-corp`, scoped as narrowly as possible —
COEP on the whole site breaks any embed or asset that isn't itself CORS-enabled,
and it kills third-party ad tags outright.
