# Markdown and llms.txt

Every page can have a Markdown version for agents to read, at the page’s URL with `.md` added. You can also generate [llms.txt](#llmstxt) from the document tree as an index of the whole docs. Both are generated at build time as plain static files.

## Markdown versions

Add one route file under `src/pages`:

```ts
// src/pages/[...path].md.ts
import type { APIRoute } from 'astro';
import { toMarkdown } from '@minidocs/astro/runtime';
import { pages } from '../lib/docs';

export function getStaticPaths() {
  return pages.map((page) => ({ params: page.markdownParams, props: page }));
}

export const GET: APIRoute<(typeof pages)[number]> = async ({ props: page }) =>
  new Response(`# ${page.entry.data.title}\n\n${await toMarkdown(page)}`, {
    headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
  });
```

`toMarkdown` converts only the content, without a title, so the route above adds the title itself. Add a description or other fields here the same way.

Then tell agents about it in the `<head>` of your layout:

```astro
<link rel="alternate" type="text/markdown" href={page.markdownHref} />
```

### URLs

Drop the trailing slash of the page’s URL and add `.md`:

| HTML                    | Markdown                  |
| ----------------------- | ------------------------- |
| `/docs/`                | `/docs.md`                |
| `/docs/guides/sidebar/` | `/docs/guides/sidebar.md` |

The `/docs.md` of the collection’s home page sits outside the `docs/` folder, so this route goes directly under `src/pages`, not in `docs/`. It takes `page.markdownParams`, not the `page.params` of page routes.

One route file generates the Markdown versions of every collection; join the pages of each: `const pages = [...docs, ...blog]`.

On multilingual sites, the route goes in `src/pages/[...locale]/[...path].md.ts`, and the Markdown version of an English page is `/en/docs/guides/sidebar.md`.

### What gets converted

`toMarkdown` has Astro render the content to HTML and then converts it to Markdown, so component output and image URLs match the HTML page. Along the way:

- Callouts become GitHub’s `> [!TIP]` syntax
- Code blocks keep their language and lose the highlighting markup
- Links to other pages point to their Markdown versions, so agents can keep reading Markdown
- Other relative URLs become paths starting with `/`

GFM syntax such as tables is kept. Formatting is normalized, such as `*` for all emphasis.

### Components

Components in your content render as usual, and their HTML is converted to Markdown. When a component’s look on the page does not convert well to text, such as an interactive widget with buttons, use `isMarkdown` to output simple HTML for the Markdown version:

```astro
---
// src/components/PropsTable.astro
import { isMarkdown } from '@minidocs/astro/runtime';

const { of } = Astro.props;
const props = await loadProps(of);
---

{isMarkdown(Astro) ? (
  <table>
    <tr><th>Prop</th><th>Type</th></tr>
    {props.map((prop) => <tr><td>{prop.name}</td><td>{prop.type}</td></tr>)}
  </table>
) : (
  <PropsExplorer props={props} />
)}
```

When your content uses components of frameworks such as React or Vue, pass the renderers of their integrations to `toMarkdown`:

```ts
import { getContainerRenderer } from '@astrojs/react';

await toMarkdown(page, { renderers: [getContainerRenderer()] });
```

## llms.txt

[llms.txt](https://llmstxt.org/) is a site index for large language models: the site name, a summary, then grouped lists of pages, each linking to the page’s Markdown version. minidocs generates it from the document tree:

```ts
// src/pages/docs/llms.txt.ts
import type { APIRoute } from 'astro';
import { toLlmsTxt } from '@minidocs/astro/runtime';
import { tree } from '../../lib/docs';

export const GET: APIRoute = () =>
  new Response(toLlmsTxt(tree, { title: 'My docs', describe: (entry) => entry.data.description }));
```

The file sits next to the docs home and generates `/docs/llms.txt`. Without `prefix`, put it at `src/pages/llms.txt.ts` to generate `/llms.txt`.

What goes in the file:

- The heading is `title`
- The summary is the description of the `index` page, read by `describe`, and is left out when there is none
- Pages on the first level are listed after the summary, groups on the first level each become a section, and deeper levels are nested lists
- With Astro’s `site` set, links are full URLs

### llms-full.txt

To put the content of every page into one file, write the same kind of route:

```ts
// src/pages/docs/llms-full.txt.ts
export const GET: APIRoute = async () => new Response(await toLlmsFullTxt(tree));
```

Pages follow the order of the document tree, each with its title, source URL and the result of `toMarkdown`.

### Multilingual sites

Generate one file per locale, with the route at `src/pages/[...locale]/docs/llms.txt.ts` and `toLocaleParam` giving each locale’s parameter:

```ts
export function getStaticPaths() {
  return locales.map((locale) => ({ params: { locale: toLocaleParam(locale) }, props: { locale } }));
}

export const GET: APIRoute<{ locale: Locale }> = ({ props: { locale } }) =>
  new Response(toLlmsTxt(tree[locale], { title: 'My docs' }));
```

`locales` is the list of the site’s locales, see [Multilingual Sites](/en/docs/guides/i18n.md).

## Limitations

- In static builds, response headers set in routes have no effect, and the hosting platform picks `Content-Type` by file extension. Some leave out `charset=utf-8`, which garbles non-ASCII text when the file is opened in a browser; agents are usually unaffected
- `client:only` components do not render on the server and are left out of Markdown versions
- Titles of code blocks are not kept
- Rendering uses Astro’s Container API, which is still experimental in Astro 7
