Dark green cover graphic for the AIScan C2 fix guide on publishing a valid llms.txt file on Next.js sites.
Dark green cover graphic for the AIScan C2 fix guide on publishing a valid llms.txt file on Next.js sites.
AI Readiness

How to publish a valid llms.txt on Next.js

Publish a valid /llms.txt on Next.js and pass AIScan's C2 check: a static file in public/, or a prerendered route handler, plus the command that verifies it.

AAsif Rahman August 27, 2026 6 min read
#llms.txt#Next.js#App Router#AI readiness

This guide covers C2 · Content — for Next.js / Vercel.

Table of contents

AIScan's C2 check is worth 6 points, and on Next.js you can earn it in about two minutes. C2 passes when /llms.txt returns 200 and the file contains an H1, at least one ## section, and markdown links. That is the whole bar. Next.js gives you two honest ways to serve it: a static file in public/, or a route handler that builds the file from your own content at build time. Both are below, with the exact paths, the static-export gotcha, and the command that proves it landed.

Quick summary

If you want…Do thisExact fileTime
The fastest possible passDrop a static filepublic/llms.txt2 min
A file that stays currentPrerender a route handlerapp/llms.txt/route.ts15 min
It to work under output: 'export'Add export const dynamic = 'force-static'app/llms.txt/route.ts1 extra line
Pages Router (no app/ dir)Generate into public/ from a prebuild scriptscripts/gen-llms.mjs20 min
To skip writing it yourselfUse the llms.txt generatorn/a1 min

Verify any of them with npx aiscan-cli yoursite.com and read the C2 row.

Why Next.js sites miss this one

There is no webroot to drop a file into. Everything is a route, and the App Router has file conventions for the neighbours (robots.ts, sitemap.ts, manifest.json, opengraph-image), but no llms.ts. So llms.txt falls into the gap between "static asset" and "generated metadata," and most teams never pick a lane. Next.js itself picked one: nextjs.org/docs/llms.txt returns 200 with a # Next.js Documentation H1 and a version note for the release it was built from.

Path 1: a static file in public/

Files inside public are referenced from the base URL, so public/llms.txt is served at /llms.txt.

  1. Create public/llms.txt in your project root (the same level as package.json, not inside app/).
  2. Paste a skeleton and edit it:
# Acme Inc.

> One or two sentences on what Acme does and who it is for.

