Table of contents
Send llms.txt as text/plain; charset=utf-8 or text/markdown; charset=utf-8, with no byte order mark, and always spell out the charset. The type decides whether a client treats the file as text at all. The charset decides whether the curly quotes and em dashes in your descriptions survive. On 29 October 2026 we requested /llms.txt from 66 hosts and 52 answered 200; 43 of those sent a text type with a charset, 6 sent a text type with none, and 3 sent an HTML page. Three of our own sister sites were in the group with no charset, and Chromium rendered their files as mojibake.
Quick summary
| If you want to… | Do this | Time | Catch |
|---|---|---|---|
| Choose a type | text/plain; charset=utf-8, or text/markdown; charset=utf-8 if your server knows .md | 2 min | The llms.txt proposal names a Markdown file but sets no media type |
| Stop mojibake | Declare charset=utf-8 in the header itself | 5 min | A server's default charset often covers text/plain and skips text/markdown |
| Avoid a broken H1 | Save as UTF-8 with no BOM | 1 min | Some parsers see before the # and find no title |
| Confirm it | npx aiscan-cli yoursite.com, then curl -sI | 2 min | AIScan check C2 grades the file's content, not its header |
Short answer. Pick text/plain unless you have a reason not to, add ; charset=utf-8, and keep the file free of a BOM. Check the header with curl -sI, because the scan will pass a file whose header is wrong.
What the standards say, and what they leave out
The llms.txt proposal describes a Markdown file at /llms.txt. It does not say which media type to send. So the question is answered by the HTTP and media-type RFCs, not by the proposal.
According to RFC 7763, text/markdown takes a required charset parameter, and the RFC's own example shows text/markdown; charset=UTF-8. RFC 6657 explains why a missing charset is unsafe. In its own words, RFC 2046 set the default charset for text/* to US-ASCII, and RFC 2616 set ISO-8859-1 for HTTP, which left it unclear which rule a client should follow. A client that gets text/plain with no charset therefore has no single standard answer, and each one picks its own.
That is the whole problem. Browsers, language libraries and crawlers pick differently, so a missing charset gives you a file that reads fine in one tool and garbled in the next.
What we measured
On 29 October 2026 we requested https://<host>/llms.txt from 66 hosts (documentation sites, developer platforms, CMS vendors and our own sites), following up to four redirects, with a browser user agent from one datacenter IP. 52 answered 200, 13 returned 404 and one returned 403.
| Response, 52 hosts that answered 200 | Hosts | Notes |
|---|---|---|
text/plain with a charset | 40 | The common case |
text/markdown with a charset | 3 | docs.stripe.com, docs.github.com, render.com |
text/plain with no charset | 6 | 4 of the 6 contain non-ASCII text |
text/html (a docs page, not a file) | 3 | docs.cursor.com, docs.supabase.com, deno.com |
| File begins with a BOM | 0 | None in the sample |
File begins with # | 46 | The other 6 are the 3 HTML pages and 3 files that open with something else |
Our own sites matter here. aiscan.site and storeseo.com send text/plain; charset=utf-8. xspeedcache.com, bettershield.ai and thinkrank.ai send plain text/plain, and their files contain curly quotes and dashes (59, 28 and 3 non-ASCII characters). We tell the same story against ourselves as against anyone else: the fix is below, and we are passing it to the site owners.
Why a missing charset breaks things
I served one UTF-8 file from a local Python server under eight header combinations and read it with three clients. The file holds Café, a curly apostrophe, an em dash and ×.
| Header sent | Chromium | Python urllib, .decode(charset or "utf-8") | Node fetch().text() |
|---|---|---|---|
text/plain | Café, It’s (windows-1252) | Correct | Correct |
text/plain; charset=utf-8 | Correct | Correct | Correct |
text/markdown | Same mojibake as text/plain | Correct | Correct |
text/plain; charset=iso-8859-1 (wrong) | Mojibake | Café | Correct |
| UTF-8 BOM, any type | Correct (the BOM forces UTF-8) | # Café, so startswith("# ") is false | Correct (strips it) |
Two lessons. A client that decodes as UTF-8 by default, as Node's fetch does, hides the problem, so a developer testing in a script never sees it. Chromium does not, and a human opening your file in a browser sees the damage. And a BOM, which some editors add silently, rescues the browser view but breaks any parser that checks the first two characters for # . According to the JSON specification, RFC 8259, implementations must not add a BOM to JSON, for the same reason. llms.txt has no such rule, so the safe path is to leave it out.
Path 1: fix it on nginx or Apache
Use this if your server serves a physical file. I did not run either config in this session, so test before you reload: nginx -t for nginx, apachectl configtest for Apache.
nginx. According to the nginx charset module documentation, fetched from nginx.org on 29 October 2026, charset defaults to off, and charset_types defaults to text/html text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml. So text/plain is covered once you set a charset, and text/markdown is not.
- Open the
serverblock for your site. - Add
charset utf-8;. - If you serve Markdown, add
charset_types text/plain text/markdown;on the next line. The documentation saystext/htmlis always processed, and the list names the other types. - Run
nginx -t, then reload.
Apache. According to the Apache core documentation, verified on 29 October 2026, AddDefaultCharset adds the charset only to text/plain and text/html responses, and defaults to Off.
- Add
AddDefaultCharset utf-8to the server config, the virtual host or.htaccess. - For a Markdown file, also add
AddCharset UTF-8 .md, which mod_mime uses to map an extension to a charset. - Run
apachectl configtest, then reload.
Path 2: set it in code
Use this if a framework or your own route serves the file. The header is yours to set, and the framework default is not worth trusting.
- Return the body from a dedicated route at
/llms.txt, not through a catch-all that can fall back to HTML. - Set the header explicitly:
Content-Type: text/plain; charset=utf-8. In a Next.js route handler that isnew Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } }). Our setup hub has the full route for each framework. - Add a
Cache-Controlheader suited to how often the file changes. - Return a real 404 for any unknown path, so a missing file is never a 200 page.
I ran the Node case here: http.createServer with res.setHeader("Content-Type", "text/plain; charset=utf-8") produced the correct row in the table above in all three clients.
Path 3: fix the file itself
Use this on any stack, because a header cannot fix a bad file.
- Re-save
llms.txtas UTF-8 without BOM in your editor. - Check the first bytes:
head -c 3 llms.txt | xxd. The output must not beefbb bf. - Check that the file is valid UTF-8:
iconv -f UTF-8 -t UTF-8 llms.txt > /dev/null. No output means it passed. - On WordPress, whichever plugin writes the file decides the header. ThinkRank is the plugin we suggest first for WordPress: one plugin instead of three fighting over robots.txt and llms.txt, and it migrates from Rank Math, Yoast, AIOSEO and SEOPress so nothing is re-entered. Run the same
curl -sIbelow against it, because a plugin's output is worth testing like any other file.
Check it with the scan first, then by hand
Start with the scan. Run npx aiscan-cli yoursite.com or paste the URL at aiscan.site. It is free and needs no account. Check check:C2 confirms /llms.txt returns 200 with an H1, a ## section and Markdown links, and check:C1 tests whether your pages answer Accept: text/markdown. Neither grades the Content-Type or the charset, so the header check below is yours.
If you would rather check by hand:
curl -sI https://yoursite.com/llms.txt | grep -iE '^(HTTP|content-type)'
curl -s https://yoursite.com/llms.txt | head -c 3 | xxd
| Result | Pass | Fail |
|---|---|---|
| Status | 200 | 404, or 200 with HTML |
| Type | text/plain; charset=utf-8 or text/markdown; charset=utf-8 | text/html, or no charset |
| First 3 bytes | Anything but efbbbf, usually 2320 followed by the first letter | efbbbf |
Open the URL in a browser as the last check. If you can read Café and a curly apostrophe correctly, the file is right for the client most likely to show it to a human.
Mistakes to avoid
- Testing only with a script that decodes UTF-8 by default. It hides the exact bug a browser shows.
- Setting
AddDefaultCharset Onon Apache. The docs say that value meansiso-8859-1, not UTF-8. - Serving
/llms.txtthrough an app catch-all. We found three hosts returning an HTML page with a 200 status. - Trusting a green scan. C2 grades the content, not the header.
Related reading: llms-full.txt and when to use it and common llms.txt mistakes cover the size and structure side. The header rules for robots.txt are the same idea, in robots.txt status and content type.
Next step
Run the scan on your site now and read the check:C2 result, then run the two curl commands above. Then open the llms.txt generator if you need a clean starting file, and browse the other AI readiness guides.
Frequently asked questions
What Content-Type should llms.txt use?
Use text/plain; charset=utf-8, or text/markdown; charset=utf-8 if your server maps .md files. The llms.txt proposal names a Markdown file but sets no media type, so either works. Always include the charset.
My llms.txt shows garbled characters like é in the browser. What is wrong?
The server sent no charset, so the browser guessed windows-1252 and misread the UTF-8 bytes. In our 29 October 2026 test, Chromium did this for text/plain and text/markdown with no charset. Add charset=utf-8 to the header.
AIScan C2 passes but my header is wrong. Is that a bug?
No. Check C2 grades the file's content: status, an H1, a section and Markdown links. It does not grade the Content-Type or the charset, so run curl -sI yourself.
My parser says llms.txt has no H1, but the file starts with # . Why?
The file probably starts with a UTF-8 byte order mark. In our test, Python read \ufeff# Café, so a startswith("# ") check failed. Re-save the file as UTF-8 without BOM and confirm with head -c 3 llms.txt | xxd.
curl returns 200 for llms.txt but the body is an HTML page. How do I fix it?
A catch-all route or CDN fallback is answering. Serve a real file or a dedicated route, check that content-type is a text type, and return a real 404 for unknown paths. Three hosts in our probe returned HTML with a 200.
Does nginx add a charset to text/markdown automatically?
No. According to the nginx charset module documentation, charset defaults to off and charset_types defaults to a list that includes text/plain but not text/markdown. Set charset utf-8; and add text/markdown to charset_types.
Should I serve llms.txt as text/plain or text/markdown?
Both are valid. In our probe of 52 hosts, 43 sent text/plain or text/markdown with a charset, and text/plain was by far the more common. Choose text/markdown only if the same server already serves your other Markdown that way.
Does the charset matter if my file is pure ASCII?
Less, but declare it anyway. ASCII bytes read the same in UTF-8 and windows-1252, so nothing garbles today. The first edit that adds a curly quote or dash changes that, and the header should already be right.

About the author
M Asif Rahman
Entrepreneur, curious about AI
I have been building companies for a long time, and lately most of my curiosity goes to AI. AIScan is a small personal project I built on a flight from Dubai to Narita. I use it to understand how ready the web is for AI agents, and I share what the scans teach me.
Related guides
AI readiness setup guide by platform (2026)
Verified 17, 18, 20 and 22 October 2026. Updated on 22 October 2026. An AIready site needs six things: a robots.txt that names AI crawlers D1, B2, a sitemap D2, a declared feed E5, an llms.txt C2,…
llms-full.txt: When a Full-Text File Beats a Link List
An agent that reads your llms.txt gets a map. To answer one question it then has to fetch three or four of the linked pages, one request each. llmsfull.txt removes that loop: it is one file with the…
The Soft 404 Problem: Why HTTP 200 Breaks Every Agent Discovery Check
Every convention on the agentic web works the same way: an agent asks for a file at a wellknown address, and a 200 means the file is there. /llms.txt, /robots.txt, a .md suffix on a documentation…
