Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/sites-edge-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bunny.net/cli": minor
---

Sites are now served by pull zone edge rules instead of a router Edge Script (HTML revalidates in browsers on every view, deploy dirs are blocked at the edge), and `sites deploy --deploy-id` lets a deploy carry your own release identifier
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,10 @@ Scripts, apps, and sites are each backed by a pull zone, which has a large setti
Hard-won, not inferable from the code, and expensive to rediscover.

- **Magic Containers requires `linux/amd64`.** Builds must pass `--platform linux/amd64`; an arm64 image (the default on Apple Silicon) breaks the pull.
- **At the edge, `ctx.request.url` is origin-facing** (`http://<edge-ip>:9000/...`), not the requested host. The client hostname must come from the `CDN-Host`/`Host` headers. Hostname routing on `url.hostname` never fires. The sites router's index-retry probe must be rebuilt on the client host so it re-enters the CDN instead of hitting unrouted storage paths.
- **Bump `ROUTER_VERSION` on any change to the sites router source.** It is recorded in site state, and `ensureRouterCurrent` republishes stale routers on deploy.
- **At the edge, `ctx.request.url` is origin-facing** (`http://<edge-ip>:9000/...`), not the requested host. The client hostname must come from the `CDN-Host`/`Host` headers. Hostname routing on `url.hostname` never fires.
- **Edge rule "Change Origin URL" replaces the URL wholesale** unless a variable like `%{Url.Path}` (full path + query) appends the request path. `SetRequestHeader`/`SetResponseHeader` extra actions on the same rule apply to that rule's origin fetch (platform-confirmed); `BlockRequest` runs pre-cache, so it applies to cache hits too. Sites builds on all three.
- **The pull zone delivery layer rewrites `Cache-Control`** from the zone's cache settings; origin- or script-set values reach browsers only when both `CacheControlMaxAgeOverride` and `CacheControlPublicMaxAgeOverride` are `-1`, and a `no-cache` the CDN respects also stops it storing the object. Sites sidesteps this with a zone-level override plus per-extension browser-cache rules.
- **Sites edge rules are identified by their `Description` strings** (constants in `sites/constants.ts`); upserts key on them, so treat them as frozen.
- **The `/storagezone/regions` endpoint is not reliable.** The region catalog is hand-maintained in the storage constants. The available set is a function of both tier and S3 support: Edge (SSD) zones can only be primaried in DE and the create API silently rewrites any other region rather than erroring, so reject a conflicting `--region` client-side.
- **Storage zone tier and S3 support are create-time only.** The update API takes neither.
- **Storage replication is irreversible.** There is no API to remove a replication region, so `update` models replication as additive: it offers only new regions, warns on omissions, and confirms before adding.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@ bun ny storage zones add my-zone --tier ssd --s3 # create an Edge (SSD) zone (
bun ny storage files list # list files in the linked storage zone
bun ny storage files download # browse the zone and pick a file to download; same picker on `files remove`
bun ny storage files remove / # empty the zone; asks twice (yes/no, then type the zone name), and unattended runs need --force
bun ny sites create my-site # provision a static site (storage zone + pull zone + edge router; zones are named sites-my-site-<suffix>, served at sites-my-site-<suffix>.b-cdn.net)
bun ny sites create my-site # provision a static site (storage zone + pull zone with edge rules; zones are named sites-my-site-<suffix>, served at sites-my-site-<suffix>.b-cdn.net)
bun ny sites create my-site --tier ssd # provision a site whose files live on the Edge (SSD) storage tier (always DE)
bun ny sites deploy # no linked site? offers to create one or pick an existing; detects the framework, offers to build, then deploys (a site's first deploy also offers to attach a custom domain)
bun ny sites deploy ./dist # deploy a directory and publish it as the live site
bun ny sites deploy --build # run `sites.build` from bunny.jsonc (else the detected framework's build), then deploy `sites.dir` (or the detected output dir)
bun ny sites deploy ./catalog --deploy-id 20260827-1433-r42 # identify the deploy with your own release ID instead of the git sha / content hash
bun ny sites deployments list # list deploys with the live one marked
bun ny sites deployments publish --previous # instant rollback to the previous deploy
bun ny sites deployments prune # delete old deploys (keeps the newest 5, never current/previous)
Expand Down
13 changes: 7 additions & 6 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -908,9 +908,9 @@ bunny scripts docs

> **Experimental**: hidden from `--help` and the landing page while it stabilizes.

Host static sites on bunny.net. Each site is three resources provisioned and wired together for you: a **storage zone** holding the files, a **pull zone** serving them over the CDN, and a **middleware router** (an Edge Script) that maps incoming requests to the deploy that should answer them. Zones are named `sites-<name>-<suffix>` (the prefix groups them in the dashboard; the suffix is because zone names are global across bunny.net) while commands take the clean site name.
Host static sites on bunny.net. Each site is two resources provisioned and wired together for you: a **storage zone** holding the files and a **pull zone** serving them over the CDN, with edge rules that route requests to the deploy that should answer them. Zones are named `sites-<name>-<suffix>` (the prefix groups them in the dashboard; the suffix is because zone names are global across bunny.net) while commands take the clean site name.

Deploys are immutable: every `sites deploy` uploads to its own `deploys/<id>/` directory and then goes live. Publishing flips the router's `CURRENT_DEPLOY` variable and purges the cache, so going live and rolling back to any earlier deploy are instant and move no files. Deploy IDs are the git short SHA when the working tree is clean and a content hash otherwise, which makes redeploying identical content a no-op.
Deploys are immutable: every `sites deploy` uploads to its own `deploys/<id>/` directory and then goes live. Publishing retargets the pull zone's rewrite rule and purges the cache, so going live and rolling back to any earlier deploy are instant and move no files. HTML is served with `max-age=0` so browsers pick up new deploys immediately, while static assets get a one-day browser cache. Deploy IDs are the git short SHA when the working tree is clean and a content hash otherwise, which makes redeploying identical content a no-op.

