---
title: Deploy to Vercel
description: Serve a Reed build on Vercel, with the _headers rules rewritten into vercel.json and trailing slashes kept.
---

<Note>
This recipe is written from Vercel's documentation and Reed's output layout. It has not
been run as a live deploy. After your first deploy, check two things: a `.md` mirror comes
back as `text/markdown`, and a nested page URL such as `/docs/ship/deploy/` resolves. The
commands are in the last section.
</Note>

Vercel serves a static output directory well, but it does not read `_headers`. The one
file Reed writes for your host is ignored here, so you restate its rules in `vercel.json`.
Everything else is choosing the output directory and the trailing-slash behaviour.

## 1. Point Vercel at the build

Choose **Other** as the framework preset, then set the build command and output directory.
You can do this in the project settings or in `vercel.json` at the repository root:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "buildCommand": "reed prefetch && reed build --out dist",
  "outputDirectory": "dist"
}
```

Only the contents of the output directory are served. The build command has to find a
`reed` CLI in Vercel's build image. See [Offline builds in CI](/docs/ship/ci-offline) for
what `reed prefetch` needs.

## 2. Stage the tree if you set `base`

Vercel serves the output directory at the root of the deployment's domain. With
`base: "/docs"`, every link Reed emits starts with `/docs/`, but the files sit at the root
of `dist/`. Serve `dist/` as it is and `/docs/` is empty. The deploy succeeds and every
page 404s.

Serve a staged directory with a real `docs/` segment:

```sh
set -eu

reed prefetch
reed build --out dist

rm -rf stage
mkdir -p stage/docs
cp -R dist/. stage/docs/

# Vercel does not read _headers. Drop it so it is not served as a file.
rm stage/docs/_headers
```

Save that as `scripts/stage.sh` and point the build at `stage/`:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "buildCommand": "sh scripts/stage.sh",
  "outputDirectory": "stage"
}
```

The general rule is on [Deploy](/docs/ship/deploy).

## 3. Translate `_headers` into `vercel.json`

Reed's `_headers` file says two things. `.md` mirrors are `text/markdown; charset=utf-8`,
and `llms.txt` is `text/plain; charset=utf-8`. Vercel needs the same rules as `headers`
entries. Each entry has a `source` pattern and a list of `key`/`value` pairs:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "outputDirectory": "stage",
  "trailingSlash": true,
  "headers": [
    {
      "source": "/(.*)\\.md",
      "headers": [
        { "key": "Content-Type", "value": "text/markdown; charset=utf-8" }
      ]
    },
    {
      "source": "/docs/llms.txt",
      "headers": [
        { "key": "Content-Type", "value": "text/plain; charset=utf-8" }
      ]
    }
  ]
}
```

Notes on the translation:

- **`source` is a path pattern, not a `_headers` splat.** `/(.*)\\.md` is a regular
  expression group. The doubled backslash is JSON escaping for a literal dot. It matches a
  `.md` path at any depth.
- **`llms.txt` is an exact path, so write the one you serve.** On a based site that is
  `/docs/llms.txt`. On a site at the root, use `/llms.txt`. Reed's `_headers` covers both
  cases with two rules. In `vercel.json` you know which one applies.
- **Nothing reads the `_headers` file on Vercel.** Keeping it only publishes it as a file,
  which is why the staging script deletes it.

Skip this step and the site still works. What you lose is the machine-readable half: the
mirrors and `llms.txt` arrive without the content type assistants rely on. See
[Response headers](/docs/ship/headers) and [llms.txt](/docs/derived/llms-txt).

## 4. Trailing slashes and `cleanUrls`

Reed writes one directory per page, `ship/deploy/index.html`, and emits its page links and
canonical URLs with a trailing slash: `/docs/ship/deploy/`. Footer links from `docs.json`
are emitted as written, without an added slash, and reach the page through the redirect
below. Match that on Vercel:

- **`trailingSlash: true`**, as in the example above. `/docs/ship/deploy` gets a 308 to
  `/docs/ship/deploy/`. Paths with a file extension are left alone, so `deploy.md`,
  `llms.txt` and everything in `_astro/` are served as they are.
- **Do not use `trailingSlash: false`.** It redirects `/docs/ship/deploy/` to
  `/docs/ship/deploy`, the opposite of every URL in the build, the sitemap and the
  canonical tags. Every internal click then costs a redirect.
- **Do not leave it unset.** Vercel then serves both forms without redirecting, and
  documents that this invites duplicate-content indexing.

**`cleanUrls` does not help here.** It strips `.html` from file names and redirects
`/about.html` to `/about`. Apart from `404.html`, Reed does not emit `page.html` files. Each page is already an
`index.html` inside its own directory, so there is nothing for `cleanUrls` to clean. Leave
it at its default of `false`.

## 5. Deploy

With Git integration, Vercel runs the build command on every push. To deploy from your own
machine or CI with the Vercel CLI:

```sh
vercel --prod
```

Run it from the project root. Vercel's first deployment of a new project is a production
deployment even without `--prod`.

## Redirects

Reed's `redirects` in `docs.json` compile to meta-refresh HTML pages and a `_redirects`
file. Vercel does not read `_redirects`, so it serves the meta-refresh pages with a 200.
See [Redirects](/docs/site/redirects). For a real status code, add a
`redirects` entry to `vercel.json`:

```json
{
  "redirects": [
    { "source": "/docs/old-guide", "destination": "/docs/guides/getting-started/", "permanent": true }
  ]
}
```

On Vercel, `permanent: true` sends 308 and `false` sends 307. Use `statusCode` if you need
exactly 301. Write the base prefix yourself. `vercel.json` sees served paths, unlike
`docs.json`, where Reed adds the prefix for you.

## Verify after the first deploy

Check that a nested Markdown mirror has the right content type:

```sh
curl -s -o /dev/null -w "%{content_type}\n" https://example.vercel.app/docs/ship/deploy.md
```

Expect `text/markdown; charset=utf-8`. Anything else means the `headers` entry did not
match. Check the `source` pattern and its escaping first.

Check that a nested page resolves, and that the slashless form redirects:

```sh
curl -s -o /dev/null -w "%{http_code}\n" https://example.vercel.app/docs/ship/deploy/
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.vercel.app/docs/ship/deploy
```

Expect `200`, then `308` to the trailing-slash URL. A 404 on the first request, on a
based site, means the output directory is `dist` rather than `stage`.

Request an unknown URL under `/docs/` too, and look at what comes back. Reed's page is at
`docs/404.html` on a based site. If Vercel shows its own 404 page instead, copy
`docs/404.html` to the root of `stage/` and test again. Then grep a live page for a string
from this build, as [Deploy](/docs/ship/deploy) describes.

## Related

- [Netlify](/docs/ship/hosts/netlify) and [Cloudflare Pages](/docs/ship/hosts/cloudflare-pages)
  read Reed's `_headers` file directly.
- [nginx](/docs/ship/hosts/nginx) for another host that needs the rules translated.
- [site and base](/docs/site/addressing) for what `base` changes.
- [Build output](/docs/ship/output) for every file in the tree you are serving.
