Deploy to Cloudflare Workers
Serve a Reed build as Workers static assets, mounted at a subpath, with _headers and _redirects in the right place.
A Worker with static assets and no script is a file server at the edge. You point it at a
directory, and it serves that directory’s tree as URL paths. This is how this site is
deployed at reedbuild.dev/docs, and every setting below comes from that deployment.
The recipe assumes base: "/docs". For a site at the root of its own hostname, skip the
staging step and point the asset directory at dist/.
How the Worker sees your files
Section titled “How the Worker sees your files”A static-assets Worker serves <directory>/a/b.html at /a/b.html. Nothing rewrites the
path. routes decides which requests reach the Worker; it never adds or strips a path
segment.
That matters because base changes the URLs inside the HTML, not where the files sit.
With base: "/docs", dist/index.html links to /docs/_astro/..., but the file itself
is at the root of dist/. Point the Worker at dist/ and /docs/ maps to nothing. The
whole site 404s, and wrangler deploy still reports success.
The fix is to give the served tree a real docs/ directory:
Directorystage/!
- _headers hoisted from dist/, read at the root
- _redirects hoisted too, only with redirects
Directorydocs/!
- index.html
- 404.html
- llms.txt
Directory_astro/
Directoryquickstart/
The general rule is on Deploy. The full list of files a build writes is on Build output.
1. Build and stage
Section titled “1. Build and stage”Build to dist/, then copy its contents one level down into stage/docs/. Delete stage/
first, every time, so that a page you removed cannot survive as a stale asset:
set -euo pipefail
reed build --out dist
rm -rf stagemkdir -p stage/docscp -R dist/. stage/docs/
# _headers and _redirects are read only at the root of the asset directory.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 itself.
Without the trailing /. you get stage/docs/dist/ and the same 404s as before.
Stop the script if dist/index.html is missing. A deploy of an empty or stale stage/ is
the one way this setup ships nothing while reporting success.
2. Hoist _headers and _redirects to the root
Section titled “2. Hoist _headers and _redirects to the root”Reed writes a _headers file into every build. It tells the host to serve .md mirrors as
text/markdown; charset=utf-8 and llms.txt as text/plain; charset=utf-8. Workers static
assets read that file, but only at the top of the asset directory. Inside stage/docs/ it
is ignored as a rule file and served as a plain file instead.
So the staging script moves it to stage/_headers. The rules still match: they are written
against served URL paths, and * is a splat that matches across segments, so /*.md
covers /docs/ship/deploy.md. The /*/llms.txt rule is the one that matches
/docs/llms.txt on a based site. The bare /llms.txt rule only helps a site at the root.
Why the charset matters, and what the file contains, is on Response headers.
When docs.json declares redirects, the build also writes _redirects, and the script
moves it up for the same reason. Its lines already carry the base prefix, so they match
served paths from the root. Workers static assets apply a matching _redirects rule
before serving a file at the same path, so each moved page answers with a real 301, or a
302 for permanent: false, instead of Reed’s meta-refresh page. See
Redirects.
3. Configure the Worker
Section titled “3. Configure the Worker”This is the wrangler.jsonc this site deploys with:
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "reed-docs", "compatibility_date": "2026-07-01", "workers_dev": false, "assets": { "directory": "./stage", "html_handling": "auto-trailing-slash", "not_found_handling": "404-page" }, "routes": [ { "pattern": "reedbuild.dev/docs", "zone_name": "reedbuild.dev" }, { "pattern": "reedbuild.dev/docs/*", "zone_name": "reedbuild.dev" } ], "env": { "preview": { "name": "reed-docs-preview", "workers_dev": true, "routes": [] } }}Replace the name, domain and zone with your own. What each part does:
assets.directoryis./stage, not./dist. This is the whole point of step 1.html_handling: "auto-trailing-slash"servesdocs/quickstart/index.htmlat/docs/quickstart/and redirects/docs/quickstartto it. Reed emits its page links and canonical URLs with the trailing slash, so this matches the build. Footer links fromdocs.jsonare emitted as written, without an added slash, and reach their page through this redirect. It is also Cloudflare’s default; setting it explicitly keeps a future default change from moving your URLs.not_found_handling: "404-page"serves the nearest404.htmlup the directory tree, with a 404 status. Reed builds a real 404 page. Without this setting, an unknown URL gets a bare 404 with an empty body.- Two routes, not one wildcard.
reedbuild.dev/docs*would also catch/documentationand/docsearch. The pair…/docsand…/docs/*catches the mount and nothing else. If another Worker already claims the whole hostname, these still win on their paths: Cloudflare picks the most specific matching route. workers_dev: falsekeeps production off theworkers.devsubdomain, so the site is reachable only at its real address.
The config has no main script and no bindings. Nothing runs per request except asset
serving.
4. Deploy
Section titled “4. Deploy”npx wrangler deployWrangler uploads stage/ and attaches the routes. There is no reed deploy: Reed’s job
ended when it wrote dist/.
A preview environment
Section titled “A preview environment”The preview environment deploys the same assets as a separate Worker on workers.dev:
npx wrangler deploy --env previewTwo details make it safe:
- It inherits
assets. You cannot preview a different layout from the one production serves, because there is only oneassetsblock. routes: []is explicit.routesis inheritable. Delete that line and the preview Worker inherits the production routes and takes over your live site.
The preview serves at https://reed-docs-preview.<your-subdomain>.workers.dev/docs/. The
/docs segment is still there, because the file tree is the same.
Verify with curl
Section titled “Verify with curl”A zero exit code from wrangler deploy tells you the upload finished, not that the site
works. Check what the Worker actually serves.
A nested page resolves, and the bare path redirects to the trailing-slash form:
curl -s -o /dev/null -w "%{http_code}\n" https://reedbuild.dev/docs/ship/deploy/curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" https://reedbuild.dev/docs/ship/deployExpect 200, then 307 pointing at the trailing-slash URL.
The Markdown mirror and llms.txt carry the right content types:
curl -s -o /dev/null -w "%{content_type}\n" https://reedbuild.dev/docs/ship/deploy.mdcurl -s -o /dev/null -w "%{content_type}\n" https://reedbuild.dev/docs/llms.txtExpect text/markdown; charset=utf-8 and text/plain; charset=utf-8. A missing charset
means _headers is not being read. Check that it sits at stage/_headers, not inside
stage/docs/.
An unknown URL gets Reed’s 404 page, not an empty body:
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" https://reedbuild.dev/docs/no-such-pageExpect 404 and a size in the thousands of bytes. A size of 0 means
not_found_handling is not set.
Finally, confirm you are looking at this build rather than a cached one, as Deploy describes. Grep a live page for a string you just added.
Related
Section titled “Related”- Cloudflare Pages if you would rather upload a directory than configure a Worker.
- nginx for the same layout on a server you run.
- llms.txt for what the
llms.txtcontent type protects. - Offline builds in CI to build
dist/in the pipeline that runswrangler deploy.