---
title: Fix a whole site that 404s after a successful deploy
description: With base set, files sit at the root of dist/ but every URL starts with the base. Serve them from a matching directory.
---

## Symptom

The deploy command exits zero and reports success. Every page on the live site returns
404 — the home page, every docs page, the CSS. Typically:

- `docs.json` sets `base`, for example `"base": "/docs"`;
- the host's asset or publish directory points straight at your `--out` directory, such
  as `dist/`;
- `https://example.com/docs/` is a 404, while `https://example.com/index.html` (no
  `/docs`) loads an unstyled page whose links all 404.

If a local static server pointed at `dist/` shows the same thing, you are looking at this
problem, not a CDN or DNS issue.

## Cause

`base` changes the URLs inside the HTML. **It does not move files.** A build with
`base: "/docs"` emits `dist/index.html` with every internal link and asset reference
prefixed `/docs/...`, but `dist/index.html` itself sits at the root of `dist/`.

Most static hosts serve their asset directory's file tree verbatim as URL paths:
`<directory>/a/b.html` is served at `/a/b.html`. Nothing adds or strips a path segment.
Point that directory at `dist/` and the tree contains no `docs/` segment at all, so
`/docs/` maps to nothing. The whole site 404s while the deploy reports success — the host
uploaded every file correctly, just at the wrong paths.

Routing does not rescue you. On a Cloudflare Worker, for example, `routes` decides which
requests reach the Worker; it never rewrites the path.

## Fix

Make the mount path physically exist in the tree you serve. Build to `dist/`, then stage
it one level down, inside a directory named after your `base`:

```txt
stage/
  docs/          <- the contents of dist/
    index.html
    404.html
    llms.txt
  _headers       <- stays at the root
```

Then point the host at `stage/`, not `dist/`.

### A staging script

This is the shape of the script Reed's own documentation uses (`scripts/build.sh`):

```sh
set -euo pipefail
MOUNT_SEGMENT="docs"   # must match docs.json's base

rm -rf dist
reed build --out dist
test -f dist/index.html

rm -rf stage
mkdir -p "stage/$MOUNT_SEGMENT"
cp -R dist/. "stage/$MOUNT_SEGMENT/"
mv "stage/$MOUNT_SEGMENT/_headers" stage/_headers
```

Three details matter:

- `dist/.` as the copy source copies the **contents** of `dist/`, not a `dist` folder.
- `stage/` is deleted and rebuilt on every run, so a page removed from a later build
  cannot survive as a stale file.
- `_headers` goes to the **root** of `stage/`, not inside `docs/`. Hosts read it only at
  the top of the served directory, and its rules match served URL paths — see
  [Response headers](/docs/ship/headers). Move it rather than copy it: a copy left in
  `docs/` is served as a public file at `/docs/_headers`.

### Point the host at the staged tree

For a Cloudflare static-assets Worker, set the asset directory in `wrangler.jsonc`:

```jsonc
{
  "assets": {
    "directory": "./stage",
    "html_handling": "auto-trailing-slash",
    "not_found_handling": "404-page"
  }
}
```

`not_found_handling: "404-page"` is worth setting while you are here: the default returns
a bare 404 with an empty body instead of serving the `404.html` Reed builds.

For other hosts, set the equivalent publish or output directory to `stage` — or, for
nginx, put the files in a `docs/` directory under the server root. Keep the segment in
sync in three places: `base` in `docs.json`, the staging directory name, and any route
pattern your host uses.

<Warning>
  Deploying a stale or absent `stage/` is the other way to ship nothing. Run the build
  script immediately before every deploy, not once.
</Warning>

## Verify

Check the tree before you upload it:

```sh
test -f stage/docs/index.html && test -f stage/_headers && echo "layout ok"
```

After deploying, fetch the live site and check for a string from this build, not just the
status code:

```sh
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/docs/
curl -s https://example.com/docs/ | grep -c "a phrase you just added"
```

Expect `200` and a count above zero. A deploy command exiting zero is not evidence that
the site is being served.

## Related

- [Deploy](/docs/ship/deploy) — mounting under a path
- [site and base](/docs/site/addressing) — what `base` changes and what it does not
- [Build output](/docs/ship/output) — the tree `reed build` writes
- [Fix links that work in reed dev but 404 in production](/docs/troubleshooting/links-404-in-production)
