DSH / Atlas
2026-08-20implementedprocess

Raw-Markdown twins and llms.txt on the documentation site

文档站的纯 Markdown 孪生页与 llms.txt

The documentation site serves rendered HTML only, so an agent reading the docs has to scrape VitePress markup or fall back to the repository, where links and image references follow source layout rather than public routes. Claude's platform documentation set the convention this feature adopts: append `.md` to any page URL for the page as raw Markdown, with a root `llms.txt` as the agent-facing index. [The site projec

Source compatibility: Chinese title uses a compatible non-canonical header

English

Problem

The documentation site serves rendered HTML only, so an agent reading the docs has to scrape VitePress markup or fall back to the repository, where links and image references follow source layout rather than public routes. Claude's platform documentation set the convention this feature adopts: append .md to any page URL for the page as raw Markdown, with a root llms.txt as the agent-facing index. The site projection rewrites every page's links for the public site already, so the gap was serving that projection as plain Markdown.

Decision

vitepress build ends by emitting a raw-Markdown twin of every published route into the build output. emitRawMarkdownPages runs the same manifest-and-projector pass that fills website/.generated/, but writes <outDir>/<route> with raw page content: no editSource/outline projection frontmatter, no locale-home truncation — frontmatter is VitePress rendering configuration, so the twin drops it and keeps the body — and the same repository-chrome stripping as the rendered site. Referenced images are copied beside the twins.

One projection serves both trees because its site-internal links are relative. ./sibling.md renders as a clean URL on the HTML site and resolves file-to-file in the raw tree, so the twins need no second link-rewriting mode. Every route is emitted, including the frontmatter-only locale homes, because published pages link to them and the raw tree must stay link-closed; a spec walks every emitted relative link to pin that closure.

An index route renders as a directory URL, so "append .md" lands on <dir>.md once the trailing slash is dropped; each index route therefore also emits a parent-level alias twin at that path. The alias is not a copy — a copied index.md would carry its relative links one directory too high — but its own projection over the alias route, resolved against the canonical manifest so links keep targeting canonical twins. The root home has no parent to alias into; / is documented as /index.md. A twin or image may never overwrite a file the build already carries, such as a public/ copy; a name collision fails the emission.

llms.txt is generated from the publication manifest at the site root: both locale trees in sidebar order, one - [label](<base><route>): <section> row per page, links site-absolute under the deploy-time DOCS_BASE. Locale homes stay out — the file itself is the agent entry point.

The dev server serves the same surface for navigations and header-less clients. A middleware in the doc-projector plugin projects .md requests from their canonical sources per hit and generates llms.txt on demand, so docs:dev matches production without a rebuild; an in-page fetch() (Sec-Fetch-Dest: empty) deliberately still reaches Vite in dev, while production static hosting answers it with the raw file.

verify-doc-site-fragments, the post-build gate, fails the build when any route's twin or llms.txt is missing from the output, so deleting the buildEnd wiring cannot pass CI.

Alternatives considered

vitepress-plugin-llms. Actively maintained, MIT, used by the Vite, Vue, and Vitest sites; it generates per-page Markdown, llms.txt, llms-full.txt, and dev-server responses. Two hard-coded behaviors break this site. It flattens dir/index.md to dir.md with no opt-out while leaving in-page links unrewritten, which 404s every relative link into a section landing page — this site publishes seven index routes per locale. And it rewrites image references to root-absolute hashed bundle paths without the site base, which 404s on this subpath GitHub Pages deployment; its adopters deploy at domain roots and never hit either. Its llms.txt also reads sidebars only from the top-level theme config, not this site's per-locale ones. Correcting all that means a fork or a post-processing layer coupled to plugin internals — more owned surface than the small emitter reusing the tested projector.

Emit through Vite's publicDir. Vite supports one public directory and the site already points it at the tracked website/public/; generated twins would land in a tracked tree the layout gate forbids.

Absolute links inside the twins. platform.claude.com links absolutely because its host is fixed. This site's base varies between local / and the Pages subpath, and the projector's relative links resolve in both trees as they are, so absolute rewriting would add a second link grammar without improving resolution.

Skip locale homes in the raw tree. Published pages link to docs/user/index.md, so omitting the home routes breaks link closure. The body that survives chrome stripping (the H1) costs nothing, and the redirect frontmatter means nothing outside VitePress.

Consequences

Agents fetch any page as plain Markdown by dropping the URL's trailing slash and appending .md, and discover the whole set at /llms.txt; the rendered site is unchanged. The build output carries one extra Markdown file per route, an alias per index route, and image copies beside them — kilobytes against the bundled assets. Twin content keeps GitHub-style heading text while the rendered site slugs punctuation-heavy headings differently; agents resolve headings themselves, so no gate covers raw-tree fragments. Deferred as unneeded for the twins' audience: llms-full.txt, and a per-page "view as Markdown" control, which would require a theme directory the stock-theme site deliberately lacks.

中文

Problem

