lintpage
~/llms-txt/nextjs
§ Next.js

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.

§ how to serve it

Serving /llms.txt from Next.js.

step 1

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.

bash
your-app/
  public/
    llms.txt        →  https://example.com/llms.txt
    llms-full.txt   →  https://example.com/llms-full.txt
step 2

The 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.

app/llms.txt/route.ts
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',
    },
  });
}
step 3

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.

pages/api/llms-txt.ts (with a rewrite from /llms.txt)
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');
}
step 4

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.

bash
curl -sI https://example.com/llms.txt | head -3
curl -s  https://example.com/llms.txt | head -5
§ what goes wrong here

What Next.js gets wrong.

The failures below are specific to Next.js. A generic llms.txt guide will not mention any of them.

01

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.

middleware.ts
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)$).*)',
  ],
};
02

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.

03

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.

04

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.

§ faq

Questions, answered.

Where do I put llms.txt in a Next.js project?
In public/llms.txt for a hand-written file - Next.js serves public/ from the domain root, so that is all it takes. For a file generated from your content, use a route handler at app/llms.txt/route.ts and set Content-Type: text/plain explicitly. Both give you the same public URL.
Why does my Next.js llms.txt return HTML?
Almost always middleware. It runs before static file serving, so a matcher that does not exclude .txt files will intercept /llms.txt and return whatever your middleware decides - commonly your app shell or a redirect to a login page, with a 200 status. The second most common cause is a rewrite in the beforeFiles group, which runs before public/ is checked, so a pattern like /:path* answers for /llms.txt before your file can.
Should I generate llms.txt at build time or on request?
Build time, unless your content changes without a deploy. A route handler with dynamic = "force-static" renders once and is served as a static asset, which is both faster and harder to break. Reach for revalidation only if your pages come from a CMS that publishes independently of your build.
Does Next.js need a special config for llms.txt?
No - there is no llms.txt-specific setting. What you may need to change is config you already have: the middleware matcher and any rewrites that could match the path, beforeFiles rewrites especially. The file itself needs nothing.
§ the part that matters

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 →