导语:框架站 DaloyJS 给全部文档页挂上 .md 兄弟文件 + 双通道 link relation,代理不用再猜路径;但缺失的 /llms-full.txt 返回 200 的 HTML 错误页,成了这条链路上最危险的一环。
背景:代理读文档,只能靠猜
DaloyJS 是一个 runtime-portable 的 TypeScript 框架(一套路由跑 Node / Bun / Deno / Cloudflare Workers / Vercel / Fastly / Lambda),文档站点同时承载三样东西:人类阅读的 HTML 页、给机器读的 API 目录(/api/v1、/openapi.json)、以及一个只读 MCP 端点 /mcp。
这个组合的痛点在代理侧很具体:
抓到一个
/docs/routing之后,代理不知道有没有更适合机器读的版本,只能把整页 HTML 连同导航、样式、脚本一起吞下去;文档被拆成上百页,代理不知道哪一页覆盖当前问题,也不知道该站的 llms.txt 在哪一级;
猜后缀(
.md、.txt、llms.txt)几乎必然错几次,错一次就多一轮 token 消耗。
站点的做法是把「机器可读版本」变成页面级的固定属性,而不是让代理去试探。
落地清单
1. 文件位置:两级 llms.txt + 每页 .md
| 路径 | 实测体积(2026-11-06) | 作用 |
|---|---|---|
/llms.txt | 89,945 字节 / 393 行 / 306 个链接 / 23 个 H2 分组 | 全站目录:文档、博客、包、安全与部署条目 |
/docs/llms.txt | 52,122 字节 / 318 行 / 239 个链接 / 21 个 H2 分组 | 只覆盖 /docs/ 子树 |
/docs/*.md | 单页 1.4 万字节量级(如 /docs/llms-txt.md 为 14,312 字节 / 140 行) | 每页的 Markdown 正文 |
/llms-full.txt | 不存在(见踩坑一) | — |
这不是「一个大文件塞全站」,而是「根文件做目录、子路径文件做分册、正文按页取」。代理拿到任意一个 URL 之后,往上一级能拿到覆盖它的 llms.txt,平级能拿到该页的 .md,不需要遍历。
2. llms.txt 原文片段
/docs/llms.txt 头部(真实抓取,未改动内容):
# DaloyJS documentation > Reference documentation for `@daloyjs/core`, a runtime-portable, contract-first > TypeScript web framework with built-in OpenAPI 3.1 generation, typed client codegen > (Hey API), and security-first defaults. It runs on Node.js, Bun, Deno, and Cloudflare Workers. This file covers the pages under `https://daloyjs.dev/docs/`. For the whole site, including the blog and the published packages, see https://daloyjs.dev/llms.txt. Every link below points at the markdown version of a docs page. Each markdown file lives at the page URL with `.md` appended (`/docs/routing` becomes `/docs/routing.md`); drop the suffix for the canonical HTML. Every docs page also advertises its markdown sibling with a `rel="alternate" type="text/markdown"` link relation, and points back here with `rel="describedby"`, on both the HTML `<link>` elements and the HTTP `Link:` response header. Agents that prefer structured tools over page fetches can query these same docs over the Model Context Protocol: `https://daloyjs.dev/mcp` is a read-only MCP server with `search_docs`, `get_doc`, and `list_docs` tools. ## Developer resources - [DaloyJS API docs](https://daloyjs.dev/docs/api-reference.md): complete public TypeScript surface. - [DaloyJS OpenAPI spec](https://daloyjs.dev/docs/openapi.md): generate OpenAPI 3.1 from routes. - [DaloyJS auth docs](https://daloyjs.dev/docs/auth.md): bearer auth and identity providers. - [DaloyJS webhooks](https://daloyjs.dev/docs/webhook-delivery.md): signed outbound webhook delivery. - [DaloyJS MCP server](https://daloyjs.dev/mcp): live docs MCP. Guide: https://daloyjs.dev/docs/mcp.md. ## Start here - [Introduction to DaloyJS](https://daloyjs.dev/docs.md): DaloyJS is a runtime-portable TypeScript web framework built around contract-first routing, Standard Schema validation, OpenAPI 3.1 generation, typed clients, and core security guardrails. Learn what makes it different. - [Installation](https://daloyjs.dev/docs/installation.md): Install DaloyJS with pnpm, npm, yarn, or bun.
根文件 /llms.txt 的分组(23 个 H2,按顺序):Developer resources、Project、Start here、Tutorials、Migrating、Core concepts、OpenAPI & typed clients、Advanced features、Observability、Security essentials、Attack protection、Hardening & operations、Outbound & webhooks、Data access、Database hosting、Email、Payments、Authentication、Deployment、Adapters & runtimes、Reference、Translations、Optional。
分组按「读者意图」切,不按站点目录树切——这是给代理用的索引,必须能按"我要做什么"跳转,而不是复刻 URL 结构。
3. 双通道 link relation:head 与响应头都发
llms.txt v2 要求页面自己声明「我的 Markdown 版是谁」「覆盖我的 llms.txt 是谁」。这个站点两边都发。
HTML <head> 实测:
<link rel="canonical" href="https://daloyjs.dev/docs/llms-txt"/> <link rel="alternate" type="text/markdown" href="https://daloyjs.dev/docs/llms-txt.md"/>
HTTP 响应头实测(HEAD https://daloyjs.dev/docs/llms-txt):
Link: </docs/llms-txt.md>; rel="alternate"; type="text/markdown", </docs/llms.txt>; rel="describedby", </.well-known/api-catalog>; rel="api-catalog", </openapi.json>; rel="service-desc"; type="application/vnd.oai.openapi+json;version=3.1"
为什么两边都要:只解析 HTML 的代理看 <link>,只做 HEAD 探测的代理看 Link 头。少一边,就有一半客户端退回猜路径。
4. 内容协商兜底:同一 URL 按 Accept 分流
除了 .md 后缀,还支持用请求头要 Markdown。实测:
curl -sI -H "Accept: text/markdown" https://daloyjs.dev/docs/llms-txt # 200, Content-Type: text/markdown; charset=utf-8, Vary: Accept, Accept-Encoding, 14,312 字节
14,312 与 /docs/llms-txt.md 的长度完全一致,说明两条路径指向同一份内容,不是两套渲染。
5. robots 配合项
# https://daloyjs.dev/robots.txt User-Agent: * Allow: / Sitemap: https://daloyjs.dev/sitemap.xml
全放行,没有给 GPTBot、ClaudeBot、OAI-SearchBot 单独开组。这里的判断是:既然发 llms.txt 的目的是让代理读,就不要再为爬虫做白名单维护;真正需要防的路径,要么在服务端不提供,要么在 robots 里写清楚,而不是把整个 /docs/ 拦掉却在 llms.txt 里列满 docs 链接——那是最常见的自相矛盾配置。
结果(2026-11-06 实测,非第三方复现)
| 检查项 | 结果 |
|---|---|
抽查 5 个 .md 兄弟文件(/docs/routing.md、/docs/auth.md、/docs/mcp.md、/blog.md、/docs/webhook-delivery.md) | 全部 200,Content-Type: text/markdown; charset=utf-8 |
Accept: text/markdown 协商 | 200,字节数与 .md 完全一致(14,312) |
页面级 rel="alternate" type="text/markdown" | 存在(HTML <link> 与 HTTP Link 头同时存在) |
页面级 rel="describedby" | 存在(指向 /docs/llms.txt) |
根 llms.txt 体积 | 89,945 字节 / 393 行 |
至于「AI 引用变多、支持工单下降」这类效果数字,本站没有任何对外口径可查,这里不编。能确认的只有机制层面的一件事:代理从一个 URL 出发,可以同时拿到「机器可读正文」和「覆盖它的目录文件」,不用猜后缀、不用遍历目录。
一个值得记录的边界:根 llms.txt 的博客条目仍然指向 HTML 路径(如 https://daloyjs.dev/blog/scaffolding-a-production-ready-daloyjs-app-in-60-seconds),而 /blog.md 实测存在且可用。也就是说「按页给 .md」这件事在 docs 段执行得很彻底,在博客段没有完全对齐——这是实测观察,不是配置错误清单。要抄这套做法,最好一次判定全站哪些路由族必须有 .md,避免同一份 llms.txt 里两种链接形态并存。
复盘
3 条可直接抄的配置项
根文件只做索引,子路径文件做分册。 根
/llms.txt声明版本、入口、"全站在哪";/docs/llms.txt只列该子树。代理先读小文件定位,再取分册,避免一次吞下全站。每条链接直接指向最终形态。 写
.../routing.md,不要写.../routing让代理自己拼后缀。v2 的链接关系是给机器省推理的,不是给人省字。同一关系双通道发布。 HTML
<link rel="alternate" type="text/markdown">与 HTTPLink:响应头同时给,rel="describedby"同时指回 llms.txt;再把api-catalog、service-desc这类机器可读入口一并挂上,一个响应里把「正文 / 目录 / API 描述」全交代完。
2 条踩坑提醒
缺失的
llms-full.txt会返回 200 的 HTML。 实测/llms-full.txt返回200 + text/html、41,906 字节的 Next.js 错误页;/docs/llms-full.txt同型号,91,641 字节。代理侧判断"200 就是拿到了",会把一整页 HTML 当 Markdown 读进来。如果站上确实不做 llms-full,路由层要显式返回 404,别让 SPA / 框架的 fallback 兜成 200。Vary头 HEAD 与 GET 不一致。 同一个 URL:HEAD响应的Vary只有rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch,不含Accept;带Accept: text/markdown的GET才返回Vary: Accept, Accept-Encoding。做内容协商的站点如果按 HEAD 结果配置 CDN 缓存键,HTML 与 Markdown 会互相串味。上线后用 GET 复核,不要只看 HEAD。
顺带一条体量判断:89,945 字节的根 llms.txt 按 4 字符/token 粗估约 2.2 万 token,v2 反复强调的"文件本身要小、让代理按需跟随链接"在这个体量上已经不成立。分册是对的,但根文件仍需要继续瘦身——把博客、包、迁移记录这些低频段拆出去,是下一步最省事的一刀。
标注
llms.txt 版本:v2(页面自行声明 "Following llms.txt v2",含
rel="alternate" type="text/markdown"+rel="describedby"双通道)规范状态:llms.txt 由 Jeremy Howard 于 2024-09-03 提出(v2 修订于 2026-08-10),属作者主张的约定,未被 OpenAI / Anthropic / Google 等 LLM 厂商标记为官方标准;站方是否真去读、读哪段,取决于各客户端实现
来源:https://daloyjs.dev/llms.txt、https://daloyjs.dev/docs/llms.txt、https://daloyjs.dev/docs/llms-txt(页面 + 响应头)、https://daloyjs.dev/robots.txt;规范文本 https://llmstxt.org/
实测时间:2026-11-06
数据口径:全部为本站当日直接抓取(
Invoke-WebRequest/curl)所得,非第三方复现;无排名、无引用量数据
标签:llms.txt、AI可见度、AIWO、结构化暴露、开发者工具