## Docs
- [Getting started](https://acme.com/docs/getting-started): install and first deploy
- [API reference](https://acme.com/docs/api): every endpoint with examples

## Company
- [Pricing](https://acme.com/pricing): plans and limits
  1. Commit and deploy. No config change, no rebuild trick.

Two things to know. Next.js applies Cache-Control: public, max-age=0 to everything in public, because it cannot safely cache assets that may change, so your edits go live on the next deploy without a purge. And the content type is your host's call: on Vercel, nextjs.org/docs/llms.txt and vercel.com/llms.txt both come back as text/plain; charset=utf-8, which passes C2 fine.

The cost of Path 1 is that it rots. Ship a page, forget the file.

Path 2: generate it with a route handler

Create app/llms.txt/route.ts. The directory name is the URL, so the file lands exactly at /llms.txt.

export const dynamic = 'force-static'

export async function GET() {
  const posts = await getAllPosts() // your existing content source

  const body = [
    '# Acme Inc.',
    '',
    '> One or two sentences on what Acme does.',
    '',
    '## Guides',
    ...posts.map((p) => `- [${p.title}](https://acme.com/blog/${p.slug}): ${p.summary}`),
  ].join('\n')

  return new Response(body, {
    headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
  })
}

Route handlers support GET, and export const dynamic = 'force-static' is what makes this prerender at build instead of running per request. Next.js documents the same shape for app/data.json/route.ts in its static-export guide.

Pick one path. Two things claiming /llms.txt is a conflict you do not want to debug in production.

If you build with output: 'export'

Three things change, and all three are documented:

  • export const dynamic = 'force-static' stops being optional. Next.js requires it to prerender a route handler when static export is enabled.
  • Only GET is supported. Fine here; there is nothing to POST to an llms.txt.
  • headers in next.config.js is on the unsupported list, alongside rewrites, redirects and proxy. The handler's output is written into out/ at build time and your static host decides the content type from there, so check it with curl rather than assuming your Response headers survived.

On Pages Router there is no route-handler equivalent that prerenders to a file. Write the file instead: put a scripts/gen-llms.mjs that writes public/llms.txt, then add "prebuild": "node scripts/gen-llms.mjs" to package.json. npm runs prebuild before build on its own, so CI picks it up with no extra step.

Verify it worked

Run the scan first. It answers this question and the two next to it in one shot:

npx aiscan-cli yoursite.com

Look for C2 — /llms.txt under the content dimension. The same run also grades C1 (markdown content negotiation) and C3 (structured HTML), which are the two checks a Next.js site usually fails alongside it. All three are explained on the content checks page.

What the scan cannot see: whether the URLs inside your llms.txt actually resolve, and whether the summaries are true. C2 grades the file's shape, not its honesty. Click three links yourself.

If you would rather check by hand:

curl -sI https://yoursite.com/llms.txt | head -1
curl -s https://yoursite.com/llms.txt | head -20

Expected: HTTP/2 200, a first line starting # , at least one ## heading, and at least one - [Label](https://…) line. A 200 that returns <!DOCTYPE html> means your catch-all route answered instead of the file. That is a fail, not a pass.

Keep it from going stale

Path 2 regenerates on every deploy for free. If you took Path 1, put a calendar reminder on it, or move to Path 2 once the list passes a dozen links. Worth knowing before you invest more: Ahrefs found 28% of 137,210 domains publish a valid llms.txt and 97% of those files got zero requests in May 2026, and Google states plainly that llms.txt files "won't harm (nor help) your visibility or rankings in Google Search." Six AIScan points for two minutes is a fair trade. A traffic strategy it is not. The longer argument is in Does llms.txt actually work in 2026?.

Running a store instead? The same check on a different stack is publishing llms.txt on Shopify, and the Next.js platform guide lists the rest of the checks this stack tends to miss.

Your next step

Scan the site and read the C2 row: npx aiscan-cli yoursite.com, or paste the URL at aiscan.site. If C2 already passes, the next cheapest wins on Next.js are C1 and C3. Every fix guide is indexed at aiscan.site/guides.

Frequently asked questions

Does /llms.txt have to be served as text/markdown?

No. AIScan's C2 check looks at the response status and the body: a 200, an H1, at least one ## section, and markdown links. Content type is not part of the check. Next.js and Vercel both serve their own llms.txt as text/plain; charset=utf-8, and those pass.

My llms.txt works locally but returns 404 in production. What went wrong?

Almost always the file is in the wrong directory. public/llms.txt must sit beside package.json at the project root, not inside app/ or src/app/. If you used a route handler instead, confirm the folder is app/llms.txt/ containing route.ts, and that the build log shows the route prerendered rather than skipped.

AIScan says C2 failed but curl shows my file. Why the mismatch?

You are probably testing a different host than the one scanned. Scan the exact URL you submitted, including www or apex, and follow redirects: curl -sIL https://yoursite.com/llms.txt. A 301 to another hostname, or an llms.txt that only exists on www, produces exactly this split result.

The build fails with a route conflict on /llms.txt. How do I fix it?

You have both public/llms.txt and app/llms.txt/route.ts claiming the same URL. Delete one. Keep the route handler if you want the file regenerated from your content on every deploy, and keep the static file if the list is short and rarely changes.

Do I need export const dynamic = 'force-static'?

Only when you build with output: 'export'. Next.js requires the handler to be explicitly marked static to prerender it under a static export. On a normal server or serverless deploy the line is harmless and still useful, because it pins the route to build time instead of running per request.

Can I do this on the Pages Router?

Yes, but not with a route handler, which is an App Router feature. Put the file in public/llms.txt, or generate it: write a scripts/gen-llms.mjs that emits public/llms.txt, then add "prebuild": "node scripts/gen-llms.mjs" to package.json so npm runs it before every build.

Should I also publish llms-full.txt?

Only if you have a real use for it. llmstxt.org treats the H1 as the only required part of llms.txt, and a full-text dump is a separate, much larger file to keep current. Get C2 passing first, then decide whether a full export earns its maintenance.

Will llms.txt improve my Google rankings?

No. Google's AI search optimization guidance states that maintaining llms.txt files won't harm nor help visibility or rankings in Google Search, because Google Search ignores them. Ahrefs also found 97% of the valid llms.txt files it surveyed received zero requests in May 2026. It earns 6 AIScan points cheaply; treat anything beyond that as unproven.

Related guides