# LLM Outputs

llms.txt, llms-full.txt, per-page Markdown, the page actions menu, and the llm config.

Documentation is read by assistants as often as by people. PzzaDocs produces three plain-text outputs from the same content so agents can index the site without scraping HTML, and surfaces them to readers through a page actions menu.

## Outputs [#outputs]

| Output | Source method | Contents |
| --- | --- | --- |
| `/llms.txt` | `getLlmsTxt()` | An index of every page with its title, URL, and description, organised by the page tree. |
| `/llms-full.txt` | `getLlmsFullTxt()` | Every page's Markdown concatenated in tree order. |
| `<page>.mdx` | `getMarkdown(slug)` | One page as raw Markdown with frontmatter stripped, the title as an H1, and `[#id]` anchors after each heading. |

The anchors in the per-page Markdown match the rendered heading ids, so an assistant can deep-link into the HTML page it was reading in text form.

## Routes [#routes]

Add two route handlers for the site-wide files:

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

export const dynamic = "force-static";

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

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

export const dynamic = "force-static";

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

Per-page Markdown needs its own segment, because a route handler cannot live next to `page.tsx` inside `app/docs/[[...slug]]`:

```ts title="app/llms.mdx/[[...slug]]/route.ts"
import { source } from "@/lib/source";

export const dynamic = "force-static";

export async function generateStaticParams() {
  const pages = await source.getPages();
  return pages.map((page) => ({ slug: page.slug }));
}

export async function GET(_request: Request, context: { params: Promise<{ slug?: string[] }> }) {
  const { slug = [] } = await context.params;
  const markdown = await source.getMarkdown(slug);
  if (!markdown) {
    return new Response("Not found", { status: 404 });
  }
  return new Response(markdown, { headers: { "content-type": "text/markdown; charset=utf-8" } });
}
```

### Linking the menu to the route [#linking-the-menu-to-the-route]

Set `siteUrl` in the config (this site uses `https://docs.pzza.works`) so the links in `llms.txt` and `llms-full.txt` and the prompts behind "Open in Claude", "Open in ChatGPT" and "Open in Cursor" are absolute; pass it to `DocsPage` as `siteUrl={config.siteUrl}`. Without it the files use relative paths and the menu falls back to the browser origin.

`llm.markdownPath` maps a page URL to its Markdown URL. The default appends `.mdx` to the page URL (`/docs/guides/llm` becomes `/docs/guides/llm.mdx`, and the index `/docs` becomes `/docs.mdx`). You have two options to make that URL resolve:

<Tabs items={["Rewrite", "Custom path"]}>
  <Tab value="Rewrite">
    Keep the default and add a `proxy.ts` that rewrites `.mdx` requests under `baseUrl` to the handler. This is what this site does, so both `/docs/guides/llm.mdx` and `/llms.mdx/guides/llm` work.

```ts title="proxy.ts"
import { NextResponse, type NextRequest } from "next/server";

const BASE_URL = "/docs";
const MARKDOWN_ROUTE = "/llms.mdx";

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  if (pathname.endsWith(".mdx")) {
    const page = pathname.slice(0, -".mdx".length);
    if (page === BASE_URL || page.startsWith(`${BASE_URL}/`)) {
      const url = request.nextUrl.clone();
      url.pathname = `${MARKDOWN_ROUTE}${page.slice(BASE_URL.length)}`;
      return NextResponse.rewrite(url);
    }
  }
  return NextResponse.next();
}
```
  </Tab>
  <Tab value="Custom path">
    Point `markdownPath` straight at the handler and skip the rewrite:

```ts title="docs.config.ts"
llm: {
  markdownPath: (url) => url.replace(/^\/docs/, "/llms.mdx"),
},
```
  </Tab>
</Tabs>

## Page actions [#page-actions]

Pass the computed Markdown URL to `DocsPage`. When `markdownUrl` or `editUrl` is present, a toolbar appears above the article with a **Copy Markdown** button and a **View options** menu containing Open in GitHub, View as Markdown, and open-in-assistant links that prefill a prompt with the page URL.

```tsx title="app/docs/[[...slug]]/page.tsx"
<DocsPage
  toc={page.toc}
  tree={tree}
  url={page.url}
  editUrl={config.editUrl?.(page)}
  markdownUrl={config.llm.enabled ? config.llm.markdownPath(page.url) : undefined}
>
```

The toolbar is the `PageActions` component from `pzzadocs/ui`; it can also be rendered on its own:

```tsx
import { PageActions } from "pzzadocs/ui";

<PageActions markdownUrl="/guides/llm.mdx" editUrl="https://github.com/pzzaworks/pzza-docs" pageUrl="/guides/llm" />
```

<TypeTable
  type={{
    markdownUrl: { type: "string", description: "Route serving the page as raw Markdown. Enables Copy Markdown and the assistant links." },
    editUrl: { type: "string", description: "Open in GitHub target." },
    pageUrl: { type: "string", description: "Current page route, used in the assistant prompt when markdownUrl is missing." },
  }}
/>

## Config [#config]

<TypeTable
  type={{
    "llm.enabled": { type: "boolean", default: "true", description: "Turns the outputs and the page actions menu on. The route handlers are yours, so disabling this only hides the menu." },
    "llm.markdownPath": { type: "(url: string) => string", default: "(url) => `${url}.mdx`", description: "Maps a page URL to the URL serving its Markdown." },
  }}
/>

## Try it [#try-it]

- [/llms.txt](/llms.txt)
- [/llms-full.txt](/llms-full.txt)
- [/docs/guides/llm.mdx](/guides/llm.mdx), this page as Markdown
