When someone pastes your documentation URL into an AI assistant, the assistant fetches the page as HTML: a header, a sidebar, a cookie banner, the content somewhere in the middle, and a script tag for every feature. Turning that back into clean text is hard, and the clutter that survives eats into the context window, the limited amount of text a model can consider at once. llms.txt is a proposal for fixing that with one small Markdown file. By the end of this post you will have written one by hand, and generated one from a Nuxt site.
What goes in an llms.txt file
The llms.txt proposal was published by Jeremy Howard in September 2024. It asks a site to serve a Markdown file at /llms.txt, a fixed address like the ones robots.txt and sitemap.xml use, with a fixed shape:
- An H1 with the name of the project or site. This is the only required part.
- A blockquote with a short summary.
- Any number of plain paragraphs or lists with background, but no further headings.
- H2 sections, each a list of links. Every item is
- [name](url), optionally followed by a colon and a short note about what the page contains. - An H2 called
Optional, for links a model can skip when it is short on context.
The links should point at Markdown versions of your pages where you have them, because a page of Markdown is far smaller than the same page as HTML. The proposal suggests serving each one at the page's own URL with .md added. A file can also live in a subpath, such as /docs/llms.txt.
A second file is not in the proposal but has become common alongside it: llms-full.txt, the whole documentation set concatenated into one Markdown document. The first file is a map; the second is the territory. Documentation hosts started producing both, and the Nuxt module below does too.
A convention, not a standard
Be clear with yourself about what this buys. No model vendor has committed to fetching llms.txt before answering a question, there is no standards body behind it, and search ranking does not change because the file exists. What it does is make a cheap, well-defined entry point for anything that fetches your site on purpose: an assistant a user pointed at your URL, an agent with a web tool, a script that builds a local knowledge base, or a human who wants the short version. The cost is a few minutes and one static file, so it is a good trade even if it only ever helps your own tooling.
Write an llms.txt by hand
Take a small documentation site for a fictional component library called Stepper. In a Vite project the file goes in public/, which is copied to the root of the build as it is.
# Stepper
> Stepper is a Vue 3 component library for multi-step forms. It ships one component and two composables.
Stepper works with Vue 3.5 and TypeScript. Each page below is available as Markdown at the same URL with a `.md` extension.
## Docs
- [Getting started](https://stepper.example.com/docs/getting-started.md): install, register the component, build a first form
- [Validation](https://stepper.example.com/docs/validation.md): per-step rules and async checks
- [API reference](https://stepper.example.com/docs/api.md): props, events, slots and the two composables
## Optional
- [Changelog](https://stepper.example.com/changelog.md): every release since 1.0
- [Migrating from 1.x](https://stepper.example.com/docs/migration.md)
Read it as a model would. The H1 says what this is. The summary, a Markdown blockquote (a line that starts with >), says it in two short sentences. The Docs section has three links, each with a note that lets a reader decide whether to fetch it. The Optional section holds the pages that are only useful sometimes. In sixteen lines, a model learns what Stepper is, which pages exist and which ones it can skip.
Two habits keep the file useful. Keep the notes to one clause, because they are read before the page is fetched. And list fewer pages than you have: the file is a map for a reader with limited attention, not a sitemap.
Generate it in Nuxt with nuxt-llms
Writing the file by hand is fine for five pages. For a larger documentation site the links should come from the content, and that is what nuxt-llms does. It is a module from NuxtLabs that serves /llms.txt from a Nuxt app, and /llms-full.txt when you ask for it. The examples use the Nuxt 4 layout, with the app in app/ and server/ at the root.
npm install nuxt-llms
The configuration mirrors the file format: a domain, a title, a description, sections of links, and notes. Only domain is required. Without it the module logs a warning and serves nothing.
export default defineNuxtConfig({
modules: ['nuxt-llms'],
llms: {
domain: 'https://stepper.example.com',
title: 'Stepper',
description: 'A Vue 3 component library for multi-step forms.',
sections: [
{
title: 'Docs',
description: 'One page per topic.',
links: [
{ title: 'Getting started', href: 'https://stepper.example.com/docs/getting-started', description: 'Install and build a first form' },
{ title: 'API reference', href: 'https://stepper.example.com/docs/api', description: 'Props, events, slots and composables' },
],
},
],
notes: ['Prefer the 2.x docs. The 1.x pages are kept for migration only.'],
full: {
title: 'Stepper: full documentation',
description: 'Every docs page in one file.',
},
},
})
Start the dev server and open /llms.txt. For the config above, the module returns this file:
# Stepper
> A Vue 3 component library for multi-step forms.
## Documentation Sets
- [Stepper: full documentation](https://stepper.example.com/llms-full.txt): Every docs page in one file.
## Docs
One page per topic.
- [Getting started](https://stepper.example.com/docs/getting-started): Install and build a first form
- [API reference](https://stepper.example.com/docs/api): Props, events, slots and composables
## Notes
- Prefer the 2.x docs. The 1.x pages are kept for migration only.
The title becomes the H1, the description the blockquote, each section an H2 with its description and link list, and notes a final ## Notes section. The Documentation Sets section at the top comes from full: setting it turns on a second route, /llms-full.txt, and links to it, with full.title as the link and full.description as its note. On its own the module writes nothing into that second file; it stays empty until the hooks below or Nuxt Content fill it.
Add your own data with Nitro hooks
The links above are still typed by hand. The module's way of filling them from data is a pair of Nitro hooks. Nitro is Nuxt's server engine, and any server plugin (a file in server/plugins/ that runs when the server starts) can listen to its hooks.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('llms:generate', (event, options) => {
options.sections.push({
title: 'Playground',
links: [{ title: 'Live examples', href: `${options.domain}/play`, description: 'Editable demos of every component' }],
})
})
nitroApp.hooks.hook('llms:generate:full', (event, options, contents) => {
contents.push('# Playground\n\nThe playground at /play lets you edit every example in the browser.')
})
})
llms:generate runs each time /llms.txt is rendered and gets a copy of the options, so pushing a section here adds it to the output without touching the config. llms:generate:full does the same for /llms-full.txt and also gets a contents array of Markdown strings; whatever you push is joined into the file. This is where a CMS client or a database query would go.
Both routes are prerendered by default. In development the hooks run on every request; in a build they run once, when the routes are prerendered, so the files show your data as it was at build time. Rebuild when it changes.
Let Nuxt Content fill the sections
If the documentation is already in Nuxt Content, you do not write the hooks yourself. Nuxt Content has shipped the integration since 3.2: when nuxt-llms is installed, Content registers both hooks for you. Add both modules to modules, as in the config below.
With no further configuration, Content adds one section per collection of type page, titled after the collection, with a link for every Markdown document, using the document's title and description. If you want to choose which documents appear, name the collection in a section and filter it.
export default defineNuxtConfig({
modules: ['@nuxt/content', 'nuxt-llms'],
llms: {
domain: 'https://stepper.example.com',
title: 'Stepper',
description: 'A Vue 3 component library for multi-step forms.',
sections: [
{
title: 'Guides',
description: 'Step by step pages.',
contentCollection: 'docs',
contentFilters: [
{ field: 'extension', operator: '=', value: 'md' },
{ field: 'path', operator: 'LIKE', value: '/docs/guides/%' },
],
},
],
full: { title: 'Stepper: full documentation', description: 'Every docs page in one file.' },
},
})
contentCollection names a collection from content.config.ts, and each contentFilters entry is a where clause with a field, an operator such as =, <>, LIKE, IN or IS NULL, and a value. Once any section names a collection, the automatic per-collection sections are switched off, so list every section you want.
For the full file, Content takes each document's parsed body, turns it back into Markdown with absolute links, and adds an H1 from the title if the body does not start with one. Unlike the hand-written file, the links Content puts in llms.txt point at your pages' normal URLs, not at Markdown copies; a model that wants the Markdown finds it in llms-full.txt.
Check the generated files
Run npx nuxt dev, then from a second terminal:
curl -s http://localhost:3000/llms.txt
curl -s http://localhost:3000/llms-full.txt | head -40
Read the output as a stranger would. Check that:
- The summary says honestly, in a sentence or two, what the project is.
- Every link has a note.
llms-full.txtstays under a few hundred kilobytes and is not pulling in the changelog of every release.
Then run npx nuxt generate and confirm both files are in .output/public/ as plain files.