minidocs

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 里仍然是实验性的
本页目录
搜索范围
输入关键词开始搜索