Skip to content

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.

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:

sh
curl -sI https://example.com/docs/llms.txt | grep -i content-type

prints 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:

.md
Content-Type: text/markdown; charset=utf-8
/llms.txt
Content-Type: text/plain; charset=utf-8
/*/llms.txt
Content-Type: text/plain; charset=utf-8

It only helps when the host reads it, and three things stop that:

  • The host does not use _headers at all. It is a convention of Cloudflare Pages, Cloudflare Workers static assets, and Netlify. Vercel, nginx, S3 and most others ignore the file.
  • _headers is in the wrong place on a base mount. Hosts read it only at the root of the served directory, and its rules match served URL paths. If you staged the build into docs/ for a base: "/docs" site, _headers came along into docs/_headers, where it is ignored.
  • You are testing reed dev. The dev server serves the .md mirrors but writes no llms.txt and 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:

txt
stage/
docs/ <- the contents of dist/
llms.txt
quickstart.md
_headers <- here

Reed’s own documentation does this in its build script, right after staging the site into stage/docs/:

sh
mv stage/docs/_headers stage/_headers

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

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" }]
}
]
}

Add a types entry inside the server or location that serves the site:

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

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.

Check the served header, not the file’s presence:

sh
curl -sI https://example.com/docs/llms.txt | grep -i content-type
curl -sI https://example.com/docs/quickstart.md | grep -i content-type

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