Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Building UI with AI agents
  4. Nextjs llms.txt: Generate llms-full.txt and .md Pages
Auf Deutsch lesen
  1. Blog
  2. Building UI with AI agents
  3. Nextjs llms.txt: Generate llms-full.txt and .md Pages
Building UI with AI agents
Building UI with AI agents

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

SR
Serban Rusu · Founder of Wingo UI
Oct 16, 2026 · 10 min read

On this page

0%
  1. What is llms.txt and what goes in it?
  2. What does a real llms.txt example look like?
  3. Where should the content come from?
  4. How do I generate llms.txt in the App Router?
  5. How do I add llms-full.txt in Next.js?
  6. How do I serve a .md twin of every page?
  7. How do agents find the Markdown version of a page?
  8. What are the llms.txt best practices?
  9. Does anyone actually read llms.txt?
  10. How do I check that it works?
  11. What should you do next?
  12. FAQ
    1. Where do I put llms.txt in a Next.js App Router project?
    2. What is the difference between llms.txt and llms-full.txt?
    3. How do I serve a .md version of every page in Next.js?
    4. Does Google use llms.txt?
    5. What content type should llms.txt use?

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.

Published Oct 16, 2026

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.

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 (opens in a new tab) 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 (opens in a new tab), nextjs.org/docs/llms-full.txt (opens in a new tab) 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:

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 and /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.

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
$ npx wingo-ui@latest add markdown
ProMarkdown docs

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.

The llms.txt route handler. Copy it with the button in the header and watch the icon turn into a check
$ npx wingo-ui@latest add code-block
FreeCode Block docs

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

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 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 (opens in a new tab) 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 (opens in a new tab), 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 covers that setup, and the AGENTS.md vs CLAUDE.md comparison 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.

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.

Components in this post

  • Code Block

    A React code block with syntax highlighting, line numbers, highlighted lines and a copy button, themed from a single accent color.

    Free
  • Markdown

    A React Markdown renderer in the library's look: scrolling tables, highlighted code, task lists and a flicker-free streaming mode for AI.

    Pro

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.

  • llms.txt
  • Next.js
  • App Router
  • AI agents
  • Markdown
  • SEO

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Building UI with AI agents

React Markdown Streaming Without Flicker or Scroll Jumps

React markdown streaming done right: repair half-typed syntax, re-parse only the last block, and keep the chat pinned to the bottom until the reader scrolls up.

SRSerban Rusu·Oct 16, 2026·10 min read
Component guides

React Rich Text Editor: Tiptap, Markdown and Mentions

Build a React rich text editor on Tiptap with Markdown shortcuts, @mentions and image uploads that saves HTML, JSON or Markdown and works well on a phone.

SRSerban Rusu·Oct 19, 2026·9 min read
Component guides

React Diff Viewer in React 19: Split, Unified, Word Diffs

How a React diff viewer works, which packages still support React 19, and how to review AI edits, contracts and configs with split views and word highlights.

SRSerban Rusu·Oct 16, 2026·9 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI