Next.js is the easiest platform there is for llms.txt: drop a plain-text file in public/ and it's live at /llms.txt, or write a ~30-line route handler that generates the file from your actual content so it never goes stale. Here's both, with complete code — including the pattern this site uses for its own file.
Method 1: Static file in public/ (2 minutes)
Create the file with the llms.txt Generator (or by hand — it's just Markdown), save it as public/llms.txt, deploy. Next.js serves everything in public/ at the site root with correct content types:
your-app/
├── public/
│ └── llms.txt ← served at yoursite.com/llms.txt
├── app/
└── ...
That's genuinely the whole method, and it's the right choice when your key pages are stable — a marketing site, a SaaS with a fixed set of core pages. The only cost is remembering to update the file when those pages change.
Method 2: Generate it with a route handler (stays in sync)
If your llms.txt should reflect living content — your latest guides, docs sections, product pages pulled from a CMS — generate it. In the App Router, create app/llms.txt/route.ts:
import { getAllPosts } from "@/lib/content"; // your content source
export const dynamic = "force-static"; // render at build time
export function GET() {
const posts = getAllPosts();
const lines = [
"# Your Site",
"",
"> One line describing what your site is about.",
"",
"## Key pages",
"- [Product](https://yoursite.com/product): What it does",
"- [Pricing](https://yoursite.com/pricing): Plans and costs",
"",
"## Latest articles",
...posts
.slice(0, 15)
.map((p) => `- [${p.title}](https://yoursite.com/blog/${p.slug}): ${p.description}`),
];
return new Response(lines.join("\n"), {
headers: { "content-type": "text/plain; charset=utf-8" },
});
}
Key details:
force-staticrenders the file once at build time — it's a static asset in production, zero runtime cost, and it updates on every deploy.- Set the content type to
text/plainexplicitly; without it some setups negotiate something else. - Keep curation in the code.
slice(0, 15)above matters — dumping every URL recreates a sitemap, and curation is the point. If you also want a full-content companion file, that'sllms-full.txt— a different job.
A hybrid also works well: hand-write the curated sections, generate only the "latest articles" list. (That's roughly the pattern this site uses — static curated file, refreshed as part of the content workflow.)
Pages Router note
public/llms.txt works identically. For a generated file on the Pages Router, an API route can't sit at the root path directly, so add a rewrite in next.config.js:
async rewrites() {
return [{ source: "/llms.txt", destination: "/api/llms-txt" }];
}
…and return text/plain from pages/api/llms-txt.ts. Honestly though: if you're choosing, the static file is less machinery for the same result.
Verify
curl -s https://yoursite.com/llms.txt | head— plain Markdown,200, no HTML.- Run the URL through the llms.txt Validator — it checks H1/summary/link structure against the spec.
- Since you're in a Next.js codebase anyway: confirm your content is server-rendered where it matters — AI crawlers don't run JavaScript, and llms.txt pointing at client-rendered pages is a map to rooms crawlers can't enter. The AI-Readiness Audit checks both in one pass.
Bottom line
public/llms.txt for stable sites, a force-static route handler when the file should track your content. Either way it's minutes of work in Next.js — generate the content, ship it, validate it, and move on to the part that actually earns citations: the pages the file points to.