文档站只提供渲染后的 HTML,agent 读文档要么抓取 VitePress 标记,要么退回仓库源文件,而后者的链接和图片引用跟随源码布局而非公开路由。Claude 平台文档确立了本特性采纳的约定:任意页面 URL 加 .md 后缀即得到该页的原始 Markdown,站根 llms.txt 作为面向 agent 的索引。站点投影本就为公开站点重写每页链接,缺的只是把这份投影以纯 Markdown 形式对外提供。

Decision

vitepress build 结束时向构建输出发射每个已发布路由的纯 Markdown 孪生页。emitRawMarkdownPages 复用填充 website/.generated/ 的同一趟 manifest 加投影器流程,但以原始页面内容写入 <outDir>/<route>:不带 editSource/outline 投影 frontmatter,不做 locale 首页截断——frontmatter 是 VitePress 的渲染配置,孪生页丢弃它并保留正文——仓库版式(语言切换行、徽章)的剥离与渲染站一致。页面引用的图片复制到孪生页旁边。

一份投影同时服务两棵树,因为站内链接是相对路径。./sibling.md 在 HTML 站渲染为 clean URL,在原始树中按文件对文件解析,孪生页不需要第二套链接改写模式。所有路由都被发射,包括仅有 frontmatter 的 locale 首页:已发布页面链接到它们,原始树必须保持链接封闭;一个 spec 遍历发射树中的每条相对链接来钉住这条闭合性。

index 路由在渲染站上呈现为目录 URL,"加 .md"在去掉末尾斜杠后落在 <dir>.md 上;因此每个 index 路由还发射一个父级别名孪生页。别名不是拷贝——拷贝的 index.md 会让相对链接整体上移一层——而是以别名 route 为基准的独立投影,链接解析仍针对 canonical manifest,始终指向 canonical 孪生页。根首页没有可放别名的父级;/ 在文档中写明用 /index.md。孪生页与图片一律不得覆盖构建目录中已存在的文件(例如 public/ 副本);同名冲突使发射失败。

llms.txt 由发布 manifest 生成于站根:两棵语言树按侧边栏顺序排列,每页一行 - [label](<base><route>): <section>,链接为携带部署期 DOCS_BASE 的站内绝对路径。locale 首页不列入——这个文件本身就是 agent 的入口。

开发服务器对导航与无头客户端提供同一表面。doc-projector 插件中的 middleware 逐请求从 canonical 源投影 .md 请求并按需生成 llms.txtdocs:dev 无需重建即与线上一致;页面内 fetch()Sec-Fetch-Dest: empty)在 dev 下刻意仍交给 Vite,而生产静态托管对它返回原始文件。

构建后门禁 verify-doc-site-fragments 在任一路由的孪生页或 llms.txt 缺失时判定构建失败,删掉 buildEnd 接线无法通过 CI。

Alternatives considered

vitepress-plugin-llms 维护活跃、MIT 协议,Vite、Vue、Vitest 官方站点在用;能生成每页 Markdown、llms.txtllms-full.txt 和开发服务器响应。但两个硬编码行为在本站产生破坏。它把 dir/index.md 展平为 dir.md 且无法关闭,同时不改写页内链接,所有指向栏目落地页的相对链接全部 404——本站每个语言树发布七个 index 路由。它还把图片引用改写为不带站点 base 的根绝对哈希资产路径,在本站的子路径 GitHub Pages 部署下必然 404;其采用者都部署在域名根,两个问题都碰不到。其 llms.txt 只认顶层 theme 配置的侧边栏,认不出本站按 locale 存放的侧边栏。修正这些意味着 fork 或一层耦合插件内部行为的后处理——比复用已测投影器的小发射器拥有更多自有面积。

经 Vite publicDir 发射。 Vite 只支持一个 public 目录,本站已将其指向被跟踪的 website/public/;生成的孪生页会落入布局门禁禁止的跟踪树。

孪生页内用绝对链接。 platform.claude.com 用绝对链接是因为其主机固定。本站 base 在本地 / 与 Pages 子路径之间变化,而投影器的相对链接在两棵树中原样可解析,绝对化改写只会引入第二套链接语法而不改善解析。

原始树跳过 locale 首页。 已发布页面链接到 docs/user/index.md,省略首页路由会破坏链接封闭。剥离版式后剩下的正文(即 H1)成本为零,重定向 frontmatter 在 VitePress 之外没有意义。

Consequences

Agent 把页面 URL 去掉末尾斜杠再加 .md 即获取该页纯 Markdown,并在 /llms.txt 发现全集;渲染站不变。构建输出为每个路由多带一个 Markdown 文件、每个 index 路由多带一个别名,外加旁置图片副本——相对打包资产只是千字节级。孪生页保留 GitHub 风格的标题文本,而渲染站对含标点的标题使用不同 slug;agent 自行解析标题,因此没有门禁覆盖原始树的 fragment。因孪生页受众不需要而暂缓:llms-full.txt,以及每页的"查看 Markdown"控件——后者需要 stock-theme 站点刻意不设的 theme 目录。