Adding llms.txt to Next.js
The easiest platform of the three. Either drop a file in public/, or add a route handler that builds the file from your content so it can never drift out of date.
Last updated
In Next.js, save the file as public/llms.txt and it is served at /llms.txt with the right content type. For content-driven sites, a route handler that generates it at build time is better, because it stays current on every deploy.
Option 1: a static file
Anything in the public folder is served from the root. Save your file as public/llms.txt and it is live at /llms.txt. Nothing else to configure.
That is the right answer for a marketing site whose structure rarely changes. For anything content-driven, generate it.
Option 2: a route handler (App Router)
Create a route handler at app/llms.txt/route.ts. With dynamic = 'force-static' it runs at build time, so the file is generated from your real content on every deploy and cannot go stale.
app/llms.txt/route.ts
import { getAllDocs } from '@/lib/content';
export const dynamic = 'force-static';
const SITE = 'https://example.com';
export async function GET() {
const docs = await getAllDocs();
const bySection = docs.reduce<Record<string, typeof docs>>((acc, doc) => {
(acc[doc.section] ||= []).push(doc);
return acc;
}, {});
const lines: string[] = [
'# Lumen CLI',
'',
'> Command-line tool for managing container deployments. Open source,',
'> MIT licensed. Current stable release is 4.2.',
'',
];
for (const [section, entries] of Object.entries(bySection)) {
lines.push(`## ${section}`);
for (const doc of entries) {
lines.push(`- [${doc.title}](${SITE}${doc.path}): ${doc.summary}`);
}
lines.push('');
}
return new Response(lines.join('\n'), {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
},
});
}
The summary field is where the quality lives. If it falls back to the first sentence of the page, you get a mediocre file automatically. Add a deliberate one-line summary to your content front matter and the same code produces a good one.
What about the Pages Router?
Use getServerSideProps in pages/llms.txt.tsx to write the response directly:
pages/llms.txt.tsx
import type { GetServerSideProps } from 'next';
import { buildLlmsTxt } from '@/lib/llms';
export const getServerSideProps: GetServerSideProps = async ({ res }) => {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=3600');
res.write(await buildLlmsTxt());
res.end();
return { props: {} };
};
export default function LlmsTxt() {
return null;
}
Adding llms-full.txt and V2 links
Use the same pattern at app/llms-full.txt/route.ts, emitting each document’s body instead of its summary. Keep the source list narrow (documentation only) and watch the output size, since a full-text file over roughly 500 KB stops being loadable. To follow llms.txt V2, add <link rel="describedby" href="/llms.txt"> in your root layout’s metadata, and serve Markdown versions of pages if your content is already Markdown. The llms-full.txt guide covers the trade-offs.
How do you check it works?
Run a production build rather than trusting dev mode, then check the response headers:
npm run build && npm start curl -sI http://localhost:3000/llms.txt | head -n 3
You want 200 and content-type: text/plain. After deploying, run the live URL through the validator.
Frequently asked questions
Static file or route handler?
A static file for a small marketing site. A route handler for anything with a content collection, because it removes the drift problem completely.
Will it work on Vercel and other hosts?
Yes. A force-static route handler is emitted as a static file at build time, so it works anywhere the build output is served.
Does this work in Astro, Nuxt or SvelteKit?
The same two patterns apply. Each has a public directory and a way to define a custom route returning plain text.
Should I cache it?
An hour is a sensible max-age. It is a small file that changes on deploy, so there is little to gain from aggressive caching.