Commands take the site as an optional positional (`[site]`), except `deploy`, `ci init`, and `deployments publish`, which use `--site`. Either accepts the site name or its storage zone ID. When omitted, the site resolves from the directory's linked site (`.bunny/site.json`, written by `sites link` or by `create`/`deploy`), then `sites.name` in `bunny.jsonc`, then an interactive picker that offers to link. Non-interactive runs (`--output json`, no TTY, or `--force` on a destructive command) error instead of prompting.

Expand All @@ -927,6 +927,7 @@ bunny sites deploy ./dist # deploy a directory and p
bunny sites deploy --build # run `sites.build` from bunny.jsonc (else the detected build), then deploy
bunny sites deploy --build "npm run build" --env API_URL=https://api.example.com
bunny sites deploy ./dist --site my-site --force # target a site explicitly; redeploy unchanged content
bunny sites deploy ./catalog --deploy-id 20260827-1433-r42 # your own release ID instead of the git sha / content hash

# Deploys: list, publish (roll back), prune
bunny sites deployments list # ● Live / ○ Previous markers, created, source, files, size
Expand All @@ -953,28 +954,28 @@ bunny sites ci init # GitHub Actions: push to
bunny sites ci init --framework astro
bunny sites link my-site
bunny sites unlink
bunny sites upgrade-router # republish the router with this CLI's version (deploy also does this automatically)
bunny sites delete my-site --keep-storage # typed-name confirmation; keeps the deploy files
```

Preconfigure the `sites` block in `bunny.jsonc` (`name`, `build`, `dir`) and a deploy needs no arguments: `bunny sites deploy --build`. `sites ci init` reads the same block, so the generated workflow builds and deploys exactly what the local command does; without it, the framework is detected from `package.json` deps, `Gemfile`, or a `hugo`/`python`/`zola` config file, with the lockfile picking the package manager. `sites create` offers to scaffold the workflow on GitHub repos.

Every deploy publishes: the files land in an immutable `deploys/<id>/` directory and the router is pointed at it, so `deployments publish` rolls back to any earlier deploy by moving that pointer, with no files moving and nothing re-uploaded. Content is root-served, so client-side routing and absolute asset paths work as-is. Site state lives at `_bunny/site.json` inside the storage zone (the router blocks it with a 403); `.bunny/site.json` is only a local pointer, so a fresh clone can `sites link` and pick up where the last machine left off.
Every deploy publishes: the files land in an immutable `deploys/<id>/` directory and the rewrite rule is pointed at it, so `deployments publish` rolls back to any earlier deploy by moving that pointer, with no files moving and nothing re-uploaded. The ID is the git short-sha when the tree is clean, a content hash otherwise, or whatever `--deploy-id` supplies (letters, digits, `-`, `_`, `.`; 4-64 chars; case-sensitive): a custom ID never aliases onto another deploy's content, and reusing one for different content asks before replacing (`--force` skips the prompt); a replacement clears the old files first, so nothing stale survives. The live deploy and the rollback target are never replaced in place; deploy those under a new ID. Content is root-served, so client-side routing and absolute asset paths work as-is. Direct `/deploys/<id>/` URLs are blocked at the edge. Site state lives at `_bunny/site.json` inside the storage zone (also blocked at the edge); `.bunny/site.json` is only a local pointer, so a fresh clone can `sites link` and pick up where the last machine left off.

| Flag | Commands | Description |
| -------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `--region`, `--domain` | `create` | Main storage region code (default `DE`); custom production domain to attach |
| `--site` | `deploy`, `ci init`, `deployments publish` | Site name or storage zone ID (defaults to the linked site) |
| `--build [cmd]`, `--env`, `--env-file` | `deploy` | Build before deploying (bare flag uses the configured or detected build); build-time env overrides |
| `--force` | `deploy` | Deploy even when the content is unchanged |
| `--force` | `deploy` | Deploy even when the content is unchanged, and replace an existing `--deploy-id` without asking |
| `--deploy-id` | `deploy` | Identify the deploy yourself (release tag, catalog ID); case-sensitive, used exactly as given |
| `--previous` | `deployments publish` | Publish the previous deploy (instant rollback) |
| `--keep` | `deployments prune` | Number of recent deploys to keep (default 5; live and previous are always kept) |
| `--ssl`, `--wait`, `--force-ssl` | `domains add` | Issue SSL now; wait up to 10 minutes for DNS then issue it; `--no-force-ssl` keeps HTTP working |
| `--force-ssl` | `ssl` | Force HTTP→HTTPS on the system host; `--no-force-ssl` allows plain HTTP |
| `--framework` | `ci init` | Framework preset for the workflow's build steps (default: detected) |
| `--print` | `open` | Print the URL instead of opening a browser |
| `--link` | `create`, `deploy`, `show`, `ci init`, `deployments` | Link the directory to the site; `--no-link` never links |
| `--keep-storage` | `delete` | Delete the pull zone and router but keep the storage zone and its deploy files |
| `--keep-storage` | `delete` | Delete the pull zone but keep the storage zone and its deploy files |
| `--force`, `-f` | `deployments publish`, `prune`, `domains remove`, `delete` | Skip the confirmation prompts |

### `bunny sandbox`
Expand Down
Loading
Loading