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 proxy 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, and a .txt file comes back as text/plain. That last part is observed behaviour rather than something the docs promise, so confirm it in production like anything else. For a file you edit by hand this is the correct answer and there is nothing else to configure, though note public/ is served with Cache-Control: public, max-age=0 by default.

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'; // removed once cacheComponents is on

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

Your proxy, formerly middleware, intercepts it before public/

This is the Next.js-specific failure, and the file it lives in was renamed: Next.js 16 deprecated the middleware convention in favour of proxy, made it Node-runtime only, and ships a codemod (npx @next/codemod@canary middleware-to-proxy). middleware.ts still runs, so this bites on either name. The behaviour is the point and has not changed: the documented execution order puts the proxy at step 3 and filesystem routes including public/ at step 5, and with no matcher it runs on every request, public/ assets included. 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.

proxy.ts (middleware.ts before Next.js 16)
export const config = {
  matcher: [
    // Add txt (and any other root file you serve) to the exclusion group,
    // or the proxy 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 your proxy - the convention Next.js 16 renamed from middleware, and which middleware.ts still feeds on earlier versions. It runs before filesystem routes, so a matcher that does not exclude .txt files will intercept /llms.txt and return whatever it 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 - though that export is removed if you have turned on Cache Components in Next.js 16, where its own caching primitives take over. 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 matcher on your proxy, or your middleware if you are on a version before Next.js 16 renamed it, and any rewrites that could match the path, beforeFiles rewrites especially. The file itself needs nothing.

LintPage is not affiliated with any platform or plugin named on this page. The claims here were last checked against vendor documentation on 2026-09-18. Settings and defaults move quickly, so confirm against your own install before relying on one.

§ 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 →