# Markdown 与 llms.txt

每个页面都可以有一个给 agent 读的 Markdown 版本，地址是在页面地址后面加上 `.md`。还可以从文档树生成 [llms.txt](#llmstxt)，作为整个文档的目录。它们都在构建时生成，是普通的静态文件。

## Markdown 版本

在 `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` 只转换正文，不带标题，所以上面自己拼上了标题。想加上描述之类的字段，也在这里拼。

再在布局的 `<head>` 里告诉 agent 有这个版本：

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

### 地址

去掉页面地址末尾的斜杠，加上 `.md`：

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

集合首页的 `/docs.md` 在 `docs/` 文件夹外面，所以这个路由放在 `src/pages` 下，而不是 `docs/` 里。它用的参数是 `page.markdownParams`，不是页面路由的 `page.params`。

一个路由文件可以生成所有集合的 Markdown 版本，把各个集合的页面拼起来就行：`const pages = [...docs, ...blog]`。

多语言站点把路由放在 `src/pages/[...locale]/[...path].md.ts`，英文页面的 Markdown 版本是 `/en/docs/guides/sidebar.md`。

### 转换了什么

`toMarkdown` 先让 Astro 把正文渲染成 HTML，再转成 Markdown，所以组件的输出、图片地址都和 HTML 页面一致。其中：

- 提示框变成 GitHub 的写法 `> [!TIP]`
- 代码块保留语言标记，去掉高亮用的标签
- 链接到其他页面时，指向那个页面的 Markdown 版本，agent 可以一直读 Markdown
- 其他相对地址写成以 `/` 开头的路径

表格等 GFM 语法会保留。格式会被统一，比如强调一律用 `*`。

### 组件

正文里的组件会照常渲染，输出的 HTML 再转成 Markdown。如果组件在页面上的样子不适合转成文字，比如带按钮的交互界面，用 `isMarkdown` 给 Markdown 版本单独输出一份简单的 HTML：

```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>参数</th><th>类型</th></tr>
    {props.map((prop) => <tr><td>{prop.name}</td><td>{prop.type}</td></tr>)}
  </table>
) : (
  <PropsExplorer props={props} />
)}
```

正文里用了 React、Vue 等框架的组件时，要把对应 integration 的渲染器传给 `toMarkdown`：

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

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

## llms.txt

[llms.txt](https://llmstxt.org/) 是给大模型读的站点目录：站点名、一段简介，然后是分组的页面列表，每一项链接到页面的 Markdown 版本。minidocs 从文档树生成它：

```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: '我的文档', describe: (entry) => entry.data.description }));
```

文件放在文档首页旁边，生成 `/docs/llms.txt`。没有 `prefix` 时放在 `src/pages/llms.txt.ts`，生成 `/llms.txt`。

文件的内容：

- 标题是 `title`
- 简介是 `index` 页面经过 `describe` 读出的描述，没有时不输出简介
- 第一层的页面列在简介后面，第一层的分组各成一节，更深的层级是嵌套列表
- 配置了 Astro 的 `site` 时，链接是完整的 URL

### llms-full.txt

把所有页面的内容拼成一个文件，写法相同：

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

页面按文档树的顺序排列，每一页是标题、来源地址和 `toMarkdown` 的结果。

### 多语言站点

每种语言生成一份，路由放在 `src/pages/[...locale]/docs/llms.txt.ts`，用 `toLocaleParam` 得到每种语言的参数：

```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: '我的文档' }));
```

`locales` 是站点的语言列表，见[多语言](/docs/guides/i18n.md)。

## 限制

- 静态构建时，路由里设置的响应头不会生效，`Content-Type` 由托管平台按扩展名决定。有的平台不带 `charset=utf-8`，在浏览器里直接打开时中文会乱码，agent 一般不受影响
- `client:only` 组件不在服务端渲染，不会出现在 Markdown 版本里
- 代码块的标题不会保留
- 渲染用的是 Astro 的 Container API，它在 Astro 7 里仍然是实验性的
