Skip to content

Deploy to Vercel

Serve a Reed build on Vercel, with the _headers rules rewritten into vercel.json and trailing slashes kept.

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.

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.

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 for what reed prefetch needs.

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.

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 and llms.txt.

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.

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.

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. 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.

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 describes.