---
title: "How to publish a valid llms.txt on Astro"
slug: llms-txt-astro
published: 2026-08-28T13:24:56.495914+00:00
updated: 2026-08-28T13:24:56.495914+00:00
author: "Asif Rahman"
author_url: https://masifrahman.com
category: "AI Readiness"
tags: check:C2, platform:astro, llms.txt, Astro, AI readiness, content collections, Starlight
description: "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."
url: https://aiscan.site/blog/llms-txt-astro
---

**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](https://llmstxt.org/) 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](https://aiscan.site/blog/does-llms-txt-actually-work-2026). This guide is about passing C2 and being useful to the tools that do fetch it.

A minimum passing file:

```markdown
# 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](https://aiscan.site/llms-txt-generator) drafts one from a URL.

## Path A: a static file in public/

Astro's [project structure guide](https://docs.astro.build/en/basics/project-structure/) 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](https://docs.astro.build/en/guides/endpoints/) 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:

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({ site: 'https://example.com' });
```

Then create the endpoint:

```ts
// 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`](https://delucis.github.io/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](https://aiscan.site/docs/checks/content), 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:

```bash
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](https://aiscan.site/blog/llms-txt-nextjs) and [llms.txt on Shopify](https://aiscan.site/blog/llms-txt-shopify), and every other fix guide sits at [aiscan.site/guides](https://aiscan.site/guides).

