Skip to content

llms.txt for a SaaS site, generated from the same copy as the page

An llms.txt file tells AI agents what your product is and where the important pages are. Build it from the landing page's data so it never goes stale, and serve it as a static file.

Next.js on Vercel2 min readships at docs/solutions/nextjs-vercel/llms-txt-for-a-saas-site.md

Tags: llms-txt · seo · ai-agents · nextjs · route-handlers

People ask ChatGPT, Claude and Perplexity which tool to use before they ever visit your site. Those agents fetch pages with a small context budget, and a landing page full of layout markup is a poor source. /llms.txt (llmstxt.org) is a short Markdown file at your root that says what the product is and links the pages worth reading.

The format

In this order:

  1. # Name (the only required part).
  2. > One-sentence summary.
  3. Free Markdown with no headings: features, how it works, the FAQ.
  4. ## Section lists of links: - [Label](https://absolute/url): note.
  5. An ## Optional section for links an agent may skip when short on room.

Use absolute URLs. An agent reads the file out of context and cannot resolve /pricing.

Generate it, do not write it

A hand-written llms.txt drifts from the site within a month. If the landing page already reads its copy from one typed object, build the file from that object with a pure function, and unit test the function:

export function buildLlmsTxt({ base, site, nav, sections }: LlmsInput): string {
  const lines = [`# ${site.name}`, "", `> ${site.description}`, ""];
  // features, steps and FAQ as plain lists, then ## Pages, ## Legal, ## Optional
  return `${lines.join("\n")}\n`;
}

Two details matter:

  • Collapse whitespace in every string. A newline in a description followed by ## would start a heading the spec does not allow there.
  • Skip section anchors. /#features is the home page again. List real pages: pricing, blog, docs, terms, privacy.

Serve it statically

In the Next.js App Router a folder named llms.txt with a route.ts inside serves /llms.txt. Route handlers are dynamic by default; this one reads no request data, so render it once at build time:

// src/app/llms.txt/route.ts
export const dynamic = "force-static";

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

If your auth proxy denies by default (Clerk does), add /llms.txt to its public routes, or every agent gets a redirect to your sign-in page.

Let integrations add sections

Pricing belongs in the file when you have it. Keep an array of extra sections that integrations append to (a payments module adds its plans and prices, a docs site adds its guides), and render each one after "Pages". The core builder never needs to know which integrations exist.