导语:Rspress 官方文档站在同一次构建里同时产出 HTML 与 Markdown 两条通道——页面挂 .md 兄弟文件、根目录自动生成 llms.txt 与 llms-full.txt,单页正文从 100,761 字节降到 17,294 字节;但真正会让代理读错的不是有没有 llms.txt,而是协商响应缺 Vary、HTTP Link 头里没有 alternate 这两处。
背景:代理读文档,先付一笔"剥离税"
Rspress 是 Rsbuild 之上的静态站点生成器,官方文档站 rspress.rs 的 llms.txt 里收录了 105 条页面链接,覆盖指南、插件、UI 三组内容。
它的痛点是文档站的通用痛点,只不过在这个站上体现得更硬:
代理抓到一个 HTML 页,得先把导航、侧边栏、脚本、样式从正文里剥出去。单页原始响应 100KB 出头,其中真正有用的正文只占一成左右。
文档被拆成上百页,代理无法判断哪一页覆盖当前问题,只能顺着链接一页页试。
站点还要同时服务两类消费者:搜索爬虫要 HTML 与
sitemap.xml,LLM 与向量检索系统要 Markdown 与llms.txt。
Rspress 的处理方式是把这件事定义成两条并行输出,而不是给 HTML 做一套"给机器看的降级版"。它自己的文档里写了一张对照表:
| 维度 | SSG(静态站点生成) | SSG-MD(静态站点生成到 Markdown) |
|---|---|---|
| 优化目标 | SEO | GEO(生成式引擎优化) |
| 目标消费者 | 搜索引擎爬虫 | LLM / 向量检索系统 |
| 索引文件 | sitemap.xml | llms.txt |
| 全量内容文件 | — | llms-full.txt |
| 核心实现 | renderToString | renderToMarkdownString |
| 访问方式 | /guide/start/introduction.html | /guide/start/introduction.md |
这张表值得单独抄一遍:它把"给 AI 的版本"当成构建产物对待,而不是当成一份手写的说明文件。手写 llms.txt 的站,索引和页面迟早对不上;构建期生成的站,索引天然与路由同源。
落地清单
1. 文件位置与路由
根目录布局实测如下:
├── llms.txt # 索引:标题 + 一句描述 + 全部指向 .md ├── llms-full.txt # 全量正文拼接 ├── robots.txt ├── sitemap.xml ├── guide/start/introduction.html ├── guide/start/introduction.md ├── zh/llms.txt # 多语言分文件,不是塞进同一个 └── zh/llms-full.txt
几个实测数字(2026-11-07 抓取):
| 资源 | 内容类型 | 体积 | 结构 |
|---|---|---|---|
/llms.txt | text/plain | 19,479 字节 | 123 行 / 5 个 H2 / 105 条链接 |
/llms-full.txt | text/plain | 545,364 字节 | 18,186 行 |
/zh/llms.txt | text/plain | 19,490 字节 | 中文版索引 |
/zh/llms-full.txt | text/plain | 548,039 字节 | 中文版全量 |
/guide/start/introduction.md | text/markdown | 17,294 字节 | 单页 Markdown |
/guide/start/introduction | text/html | 100,761 字节 | 单页 HTML |
注意 llms-full.txt 的体积:54 万字节、1.8 万行。这个量级任何代理都不会整包读进上下文,它的实际价值是"可下载、可切片建索引",而不是"让模型通读"。下面复盘第 2 条会展开这一点。
2. llms.txt 原文片段
真实头部(截取前 15 行,原文 105 条链接):
# Rspress > Rsbuild based static site generator ## Guide - [Introduction](https://rspress.rs/guide/start/introduction.md): Rspress is a high-performance static site generator based on Rsbuild, featuring millisecond-level startup, AI-native SSG-MD, MDX support, full-text search, i18n and multi-version docs. - [Quick start](https://rspress.rs/guide/start/getting-started.md): Quickly create an Rspress project via the scaffold or manual setup, and learn the basic workflow of dev server startup, production build, and local preview. - [AI](https://rspress.rs/guide/start/ai.md): Use Agent Skills, llms.txt, Markdown documentation, and AGENTS.md to help coding agents work with Rspress. - [llms.txt (SSG-MD)](https://rspress.rs/guide/basic/ssg-md.md): Generate llms.txt and Markdown files for AI-native documentation using SSG-MD, enabling better understanding by large language models. - [WebMCP](https://rspress.rs/guide/basic/webmcp.md): Rspress enables webMcp by default, so agents can use WebMCP to search, read, and navigate Rspress documentation.
三个可直接抄的写法:
链接直接给
.md,不给 HTML 页。 105 条链接 105 条以.md结尾(实测 105/105)。代理少跳一次重定向、少剥一层 HTML。每条链接后面跟一句描述。 这句话决定代理选不选这一页,比标题重要。Rspress 的描述里塞满了检索词("millisecond-level startup"、"i18n and multi-version docs"),不是营销话术。
分组用 H2,与侧边栏导航同序。 索引顺序 = 人类阅读顺序,代理按顺序读时能还原文档的章节结构。
3. 构建配置
官方文档给出的最小配置(llms 在 v2.1.0 起默认开启,此前默认 false):
```ts title="rspress.config.ts" import { defineConfig } from '@rspress/core';
export default defineConfig({ llms: true, themeConfig: { llmsUI: { injectLlmsHint: true, // .md 头部注入 "For AI agents" 提示行 viewOptions: ['markdownLink', 'chatgpt', 'claude'], // H1 下的复制/打开按钮 placement: 'outline', // 改挂到右侧大纲 }, }, });
要点:
- `llms: true` 开启后,所有首屏 H1 下会自动出现 `LlmsCopyButton` 与 `LlmsViewOptions` 两个组件——既方便人类复制 Markdown,也把这份文件的存在暴露给用户。
- **SSR 不兼容的场景**(有些组件只能在浏览器跑,`renderToMarkdownString` 会崩),退回 `llms: false` + `@rspress/plugin-llms` 插件做后置生成。
- MDX 里混了 React 组件时,构建先用 `remarkSplitMdx` 把纯 Markdown 与 JSX 分开:Markdown 原样序列化成字符串,JSX 走 `renderToMarkdownString`。组件边界用 `llms.remarkSplitMdxOptions` 的 `includes` / `excludes` 控制,被 `excludes` 命中的组件转成纯文本。这一步不做,`.md` 里会漏出 `<MyComponent prop={...} />` 这种模型读不懂的残片。
- 索引要改写就用 `llms.llmsTxt({ title, description, sections })` 自己拼,函数签名把 sections 的页面列表直接给你,不用再扫路由。
### 4. v2 三件套:HTML head 实测
页面 head 里确实挂了完整的三件套,原文如下(`/guide/start/introduction`):
```html
<link rel="alternate" hreflang="zh" href="https://rspress.rs/zh/guide/start/introduction">
<link rel="alternate" hreflang="en" href="https://rspress.rs/guide/start/introduction">
<link rel="alternate" type="text/markdown" href="https://rspress.rs/guide/start/introduction.md">
<link rel="describedby" href="https://rspress.rs/llms.txt">rel="alternate" type="text/markdown":页面级入口,代理抓到任意 HTML 页都能顺着它找到同页 Markdown。rel="describedby":指向llms.txt,等于告诉代理"这个站的结构说明书在这儿"。hreflang同时给出多语言同页关系,与{lang}/llms.txt的分文件策略配套。
5. Accept 协商:同一 URL,两种表示
GET /guide/start/introduction Accept: text/markdown 200 OK Content-Type: text/markdown; charset=utf-8 Content-Length: 17294
GET /guide/start/introduction 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 100761(原始响应,未压缩口径)
两行请求头的差别,换掉整个正文表示。对代理来说这比"改后缀"更省事:它不用知道该站有没有 .md,只要声明自己接受 Markdown。
Markdown 响应的正文首行固定是这样一句:
> For AI agents: the complete documentation index is available at https://rspress.rs/llms.txt, the full documentation bundle is available at https://rspress.rs/llms-full.txt.
由 llmsUI.injectLlmsHint 控制。作用很直接:代理即使是从单页进入的,也知道上面还有一个索引和一份全量包,不必靠猜路径。
6. robots.txt 实测:1,248 字节,一条有效指令都没有
这一项值得单独记。/robots.txt 返回 200、1,248 字节,但把非注释行过滤之后是空的:
User-agent指令数:0Allow/Disallow指令数:0Sitemap指针:0Content-Signal指令:0
文件内容全部是 Cloudflare Content Signals Policy 的注释说明,逐段解释 search、ai-input、ai-train 三个信号的语义,并援引欧盟 DSM 指令第 4 条。也就是说:这是一份"政策声明模板",不是一个生效的配置——放行一切,但没有显式声明任何取舍,也没给出 Sitemap 地址。
对 llms.txt 的启示:llms.txt 是"我愿意给你读"的意向表达,robots.txt 才是"你能不能读"的执行开关。两者必须同时成立。只写 llms.txt 却在 robots 里拦掉 ClaudeBot / OAI-SearchBot,是这类落地最常见的翻车(本站不属此列——它什么都没拦,也什么都没声明)。
结果
口径:单页 /guide/start/introduction,2026-11-07 本机 Invoke-WebRequest / .NET HttpClient 抓取,未压缩原始字节;未使用任何第三方审计工具打分。
| 指标 | HTML 通道 | Markdown 通道 | 变化 |
|---|---|---|---|
| 单页正文响应字节 | 100,761 | 17,294 | -82.8%(5.8×) |
| Content-Type | text/html; charset=utf-8 | text/markdown; charset=utf-8 | — |
| 导航/脚本混入 | 有(含 Monaco CDN 预加载等 6 条 preload) | 无 | — |
| 索引可用性 | sitemap.xml 35,191 字节 | llms.txt 19,479 字节 / 105 条 | — |
| 抽样可达性 | — | 105/105 链接为 .md;抽 10 条均 200 + text/markdown | 0 失败 |
| 不存在页面 | — | /guide/not-exist-page.md → 404 | 无 fallback 兜 200 |
定性观察(无量化数据,不给数字):llms-full.txt 54 万字节这个量级,实际消费方式是"下载后切片建索引"而非"塞进上下文",因此它的价值取决于下游是否做检索,而不是文件本身写得多全。作者文档里也把它定位成 full bundle,而非推荐入口。
复盘
3 条可直接抄的配置动作
让
.md、llms.txt、llms-full.txt在同一次构建里生成,不要用后置脚本补。 105 条链接必须与路由函数同源;后置脚本补出来的链接,路由一改就成批 404,而llms.txt里的死链代理不会去验证,只会照着读错。head 三件套一次挂全:
alternate type="text/markdown"给页面级入口,describedby给站点级索引,hreflang给多语言同页。只有llms.txt而没有alternate,代理抓到内页就断了入口。不存在的
.md要返回真 404(本站实测返回 404),别让 SPA fallback 把 404 兜成"200 + HTML 首页"。这种 200 会让代理把导航页当正文吃下去,比报错更坏——它不报错,它就开始编。
2 条踩坑提醒
协商响应没有
Vary: Accept。 同一个 URL 返回 HTML 还是 Markdown 完全取决于请求头,但响应头里没有任何Vary字段(实测协商响应与 HTML 响应的 header 列表里均无Vary)。当前Cache-Control: public, must-revalidate, max-age=0能缓解串味,但那是靠"不给缓存"绕过去的,不是正确解法。自建这套协商时,务必显式加Vary: Accept,否则中间层按 URL 缓存就会把 HTML 喂给接受 Markdown 的代理。HTTP
Link头里没有alternate/describedby。 响应Link头只有 6 条rel="preload"(JS 与图片),v2 的两个关系只存在于 HTML<link>标签里。后果是:只发 HEAD、或只解析响应头不解析 body 的代理,拿不到 Markdown 版本地址——双通道变成了单通道。
标注
llms.txt 版本:按 v2 形态记录(
.md协商 +rel="alternate" type="text/markdown"+rel="describedby"+llms-full.txt)。规范状态:
llms.txt目前是 llmstxt.org 社区提案(作者主张),未被任何 LLM 厂商确认为官方标准;llms-full.txt同理。文中所有判断均基于线上实际行为,不引用规范承诺。版本口径:rspress.rs 线上行为,2026-11-07 实测;Rspress 文档自述
llms自 v2.1.0 起默认开启(此前为false)。数据口径:本机抓取,未压缩原始字节;抽样而非全站(105 条链接抽 10 条验证可达性)。
来源:
https://rspress.rs/llms.txt、/llms-full.txt、/robots.txt、/sitemap.xml、/guide/basic/ssg-md、/guide/start/introduction(HTML 与.md)、/zh/llms.txt。复盘时间:2026-11-07。
标签:llms.txt、AI可见度、AIWO、结构化暴露、静态站点生成器