Markdown 与 llms.txt
每个页面都可以有一个给 agent 读的 Markdown 版本,地址是在页面地址后面加上 .md。还可以从文档树生成 llms.txt,作为整个文档的目录。它们都在构建时生成,是普通的静态文件。
Markdown 版本
在 src/pages 下加一个路由文件:
// 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 有这个版本:
<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:
---
// 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:
import { getContainerRenderer } from '@astrojs/react';
await toMarkdown(page, { renderers: [getContainerRenderer()] });
llms.txt
llms.txt 是给大模型读的站点目录:站点名、一段简介,然后是分组的页面列表,每一项链接到页面的 Markdown 版本。minidocs 从文档树生成它:
// 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
把所有页面的内容拼成一个文件,写法相同:
// 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 得到每种语言的参数:
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 是站点的语言列表,见多语言。
限制
- 静态构建时,路由里设置的响应头不会生效,
Content-Type由托管平台按扩展名决定。有的平台不带charset=utf-8,在浏览器里直接打开时中文会乱码,agent 一般不受影响 client:only组件不在服务端渲染,不会出现在 Markdown 版本里- 代码块的标题不会保留
- 渲染用的是 Astro 的 Container API,它在 Astro 7 里仍然是实验性的