Skip to content

Response headers

The _headers file, why it exists, and where it has to live.

Reed writes a _headers file into every build:

.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

Static hosts guess content types from file extensions, and most guess wrong for .md — commonly application/octet-stream, which makes a browser download the file rather than render it. The markdown mirrors and llms.txt are the machine-readable half of your documentation; served as a binary download they are close to useless.

Cloudflare Pages and Netlify read _headers from the top of the served directory, and its rules match served URL paths.

If your site is mounted under a path, this creates a split that is easy to get wrong:

served-root/
docs/ ← the contents of dist/
index.html
llms.txt
_headers ← stays HERE, not inside docs/

The /*/llms.txt rule in the emitted file is what covers the mounted case — it matches llms.txt one directory down, which is where a based site’s copy actually lives.

_headers is a Cloudflare Pages and Netlify convention. Other hosts need the equivalent expressed their own way:

  • nginx — a types block or location with add_header
  • S3 + CloudFrontContent-Type metadata set per object at upload
  • Vercel — a headers entry in vercel.json

Reed emits the file it can emit. Translating it is your host’s business, and skipping the translation costs you the markdown surfaces rather than the site.

Check the served content type rather than the file’s presence:

sh
curl -s -o /dev/null -w "%{content_type}\n" https://docs.example.com/llms.txt

Expect text/plain. Getting application/octet-stream means the rules are not being applied — most often because _headers ended up inside the mount directory instead of at the root.