Deploy to Netlify
Publish a Reed build on Netlify with netlify.toml, _headers and _redirects at the publish root, and forced redirects.
This recipe is written from Netlify’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.
Netlify serves a publish directory. It reads _headers and _redirects from that
directory, serves 404.html for paths that match no file, and redirects /about to
/about/ through its Pretty URLs setting, which is on by default. Reed’s output works with
all four.
1. Point Netlify at the build
Section titled “1. Point Netlify at the build”Put the build settings in a netlify.toml at the root of your repository:
[build] command = "reed prefetch && reed build --out dist" publish = "dist/"publish is the directory Netlify serves. Paths in netlify.toml are relative to the base
directory, which is the repository root unless you set base. Netlify’s base key is a
different thing from Reed’s base in docs.json.
The command has to find a reed CLI in Netlify’s build image. reed prefetch needs the
network and a Corepack-resolvable pnpm; see Offline builds in CI
for the Node version and cache details. If you already build in your own CI, deploy the
finished directory instead and leave command out.
2. Stage the tree if you set base
Section titled “2. Stage the tree if you set base”A Netlify site is served at the root of its domain. With base: "/docs" in docs.json,
every link Reed emits starts with /docs/, but the files sit at the root of dist/.
Publish dist/ directly and /docs/ is empty. The deploy succeeds and every page 404s.
Publish a staged directory with a real docs/ segment instead:
[build] command = "sh scripts/stage.sh" publish = "stage/"scripts/stage.sh builds, then copies the output one level down:
#!/bin/shset -eu
reed prefetchreed build --out dist
rm -rf stagemkdir -p stage/docscp -R dist/. stage/docs/
# Netlify reads _headers and _redirects from the publish directory's root.mv stage/docs/_headers stage/_headers# _redirects exists only when docs.json declares redirects.if [ -f stage/docs/_redirects ]; then mv stage/docs/_redirects stage/_redirects; ficp -R dist/. stage/docs/ copies the contents of dist/, not the directory. Rebuild
stage/ from empty on every run so removed pages do not linger.
The general rule, for every host, is on Deploy.
3. _headers belongs at the publish root
Section titled “3. _headers belongs at the publish root”Reed writes this _headers file into every build:
Content-Type: text/markdown; charset=utf-8
/llms.txt Content-Type: text/plain; charset=utf-8
/*/llms.txt Content-Type: text/plain; charset=utf-8Netlify applies _headers from the root of the publish directory. Its rules match served
URL paths, which is why the staging script moves the file out of stage/docs/. On a site
without base, it is already in the right place in dist/.
On a based site, /*/llms.txt is the rule that matches /docs/llms.txt. The bare
/llms.txt rule only covers a site at the root.
You can also write headers in netlify.toml as [[headers]] tables. You do not need to
here. Reed regenerates _headers on every build, and a hand-copied rule in
netlify.toml is one more thing to keep in step. What each rule protects is on
Response headers.
Netlify’s documentation describes * in header paths as matching any character, but it
also says wildcards work “inside of a path segment”. It does not say whether /*.md
matches a .md file two directories deep. Check a nested mirror, not only a top-level
one. The command is in the last section.
4. The 404 page on a based site
Section titled “4. The 404 page on a based site”Netlify serves a 404.html automatically for paths that match no file. On a based site
Reed’s 404 page is at stage/docs/404.html, not at the root. Tell Netlify to use it for
unknown paths under the mount:
[[redirects]] from = "/docs/*" to = "/docs/404.html" status = 404This does not hide real pages. By default Netlify does not apply a redirect when a file exists at the requested path, so it only fires for paths that would 404 anyway.
Real redirects need a forced rule
Section titled “Real redirects need a forced rule”When docs.json declares redirects, reed build writes two things for each entry: a
line in _redirects, with the base prefix already applied, and a small meta-refresh
page at the old path. See Redirects for what is emitted.
On Netlify the page wins. Netlify skips a redirect rule when a file exists at the source
path, which it calls shadowing, and Reed’s meta-refresh page is that file. Reed’s
_redirects lines carry no ! force flag, because Cloudflare reads the same file and
drops any line whose status is not a plain number. So out of the box, Netlify serves the
meta-refresh page with a 200, whatever permanent says.
For a real 301, add a forced rule in netlify.toml:
[[redirects]] from = "/docs/old-guide" to = "/docs/guides/getting-started/" status = 301 force = trueforce = true makes the rule win over the file at that path. Keep it in netlify.toml
rather than adding ! to the generated _redirects: reed build rewrites that file on
every run, and Cloudflare would reject the edited line.
Other rules from Netlify’s documentation that affect this:
- The default status is 301 if you leave it out.
- Rules in
_redirectsrun before rules innetlify.toml. Within a file, the first matching rule wins. - Netlify matches paths with and without a trailing slash as the same path. Do not write a rule that only adds or removes a slash. It loops.
- Write the base prefix yourself. In
docs.jsonReed adds it for you; innetlify.tomlnothing does.
Check each forced rule after the first deploy, with the command in the next section. If the old path still answers with a 200, the meta-refresh page is being served.
Verify after the first deploy
Section titled “Verify after the first deploy”Check that a nested Markdown mirror has the right content type:
curl -s -o /dev/null -w "%{content_type}\n" https://example.netlify.app/docs/ship/deploy.mdExpect text/markdown; charset=utf-8. If you get anything else, _headers is not at the
publish root, or the /*.md rule is not matching nested paths. In the second case, add a
rule per directory depth, or use a [[headers]] table and test again.
Check that a nested page resolves:
curl -s -o /dev/null -w "%{http_code}\n" https://example.netlify.app/docs/ship/deploy/Expect 200. A 404 on a based site means publish points at dist/ instead of stage/.
If you added forced redirect rules, check one:
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.netlify.app/docs/old-guideExpect 301 and the destination URL. A 200 means the meta-refresh page is still
winning: the rule is missing force = true, or its from lacks the base prefix.
Also request an unknown URL under /docs/. You should get a 404 status and Reed’s page.
Then grep a live page for a string from this build, as Deploy
describes.
Related
Section titled “Related”- Cloudflare Pages reads the same two files, but its
redirects win over existing files without a
!. - Vercel for a host that does not read
_headers. - site and base for the two link-prefix rules.
- What you actually own for why any static host works.
- llms.txt for what the
text/plainrule protects.