llms.txt: make your site readable by models
Publish a small /llms.txt that points agents at Markdown versions of your pages, generate it with nuxt-llms and Nuxt Content, link pages to it, and keep private and oversized content out.
What you'll learn
When you finish, you can do these.- 1Write an llms.txt that points agents at Markdown pages
- 2Generate llms.txt from Nuxt Content with nuxt-llms
- 3Link each page to its Markdown copy and to llms.txt
- 4Keep private pages and oversized files away from agents
Picture a notes app with a public API, addresses other programs call, and a docs site that explains it. A developer asks a coding assistant how to create a note. The assistant fetches the API reference and gets a large page of HTML: navigation, a cookie banner, scripts, and somewhere inside, the one address it needs. It guesses from what it learned a year ago and calls an address the team renamed in the spring.
llms.txt fixes that: a small file at /llms.txt that tells an agent, an AI tool like that assistant, what the site is and where clean copies of its pages live in Markdown, plain text with a few formatting marks. The Code tab holds the docs site's main files, built with Nuxt, a framework that gives Vue a server. By the end its build writes that file, its pages point to it, and private pages stay out. You need one idea from the lesson Retrieval (a chatbot that knows your docs): Nuxt Content, an add-on that turns Markdown files into pages, groups them into collections.
A small index of Markdown links
The format is a proposal by Jeremy Howard, in version 2 since 10 August 2026. This is the notes app's file as its build writes it, with its release-note links cut to …:
1# Notes23> A small notes app with a public API and a Vue SDK.45## Documentation Sets67- [Notes, complete documentation](https://notes.example.com/llms-full.txt): Every docs page in one file.89## Docs1011Guides and the API reference.1213- [API reference](https://notes.example.com/raw/docs/api.md): Every endpoint, with examples.14- [Getting started](https://notes.example.com/raw/docs/start.md): Install the app and create your first note.1516## Optional1718Release notes an agent can skip.1920- …2122## Notes2324- The API is versioned under /v1.
# Notes, an H1 or top heading (one # in Markdown), names the project and is the only required part. The > line under it is a one-line summary. Each H2, a heading that starts with ##, opens a section: an optional short line, then ordinary Markdown links, each with an optional description after a colon.
A section called Optional, by convention, holds links an agent can skip when it is short of room. The last section, ## Notes, is not the project's name again: it holds remarks for every reader.
The links point at Markdown, not HTML. The spec suggests the page's own address with .md added; Nuxt Content serves its copies under /raw/ instead. The file stays small enough to fit in a model's context, the text it can read at once, and an agent fetches only the links it needs.
Generate it with nuxt-llms and Nuxt Content
In Nuxt, two modules write the file each time nuxt build prepares the site for its server: nuxt-llms writes it, and Nuxt Content fills in the pages from its collections. On a plain Vue site, you would write it by hand in public/.
- Content pagespage collections only
- nuxt-llmsruns at build time
- /llms.txta small index of links
- Assistantfollows the links it needs
- /raw/…mdclean Markdown
A module is an add-on listed under modules in nuxt.config.ts, the project's settings file in the Code tab. There modules lists both, and the llms block holds what nuxt-llms writes:
modules: ['@nuxt/content', 'nuxt-llms'],
llms: {
domain: 'https://notes.example.com',
title: 'Notes',
description: 'A small notes app with a public API and a Vue SDK.',
sections: [
{ title: 'Docs', description: 'Guides and the API reference.', contentCollection: 'docs' },
{ title: 'Optional', description: 'Release notes an agent can skip.', contentCollection: 'changelog' },
],
notes: ['The API is versioned under /v1.'],
full: { title: 'Notes, complete documentation', description: 'Every docs page in one file.' },
},
title, description and notes fill the H1, the summary and the Notes section. Each entry in sections becomes a ## section listing the pages of its contentCollection, and full adds /llms-full.txt, every docs page in one file (a convention outside the spec), linked from Documentation Sets.
In content.config.ts, also in the Code tab, docs and changelog are page collections, and authors is a data collection of plain data. When both modules are installed, Content does three things. It adds the pages of its page collections to the file, so authors stays out. It serves every page as Markdown at /raw/<path>.md with the type text/markdown. And it rewrites the links in llms.txt to those /raw URLs.
The pages point back with two link relations from version 2, labels that say what a link is for. rel="alternate" type="text/markdown" points from a page to its Markdown copy, and rel="describedby" to the llms.txt that covers it. app/pages/docs/[...slug].vue, the page component behind every docs URL, adds both to the page's head with Nuxt's useHead():
useHead({
link: [
{ rel: 'alternate', type: 'text/markdown', href: `/raw${route.path}.md` },
{ rel: 'describedby', href: '/llms.txt' },
],
})
Every docs page now names its Markdown copy and its index, so an agent that lands on one page finds the rest. routeRules, a nuxt.config.ts option, also sends the index link as an HTTP Link header, a line the server sends with the page, so even a program that never reads HTML finds it.
llms.txt is not a ranking signal and not a lock. robots.txt, another file at the site's root, tells crawlers, programs that fetch pages in bulk, what they may fetch: a request that well-behaved crawlers honour. In the Code tab, public/robots.txt asks GPTBot and ClaudeBot, the crawlers that gather training data for OpenAI and Anthropic, to stay away.
Assistants that fetch a page because someone asked use other names, ChatGPT-User and Claude-User, and OpenAI says robots.txt rules may not apply to its user-initiated requests.
What goes wrong
Three mistakes publish too much, or nothing an agent can use. First, every page collection is listed. Say the notes team keeps staff runbooks, internal how-to pages, in Content so they render in an admin area, as a third page collection in content.config.ts:
internal: defineCollection({ type: 'page', source: 'internal/**/*.md' }),
With no sections in the llms block, the build adds ## Internal to /llms.txt with a link to /raw/internal/runbook.md, and that URL serves the runbook as Markdown, as does /llms-full.txt. It was never private: Content also publishes every collection as a database file any browser can download. Keep private pages out of the content/ folder entirely, and name what belongs in the file:
sections: [
{ title: 'Docs', description: 'Guides and the API reference.', contentCollection: 'docs' },
{ title: 'Optional', description: 'Release notes an agent can skip.', contentCollection: 'changelog' },
],
The file now lists the docs and the release notes only.
Second, hand-written links to HTML. Links you type yourself, in a section's links list, are not rewritten:
links: [{ title: 'API reference', href: 'https://notes.example.com/docs/api' }],
The agent follows it and gets the full HTML page, which is the problem llms.txt exists to solve. Point at the Markdown, or let contentCollection generate the link:
links: [{ title: 'API reference', href: 'https://notes.example.com/raw/docs/api.md' }],
The agent now gets the page's Markdown.
Size is the third trap. A large framework's llms-full.txt passes 4 MB, more than a million tokens (the word pieces a model counts): too much for most models, so an agent truncates it or gives up. The full file holds every page of the collections in sections: list only what an agent needs there, or leave full out.
What to remember
/llms.txtis a small Markdown index: an H1, a summary, and sections of links to Markdown versions of your pages.- With Nuxt Content and nuxt-llms, page collections are added, served at
/raw/<path>.md, and linked from the file. Name them withsections, or every page collection appears. - Version 2 links pages to their Markdown with
rel="alternate" type="text/markdown"and to their index withrel="describedby". - Nothing in
content/is private: keep private pages out.robots.txtasks crawlers to stay away; it does not stop them. llms-full.txtholds every listed page: keep it small enough for a model to read, or leave it out.
In your project
Do these in a real project. Each one says how to confirm it worked.- Add both modules and the
llmsblock, runnuxt build, thennode .output/server/index.mjsandcurl localhost:3000/llms.txt. It starts with# Notesand a>summary, and every docs link ends in.mdunder/raw/. - Run
curl -i localhost:3000/raw/docs/start.md: the header saysContent-Type: text/markdown; charset=utf-8and the body is the page's Markdown. Thencurl -si localhost:3000/docs/start | grep -i '^link'printslink: </llms.txt>; rel="describedby". - Run
wc -c .output/public/llms.txt .output/public/llms-full.txtandgrep -c raw/ .output/public/llms.txt. Write down the sizes, and check that every collection named in the file is one you meant to publish. Ifllms-full.txtpasses a few hundred kilobytes, narrow its sections.