Dark green AIScan cover: How to publish a valid llms.txt on Astro, with an isometric stack of document sheets flowing into one file card.
Dark green AIScan cover: How to publish a valid llms.txt on Astro, with an isometric stack of document sheets flowing into one file card.
AI Readiness

How to publish a valid llms.txt on Astro

Publish a valid llms.txt on Astro in three ways: a static file in public/, a build-time endpoint from your content collections, or the Starlight plugin.

AAsif Rahman August 28, 2026 5 min read
#llms.txt#Astro#AI readiness#content collections#Starlight

This guide covers C2 · Content.

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 toDo thisWhere it livesTime
Ship something valid todayHand-write the file as a static assetpublic/llms.txt2 minutes
Keep it current without editing itGenerate it from getCollection()src/pages/llms.txt.ts15 minutes
Cover a Starlight docs siteInstall starlight-llms-txtastro.config.mjs5 minutes
Confirm it passesnpx aiscan-cli yoursite.com, read row C2your terminal30 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.

  1. Create public/llms.txt at the top level of your project, beside src/ and astro.config.mjs.
  2. Paste the Markdown above and swap in real absolute URLs.
  3. Run npx astro build and confirm the file appears at dist/llms.txt.
  4. 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 youC2 cannot tell you
The file exists and returns 200Whether the URLs inside it resolve
It has an H1 and a ## sectionWhether the descriptions are accurate
It carries Markdown linksWhether 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