# Nextjs llms.txt: Generate llms-full.txt and .md Pages

> Build a nextjs llms.txt with App Router route handlers: an index, an llms-full.txt and a .md twin of every page, all generated from one content source.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 16, 2026
- Category: [Building UI with AI agents](https://wingo-ui.com/blog/category/ai-agents)
- Reading time: 10 min
- Canonical: https://wingo-ui.com/blog/nextjs-llms-txt

## TL;DR

In the Next.js App Router, serve llms.txt and llms-full.txt from route handlers at app/llms.txt/route.ts and app/llms-full.txt/route.ts, marked force-static so they build once. Give every page a .md twin with a route handler under app/docs/md/[slug] plus a rewrite from /docs/:slug.md, and link it from the HTML head with alternates.types. Generate all three from the same data your pages render, so the files never drift from the site.

You want coding agents and AI assistants to read your docs the way you wrote them, without the nav, cookie banner and script tags around every page. A nextjs llms.txt setup does that in three pieces: an `llms.txt` index at the root, an `llms-full.txt` with everything in one file, and a clean Markdown twin of every page at its URL plus `.md`. This tutorial builds all three with App Router route handlers, generated from the same data your pages render, and shows what we learned shipping them for the Wingo UI component catalog.

Every snippet is TypeScript for Next.js 16 and React 19. More on how agents read component libraries is in [Building UI with AI agents](https://wingo-ui.com/blog/category/ai-agents).

## What is llms.txt and what goes in it?

llms.txt is a Markdown file at your site root that tells a language model what the site is and where the useful pages are. Jeremy Howard published the [llms.txt proposal](https://llmstxt.org/) on September 3, 2024, and last revised it on August 10, 2026 (checked October 16, 2026).

The format is short:

- An H1 with the site name. It is the only required part.
- A blockquote with a summary.
- Optional paragraphs or lists with details (no headings).
- Any number of `##` sections, each a list of links in the form `[name](url): notes`.
- A section named `## Optional` holds secondary links that an agent can skip when it needs a shorter context.

The proposal also asks for a clean Markdown version of each page at the same URL with `.md` added (`page.html.md`), or with `.md` in place of the extension (`page.md`). Its current version adds a `rel="alternate"` link of type `text/markdown` that points to that version, either as an HTML `<link>` element or as an HTTP `Link` header.

`llms-full.txt` comes from community practice, and the proposal does not mention it: the full text of the pages in one file. Next.js publishes all three for its own docs, at [nextjs.org/docs/llms.txt](https://nextjs.org/docs/llms.txt), [nextjs.org/docs/llms-full.txt](https://nextjs.org/docs/llms-full.txt) and a `.md` version of every docs page (checked October 16, 2026).

## What does a real llms.txt example look like?

Here is the top of ours on October 16, 2026, trimmed. The full file is at [/llms.txt](https://wingo-ui.com/llms.txt):

```markdown
# Wingo UI

> Wingo UI is a library of animated, configurable, mobile-first React components and blocks you copy into your project, with a CLI and an MCP server for coding agents. 352 items (226 components, 90 blocks, 17 hooks, 19 helper libraries) for React 19 and Tailwind CSS v4, release 1.9.0. 75 items are free; the rest need a Pro plan.

Every item has an HTML docs page (live preview, props, usage) and the same docs as Markdown at the page URL plus `.md`.

## Docs

- [Installation](https://wingo-ui.com/docs/installation): Set up a project and add components
- [MCP server](https://wingo-ui.com/docs/mcp): Connect your coding agent

## Inputs

- [OTP Input](https://wingo-ui.com/components/otp-input): A React OTP input for one-time verification codes and PINs, with digits that roll into place and a caret that slides between slots. [Markdown](https://wingo-ui.com/components/otp-input.md)
```

Two choices in it are worth copying. The counts and the release number are computed from the registry at build time, so the summary is never out of date. And each line carries both URLs: the HTML page for people and the `.md` twin for agents. The [OTP Input docs](https://wingo-ui.com/components/otp-input) and [/components/otp-input.md](https://wingo-ui.com/components/otp-input.md) are the same docs in two formats.

Rendered for people, the same Markdown is a readable index. The demo below runs a slice of our file through the [Markdown component](https://wingo-ui.com/components/markdown).

> Live demo (Markdown): A slice of our llms.txt rendered as Markdown: the blockquote summary, a Docs list and an Inputs list. Tab through the links; each line pairs a page with its notes. Try it at [Markdown](https://wingo-ui.com/components/markdown) and install it with `npx wingo-ui@latest add markdown`.

## Where should the content come from?

From the same source your pages render. If llms.txt is a hand-written file in `public/`, it is wrong the first time someone adds a page and forgets it. Our catalog has one `meta.ts` per component, and the HTML docs page, the `.md` twin, `llms.txt`, `llms-full.txt` and the MCP server all read those objects.

For this tutorial, the source is a typed module. Swap the array for your CMS client, your MDX loader or your registry; the rest of the code only needs `docs` and `getDoc`.

```ts
// lib/docs.ts: the one source the pages, the twins and the llms files read
export type Doc = {
  slug: string;
  title: string;
  description: string;
  section: "Guides" | "Components";
  body: string; // markdown, no h1
};

export const SITE = {
  name: "Acme UI",
  origin: process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000",
  summary: "A React component library for dashboards, installed as source.",
};

export const docs: Doc[] = [
  {
    slug: "installation",
    title: "Installation",
    description: "Set up a Next.js project and add your first component.",
    section: "Guides",
    body: "## Requirements\n\nNext.js 16, React 19 and Tailwind CSS v4.\n\n## Add a component\n\n```bash\nnpx acme-ui add button\n```",
  },
  {
    slug: "button",
    title: "Button",
    description: "A button with five variants and a loading state.",
    section: "Components",
    body: "## Usage\n\n```tsx\n<Button variant=\"soft\">Save</Button>\n```",
  },
];

export function getDoc(slug: string) {
  return docs.find((doc) => doc.slug === slug);
}
```

## How do I generate llms.txt in the App Router?

Put the formatting in one module, then serve it from a route handler. The module builds the page Markdown once and reuses it for the twin and the full file.

```ts
// lib/llms.ts
import { SITE, docs, type Doc } from "@/lib/docs";

export const pageUrl = (doc: Doc) => `${SITE.origin}/docs/${doc.slug}`;
export const markdownUrl = (doc: Doc) => `${pageUrl(doc)}.md`;

// one page as markdown: the .md twin and one chunk of llms-full.txt
export function docMarkdown(doc: Doc) {
  return [`# ${doc.title}`, "", `> ${doc.description}`, "", doc.body.trim(), "", `Source: ${pageUrl(doc)}`, ""].join("\n");
}

export function llmsTxt() {
  const lines = [`# ${SITE.name}`, "", `> ${SITE.summary}`, "", "Every page is also Markdown at its URL plus `.md`.", ""];
  for (const section of new Set(docs.map((doc) => doc.section))) {
    lines.push(`## ${section}`, "");
    for (const doc of docs.filter((d) => d.section === section)) {
      lines.push(`- [${doc.title}](${pageUrl(doc)}): ${doc.description} [Markdown](${markdownUrl(doc)})`);
    }
    lines.push("");
  }
  lines.push("## Optional", "", `- [Full docs](${SITE.origin}/llms-full.txt): every page above in one file`);
  return lines.join("\n") + "\n";
}

// "# Title" becomes "## Title" outside fenced code, so pages nest under the file's own h1
function shiftHeadings(markdown: string, levels: number) {
  let inFence = false;
  return markdown
    .split("\n")
    .map((line) => {
      if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
      if (inFence) return line;
      return line.replace(/^(#{1,6})(?=\s)/, (hashes) => (hashes + "#".repeat(levels)).slice(0, 6));
    })
    .join("\n");
}

export function llmsFullTxt() {
  return [`# ${SITE.name}`, "", `> ${SITE.summary}`, "", ...docs.map((doc) => shiftHeadings(docMarkdown(doc), 1))].join("\n") + "\n";
}
```

The route handler lives in a folder named after the file. Route handlers are not cached by default, so `force-static` tells Next.js to run it once at build time and serve the result as a static file.

> Live demo (Code Block): The llms.txt route handler. Copy it with the button in the header and watch the icon turn into a check. Try it at [Code Block](https://wingo-ui.com/components/code-block) and install it with `npx wingo-ui@latest add code-block`.

The handler in the demo, as text:

```ts
// app/llms.txt/route.ts
import { llmsTxt } from "@/lib/llms";

export const dynamic = "force-static";

export function GET() {
  return new Response(llmsTxt(), {
    headers: { "content-type": "text/plain; charset=utf-8" },
  });
}
```

If you turned on `cacheComponents`, delete the `dynamic` and `dynamicParams` exports in every file of this tutorial: Next.js 16 removed them under Cache Components. A `GET` handler that reads no request data then prerenders on its own, uncached data goes in a helper marked `use cache`, and the `if (!doc)` checks below still return the 404s.

## How do I add llms-full.txt in Next.js?

With a second route handler that calls the other function. The whole Next.js llms-full.txt route:

```ts
// app/llms-full.txt/route.ts
import { llmsFullTxt } from "@/lib/llms";

export const dynamic = "force-static";

export function GET() {
  return new Response(llmsFullTxt(), {
    headers: { "content-type": "text/plain; charset=utf-8" },
  });
}
```

Watch the size. On October 16, 2026 our dev server served a 5.3 MB [/llms-full.txt](https://wingo-ui.com/llms-full.txt) (the install guide, the design rules, every item's props and usage, and the blog posts) and a 139 KB index, because the index lists every item. The Next.js docs file was 4.4 MB the same day. A fetch tool may cut a long response off, and a model that does get the whole file spends its context on pages it does not need. So we also split the item docs per category into `/llms/<category>.txt` files. The code sets `PART_BYTES = 100_000`, and a category that outgrows it continues in `-2.txt`, `-3.txt` and so on. Inputs (37 items) comes in four parts, starting with `/llms/inputs.txt`, and Templates (45 items) in five. The index links every part.

## How do I serve a .md twin of every page?

Put a route handler under an internal path and rewrite the public `.md` URL to it. A dynamic segment takes the whole folder name, so `app/docs/[slug].md/` is not a route, and without a rewrite your `app/docs/[slug]` page receives `button.md` as the slug. The payoff: an agent that needs one page fetches a few KB instead of a multi-megabyte file.

```ts
// app/docs/md/[slug]/route.ts
import { docs, getDoc } from "@/lib/docs";
import { docMarkdown, pageUrl } from "@/lib/llms";

export const dynamicParams = false;

export function generateStaticParams() {
  return docs.map((doc) => ({ slug: doc.slug }));
}

export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
  const doc = getDoc((await params).slug);
  if (!doc) return new Response("Not found\n", { status: 404 });
  return new Response(docMarkdown(doc), {
    headers: {
      "content-type": "text/markdown; charset=utf-8",
      // search engines fold the twin into the html page
      link: `<${pageUrl(doc)}>; rel="canonical"`,
    },
  });
}
```

`generateStaticParams` prerenders one response per page and `dynamicParams = false` returns a 404 for any slug you did not list. The canonical `Link` header is what nextjs.org sends on its own `.md` pages too (checked October 16, 2026). The rewrite goes in `beforeFiles`, which runs before Next.js matches pages, so `/docs/button.md` never reaches the `app/docs/[slug]` page as a slug called `button.md`:

```ts
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async rewrites() {
    return {
      beforeFiles: [{ source: "/docs/:slug\\.md", destination: "/docs/md/:slug" }],
      afterFiles: [],
      fallback: [],
    };
  },
};

export default nextConfig;
```

Our site runs the same markdown routes for AI agents under three prefixes: `/components/:slug\\.md`, `/templates/:slug\\.md` and `/blog/:slug\\.md` each rewrite to a handler under `md/[slug]`. This post has one too, at [its .md URL](https://wingo-ui.com/blog/nextjs-llms-txt.md).

## How do agents find the Markdown version of a page?

Through a link in the HTML head. In `generateMetadata`, `alternates.types` renders a `<link rel="alternate" type="text/markdown">`, which is the link relation the proposal describes:

```tsx
// app/docs/[slug]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { Markdown } from "@/components/ui/markdown";
import { docs, getDoc } from "@/lib/docs";

export const dynamicParams = false;

export function generateStaticParams() {
  return docs.map((doc) => ({ slug: doc.slug }));
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params;
  const doc = getDoc(slug);
  if (!doc) return {};
  return {
    title: doc.title,
    description: doc.description,
    alternates: { canonical: `/docs/${slug}`, types: { "text/markdown": `/docs/${slug}.md` } },
  };
}

export default async function DocPage({ params }: { params: Promise<{ slug: string }> }) {
  const doc = getDoc((await params).slug);
  if (!doc) notFound();
  return (
    <article className="mx-auto max-w-2xl px-4 py-12">
      <h1 className="text-3xl font-semibold text-foreground">{doc.title}</h1>
      <p className="mt-2 text-muted-foreground">{doc.description}</p>
      <Markdown size="lg" headingIds className="mt-8">
        {doc.body}
      </Markdown>
    </article>
  );
}
```

Relative URLs in metadata need `metadataBase` in the root layout. The page renders the same `doc.body` the twin is built from, so the two cannot disagree. `headingIds` gives each heading an anchor, and fenced code renders through the [Code Block](https://wingo-ui.com/components/code-block) with a copy button.

Next.js also answers `Accept: text/markdown` on its own docs pages with Markdown and sends `Vary: Accept` (checked October 16, 2026). You can copy that with a `has` header condition on a second rewrite, but a CDN that caches by URL alone can then hand Markdown to a browser unless every response sends `Vary: Accept`. We skipped it: separate `.md` URLs cache cleanly and are easy to paste into a chat.

## What are the llms.txt best practices?

Seven rules, all of them in our own setup:

1. **Generate, never hand-edit.** Counts, prices and page lists come from code, so the files ship with each deploy.
2. **Keep the index a map.** One line per page, with notes that say when to read it. Long text belongs in the twins and the full file.
3. **Split the full file.** Per section files under about 100 KB are easier to fetch than one file of several megabytes.
4. **Leave out drafts.** Our loader drops draft posts from the production build, so they never reach the files. In dev they render, and their twins send `X-Robots-Tag: noindex`.
5. **Put a canonical on every twin.** The `Link` header keeps the `.md` URL from competing with the HTML page in search.
6. **Do not block them.** Our `robots.ts` disallows only private paths (account, checkout, admin, API, previews), so `/llms.txt` and every `.md` route stay crawlable. A disallow would hide them from the agents you made them for.
7. **Never leak private content.** Our `llms-full.txt` holds props and usage for every item but never the source of a Pro item.

## Does anyone actually read llms.txt?

Coding agents and assistants that fetch your URLs while they work can read it. Google does not need it: its [AI features documentation](https://developers.google.com/search/docs/appearance/ai-features) says you do not need AI text files or other new machine readable files to appear in AI Overviews or AI Mode (last updated December 10, 2025).

A coding agent told to use your library, or a chat assistant given your URL, gets your real props in one fetch. That is also why the twins matter more than the index. "Read https://wingo-ui.com/components/data-table.md, then write an orders table with it" is a prompt that works in any assistant that can fetch a URL.

When an agent can do more than read, an MCP server is the better channel: it can search, return props and source, and check for updates. Our [component library MCP guide](https://wingo-ui.com/blog/component-library-mcp-guide) covers that setup, and the [AGENTS.md vs CLAUDE.md comparison](https://wingo-ui.com/blog/agents-md-vs-claude-md-react) covers the rules file that tells agents to use it. If you are setting up the app itself, start with our [Next.js component library setup](https://wingo-ui.com/blog/next-js-component-library-setup).

## How do I check that it works?

With curl, against the dev server or a production build:

```bash
curl -s http://localhost:3000/llms.txt | head -20
curl -sI http://localhost:3000/docs/button.md   # content-type: text/markdown, link: rel="canonical"
curl -s http://localhost:3000/docs/button | grep -o '<link rel="alternate" type="text/markdown"[^>]*>'
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/docs/missing.md   # 404
```

Run `next build` once too: its route list should mark `/llms.txt`, `/llms-full.txt` and `/docs/md/[slug]` as prerendered (static or SSG), not dynamic, which confirms nothing runs per request.

## What should you do next?

Ship the index and the twins first: the index is the map, and the twins answer the one-page fetches. Then add llms-full.txt, and split it per section once a file grows past about 100 KB. If your docs show code, start with the free Code Block: `npx wingo-ui@latest add code-block`. The Markdown renderer used above is part of [Pro](https://wingo-ui.com/pricing).

## Components in this post

- [Code Block](https://wingo-ui.com/components/code-block) (Free): A React code block with syntax highlighting, line numbers, highlighted lines and a copy button, themed from a single accent color. Install: `npx wingo-ui@latest add code-block`
- [Markdown](https://wingo-ui.com/components/markdown) (Pro): A React Markdown renderer in the library's look: scrolling tables, highlighted code, task lists and a flicker-free streaming mode for AI. Install: `npx wingo-ui@latest add markdown`

## FAQ

### Where do I put llms.txt in a Next.js App Router project?

Create app/llms.txt/route.ts with a GET handler that returns the text, and export const dynamic = "force-static" so it is generated at build time. A static public/llms.txt also works, but it goes stale the first time you add a page.

### What is the difference between llms.txt and llms-full.txt?

llms.txt is a short index: the site name, a summary and lists of links with one-line notes. llms-full.txt puts the full text of those pages in one file; it is a community convention that the llmstxt.org proposal does not define.

### How do I serve a .md version of every page in Next.js?

A dynamic segment takes the whole folder name, so app/docs/[slug].md does not work. Add a route handler at app/docs/md/[slug]/route.ts and a beforeFiles rewrite from /docs/:slug\.md to /docs/md/:slug in next.config.ts. Return the Markdown with content-type text/markdown and a canonical Link header that points to the HTML page.

### Does Google use llms.txt?

Google says you do not need AI text files or other new machine readable files to appear in AI Overviews or AI Mode (Search Central, last updated December 10, 2025). The files help agents and assistants that fetch your URLs while they work, such as coding agents reading your docs.

### What content type should llms.txt use?

The proposal does not mandate one. text/plain; charset=utf-8 is the safe choice for the .txt files because browsers show it inline, and text/markdown; charset=utf-8 fits the .md twins.

---

Source: https://wingo-ui.com/blog/nextjs-llms-txt
