Table of contents
Verified 28 August 2026.
A valid llms.txt on Astro is either a two-minute file drop or a twenty-line endpoint that rebuilds itself from your content collections every time you run astro build. AIScan's C2 check wants four things and nothing more: the file returns HTTP 200, it has an H1, it has at least one ## section, and it contains Markdown links. Astro's own documentation site does not publish one (checked 28 August 2026), so there is no upstream example to copy.
Quick summary
| If you want to | Do this | Where it lives | Time |
|---|---|---|---|
| Ship something valid today | Hand-write the file as a static asset | public/llms.txt | 2 minutes |
| Keep it current without editing it | Generate it from getCollection() | src/pages/llms.txt.ts | 15 minutes |
| Cover a Starlight docs site | Install starlight-llms-txt | astro.config.mjs | 5 minutes |
| Confirm it passes | npx aiscan-cli yoursite.com, read row C2 | your terminal | 30 seconds |
The endpoint is the version that survives a real blog. A hand-written file is stale the day you publish your next post.
What the file has to contain
The llms.txt spec was proposed by Jeremy Howard of Answer.AI on 3 September 2024, and an H1 carrying the project name is the only required section. Version 2, published 10 August 2026, added rel="alternate" and rel="describedby" link relations for discovery. No standards body owns it, and Google's AI optimization guide says Google Search ignores these files entirely. Whether anything else reads them is a separate argument, covered in does llms.txt actually work in 2026. This guide is about passing C2 and being useful to the tools that do fetch it.
A minimum passing file:
# Example
> Guides and API reference for Example.
## Docs
- [Quickstart](https://example.com/docs/quickstart): install and first request
- [API reference](https://example.com/docs/api): every endpoint, with examples
If you would rather not hand-roll it, our llms.txt generator drafts one from a URL.
Path A: a static file in public/
Astro's project structure guide describes public/ as the home for "non-code, unprocessed assets", and its own example tree puts robots.txt there. Everything in public/ is copied to the build root untouched.
- Create
public/llms.txtat the top level of your project, besidesrc/andastro.config.mjs. - Paste the Markdown above and swap in real absolute URLs.
- Run
npx astro buildand confirm the file appears atdist/llms.txt. - Deploy.
That is a correct answer for a marketing site with eight pages. On anything with a content collection behind it, you will forget to update it.
Path B: an endpoint built from your content collections
This is the Astro-specific route, and it is why an Astro site can keep llms.txt accurate with no maintenance at all. Astro's endpoints guide says a .js or .ts file in src/pages becomes a route with the extension stripped, so src/pages/data.json.ts builds /data.json. The same rule gives you /llms.txt from src/pages/llms.txt.ts. In a statically generated site, custom endpoints "are called at build time to produce static files", so this costs nothing at runtime.
Set site in astro.config.mjs first, because the spec wants absolute URLs:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({ site: 'https://example.com' });
Then create the endpoint:
// src/pages/llms.txt.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
export const GET: APIRoute = async ({ site }) => {
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => +b.data.pubDate - +a.data.pubDate);
const body = [
'# Example',
'',
'> Guides, API reference and engineering notes.',
'',
'## Blog',
'',
...posts.map(
(p) => `- [${p.data.title}](${new URL(`/blog/${p.id}/`, site)}): ${p.data.description}`,
),
'',
].join('\n');
return new Response(body, {
headers: { 'content-type': 'text/plain; charset=utf-8' },
});
};
getCollection() takes an optional filter function, which is how drafts stay out. Your src/content.config.ts schema already guarantees that title and description exist on every entry, and that is the part that makes the generated file reliable rather than hopeful.
Path C: Starlight documentation sites
If your docs run on Starlight, starlight-llms-txt does the whole job. It is written by Chris Swithinbank, a Starlight maintainer, and version 0.11.0 was published on 1 July 2026. Install it with npm i starlight-llms-txt, then add starlightLlmsTxt() to the plugins array inside starlight() in astro.config.mjs and set site. It generates llms.txt, llms-full.txt and llms-small.txt, and the plugin docs tell you to preview at localhost:4321/llms.txt.
Three Astro traps
A trailing slash returns 404. Astro's endpoints guide is explicit: routes whose URLs include a file extension "can only be accessed without a trailing slash", whatever your build.trailingSlash setting says. So /llms.txt/ fails while /llms.txt works. Always test the bare path.
On-demand rendering regenerates the file on every request. If you run an adapter with server output, add export const prerender = true; to the endpoint so it is written once at build time instead.
Do not chase the content type. C2 does not check it. Both nextjs.org/docs/llms.txt and vercel.com/llms.txt serve text/plain; charset=utf-8, and both pass.
Confirm the file passes C2
Scan first. npx aiscan-cli yoursite.com returns the whole content dimension, and row C2 names which of the four conditions failed rather than telling you something is wrong. It is free and needs no account.
To check by hand instead:
curl -sSL -o llms.txt -w '%{http_code}\n' https://example.com/llms.txt # expect 200
head -1 llms.txt # expect "# Example"
grep -c '^## ' llms.txt # expect >= 1
grep -cF '](http' llms.txt # expect >= 1
Where AIScan fits, and where it doesn't
| C2 can tell you | C2 cannot tell you |
|---|---|
| The file exists and returns 200 | Whether the URLs inside it resolve |
It has an H1 and a ## section | Whether the descriptions are accurate |
| It carries Markdown links | Whether any assistant has ever fetched it |
Server logs answer the last one. Filter for requests to /llms.txt and see who turns up.
Where to go from here
Run npx aiscan-cli yoursite.com and read C2 next to C1 (Markdown for agents) and C3 (structured HTML), because the same build step usually moves all three. The same file on other stacks is covered in llms.txt on Next.js and llms.txt on Shopify, and every other fix guide sits at aiscan.site/guides.
Frequently asked questions
Where does llms.txt go in an Astro project?
Either place. A hand-written file belongs at public/llms.txt, which Astro copies to the build root untouched. A generated one belongs at src/pages/llms.txt.ts, because Astro strips the .ts extension from endpoint filenames and serves the route as /llms.txt.
My llms.txt works in dev but returns 404 in production. What is wrong?
Check the trailing slash first. Astro's endpoints guide says routes whose URLs include a file extension can only be accessed without a trailing slash, whatever build.trailingSlash is set to, so /llms.txt/ returns 404 while /llms.txt works. If the bare path also 404s, confirm the file is present in dist/ after astro build.
AIScan says C2 failed but the file loads in my browser. Why?
C2 checks four things, not one: HTTP 200, an H1, at least one ## section, and Markdown links. A file that returns 200 but starts with a paragraph instead of a # heading fails. Open the raw file and confirm line one begins with a single #.
I added an adapter and now the endpoint runs on every request. How do I stop that?
Add export const prerender = true; to src/pages/llms.txt.ts. Routes render on demand by default in server mode, so the endpoint is executed per request instead of written once at build time.
Do I need to set site in astro.config.mjs?
Yes, if you generate the file from content collections. The llms.txt spec expects absolute URLs, and site is what lets you build them with new URL(path, site). Astro's configuration reference calls site your final deployed URL and recommends setting it regardless.
Should llms.txt be served as text/markdown?
It does not matter for C2, which does not check the content type. nextjs.org/docs/llms.txt and vercel.com/llms.txt both serve text/plain; charset=utf-8 and both pass. Serve text/plain and spend the effort on the link list instead.
Does Google use llms.txt?
No. Google's AI optimization guide states that Google Search ignores these files and that maintaining one will neither harm nor help your rankings. Publish it for the agents and directed tools that do fetch it, not for Google.
Do I need starlight-llms-txt if my site is not a Starlight docs site?
No. The plugin is a Starlight plugin and only works inside a Starlight configuration. On a regular Astro site, use the endpoint at src/pages/llms.txt.ts, which gives you the same result from getCollection().
Related guides
The State of AI Agent Readiness in 2026: 473 Sites Measured
Half of the web's agentreadiness problem is already solved, and almost nobody has noticed which half. Across 473 real websites scanned by AIScan between 24 August and 3 September 2026, the median…
How to publish a valid llms.txt on Framer
Verified on 2 September 2026. Every path, plan limit and status code below was either read from Framer's own help centre or measured live against www.framer.com on that date. On Framer, llms.txt is…
The complete AI readiness setup for Replit in 2026
Verified 2 September 2026. Every command, config key and file path below was run against live Replit apps or fetched from Replit's own documentation on that date. On most platforms a missing…
How to publish a valid llms.txt on Replit
Verified 1 September 2026. On Replit, publishing /llms.txt is not one job. It is two, and which one you have depends on how the app is published. An app built by Agent runs on an Autoscale Deployment…
