> ## Documentation Index
> Fetch the complete documentation index at: https://bunnynet-cb9733c2-docs-edge-scripting-astro-guide.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy an Astro Site on Edge Scripting

> Deploy Astro to bunny.net with one command. Server-side rendering runs on Edge Scripting, and the build's files come from Bunny Storage.

[Astro](https://astro.build/) is a server-first framework. It renders HTML on the server, and it ships only the JavaScript a page really needs.

The [`@bunny.net/astro-adapter`](https://github.com/BunnyWay/bunny-adapters/tree/main/packages/astro) package runs that server inside an Edge Script. Astro renders pages and API routes per request, on a node near your visitor. The build's static files live in [Bunny Storage](/storage), and the script serves them from there.

One command does all of it:

```bash theme={null}
bunny sites deploy
```

<Card title="See it running" icon="rocket" href="https://astro-ssr-demo.bunny.run">
  A live Astro site on Edge Scripting. Every page demonstrates one adapter
  capability, and the [source is on
  GitHub](https://github.com/BunnyWay/bunny-adapters/tree/main/examples/astro-showcase).
</Card>

## Deploy your first site

<Steps>
  <Step title="Install the CLI and log in">
    ```bash theme={null}
    curl -fsSL https://cli.bunny.net/install.sh | sh
    bunny login
    ```

    See [installation](/cli/installation) for npm, Bun, and Windows.
  </Step>

  <Step title="Run bunny sites deploy">
    From your Astro project:

    ```bash theme={null}
    bunny sites deploy
    ```

    The CLI reads your project. A project that renders a page on demand is offered the adapter, and the command then builds the site, creates what the site needs, and publishes it:

    ```
    ℹ This Astro project renders on demand: src/pages/api/time.ts sets prerender = false.
    ? Add @bunny.net/astro-adapter to astro.config.mjs? › (Y/n)
    ✓ Added the adapter to astro.config.mjs.

    ℹ Running build: npm run build
    …
    [@bunny.net/astro-adapter] Bundled to dist/index.js (653 kB, limit 10.00 MB). 3 route(s) render per request.
    [@bunny.net/astro-adapter] Wrote .bunny/build.json. Deploy it with: bunny sites deploy

    ? Site name: › my-site
    ✓ Created site "my-site".
      storage zone   sites-my-site-k3f9wq
      edge script    my-site-k3f9wq-server
      pull zone      sites-my-site-k3f9wq.b-cdn.net

    ✓ Deployed a1b2c3d4: 42 files (1.9 MB), script 653 KB.
    ℹ Production  https://sites-my-site-k3f9wq.b-cdn.net
    ```

    Open the URL. Reload it, and anything the page renders per request changes each time.
  </Step>

  <Step title="Add your domain">
    The first deploy offers one. To add it later:

    ```bash theme={null}
    bunny sites domains add www.example.com
    ```

    The CLI adds the hostname, prints the `CNAME` record to create, waits for DNS, and issues a free certificate.
  </Step>
</Steps>

<Note>
  Nothing above asks for a password. The CLI creates the storage zone, so it
  already holds the credentials, and it sets them on the script as
  [secrets](/scripting/secrets).
</Note>

### What the deploy created

| Resource            | What it does                                                        |
| ------------------- | ------------------------------------------------------------------- |
| A storage zone      | Holds each deploy's files, and your sessions                        |
| A pull zone         | The site's URL, its cache, and its domains                          |
| An Edge Script      | Astro's server, published as one file                               |
| `.bunny/site.json`  | Links this directory to the site                                    |
| `.bunny/build.json` | What the build produced. The CLI reads it, and it is a build output |

The CLI also applies the pull zone settings the adapter asks for, and it says which ones it changed: cookies pass through, so `Astro.cookies.set()` works, and [Smart Cache](/cdn/smart-cache) goes off so the adapter's own cache headers count.

### When the adapter is offered

Only a project that asks for a server is offered one. Any one of these counts as asking:

| The signal                                          | What it means                                      |
| --------------------------------------------------- | -------------------------------------------------- |
| A route under `src/pages/` sets `prerender = false` | That one route renders per request                 |
| The config sets `output: "server"`                  | Every page renders per request                     |
| The config names another host's adapter             | The project renders on demand somewhere else today |
| `@bunny.net/astro-adapter` is already a dependency  | Somebody installed it and stopped                  |

A project with none of these prerenders every page. `bunny sites deploy` uploads the build, the site is served as files, and the adapter is never mentioned. That is the right answer for such a site: a site with no server should not carry a megabyte of one.

The first signal is the one that matters most. `astro build` stops with its own error when a route opts out of prerendering and no adapter is installed, so the offer arrives before that failure.

### Moving from another host

A project that already has an adapter keeps everything but that one line. `bunny sites deploy` names the adapter it found, and swaps it:

```
ℹ Astro detected, using @astrojs/cloudflare.
? Replace @astrojs/cloudflare with @bunny.net/astro-adapter in astro.config.mjs? › (Y/n)
✓ Replaced @astrojs/cloudflare with @bunny.net/astro-adapter in astro.config.mjs.
```

Adapter options do not carry over, because no two hosts share them. Anything the old adapter was configured with is worth reading again in [the options below](#adapter-options).

### In a monorepo

Run `bunny sites deploy` at the repository root, and it finds the projects below it:

```
ℹ This directory holds no Astro project, and 4 below it do.
? Which one should be deployed? › docs
```

Or run it in the project's own directory, and it deploys that one. The package manager comes from the workspace, not from the directory, so a `pnpm` monorepo gets `pnpm`.

## What the adapter does not change

The adapter adds itself to your config, and nothing else. It does not set `output`.

Since Astro 5, a project that says nothing about `output` prerenders its pages, and a page asks for the edge with `export const prerender = false`. That default is the right one, and setting `output: "server"` over it is expensive: on [astro.build](https://github.com/withastro/astro.build) it turned 4499 prerendered pages into pages that render per request, and took the script from 7.83 MB to 22.30 MB, past the limit.

So the build asks Astro what it built, and Astro answers from the routes:

| What Astro built                 | What `bunny sites deploy` sends                             |
| -------------------------------- | ----------------------------------------------------------- |
| A route that renders per request | The script, and the files it renders from                   |
| Every route prerendered          | The files alone. No script, and nothing invoked per request |

The build says which one it chose. A site can also ask for the script outright. That is what a prerendered page holding a `server:defer` component needs, because Astro reports such a project as a fully prerendered one:

```js astro.config.mjs theme={null}
adapter: bunny({ deploy: "server" }),
```

## The everyday loop

```bash theme={null}
bunny sites deploy                           # build and publish
bunny sites deployments list                 # what is live, what was live, what is kept
bunny sites deployments publish --previous   # back to the deploy that was live before
bunny sites open                             # open the live URL in the browser
```

A deploy with no changes does nothing, and says so. Every deploy publishes to production; a preview URL per branch is [not built yet](#what-is-not-here-yet).

### Rolling back

```bash theme={null}
bunny sites deployments list
bunny sites deployments publish --previous    # the previous deploy
bunny sites deployments publish a1b2c3d4     # a named one
```

Each deploy keeps both halves in the storage zone: the files, and the server bundle that was built against them. Neither is ever overwritten, so a rollback restores a matched pair in one step. It takes a few seconds to reach every edge node.

The CLI keeps the newest 5 deploys. `bunny sites deployments prune` clears the rest.

## Configure the site

`.bunny/site.json` is all a repeat deploy needs. Add a `bunny.jsonc` when you want the site's name and build command in version control:

```jsonc bunny.jsonc theme={null}
{
  "version": "2026-05-11",
  "sites": {
    "name": "my-site",
    "build": "npm run build"
  }
}
```

Every developer and every workflow then deploys the same way, with no flags.

Build-time variables come from the command line:

```bash theme={null}
bunny sites deploy --build --env PUBLIC_API=https://api.example.com
bunny sites deploy --build --env-file .env.production
```

They apply to the build, so they need `--build`. A deploy without it says so rather than ignoring them.

Runtime variables belong to the script, and `astro:env` reads them:

```bash theme={null}
bunny scripts env set STRIPE_KEY sk_live_… --secret
bunny scripts env list
bunny scripts env pull                     # into .env, for local work
```

`bunny sites show` prints the script's ID, and the commands above use the script this directory is linked to.

## Continuous integration

A deploy needs an API key and nothing else, so a workflow is three lines:

```yaml .github/workflows/deploy.yml theme={null}
- run: npm ci
- run: npm install -g @bunny.net/cli
- run: bunny sites deploy --build
  env:
    BUNNY_API_KEY: ${{ secrets.BUNNY_API_KEY }}
```

`--build` is what runs the build here. A workflow has nobody to ask, so an unattended deploy never offers one, and it deploys what the build wrote.

Commit `.bunny/site.json` so the workflow deploys the same site you do, or pass `--name <site>`. See [GitHub Actions](/scripting/github-actions) for the details.

## Run it locally

Use `npm run dev` for everyday work on the site. It is Astro's dev server, and the adapter stays out of the way.

`npm run preview` runs the file you are about to deploy. A local storage zone stands in for Bunny Storage, so assets, prerendered pages, and sessions all work with no account and no network:

```bash theme={null}
npm run build
npm run preview
```

It needs [Deno](https://deno.com/) 2, because Deno is the Edge Scripting runtime. The `cdn-` request headers only exist on the bunny.net network, so anything your code reads from them is absent locally.

A build with [no script](#a-site-with-no-script) has no file to run, so `npm run preview` serves `dist/client` from Astro's own static server. That needs no Deno.

## How it works

```
browser ──▶ pull zone ──▶ Edge Script (Astro's server)
                              │
                              ├─ a route Astro owns ── rendered now
                              │
                              └─ an asset or a prerendered page ─▶ Bunny Storage
```

The script sees every request the cache does not answer. It renders the routes Astro owns, and reads everything the build produced out of Bunny Storage. The adapter inlines the list of built files into the script, so a request for a path the build never produced costs no lookup at all.

Each deploy is immutable, and it has two halves in the same storage zone:

| Path                            | Holds                                     |
| ------------------------------- | ----------------------------------------- |
| `deploys/{id}/`                 | that deploy's client files                |
| `_bunny/deploys/{id}/server.js` | that deploy's server bundle. Never served |

The CLI writes the deploy's own folder name into the top of the bundle it publishes. So a published version can only read the files it was built with, which is what makes a rollback restore a page and its assets together. An Astro server bundle names the hashed CSS file it renders, so the two really are one unit.

### What runs where

| Request                                     | Handled by                        |
| ------------------------------------------- | --------------------------------- |
| A server-rendered page                      | Astro, inside the script          |
| A route in `src/pages/api/`                 | Astro, inside the script          |
| `src/middleware.ts`                         | Astro, on every matched route     |
| A page with `export const prerender = true` | Bunny Storage, read by the script |
| Hashed assets in `_astro/`                  | Bunny Storage, read by the script |
| An unknown path                             | Your prerendered 404 page         |

## The edge context

Every page gets `Astro.locals.runtime`, which carries what the bunny.net network knows about the request:

```ts src/middleware.ts theme={null}
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware((context, next) => {
  // Middleware also runs while Astro prerenders, where there is no request.
  if (context.isPrerendered) return next();

  context.locals.country = context.locals.runtime.country ?? "unknown";
  return next();
});
```

| Value           | What it is                                                                              |
| --------------- | --------------------------------------------------------------------------------------- |
| `country`       | The visitor's country, from `cdn-requestcountrycode`                                    |
| `requestId`     | The bunny.net request id. Quote it in a support ticket                                  |
| `clientAddress` | The visitor's IP. Also `Astro.clientAddress`                                            |
| `waitUntil`     | Keep working after the response goes out. See [waitUntil](/scripting/runtime#waituntil) |
| `caches`        | The edge [Cache API](/scripting/cache)                                                  |
| `env`           | Read a script environment variable                                                      |

Add the types to your project:

```ts src/env.d.ts theme={null}
type BunnyRuntime = import("@bunny.net/astro-adapter").BunnyRuntime;

declare namespace App {
  interface Locals {
    runtime: BunnyRuntime;
  }
}
```

## Caching

Astro's `routeRules` become the headers the CDN reads:

```js astro.config.mjs theme={null}
export default defineConfig({
  adapter: bunny(),
  routeRules: {
    "/products/[...slug]": { maxAge: 3600, swr: 600, tags: ["products"] },
  },
});
```

```
Cache-Control: public, max-age=0, must-revalidate, s-maxage=3600, stale-while-revalidate=600
CDN-Tag: products,astro-path:/products/socks
```

`s-maxage` is for the CDN and `max-age` is for the browser. Splitting them is what makes a purge take effect straight away.

A page that sets no `Cache-Control` gets `private, no-store`. That matters: a pull zone applies its own expiration to a response that carries no directive, and a page rendered for one visitor must never reach another. A route with a `routeRules` entry keeps what the rule gave it.

<Note>
  [Smart Cache](/cdn/smart-cache) only caches known static file extensions, and
  HTML is not one of them, so a `routeRules` entry does nothing while it is on.
  `bunny sites deploy` turns it off for you when your project sets `routeRules`, and
  reports the change.
</Note>

### Purging

```ts theme={null}
await Astro.cache.invalidate({ tags: ["products"] });
await Astro.cache.invalidate({ path: "/products/socks" });
```

That calls the [purge API](/cdn/purge-cache), which is an account operation. The CLI sets the pull zone id for you. The key is yours to add, because it is account-wide:

```bash theme={null}
bunny scripts env set BUNNY_API_KEY <key> --secret
```

## Sessions

`Astro.session` works with no setup. Each session is one object in the site's storage zone, so every edge node reads the same value, and nothing serves those objects to the web.

```astro theme={null}
---
const cart = (await Astro.session.get("cart")) ?? [];
---
```

<Note>
  Bunny Storage does not expire an object, so `session.ttl` controls the cookie
  and not the stored object. Delete old objects yourself if the zone grows.
</Note>

Pass `sessions: false` to configure your own driver.

## Images

Set `imageService: "bunny"` and Astro's `<Image>` component uses [Bunny Optimizer](/optimizer), which resizes and re-encodes at the edge. There is no build step, and no `sharp`:

```js astro.config.mjs theme={null}
bunny({
  imageService: "bunny",
  image: {
    widths: [360, 720, 1080], // used to build a srcset
    quality: 82,
  },
});
```

<Warning>
  **Image transformation on a script-backed site is not ready yet.** Turn
  Optimizer on today and an image request that misses the cache can answer `523
      Origin Connection Failed`. We are fixing it. Until then, leave `imageService`
  at its default: the parameters are ignored, and the original image is served.
</Warning>

Optimizer only works on files your own pull zone serves, so an image on another host passes through untouched.

## Large files

A stored object can be fetched in pieces. The script answers a `Range` request with `206` and a `Content-Range`, and it says `Accept-Ranges: bytes` on everything it serves out of storage.

That header is the part that matters. A pull zone will not answer a range from its cache, and will not slice an object, unless the origin says it accepts ranges. Without it a video is only seekable once it is fully cached.

For a large file that nobody has requested yet, turn on **Optimize for large object delivery** in the pull zone's caching settings, and the zone fetches the object in chunks so the first request is seekable too:

```bash theme={null}
bunny api POST /pullzone/<pull-zone-id> --body '{"EnableCacheSlice": true}'
```

The script also passes `If-None-Match` and `If-Modified-Since` through, so a browser and the pull zone both revalidate with a `304` instead of downloading the object again. See [range requests](/cdn/frequently-asked-questions/range-requests) for what the CDN does with them.

## Supported features

| Astro feature                                                | Supported                                       |
| ------------------------------------------------------------ | ----------------------------------------------- |
| Server-rendered pages                                        | Yes                                             |
| Server endpoints and dynamic routes                          | Yes                                             |
| Middleware                                                   | Yes                                             |
| `Astro.request`, `Astro.locals`, `Astro.url`, `Astro.params` | Yes                                             |
| `Astro.clientAddress`                                        | Yes, from `x-forwarded-for`                     |
| `Astro.cookies`, including `set()`                           | Yes                                             |
| `Astro.session`                                              | Yes, in the site's storage zone                 |
| Prerendered pages, including `404.astro` and `500.astro`     | Yes, served from Storage                        |
| A site under a path, with `base`                             | Yes                                             |
| `redirects`, internal and external                           | Yes, with the status the route asks for         |
| Locale routing with `i18n`                                   | Yes                                             |
| Content collections                                          | Yes                                             |
| Framework islands and `server:defer`                         | Yes                                             |
| Astro actions                                                | Yes                                             |
| Static page headers, such as a CSP                           | Yes. In the script, or in `_headers`            |
| `routeRules` and cache purging                               | Yes                                             |
| Image transformation                                         | Not yet. See [Images](#images)                  |
| Range requests on a stored object                            | Yes, and conditional requests too               |
| `node:fs` and the other built-ins Deno provides              | Yes. The file system is virtual and per isolate |
| `astro:env` secrets                                          | Yes, from the script's environment variables    |
| `astro preview`                                              | Yes. With Deno for a build that has a script    |
| The `sharp` image service                                    | No. Native binaries cannot run on the edge      |

## Node built-ins

Edge Scripting provides most `node:` modules, so a dependency that imports one is usually fine.

[`node:fs`](/scripting/node-fs) works over a virtual file system. It starts empty on every cold start, one isolate cannot see what another wrote, and what it holds counts against the script's memory. So it is a scratch pad for one request, and never a store. Anything that has to outlive a request belongs in Bunny Storage, which is where the adapter keeps files and sessions.

A package with a native binary never works. `sharp` is the usual one, and the adapter already replaces it.

## Adapter options

Every option has a sensible default, so `bunny()` on its own is usually right.

```js astro.config.mjs theme={null}
bunny({
  // Caching
  assetCacheControl: "public, max-age=31536000, immutable",
  pageCacheControl: "public, max-age=60",
  serverCacheControl: "private, no-store", // for a page that sets none itself

  // Features
  imageService: "noop", // "bunny" for Optimizer, false to keep your own
  sessions: true, // false to configure your own driver
  cache: true, // false to configure your own cache provider

  // Build
  outfile: "dist/index.js",
  bundle: true, // false to run your own bundler
  assetManifest: true, // false to look up every path in Storage instead
  external: [], // modules esbuild must not bundle
  sourcemap: false,
});
```

No credential is ever an option. The CLI sets each one on the script, so nothing reaches the bundle or your configuration file.

## Moving from `bunny-astro`

Earlier versions of the adapter shipped a `bunny-astro` command. `bunny sites deploy` replaces it, and adds provisioning, immutable deploys, and rollback.

```bash theme={null}
bunny sites deploy
```

That creates a **new** site: its own storage zone, script, and pull zone, and its own URL. Adopting the zone and script you already have is [not built yet](#what-is-not-here-yet), so until it is, either move to the new URL (add your domain to it with `bunny sites domains add`) or keep deploying the old pair by hand:

```bash theme={null}
npm run build
bunny storage files upload dist/client --zone my-site-assets   # or your own uploader
bunny scripts deploy dist/index.js
```

A deploy you run yourself puts the files at the zone root, which is where the script looks when nothing sets `BUNNY_ASSET_PREFIX`.

## A site with no script

A site whose routes are all prerendered needs no script, and no adapter. `bunny sites deploy` uploads the build, every deploy gets its own immutable URL, and nothing is invoked per request. The build says so:

```
[@bunny.net/astro-adapter] Every route is prerendered, so this build deploys no
script: `bunny sites deploy` uploads dist/client, and the site is served as files.
```

Files alone cannot answer a 404 with a page, send a redirect, or add a header. The [`bunny sites` router](/cli/commands/sites#what-the-deploy-configures) does all three, for every framework. It learns what to do from three file names, which Cloudflare Pages and Netlify read too:

| The router reads | Where it comes from                                                       |
| ---------------- | ------------------------------------------------------------------------- |
| `404.html`       | Your own `404.astro`                                                      |
| `_redirects`     | Written by the build, from your `redirects` config                        |
| `_headers`       | Written by the build: your CSP, your `routeRules`, and the asset lifetime |

So keeping the adapter on a prerendered site is worth it for those two files. It costs nothing at run time.

Astro also writes its own `<meta http-equiv="refresh">` page for each redirect. A deploy that never reaches the router therefore still sends the visitor on. The rule in `_redirects` carries `!`, and that is what makes the router's real `301` win over the page.

`npm run preview` on such a build serves `dist/client` from Astro's own static server, and needs no Deno.

A site with no adapter at all works too, and [the static site hosting guide](/storage/static-site-hosting) covers it. What it loses is those two files: a redirect becomes a `<meta>` refresh, and a header cannot be set.

## What is not here yet

Three things this guide's commands do not cover:

* **A preview URL per branch.** A build that renders per request publishes to production, and the command says so. Such a preview has to be its own Edge Script, because one script publishes one version at a time, and a page and its assets are one unit. Until then, deploy a second site for staging: `bunny sites deploy --name my-site-staging` from a checkout of the branch. A build with every route prerendered is a static deploy, and every one of those already gets [its own preview URL](/cli/commands/sites#immutable-deploys).
* **Adopting a site you built by hand.** `bunny sites deploy` creates its own storage zone, script, and pull zone. There is no command yet that takes over a pair you made yourself.
* **Logs from the command line.** Read them in the dashboard, under **Scripting** > your script > **Logs**.

The design for all three is in [`plans/one-command-deploys.md`](https://github.com/BunnyWay/bunny-adapters/blob/main/plans/one-command-deploys.md).

## Troubleshooting

<AccordionGroup>
  <Accordion title="The site answers 400 with an empty body">
    The script did not start, so nothing answered the request. A large script is
    the usual reason, and 10 MB is not the size to aim at: a script has
    [500ms to start](/scripting/limits), and every byte of it is parsed and
    evaluated first.

    Measured in August 2026, on a standalone script in DE: the same code served
    every request at 7.44 MB, and answered 400 to all of them at 7.83 MB. Around
    7.5 MB the first request failed and later ones worked. How much code fits
    depends on what it does as it loads, so treat 7.5 MB as a warning and not a
    line.

    The build warns above 7.5 MB, and `bunny sites deploy` asks the site for a page
    before it reports success, so neither is silent about it. What to do is the
    same as for a script that is too large, below. `bunny sites deployments
            publish --previous` puts the
    previous deploy back while you work on it.
  </Accordion>

  <Accordion title="The build says the script is too large">
    Edge Scripting takes one file of up to 10 MB, so a build that produced more
    than that fails, and names the packages that filled it:

    ```
    dist/index.js is 22.30 MB, and Edge Scripting takes 10.00 MB.

    The largest parts of it are:
      6.41 MB  shiki
      2.20 MB  @astrojs/mdx
      1.12 MB  astro
    ```

    Two things usually help. Prerender the routes that do not need a server, so
    that the pages which pulled a package in stop being part of the script. And
    keep a heavy dependency out of a page, and out of anything a page imports: a
    package that only runs at build time does not belong in the script.

    A small Astro site lands near 660 kB. Aim below 7.5 MB, not below 10 MB: see
    the 400 above.
  </Accordion>

  <Accordion title="The script is too slow to start">
    A script has 500 ms to start. Move work out of module scope and into the
    request handler, and drop dependencies you do not need. See the
    [limits](/scripting/limits).
  </Accordion>

  <Accordion title="Astro.rewrite('/404') throws a 500">
    A prerendered route has no server component to rewrite to. Return
    `new Response(null, { status: 404 })` from the page instead, and your
    prerendered 404 page is served out of Bunny Storage.

    An endpoint in `src/pages/api/` keeps its own 404, so a client asking for
    JSON is never handed a web page.
  </Accordion>

  <Accordion title="A routeRules entry changes nothing">
    Smart Cache is on, and it does not cache HTML. `bunny sites deploy` turns it off
    and says so, so run the deploy again and read what it reports.
  </Accordion>

  <Accordion title="Astro.cookies.set() has no effect">
    The pull zone is stripping the header. `bunny sites deploy` turns **Disable
    cookies** off for you. Run `bunny sites deploy` again, and check the settings it
    reports.
  </Accordion>

  <Accordion title="A page shows another visitor's content">
    Something removed the `Cache-Control` the adapter sets. Check that no edge
    rule overrides it, and that the page is not setting a `public` directive
    itself.
  </Accordion>

  <Accordion title="A POST request returns 403">
    Astro checks the request origin for server output, and rejects a form POST
    whose `Origin` header does not match the site. This is Astro's CSRF
    protection, not an Edge Scripting error. Send the request from the site
    itself, or change `security.checkOrigin` in `astro.config.mjs`.
  </Accordion>

  <Accordion title="The bundle fails on a Node-only module">
    Most `node:` built-ins work here, including
    [`node:fs`](/scripting/node-fs), so check which module it is. The adapter
    rewrites a bare `fs` to `node:fs` for you, because the runtime only answers
    to the prefixed name.

    A package with a native binary never works. Replace that dependency, or
    keep it out of the server build.
  </Accordion>

  <Accordion title="A missing path shows bunny.net's error page, not mine">
    The site's router is older than the CLI that reads your `404.html`. Update
    it, and deploy again:

    ```bash theme={null}
    bunny sites upgrade-router
    bunny sites deploy
    ```

    A deploy checks this for you now: it asks the live site for a path the build
    cannot hold, and warns when the answer was not your own page.
  </Accordion>

  <Accordion title="A page 404s after a deploy, and the old one worked">
    Roll back while you look: `bunny sites deployments publish --previous`.
    Each deploy keeps its own files
    and its own bundle, so the previous version comes back whole.
  </Accordion>

  <Accordion title="Images return 523">
    Optimizer cannot transform images for a script-backed site yet. Leave
    `imageService` at its default. See [Images](#images).
  </Accordion>
</AccordionGroup>
