---
title: "How to publish a valid llms.txt on Next.js"
slug: llms-txt-nextjs
published: 2026-08-27T13:24:07.555866+00:00
updated: 2026-08-27T13:45:28.641034+00:00
author: "Asif Rahman"
author_url: https://masifrahman.com
category: "AI Readiness"
tags: check:C2, platform:nextjs, llms.txt, Next.js, App Router, AI readiness
description: "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."
url: https://aiscan.site/blog/llms-txt-nextjs
---

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 this | Exact file | Time |
|---|---|---|---|
| The fastest possible pass | Drop a static file | `public/llms.txt` | 2 min |
| A file that stays current | Prerender a route handler | `app/llms.txt/route.ts` | 15 min |
| It to work under `output: 'export'` | Add `export const dynamic = 'force-static'` | `app/llms.txt/route.ts` | 1 extra line |
| Pages Router (no `app/` dir) | Generate into `public/` from a prebuild script | `scripts/gen-llms.mjs` | 20 min |
| To skip writing it yourself | Use the [llms.txt generator](https://aiscan.site/llms-txt-generator) | n/a | 1 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:

```markdown
# 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
```

3. 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`.

```ts
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:

```bash
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](https://aiscan.site/docs/checks/content).

**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:

```bash
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?](https://aiscan.site/blog/does-llms-txt-actually-work-2026).

Running a store instead? The same check on a different stack is [publishing llms.txt on Shopify](https://aiscan.site/blog/llms-txt-shopify), and the [Next.js platform guide](https://aiscan.site/docs/platforms/nextjs) 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](https://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](https://aiscan.site/guides).


