# Deploying Garden Street Games

This is a zero-dependency static site: plain HTML/CSS/JS, no build step,
no `npm`, no framework. Any static file host works. This doc covers local
preview and three hosting options.

## Prerequisites on a fresh Mac

This machine currently has **no Xcode Command Line Tools installed**,
which means the system `/usr/bin/python3` and `/usr/bin/git` are stub
binaries — running them just prints a note about missing developer tools
and exits. Node/npm/Homebrew/PHP are not installed either.

What *does* work out of the box on stock macOS: **Ruby** (`/usr/bin/ruby`,
ships with WEBrick) and **Perl**. That's what the commands below use.

If you'd rather use `python3`, `git`, or `npx`, install the Command Line
Tools first:

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

(and install Node separately if you want `npx serve` — the Xcode tools do
not include Node.) Until you've done that, use the Ruby command below.

## Local preview

From the project root:

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

Then open **http://localhost:8000**. This uses WEBrick, which ships with
the system Ruby install — no gems, no setup. Stop it with `Ctrl+C`.

Every page in this site uses relative paths (`../css/...`, `games/...`,
etc.), so it must be served over HTTP — opening `index.html` straight from
Finder (a `file://` URL) will break the game embeds, since Godot's Web
export loads its assets via `fetch()`, which browsers block under
`file://`.

**Alternatives**, if you've installed the tooling they need:

```
python3 -m http.server 8000     # needs Xcode Command Line Tools (see above)
npx serve .                      # needs Node.js installed separately
```

Both are equivalent to the Ruby command above — pick whichever is already
working on your machine.

## How this site is actually deployed today

`gardenstreetgames.com` is served by a **Cloudflare Worker with static assets**
named `old-cherry-5132` — not Pages. It was created by dashboard upload, has no
Git connection and no build, and its versions are listed under
Workers & Pages → old-cherry-5132 → Deployments. Version history keeps the last
100 versions and supports one-click rollback, which is the safety net if a
deploy goes wrong.

Both `gardenstreetgames.com` and `www.gardenstreetgames.com` are Custom Domains
on that Worker, and **Always Use HTTPS** is on, so `http://` 301s to `https://`
on both hostnames.

To ship a new version: Worker → **New deployment** → drag the site folder in →
Deploy. It must be a *folder* drag, not a zip and 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.

Deploying with `wrangler` instead would need Node, which is deliberately not on
this machine (see the top of `CLAUDE.md`).

## Other hosts

If this ever moves off Cloudflare, all of these serve static files as-is with no
build step required.

### GitHub Pages

1. Repo → Settings → Pages.
2. Source: **Deploy from a branch**, branch `main`, folder `/ (root)`.
3. Save. The site publishes at `https://<user>.github.io/<repo>/`.
4. **Custom domain**: Settings → Pages → Custom domain, enter it, and add
   either a `CNAME` record (subdomain) or `A` records to GitHub's IPs
   (apex domain) at your DNS provider. GitHub writes a `CNAME` file into
   the repo automatically — check it back in if you ever move the site.
5. **COOP/COEP headers**: GitHub Pages **cannot set custom response
   headers** at all — there is no config surface for it. If a game ever
   needs `SharedArrayBuffer` (a threaded Godot 4 export), it will not run
   on GitHub Pages, period. Either export that game single-threaded (see
   `games/README.md`) or use Cloudflare Pages / Netlify instead.

### Netlify

**Drop deploy** (fastest, no git integration needed): Netlify dashboard →
Sites → drag the project folder onto the "Deploy manually" drop zone. It
publishes immediately at a generated `*.netlify.app` URL.

**Git-connected** (recommended for ongoing updates): New site from Git →
pick the repo → leave Build command **empty** and Publish directory as
`.` (project root) → Deploy.

- **Custom domain**: Site settings → Domain management → Add a domain,
  then either use Netlify DNS (they give you nameservers) or add a CNAME
  at your own DNS provider pointing to the `*.netlify.app` address.
- **COOP/COEP headers**: add a `netlify.toml` at the project root (or a
  `_headers` file) — e.g. to cover a specific threaded game only:

  ```toml
  [[headers]]
    for = "/games/some-threaded-game/*"
    [headers.values]
      Cross-Origin-Opener-Policy = "same-origin"
      Cross-Origin-Embedder-Policy = "require-corp"
  ```

  Scope the `for` path narrowly — COEP on the whole site can break any
  embed or asset that isn't itself CORS-enabled.

### Cloudflare Pages

> **Read this first: the 25 MiB per-file cap.** Cloudflare Pages refuses any
> single asset over **25 MiB**, and so does Workers static assets. A Godot 4.7
> web export is a ~38 MB `.wasm` — over the cap, and Pages rejects the whole
> deploy, not just that file. This is the single biggest constraint on hosting
> Godot games here, and every future game will hit it. See
> **Godot exports vs. the 25 MiB cap** below before deploying.

1. Cloudflare dashboard → Workers & Pages → Create → Pages → Connect to
   Git, pick the repo.
2. Build command: **empty**. Build output directory: `/` (project root).
3. Deploy. Publishes at `*.pages.dev`.
4. **Custom domain**: Pages project → Custom domains → Add. If the domain
   is already on Cloudflare DNS this is one click; otherwise add a CNAME
   at your registrar.
5. **COOP/COEP headers**: add a `_headers` file at the project root:

   ```
   /games/some-threaded-game/*
     Cross-Origin-Opener-Policy: same-origin
     Cross-Origin-Embedder-Policy: require-corp
   ```

   Same scoping advice as Netlify — apply it only to the specific game
   folder that needs it, not `/*`. Exporting single-threaded (the default
   rule here) means you never need these at all.

### Godot exports vs. the 25 MiB cap

A Godot 4.7 web export ships a ~38 MB `index.wasm`. Cloudflare refuses any
single asset over 25 MiB, so it cannot be uploaded raw.

**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. Tested against the live site: a request with
`Accept-Encoding: identity` came back as the stored gzip bytes with no
`Content-Encoding` header at all, and a normal browser request came back
double-compressed. The browser peels off one layer, is left holding gzip, and
Godot hangs forever on load with no console error. 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 short shim at the top of `games/<slug>/index.html`, before `index.js` loads,
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.

That works because `fetch()` transparently undoes whatever the host did to the
transfer encoding, so the shim always sees exactly the bytes that were stored.
It needs no `_headers`, no host configuration, and behaves identically on a
plain `ruby -run -e httpd` locally and on Cloudflare in production.

To repeat it for the next game:

```
gzip -9 -c build/web/index.wasm > games/<slug>/index.wasm.gz
rm games/<slug>/index.wasm
```

then copy the shim block out of `games/pioneering/index.html`, changing nothing.

If a future export is small enough to stay under 25 MiB, drop the shim and the
`.gz` and ship `index.wasm` normally.

## Recommendation

Default to exporting Godot games **single-threaded** (see
`games/README.md`) so no host-level header configuration is ever needed.
That keeps GitHub Pages on the table, which is the simplest option if this
stays a personal/portfolio project. Reach for Cloudflare Pages or Netlify
only when a specific game genuinely needs threading.
