Fix llms.txt or .md pages downloading instead of displaying
The host serves .md and .txt as a download. Put Reed's _headers at the served root, or set the types on the host.
Symptom
Section titled “Symptom”Opening https://example.com/docs/llms.txt or a page’s markdown mirror such as
https://example.com/docs/quickstart.md makes the browser download a file instead of
showing text. Assistant tools that fetch the URL — “Open in ChatGPT”, “Open in Claude”, an
agent reading llms.txt — fail or return nothing useful, even though the URL answers 200.
The response header gives it away:
curl -sI https://example.com/docs/llms.txt | grep -i content-typeprints content-type: application/octet-stream.
A bare text/markdown with no charset is a milder form of the same problem: some
fetchers reject a text/* response that does not declare its encoding.
Static hosts pick a content type from the file extension, and many have no mapping for
.md — they fall back to application/octet-stream, which browsers treat as a download.
Reed writes a _headers file into every build to fix exactly this:
Content-Type: text/markdown; charset=utf-8
/llms.txt Content-Type: text/plain; charset=utf-8
/*/llms.txt Content-Type: text/plain; charset=utf-8It only helps when the host reads it, and three things stop that:
- The host does not use
_headersat all. It is a convention of Cloudflare Pages, Cloudflare Workers static assets, and Netlify. Vercel, nginx, S3 and most others ignore the file. _headersis in the wrong place on abasemount. Hosts read it only at the root of the served directory, and its rules match served URL paths. If you staged the build intodocs/for abase: "/docs"site,_headerscame along intodocs/_headers, where it is ignored.- You are testing
reed dev. The dev server serves the.mdmirrors but writes nollms.txtand no_headers. Check a real build.
Cloudflare and Netlify: move _headers to the served root
Section titled “Cloudflare and Netlify: move _headers to the served root”On a base mount, move it to the top of the tree you deploy, beside the mount directory,
not inside it:
stage/ docs/ <- the contents of dist/ llms.txt quickstart.md _headers <- hereReed’s own documentation does this in its build script, right after staging the site into
stage/docs/:
mv stage/docs/_headers stage/_headersThe file is the one Reed emitted, so there is nothing to keep in sync by hand. On
Cloudflare, the * in /*.md is verified to match across path segments, so /docs/a/b.md
is covered from the root, and /*/llms.txt covers the mounted /docs/llms.txt. Netlify’s
documentation does not say whether * crosses segments, so on Netlify check a nested
mirror such as /docs/a/b.md after you deploy, not only a top-level one. Without a
base, deploy dist/ as it is — _headers is already at its root.
Vercel: add headers to vercel.json
Section titled “Vercel: add headers to vercel.json”{ "headers": [ { "source": "/(.*)\\.md", "headers": [{ "key": "Content-Type", "value": "text/markdown; charset=utf-8" }] }, { "source": "/(.*)llms.txt", "headers": [{ "key": "Content-Type", "value": "text/plain; charset=utf-8" }] } ]}nginx: map the extension
Section titled “nginx: map the extension”Add a types entry inside the server or location that serves the site:
types { text/markdown md; text/plain txt;}charset utf-8;charset_types text/markdown text/plain;A types block replaces the inherited MIME map for that context, so include
mime.types again or scope the block to a location that serves only .md and .txt.
Reload nginx after editing.
Other hosts
Section titled “Other hosts”Set Content-Type per object at upload (S3 and similar), or use the host’s own header
rules. Translate the three rules above; the values are what matter.
Verify
Section titled “Verify”Check the served header, not the file’s presence:
curl -sI https://example.com/docs/llms.txt | grep -i content-typecurl -sI https://example.com/docs/quickstart.md | grep -i content-typeExpect text/plain; charset=utf-8 and text/markdown; charset=utf-8. Still getting
application/octet-stream on Cloudflare or Netlify almost always means _headers is
inside the mount directory. If the values look right but have not changed, you may be
reading a cached response — purge the CDN cache or add a query string and retry.
Related
Section titled “Related”- Response headers — the
_headersfile and where it lives - llms.txt and markdown mirrors
- Host setup: Cloudflare Workers, Cloudflare Pages, Netlify, Vercel, nginx
- Fix a whole site that 404s after a successful deploy