Skip to content

Redirects

What a redirect entry compiles to, the _redirects file, and which hosts serve a real 301 or 302.

json
{
"redirects": [
{ "source": "/old-guide", "destination": "/guides/getting-started" },
{ "source": "/legacy", "destination": "https://example.com/elsewhere", "permanent": false }
]
}
sourcestringrequired

Site-root-relative path, without the base prefix — Reed adds it.

destinationstringrequired

Site-root-relative path, or an absolute http(s) URL.

permanentbooleandefault: true

true writes a 301, false a 302. The status is real on hosts that apply the _redirects file; see below.

reed build writes two things for each entry.

A _redirects file at the root of the output, next to _headers. It has one line per entry: the source, the destination and the status, with no trailing !. Both columns carry the base prefix when they are root-relative; an absolute destination is written as-is. With base: "/docs", the example above becomes:

txt
/docs/old-guide /docs/guides/getting-started 301
/docs/legacy https://example.com/elsewhere 302

The file is written whether or not site is set. A project with no redirects entries gets no _redirects file. The redirect Reed adds for the site root, when no page lives at /, is not written to it.

A meta-refresh page at the source path, as before. It carries a <meta http-equiv="refresh"> to the destination, a noindex robots tag, a canonical link to the destination and a visible link for anyone whose browser does not follow the refresh. It is the fallback for hosts that do not read _redirects.

What a reader gets depends on the host:

Host Result
Cloudflare Pages, Cloudflare Workers static assets A real 301 or 302. Cloudflare applies _redirects rules before serving a static file at the same path.
Netlify The meta-refresh page, served with a 200. Netlify lets a static file at the same path win over a _redirects rule. Add a forced rule in netlify.toml for a real status.
A host that does not read _redirects The meta-refresh page, served with a 200.

On Cloudflare, permanent now changes what readers and crawlers receive. Think before accepting the default true: a 301 is cached aggressively by browsers and intermediaries, and undoing one in the field is genuinely hard. Use false while a move is still provisional. On other hosts, true and false produce the same meta-refresh page. If you need a real status there, add the rule at the host or CDN.

Two entries whose source normalizes to the same path is a schema error. Previously this was a silent last-one-wins, which is the kind of behaviour that makes a redirect table impossible to reason about as it grows. Now the config simply refuses to build.

A source must not shadow a page or a generated file

Section titled “A source must not shadow a page or a generated file”

A source that equals the route of a page in navigation is an error. So is a source that equals the path of a file the build writes: /llms.txt, /_headers, /_redirects and every page’s .md mirror, plus /robots.txt, /sitemap-index.xml and /sitemap-0.xml when site is set. reed check and reed build both report it as redirect-source-conflict, naming the source and what it collides with, and the build writes nothing:

txt
✗ [redirect-source-conflict] docs.json redirects entry source "/guides/intro" collides with the route of navigation page "guides/intro"; a redirect source must not shadow an existing page

A trailing slash makes no difference: /guides/intro/ collides the same way. To redirect away from a page, remove it from navigation and the content directory first.

source and destination follow the docs.json rule: root-relative values are base-prefixed by Reed. So on a site with base: "/docs", a source of /old-guide matches the served URL /docs/old-guide.

This is the opposite of what you write in page prose, where you supply the prefix yourself. If you find yourself writing /docs/... in a redirect, you have almost certainly produced /docs/docs/....

There is no pattern matching, no wildcard, and no capture-group substitution — each entry is one exact path to one destination. Moving a whole subtree means one entry per page.

That is a real limitation. When the list gets long enough to be painful, the alternative is a rule at your host or CDN layer, which is outside Reed and stays outside it.