llms.txt on Next.js
Two ways to serve /llms.txt from Next.js - a static file in public/, or a route handler that generates it from your content. Plus the middleware and rewrite traps that make a correct file return HTML.
Anything in public/ is served from the root, so public/llms.txt is already the whole job. A route handler is the alternative when the file should be generated.
Serving /llms.txt from Next.js.
The static file: drop it in public/
Next.js serves public/ from the domain root, verbatim, with the right content type inferred from the extension. For a file you edit by hand this is the correct answer and there is nothing else to configure.
your-app/
public/
llms.txt → https://example.com/llms.txt
llms-full.txt → https://example.com/llms-full.txtThe generated file: a route handler
If the file should track your content - a docs site, a blog, anything with a content collection - generate it. A route handler at app/llms.txt/route.ts gives you the same URL with the content built at request or build time. Set the content type explicitly: the framework will not infer it from a route segment that happens to end in .txt.
import { getAllDocs } from '@/lib/docs';
// Static at build time; drop this and add a revalidate if the content moves.
export const dynamic = 'force-static';
export function GET() {
const docs = getAllDocs();
const body = [
'# Acme',
'',
'> Payments infrastructure for marketplaces.',
'',
'## Docs',
'',
...docs.map((d) => `- [${d.title}](https://acme.com${d.path}): ${d.summary}`),
].join('\n');
return new Response(body, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
},
});
}Pages Router: the same idea, one directory over
On the Pages Router, public/llms.txt works identically. For a generated file, use an API route and write the body directly - there is no route handler equivalent, and getServerSideProps is the wrong tool because it wants to render a page.
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(_req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.status(200).send('# Acme\n\n> Payments infrastructure for marketplaces.\n');
}Verify it in production, not locally
Local dev serves public/ from disk with no CDN, no rewrites, and no edge middleware in front of it. Every failure mode below only appears once deployed, so the check that counts is a curl against the live domain.
curl -sI https://example.com/llms.txt | head -3
curl -s https://example.com/llms.txt | head -5What Next.js gets wrong.
The failures below are specific to Next.js. A generic llms.txt guide will not mention any of them.
Middleware intercepts it before public/ is reached
This is the Next.js-specific failure. Middleware runs before static file serving, so a matcher broad enough to catch /llms.txt will rewrite, redirect, or auth-gate it - and the symptom is your app shell or a login page returned with a 200 at that URL. The default matcher in most templates excludes _next and static assets by extension, and .txt is usually not on that list.
export const config = {
matcher: [
// Add txt (and any other root file you serve) to the exclusion group,
// or middleware will answer for /llms.txt before public/ ever sees it.
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:txt|xml|ico|png|svg)$).*)',
],
};A beforeFiles rewrite answers before public/ does
Next.js runs rewrites in the beforeFiles group before it looks in public/, so a broad pattern there - /:path* proxied to another app, say - answers for /llms.txt too, and the file you committed is never served. Rewrites returned as a plain array run after public/ and do not have this problem. Exclude your root text files from the pattern, or move the rule to afterFiles.
A route handler without an explicit Content-Type
A Response built from a string defaults to text/plain;charset=UTF-8, but one built from a stream or an untyped Blob carries no Content-Type at all, and whatever sits in front of it - a proxy, a CDN, a browser - is then left to guess. Set the header yourself so the type does not depend on how the body happened to be built.
Serving it from a rewrite that strips the extension
A rewrite from /llms.txt to /api/llms-txt works, but the response content type comes from the handler, not the source path. People assume the .txt in the public URL decides it. It does not.
Next to read.
Questions, answered.
Where do I put llms.txt in a Next.js project?
Why does my Next.js llms.txt return HTML?
Should I generate llms.txt at build time or on request?
Does Next.js need a special config for llms.txt?
An llms.txt will not fix a site AI cannot read.
Before writing an index for AI clients, check that they can fetch your pages at all. LintPage runs 60 checks against a URL in about 30 seconds - free, no signup.
run a full scan →