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;
}

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.