AI智能体读网页的方式很贵:先下载整页HTML,再把导航、脚本和样式全部丢掉,试图从残渣里还原正文。现在有一部分智能体开始发送 Accept: text/markdown,服务器愿意给Markdown,它们就直接收下。
有人想让自己的落地页也能回应这种请求,但不想再手工维护一份Markdown副本、还得时刻和站点同步。站点是放在Cloudflare Worker后面的静态文件,于是转换被放在Worker出口处完成:一个约160行、零依赖、基于HTMLRewriter的模块。
第一步:先决定什么时候转换
规则被刻意收得很窄。只有同时满足几个条件才转换:请求方法是GET、客户端确实要Markdown、响应成功、且内容类型是HTML。
图片、CSS、站点地图以及其他一切资源都原样放行,哪怕请求头里偏好Markdown。浏览器从不发送text/markdown,所以普通访客拿到的仍然是HTML。
判断函数会正经解析Accept头,而不是在里面搜子串:先按逗号切分媒体范围,找到text/markdown,再读取q值。没有q参数视为接受,q值大于0才接受。
第二步:把HTML映射成Markdown
转换器维护一个skippedDepth计数器,用来标记那些整棵子树都不该出现的元素。页面外壳一律跳过:head、nav、footer、script、style、noscript、svg、button、template,以及任何带hidden、aria-hidden="true"、role="navigation"或skip-link类的元素。
这类元素打开时深度加一,onEndTag时减一,文本回调只在深度为零时写入。这里有个容易漏掉的细节:像br、img这样的空元素没有结束标签,若给它们注册onEndTag,计数器会卡住,页面后半部分会被静默丢弃。
映射本身很常规:标题加#前缀,段落和分节变成空行,strong和em变成**和*,pre变成围栏代码块。两处需要小心:
链接被改写成绝对URL,且只保留http、https、mailto和tel。相对路径/privacy会变成https://.../privacy,智能体能跟过去;javascript:链接直接丢弃。
图片只有在同时具备alt文本和可解析src时才出现。没有alt的装饰性图片,对智能体没有任何可用信息。
文本节点会解码实体(&、<、数字实体),空白被折叠。流结束后再跑一遍清理:去掉换行两侧的空格,连续空行压到一行。
第三步:保留元数据
head里对智能体有意义的部分被保留下来,只是不再以HTML形式。独立的处理器收集title、meta description、canonical URL,以及所有application/ld+json块的文本。输出以front matter开头:
title
description
url
JSON-LD追加在末尾的“## Structured data”标题下。搜索引擎能看到的结构化数据,比如FAQ和产品详情,同样能抵达一个从不解析HTML的智能体。
第四步:修正响应头
这一步跳过就会弄坏缓存。正文变了,从HTML资源复制来的几个头现在都是假话:content-type要改成text/markdown; charset=utf-8;content-length、content-encoding、content-md5、etag、content-range、accept-ranges、digest全部删除。
Vary: Accept是防止共享缓存把Markdown版本发给下一个请求同一URL的浏览器;丢掉旧ETag则避免另一种错误——条件请求被回以304,却指向HTML正文。
整个转换包在try/catch里。任何环节抛错,原始HTML响应照常发出。智能体拿到HTML是降级结果,拿到500是坏掉的结果。
验证与边界
用curl带上Accept: text/markdown请求同一URL,前20行就是Markdown;同一URL在浏览器里仍然返回正常页面。
转换器是针对这个站点的语义化HTML调的,源码注释里也这么写。表格输出为竖线分隔的行,但没有|---|表头分隔线,严格来说不算合法的Markdown表格。加载后由JavaScript渲染的内容不会出现,因为Worker只看得见静态HTML。组件丰富或表格复杂的站点,更适合用真正的HTML转Markdown库,代价是打包一个依赖。
对于一个由标题、段落、列表和FAQ组成的营销页,160行HTMLRewriter够用了,而且只有一份事实来源。
热门跟贴