响应头和meta打架的时候,赢的从来不是更严的那条
本文目录
- 同一件事写在两层,谁说了算?
- 造两个互相矛盾的页面
- 为什么必须在本机测
- 这不是理论,样本里就有4个
- 对照组:把上游那条撤掉会怎样
- 那82份不带编码的响应头呢
- 一个页面能不能被嵌进iframe,是怎么判出来的?
- 六格实验
- 第一格是全场最反直觉的一格
- 第五格和第六格:写在HTML里的那两条,从头到尾没生效
- 第四格为什么必须做
- 把六格结论排成一条链
- 安全头这一层还有别的连带伤害
- meta http-equiv到底能不能当响应头用?
- 规范认的就那么几个
- 真实站点上都写了些什么
- 拼错的那两条更有意思
- 缓存那三条为什么特别常见
- 响应头那一层还有多少字段是没人读的
- 那69个还在发IE兼容声明的站,在等谁?
- 它对应的浏览器已经退役四年多了
- 为什么是69个站而不是6个
- 它和"写错层"是同一类问题的两面
- 删掉它要冒什么风险
- 同一类东西还有哪些
- 响应头这一层,实际有多少人在用?
- 三个近乎为零的字段
- 三个零凑在一起说明什么
- 真正有存在感的是安全那几条
- 344这个数字值得盯一眼
- 这些头是谁配的,站点自己多半不知道
- 响应头这一层能做而HTML做不到的事
- 站点、页面、响应,三层规则的方向性是什么?
- 先把三层的分工说清楚
- 下层锁了门,上层的信就送不出去
- 真实站点上这么干的多不多
- 反过来那种组合更常见,但不要紧
- 三层各自的失败模式不一样
- 拦AI爬虫这件事同样横跨三层
- 响应头那一层是唯一能管住没有head的资源的
- 新冒出来的那两层,会不会重演同一件事?
- 这张表里有三种不同的"没人读"
- 129个站在写一条Google不读的指令
- 新的两层正在重复老路
- 判断一层新声明值不值得跟,问四件事
- 还有一类不是新层,是老层换了读者
- 哪一层该写什么,有没有一张对照表?
- 这张表的用法
- 排期的时候按什么顺序做
- 语言那一行容易被误解
- 怎么在自己站上把两层对一遍?
- 先问自己三个问题
- 先把两层拉出来放在一起
- 查那个已经失效的防嵌套字段
- 把meta里那些不算数的挑出来
- 批量对一批地址
- 把响应头这一层的变化盯起来
- 验证防嵌套到底生不生效
- 改完之后怎么确认真的生效了
- 常见问题解答
- 响应头和meta都写了编码,会不会有冲突风险?
- X-Frame-Options现在还需要写吗?
- meta里的CSP完全没用吗?
- 为什么规范网址的响应头形式一份都没有?
- 抓取间隔这条指令要删掉吗?
- 给AI代理的那份清单文件,现在值得做吗?
- 为什么响应头里的机器人指令几乎没人用?
- 写在meta里的页面刷新指令还能用吗?
- 层级这套判断,对结构化数据也适用吗?
- 这些结论对Bing和其他搜索引擎适用吗?
- 为什么样本里meta形式的CSP在内页是0,首页却有11个?
- 如果CDN和源站都发了同一个头,会怎样?
- 怎么知道我的响应头是哪一层加的?
- 权威参考资料
摘要:同一条禁令写在响应头和HTML里,浏览器只听响应头的。更反直觉的是同为响应头的两条:X-Frame-Options写着DENY,旁边一条CSP写着允许同源,实测页面照样被嵌进了iframe——更严的那条输给了层级更高的那条。674份页面配着响应头对账下来,403份带X-Frame-Options,其中344份被同一响应里的CSP架空;69个站还在发为已退役浏览器准备的兼容声明。文中给出六格iframe裁决实验、规范只承认的那几个pragma、三层规则的方向性与一张按层归位的对照表。
8月18日凌晨,给AI代理看的那份索引文件出了第二版,Search Engine Journal报道说它加进了正式的Markdown链接规则,方便代理顺着文件往下找内容。
又多了一层。
一个网站对外说话的地方本来就不少:站点根目录的规则文件、页面里的meta、HTTP响应头,现在再添一份给AI代理的清单。这些层各说各的,多数时候相安无事,直到某两层说了相反的话。
那时候谁赢?
很多人的直觉是"更严格的那条赢",或者"更具体的那条赢"。保哥用一组自建实验和674份配着响应头的真实页面对了一遍账,结论跟直觉差得挺远:赢的既不是更严的,也不是更具体的,而是更靠近传输链路上游的那一条。而上游那条,通常不是你写的。
同一件事写在两层,谁说了算?
先从最容易验证的一层开始:字符编码。它可以写在HTTP响应头里,也可以写在HTML的meta里。
造两个互相矛盾的页面
本机起一个最小的PHP服务,两个测试地址:
第一个,响应头声明charset=iso-8859-1,HTML里写<meta charset="utf-8">,正文放一段中文。
第二个反过来,响应头声明charset=utf-8,HTML里写<meta charset="iso-8859-1">,正文同样是那段中文。
加载之后读document.characterSet:第一个返回windows-1252,中文变成层级è£å†³;第二个返回UTF-8,中文正常。
两个方向都指向同一个结论:响应头赢,而且赢得干净利落,HTML里那条连争一下的机会都没有。
为什么必须在本机测
第一版实验放在线上服务器跑,做不出"响应头不带编码"这个场景——PHP自己会补一个,nginx还会再补一个。想造一个干净的对照组,就得找一台中间层最少的机器。
这件事本身就是本文的注脚:你以为响应头是你写的,实际上它是一条流水线的产物,你只是其中一环。
这不是理论,样本里就有4个
168个首页配上同期抓到的响应头,147份带编码声明,其中143份写UTF-8,4份写的是ISO-8859-1。这4个站眼下没出事,因为它们的内容基本是拉丁字符。哪天上了一个带重音符号的产品名,或者接了一批中文评论,问题就来了——而排查的人多半会先去翻HTML,翻半天发现那里写的明明是UTF-8。
对照组:把上游那条撤掉会怎样
光测"两层打架谁赢"还不够,得确认另一件事:赢的那一条是不是真的在起作用,会不会是浏览器自己猜出来的。
所以又做了两个对照页:一个响应头不带编码、HTML里也一条不写,正文照样放中文;另一个响应头不带编码、HTML里写gbk,正文实际是UTF-8字节。
第一个返回windows-1252,中文乱码。第二个返回GBK,乱成另一副样子。浏览器不做内容识别,它只认声明;没有声明就用默认值兜底。这两组一跑,前面那个"响应头赢"的结论才算立住——不然完全可能是浏览器按内容猜对了,跟谁声明的没关系。
那82份不带编码的响应头呢
674份配对样本里,592份响应头带编码,82份不带。这82份完全靠HTML里那条活着。它们目前也没出事,因为编码声明都写在head很靠前的位置。
这里有个隐蔽的依赖关系:同一个页面里重复声明谁生效那篇量过,有33个站把编码声明挤到了1024字节之后——而这33个站的响应头,全都带着编码。两组配置各自成立,没有一个站是靠运气活着的,但这种巧合本身就说明它们依赖的是平台的默认行为,不是自己的判断。
一个页面能不能被嵌进iframe,是怎么判出来的?
编码那一层是"两层",只有两个参赛者。真正热闹的是防嵌套:同一件事可以写在三个地方,而且它们的关系不是简单的覆盖。
六格实验
做了六个测试页,每个页面声明一种组合,然后从同源的父页面用iframe去嵌,看能不能渲染出内容。
| 页面 | 它声明了什么 | iframe里能看到内容吗 |
|---|---|---|
| 第一格 | 响应头 X-Frame-Options: DENY + 响应头 CSP: frame-ancestors 'self' | 能 |
| 第二格 | 响应头 X-Frame-Options: SAMEORIGIN + 响应头 CSP: frame-ancestors 'none' | 不能 |
| 第三格 | 只有响应头 X-Frame-Options: DENY | 不能 |
| 第四格(对照组) | 什么都不写 | 能 |
| 第五格 | <meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none'"> | 能 |
| 第六格 | <meta http-equiv="X-Frame-Options" content="DENY"> | 能 |
第一格是全场最反直觉的一格
这个页面同时写了两条防护:一条说谁都不许嵌,一条说同源可以嵌。结果是同源嵌进去了。
写DENY的人本意是最严格的封锁,他大概不知道旁边那条CSP会把自己整个作废掉。MDN在这个字段的说明里写得很直白:如果响应里同时存在CSP的frame-ancestors指令,X-Frame-Options会被忽略。不是取交集,不是取更严的,是直接忽略。
第三格是这条规则的反证:把CSP那条撤掉,只留DENY,页面立刻嵌不进去了。同一个DENY,有邻居的时候是废纸,没邻居的时候是铁门。
第五格和第六格:写在HTML里的那两条,从头到尾没生效
第五格用meta的形式写CSP,指令是frame-ancestors 'none',看起来跟第二格一样严。实测能嵌。原因是CSP规范明确规定,meta形式的策略不支持frame-ancestors、report-uri和sandbox这三个指令,写了直接忽略。
第六格更彻底:X-Frame-Options从来就不是一个合法的pragma,把它写进meta等于写了一行注释。
这两格加起来说明一件事:同一句话,写在响应头里是命令,写在HTML里可能什么都不是。
第四格为什么必须做
第四格什么都不声明,页面被顺利嵌进来。这一格看起来毫无信息量,但它是整组实验的地基。
如果第四格也嵌不进去,说明拦截来自别的地方——父页面的策略、浏览器的设置、或者测试代码本身写错了。那样的话前面三格的"不能"就全是假的。做实验最怕的就是这个:你以为在测A,实际上在测B,而两者恰好给出同样的现象。
这也是给测量补一组基线之后假阳性从74%掉到2.2%那次踩过的坑,教训是一样的:先证明你的尺子在没有被测对象的时候读数为零。
把六格结论排成一条链
六格跑完,防嵌套这件事的层级关系就很清楚了:
CSP响应头 > X-Frame-Options响应头 > meta里的任何写法(等于零)。
注意中间那个大于号的含义跟通常的"覆盖"不太一样。它不是"两条都读、CSP优先",而是"只要CSP里出现了frame-ancestors这个指令,X-Frame-Options这条头就当不存在"。哪怕CSP里写的是允许所有人嵌,X-Frame-Options的DENY也不会被拿来补位。
另一个容易混的点:这条规则只在frame-ancestors指令出现时触发。如果响应里有CSP、但CSP里没写frame-ancestors,那X-Frame-Options照常生效。样本里165份响应属于"只有frame-ancestors没有老字段",这些站是干净的;344份两条都有,老字段在里面纯属陪跑。
安全头这一层还有别的连带伤害
防嵌套只是加固清单里的一条。同一份清单里还有一条管特性权限的头,加错了会把嵌进来的地图、支付按钮一起变成摆设——照着加固清单加的一行响应头把第三方挂件全废掉那篇量过这种静默失败。它和本文说的层级问题是同一类:改的人看得见自己那一行,看不见它跟别处的相互作用。
站点上有支付流程的话,CSP这一层还得单独看一遍。53个支付页里45个配了CSP、真正管住脚本的只有5个,那组数据说明配了不等于配对了。
meta http-equiv到底能不能当响应头用?
这个属性的名字起得很有误导性。http-equiv字面意思是"等价于HTTP",很多人理解成"在这里写等于在响应头里写"。
实际上它只是一个受严格限制的白名单。
规范认的就那么几个
HTML规范列出的pragma指令一共只有几个:内容语言(已标记为废弃)、内容类型、默认样式、刷新、Cookie(已废弃)、IE兼容模式、内容安全策略。除此之外的任何写法,浏览器都不会当成响应头处理。
换句话说,缓存控制、过期时间、机器人指令、防嵌套,这几件在响应头里天天用的事,写进meta全都不算数。
真实站点上都写了些什么
| 写在meta里的pragma | 首页样本(168) | 内页样本(674) | 响应头里也有同名字段 | 规范承认吗 |
|---|---|---|---|---|
| IE兼容模式 | 85条 | 401条 / 69站 | 0篇 | 承认 |
| 内容类型 | 24条 | 24条 / 6站 | 24篇 | 承认 |
| 页面刷新 | 17条 | — | — | 承认 |
| 内容安全策略 | 11条 | 0条 | — | 承认(部分指令无效) |
| 内容语言 | 3条 | 37条 / 8站 | 6篇 | 已废弃 |
| 缓存控制 | 3条 | 6条 / 1站 | 6篇 | 不承认 |
| Pragma | 3条 | 6条 / 1站 | 5篇 | 不承认 |
| 过期时间 | 3条 | 6条 / 1站 | 5篇 | 不承认 |
| 字体渲染开关 | 1条 | 6条 / 1站 | 0篇 | 不承认 |
| 客户端提示委派 | 1条 | 6条 / 1站 | 0篇 | 不承认 |
| 来源试用令牌 | 1条 | 6条 / 1站 | 0篇 | 不承认 |
| 拼错的兼容模式 | 2条 | 5条 / 1站 | 0篇 | 不承认 |
缓存那三条最典型。写Cache-Control、Pragma、Expires的那个站,响应头里恰好也有对应字段——所以它的缓存行为完全正常,meta里那三行纯粹是摆设。摆设不碍事,但它会让下一个接手的人以为缓存是在这里配的。
拼错的那两条更有意思
有个站把兼容模式写成了x-ua compatible,连字符掉了。这种拼错不报错、不生效、也不会有人来提醒你。同一份样本里还有几个私有字段:字体渲染开关、客户端提示委派、来源试用令牌——它们不在规范的白名单里,浏览器要么另有专门的处理路径,要么直接无视。
缓存那三条为什么特别常见
把缓存策略写进meta是上个时代留下的习惯。二十年前有些浏览器确实会读这几个,后来标准收紧,它们就彻底出局了。但这段代码有极强的生命力——它在无数篇老教程里躺着,一代代复制到新项目。
要判断自己站上的缓存到底由谁说了算,看响应头就够了。浏览器缓存头怎么配才不犯改了不更新的事故那篇把这一层的字段关系讲全了;如果前面还挂着CDN,边缘缓存的回源与TTL分层那一套决定了最终生效的是哪一份配置。
还有一种更隐蔽的情况:响应头里的缓存字段根本不是你写的。PHP会在开启会话时自动写一行1981年的过期时间,把整个CDN架空,而应用代码里一个字都没提缓存。这跟meta写了不算数是同一件事的两面——一面是你写了没人读,另一面是没写却有人替你写了。
响应头那一层还有多少字段是没人读的
跨层的账算完,同一层内部也值得清一遍。248个死字段里多数不是站点自己写的,那批数据和本文的meta普查是对称的:一边是HTML里写了一堆响应头不认的东西,另一边是响应头里带着一堆没有接收方的字段。
那69个还在发IE兼容声明的站,在等谁?
内页样本里401条兼容模式声明,分布在69个站上,占有配对响应头的113个站的61.1%。
它对应的浏览器已经退役四年多了
这个字段是给IE看的,告诉它用哪个版本的引擎渲染。微软的生命周期公告写着IE11在2022年6月15日停止支持,此后桌面版被彻底停用。
今天还在发这一行的页面,等的是一个已经不来的读者。它不占带宽(一行几十字节),不影响渲染,也不会有任何工具报警——所以它会一直待下去。
为什么是69个站而不是6个
这个比例高得有点反常。同一批站里,用最新前端框架、跑着服务端渲染、CSP配得一丝不苟的站,head里照样躺着这一行。
原因大概有三层。最外面一层是模板继承:站点从某个起步模板拉的代码,那份模板可能是2015年写的。第二层是代码审查的盲区——评审会看逻辑、看样式、看接口,很少有人逐行看head里的meta。第三层最根本:这行代码从来没有失败过。它不报错、不告警、不影响任何测试用例,没有任何一个环节会把它标出来。
软件工程里有个说法,代码不会因为放着不动就腐烂。这句话对逻辑代码成立,对声明式的配置不成立——因为它的正确性不取决于它自己,取决于外面那个世界还认不认它。
它和"写错层"是同一类问题的两面
兼容模式这一条其实是合规的:规范承认它,写在meta里也是它的标准位置。它的问题不是写错了层,而是那一层的读者走了。
把这两类放在一起看会清楚很多:一条声明失效有两种方式,要么它写在了不该写的层,要么它写对了层但那层已经没人在读。前一种是即时失效,后一种是慢慢失效。样本里两种都有,而且体量差不多。
删掉它要冒什么风险
基本没有。这个字段只对IE和Edge的IE模式有意义,前者已经停用,后者是企业内网的兼容方案,跟公开的电商站没什么关系。
真要谨慎,可以先看一眼自己站的访问日志里还有没有IE的痕迹。样本里这69个站显然没人做过这件事——它们连是否还有IE访客都不知道,只是那行代码一直在模板里,没人动过。
一行几十字节,删不删都不影响生意。它的价值在于当成一个信号:如果这一行还在,说明这个站的head已经很久没有人系统清理过了。顺着它往下找,通常还能挖出别的陈年配置。
同一类东西还有哪些
样本里能找到的历史遗迹不止这一个:keywords字段28个站还在写,Google早就宣布不用了;微软自家的磁贴配置字段24个站;苹果的Web应用状态栏字段一堆。它们的共同点是当年某个平台需要,后来那个平台变了,代码留下了。
清理这些东西的收益不在性能——几十行meta对加载速度的影响可以忽略。收益在于降低下一次排查的噪音。head里干净的站,出问题的时候两分钟能看完;塞了四十条声明的站,光分辨哪些还有用就得半天。
响应头这一层,实际有多少人在用?
既然响应头压过HTML,那用响应头下指令应该是个好办法。674份带响应头的页面对了一遍账,结果有点出人意料。
三个近乎为零的字段
机器人指令的响应头形式:674份里只有6份带了,而且内容全是允许抓取。带这个头的页面,HTML里一条机器人指令都没写,两层没有一处交叉。
规范网址的响应头形式:674份里0份。这个字段本来是为PDF、图片这类没有head的资源准备的,HTML页面上确实用不着,但0这个数字还是说明了它有多冷门。
编码声明与HTML的冲突:592份响应头带编码,与HTML里的声明不一致的有0份。两层说的完全一样。这也是个负结果——跨层冲突在真实世界里比想象的少,多数时候两层根本没同时说话。
三个零凑在一起说明什么
规范网址响应头0份、编码跨层冲突0份、机器人指令响应头6份——这三个数字放在一起,得出的结论跟本文的主线有点矛盾:既然跨层冲突这么罕见,那讲层级优先还有什么意义?
意义在于,罕见不等于不存在,而且它的分布是极不均匀的。编码那一层冲突为0,是因为绝大多数站点两层写的都是UTF-8——这个共识是二十年才建立起来的。换个字段就没这么幸运:防嵌套那两条同时存在的有344份,占样本的一半以上。
更实际的一点是,跨层冲突少,恰恰是因为多数人根本不知道有另一层可以写。不是配置得好,是没配。等到哪天要用响应头做点什么的时候,才会撞上这些规则。
真正有存在感的是安全那几条
| 响应头 | 674份里出现 | 说明 |
|---|---|---|
| 内容安全策略 | 521份 | meta形式在内页样本里是0 |
| X-Frame-Options | 403份 | 其中344份同时有frame-ancestors |
| 只有frame-ancestors | 165份 | 没写老字段,直接用新的 |
| 内容语言 | 320份 | 与html的语言属性主语言不同的12份 |
| 内容类型带编码 | 592份 | 82份不带 |
344这个数字值得盯一眼
403份响应带着X-Frame-Options,其中344份同时带着CSP的frame-ancestors。按前面那个实验的结论,这344份里的老字段是完全无效的,浏览器读都不读。
它们为什么还在?因为加固清单是叠加的。三年前的清单说要加X-Frame-Options,两年前的清单说要加CSP,执行的人两条都加了,没人告诉他第二条会让第一条作废。删掉老字段是安全的——对现代浏览器毫无影响;留着也不算错,只是给下一个读配置的人多埋一个坑。
顺带一提,响应头这一层内部还有一类问题:同一层里两条字段互相矛盾。142个站里108个在同一份响应里写了打架的字段,那是同层的事,跟本文这种跨层压制不是一回事,但同样没人报警。
这些头是谁配的,站点自己多半不知道
521份响应带CSP、403份带防嵌套字段,这个覆盖率高得不像是各家站点分别配出来的。逐个看内容会发现,同一家电商平台上的站,安全头几乎一模一样,连指令的书写顺序都一致。
这说明它们来自同一处:平台或者CDN在边缘统一下发。好处是站点什么都不用做就有基础防护;代价是这一层不受站点控制,平台哪天改了策略,你的页面行为跟着变,而你的代码库里一行相关代码都没有。
更麻烦的是排查。发现某个头有问题,第一反应是去翻应用代码,翻不到;再翻Web服务器配置,还是翻不到。Nginx配置每次reload都通过、Googlebot每8次抓取却撞一次301那篇讲的是同一类痛苦:配置层面全都"正常",问题出在几层配置叠加之后的合成效果上。
响应头这一层能做而HTML做不到的事
把两层的能力摆在一起,差别就出来了。
响应头独有的:传输安全策略、内容类型嗅探开关、跨源资源共享、特性权限、防嵌套、条件请求校验字段、缓存指令。这些在HTML里没有对应写法,或者有写法但不生效。
HTML独有的:规范网址、结构化数据、社交分享属性、视口设置。这些没有响应头形式,或者响应头形式只对特殊资源有意义。
两层都能写的其实只有三样:字符编码、机器人指令、内容语言。三样里有两样是响应头压过HTML,剩下那一样在语义上根本不是同一件事。响应头在SEO里能做的那些事算是这一层的系统梳理,配合本文的层级判据看,选起来会快很多。
站点、页面、响应,三层规则的方向性是什么?
抓取和索引这件事横跨三层:站点根目录的规则文件管抓不抓,页面里的meta管收不收录,响应头两件事都能管。
这三层的关系不是覆盖,是有方向的。
先把三层的分工说清楚
这三层经常被混着讲,其实各管各的。
站点根目录那份规则文件管的是"要不要来取这个地址"。它在请求发生之前起作用,抓取方读完规则才决定发不发请求。它管不了索引——一个从没被抓过的地址,照样可能因为别处的链接而出现在搜索结果里,只是没有摘要。
页面里的meta管的是"取到之后要不要收进索引"。它必须先被取到才能起作用,这是它跟第一层最本质的区别。
响应头这一层两件事都能管,而且它比meta早到——响应头先于响应体送达,抓取方读完头就能决定要不要继续读下去。理论上这是最高效的一层,实际上几乎没人用。
下层锁了门,上层的信就送不出去
最经典的一个陷阱:一个页面被规则文件禁止抓取,同时页面里写着不许收录。
结果是不许收录这条永远送不到。抓取被拦在门外,抓取方看不到页面内容,自然也读不到里面的指令。Google在自己的文档里专门提示过这一点:要让页面不被收录,就不能同时禁止抓取它。
这不是覆盖关系,是物理上的送达失败。用一句话概括:你把纸条压在了一扇自己锁上的门后面。
真实站点上这么干的多不多
拿161份可解析的规则文件,去核对同一批站点提交在站点地图里的50783条地址,逐条判断Googlebot能不能抓。
结果是只有37条被自己的规则文件挡住,涉及5个站,占万分之七。anker.com有24条,多数是搜索页、订单查询和账户页;mejuri.com有6条账户页;boohoo.com有4条购物车。这些页面本来也不该出现在站点地图里,属于导出程序没做过滤,跟"noindex送不出去"那个陷阱还差一层。
这是个负结果,而且是个让人踏实的负结果:大站在这件事上做得挺干净。真正常见的反而是相反的组合——地址在站点地图里、规则文件也放行、页面上却写着不许收录,这种只是浪费一点抓取,不会出事。
反过来那种组合更常见,但不要紧
规则文件放行、页面上写着不许收录——这是正确的写法,也是最常见的一种。抓取方进得来,读到指令,把页面从索引里拿掉。代价只是每次抓取花掉一点预算,换来的是指令确实送到了。
两条规则该怎么搭配,规则文件和页面指令什么时候用哪个那篇给的判据可以直接照抄:想省抓取预算用前者,想控制索引用后者,两个目的不要用同一个工具去实现。
三层各自的失败模式不一样
| 层 | 它管什么 | 典型失败 |
|---|---|---|
| 站点根目录的规则文件 | 抓不抓 | 指令没有接收方,或写了搜索引擎不支持的指令 |
| 页面里的meta | 收不收录 | 被禁止抓取时送不出去;或写在head之外 |
| HTTP响应头 | 两者都能管 | 几乎没人用,能力被闲置 |
第一层的失败模式已经量过一次:规则文件里35.7%的站在写没人读的指令。第二层的失败模式本文和上一篇各覆盖了一半。第三层的失败模式是最特别的——它不是写错,是根本没写。674份样本里带机器人指令头的只有6份,这一层几乎是空的。
拦AI爬虫这件事同样横跨三层
近两年多出来的需求是拦AI爬虫,它和收录控制的层级结构一模一样:规则文件声明意愿、响应头下硬指令、防火墙做物理拦截。这三层怎么选那篇给了一个选型框架,核心判据也是层级——越靠上游的层执行力越强,但改起来越贵。
响应头那一层是唯一能管住没有head的资源的
PDF、图片、订阅源、接口返回的JSON,这些东西没有地方写meta。要让它们不被收录,只能用响应头。这一层的不可替代性就在这儿。这类地址拿什么说自己不想被收录那篇专门量过这个场景,样本里的落实情况很不理想。
新冒出来的那两层,会不会重演同一件事?
规则文件里除了允许和禁止,还能写别的东西。161份文件里翻出来这些:
| 指令 | 出现的站数 | 谁在读 |
|---|---|---|
| 抓取间隔 | 129 | Google明确不支持,Bing与Yandex支持 |
| 站点地图声明 | 144 | 主流抓取方都读 |
| 主域名指定 | 10 | Yandex的私有指令,已废弃 |
| 不许收录 | 7条(2个站) | Google在2019年9月停止支持 |
| 内容信号 | 2 | 2026年才出现的新提案 |
| 指向AI清单的字段 | 2 | 没有统一规范 |
| AI使用策略 | 1 | 没有统一规范 |
这张表里有三种不同的"没人读"
看起来都是"写了可能白写",成因完全不同,处理方式也该不同。
第一种是从来就不通用:主域名指定这条是某一家搜索引擎的私有扩展,别家从来没支持过,那家自己后来也弃用了。这类东西删掉就好。
第二种是曾经支持后来撤销:规则文件里的不许收录指令属于这一类。它在2019年之前是有效的非正式用法,后来Google宣布停止支持。写这条的人当年没错,是世界变了。这类要迁移——把意图挪到页面meta或者响应头里去,别只是删掉了事,否则那个页面就真的不设防了。
第三种是部分引擎支持:抓取间隔就是这一类,Google不认,别家认。这类不能删,要做的是修正预期,并且在另一个地方补上对Google的控制。
把三种混成一句"这条没用",处理起来就会出错——第二种直接删会留下真空,第三种直接删会失去对其他引擎的控制。
129个站在写一条Google不读的指令
抓取间隔这一条出现在161个站里的129个,占80.1%。Google的规则文件文档里明确写着不支持这个指令,抓取频率要去搜索后台调。写它不算错——Bing和Yandex是认的——但如果你写它的目的是让Google慢点抓,那这一条从来没起过作用。
新的两层正在重复老路
内容信号、指向AI清单的字段、AI使用策略,这三样加起来只出现在5个站上。它们都很新,都还没有统一规范,都被写进了同一个文件里。
给AI代理准备的那份清单文件也是同样的处境:它是一份社区提案,不是任何一家搜索引擎或者模型厂商的正式约定。第二版加了正式的链接规则,工程上更完整了,但"谁会读它"这个问题还是没答案。
历史上每一层新声明都要走一遍这个过程:先有人提出格式,然后有人开始写,最后才知道有没有人读。写一层新声明的成本很低,指望它生效的代价却可能是把该做的事漏掉。在AI清单这件事上,比较稳妥的顺序是先把响应头和页面这两层做对,再去添新的。
判断一层新声明值不值得跟,问四件事
第一,它有没有一个明确的读取方?不是"某某厂商表示关注",是"某某产品的文档里写了它会读"。给AI代理的那份清单目前答不上这一问。
第二,它和已有的层是什么关系?替代、补充、还是并列?如果是替代,老的那层什么时候能撤;如果是并列,两层说的不一样时谁赢——这个问题多数新提案都没定义。
第三,写错了会怎样?有些层写错是静默失败,有些层写错会连累别的东西。规则文件里加一条无法识别的指令是安全的,抓取方跳过就完了;但如果新提案要求你在响应头里加字段,那就得当心跟已有字段的相互作用。
第四,撤回的成本多大?这一问最容易被忽略。一份放在根目录的静态文件,删掉就没了;一条写进CDN边缘规则的响应头,撤回要重新走一遍发布。先做便宜的、可逆的那一层,是所有新标准的正确打开方式。
还有一类不是新层,是老层换了读者
比起真正的新层,更需要留意的是老层来了新读者。规则文件这两年就在经历这件事:一批以前不存在的抓取器开始读它,而它们对指令的解释跟传统搜索引擎不完全一样。
连"读不读"这件事本身都在变。Google有一整类抓取器根本不看规则文件,这次改名只是把这个事实推到了台前。同一份文件,有的读者逐条遵守,有的读者压根不打开——这已经不是层级问题,是读者问题了。
哪一层该写什么,有没有一张对照表?
有。把前面所有实验和普查的结论收成一张表,按"这件事该写在哪一层"来排。
| 要表达的事 | 写在哪一层 | 写在别的层会怎样 |
|---|---|---|
| 字符编码 | 响应头为准,HTML里再写一遍做保险 | 只写HTML:响应头一变就被压过 |
| 禁止被嵌套 | 响应头的CSP frame-ancestors | 写meta:完全无效 |
| 兼容老字段X-Frame-Options | 可留可删,有CSP时它已失效 | 只写它:老浏览器还认,新的不认CSP才认它 |
| 不许收录 | 页面meta,或响应头的机器人指令 | 写在规则文件里:Google从2019年起不读 |
| 不许抓取 | 规则文件 | 写meta:抓都抓了,只是不收录 |
| 没有head的资源不许收录 | 只能用响应头 | 没有别的选择 |
| 缓存策略 | 响应头 | 写meta:三个字段全都不算数 |
| 页面语言 | html标签的语言属性 | 响应头那个字段说的是目标受众,不是文本语言 |
| 规范网址 | HTML的head | 响应头形式只对非HTML资源有意义 |
这张表的用法
它不是让你照着把每一行都配齐。多数站点只需要用到其中三四行,其余的保持默认就好。
正确的用法是反过来查:手上有一条已经写好的声明,先在表里找到它,确认它现在待的那一层是不是该待的地方。如果不是,两个选择——挪到对的层,或者删掉。最糟的处理是两层都写一份,因为那样你就永远不知道生效的是哪一份。
还有一种情况表里没列:同一件事在两层都写、而且值相同。这不算错,但要清楚它的真实作用是冗余备份,不是双保险加强。真出问题的时候,改一层不改另一层,问题会以一种非常难查的方式活下来。
排期的时候按什么顺序做
按"错了会怎样"排,跟技术SEO先修哪个那套优先级的思路一致。
第一档是会改变索引行为的:机器人指令写错层、禁止抓取和不许收录同时写。这两种会让页面进不去索引或者出不来。
第二档是会改变安全边界的:防嵌套写在meta里、CSP的关键指令被放在无效位置。它们不影响搜索,但影响真实风险敞口。
第三档是纯冗余的:写在meta里的缓存字段、已退役浏览器的兼容声明、老平台的私有字段。这些什么时候顺手清什么时候清。
语言那一行容易被误解
响应头里的内容语言字段和html标签上的语言属性,看起来在说同一件事,其实不是。前者按规范讲的是"这份文档面向哪些语言的受众",后者说的是"这段文本本身是什么语言"。一个英文写的德国市场页面,两者写得不一样是合理的。
样本里320份带内容语言头的响应,与html语言属性主语言不同的只有12份,其中还有两个站是html上压根没写语言属性。数字很小,说明多数站根本没在这两个字段之间做区分——它们同时被同一套模板输出成了同一个值。
怎么在自己站上把两层对一遍?
一条命令一层,对照着看就行。
先问自己三个问题
动手查之前,把要查的东西想清楚,能省掉一大半无效操作。
第一,这件事我到底想让谁听见?浏览器、搜索引擎、社交平台、AI爬虫,它们读的层不一样。想让浏览器执行的写响应头,想让搜索引擎执行的看它支持哪一层,想让社交平台读的只能写在HTML里。
第二,这件事现在写在哪几层?多数时候答案是"不止一层",而且写的人不知道另一层也写了。
第三,如果两层说的不一样,我希望谁赢?想清楚这个再去改,否则很容易改了不生效的那一条,然后以为改动没用。
先把两层拉出来放在一起
U=https://example.com/
curl -sI "$U" | grep -iE '^(content-type|content-language|x-robots-tag|x-frame-options|content-security-policy|cache-control|link)'
curl -s "$U" | sed -n '1,/<\/head>/p' | grep -o -iE '<meta[^>]*http-equiv[^>]*>'上下两段对着看:有没有同一件事在两处都写了?两处说的一样吗?
查那个已经失效的防嵌套字段
curl -sI "$U" | grep -i 'x-frame-options'
curl -sI "$U" | grep -i 'content-security-policy' | grep -o 'frame-ancestors[^;]*'两条都有输出,说明第一条已经被第二条架空。要么删掉第一条,要么确认第二条写得跟你的意图一致。
把meta里那些不算数的挑出来
curl -s "$U" | grep -o -iE '<meta[^>]*http-equiv="[^"]*"' \
| grep -o -iE 'http-equiv="[^"]*"' | sort | uniq -c \
| grep -viE '(content-type|default-style|refresh|x-ua-compatible|content-security-policy)'剩下的就是规范不认的那些。逐条判断:是该挪到响应头,还是干脆删掉。
批量对一批地址
单页看完要看面。把地址存成文本文件,一行一个:
while read -r u; do
h=$(curl -sI -m 20 "$u")
xfo=$(printf '%s' "$h" | grep -ci '^x-frame-options')
fa=$(printf '%s' "$h" | grep -i '^content-security-policy' | grep -c 'frame-ancestors')
ct=$(printf '%s' "$h" | grep -i '^content-type' | grep -c 'charset')
printf '%s\txfo=%s\tfa=%s\tcharset=%s\n' "$u" "$xfo" "$fa" "$ct"
done < urls.txt第二列和第三列同时为1的,说明那条老字段已经失效;第四列为0的,说明这个地址的编码完全靠HTML撑着。
把响应头这一层的变化盯起来
响应头不在你的代码库里,所以它变了不会有提交记录。想知道它什么时候变的,只能自己存快照:
curl -sI -m 20 "$U" | grep -viE '^(date|age|x-request-id|cf-ray|set-cookie|etag|expires)' \
| sort > "hdr-$(date +%F).txt"
diff "hdr-$(date -d yesterday +%F).txt" "hdr-$(date +%F).txt"过滤掉那些每次都变的字段,剩下的就是配置。挂个定时任务每天跑一遍,平台悄悄改了策略的时候你能第一时间看见。这条比事后排查便宜得多——用现成的接口测试工具看响应头适合临时查一个地址,长期监控还是脚本更省事。
验证防嵌套到底生不生效
命令行看不出iframe的行为,得用浏览器。在任意一个页面的控制台里跑:
const f = document.createElement('iframe');
f.src = 'https://example.com/';
document.body.appendChild(f);
setTimeout(() => {
try { console.log('渲染了', !!f.contentDocument.body.innerText.length); }
catch (e) { console.log('被拦了', e.name); }
}, 2000);跨源的时候拿不到内容会抛异常,所以这段只适合测同源,或者用来确认"能不能加载"这个粗粒度的结论。要精确判断得看控制台里浏览器自己报的拦截信息。
改完之后怎么确认真的生效了
这一步最容易被跳过,而跨层的改动恰恰最需要它——因为你改的那一层可能压根不是生效的那一层。
编码:改完用浏览器控制台读一次document.characterSet,别只看源码里的meta写对了没有。
防嵌套:改完用一个同源页面真的嵌一次,看能不能渲染。只看响应头有没有那个字段是不够的,因为它可能被旁边的CSP架空。
收录控制:改完在搜索后台用实时抓取工具跑一次,看抓取方读到的指令是什么。它给出的是抓取方视角,跟你在浏览器里看到的可能不一样。
缓存:改完隔几分钟用带随机参数和不带参数各请求一次,对比响应头里的缓存状态标记。
四件事有一个共同点:验证要在生效的那一层做,不在你改动的那一层做。改了HTML就去看渲染结果,改了响应头就去看真实请求,改了规则文件就去看抓取方的反馈。
常见问题解答
响应头和meta都写了编码,会不会有冲突风险?
不会有渲染风险,因为规则很明确:响应头赢。两处都写反而是好事——万一哪天CDN不发编码头,或者页面被另存成本地文件,HTML里那条就顶上了。要注意的是两处别写不一样的值,否则本地打开和线上访问会是两种结果。
X-Frame-Options现在还需要写吗?
可以不写。MDN的建议是用CSP的frame-ancestors替代它。如果你的访客里还有非常老的浏览器,留着也没坏处,但要清楚它在支持CSP的浏览器上是死的。样本里344份响应正处在这个状态。
meta里的CSP完全没用吗?
有用,只是能力少一截。脚本来源、样式来源、图片来源这些指令在meta形式下都正常工作。不支持的是frame-ancestors、report-uri和sandbox三个。另外meta形式的策略只能加严不能放松——如果响应头里已经有一条CSP,meta里那条是叠加上去取交集,不会把头里的限制放开。
为什么规范网址的响应头形式一份都没有?
因为HTML页面用不着。这个字段的响应头形式是给PDF、图片这类没有head的资源准备的。674份样本全是HTML页面,0份是符合预期的。如果你的站有大量可下载文件参与搜索,那这一层就该用起来。
抓取间隔这条指令要删掉吗?
不用删。Bing和Yandex认它,删了会失去对这两家的控制。要改的是预期:别指望它对Google有任何作用,要限制Google的抓取频率得去搜索后台设置,或者在服务端做限速。
给AI代理的那份清单文件,现在值得做吗?
成本低就做,但别把它当成防线。它是社区提案,没有哪家模型厂商承诺一定读。真正管用的仍然是规则文件里的抓取权限、响应头里的指令,以及页面本身能不能被读懂。新层可以加,老层不能省。
为什么响应头里的机器人指令几乎没人用?
三个原因叠在一起。一是它得改服务器配置,而meta改模板就行,后者的门槛低得多;二是它作用在整个响应上,要按页面区分就得写条件规则,配置复杂度上一个台阶;三是它不可见——打开源码看不到,只有主动去查响应头才发现,团队协作时容易被遗忘。
所以它主要用在两个场景:一是没有head的资源,二是需要批量处理一整个目录。除此之外,用meta更省事,效果一样。
写在meta里的页面刷新指令还能用吗?
能用,规范承认它,浏览器也执行。但它不适合做跳转——搜索引擎对它的处理不如301明确,而且用户按返回键会陷入循环。样本里17个站在首页写了这一条,多数是做地区选择页的自动跳转。要做跳转还是用服务端的状态码,那一层的语义清楚得多。
层级这套判断,对结构化数据也适用吗?
部分适用。结构化数据只能写在HTML里,没有响应头形式,所以不存在跨层竞争。但它内部有同样的问题:同一个实体可以用两种格式各写一遍,两份内容不一致时消费方怎么挑,各家实现不同。这属于同层冲突,跟本文这条线是并行的另一条。
这些结论对Bing和其他搜索引擎适用吗?
跨层的那部分适用,因为它们由浏览器和HTTP规范定义,跟哪家搜索引擎无关。搜索引擎自己那部分不一定:抓取间隔、规则文件里的收录指令、各家对冲突的处理,各有各的口径。做多引擎的站要分别查一遍各家文档。
为什么样本里meta形式的CSP在内页是0,首页却有11个?
因为首页和内页往往由不同的东西生成。首页更可能是单独做的落地页,有人手工写过head;内页由模板批量输出,模板里没有这一段。11这个数字本身也不大,占首页样本的6.5%左右。
顺带说,这11个站的meta里都没写frame-ancestors,写的是脚本来源和样式来源那些meta支持的指令。所以它们不属于本文说的"写错层",属于选了一个能力较弱但合法的位置。
如果CDN和源站都发了同一个头,会怎样?
看CDN的配置模式。有的是覆盖,有的是追加,有的是源站有就不加。CSP这个头比较特殊:规范规定多条策略同时生效、取交集,也就是每一条都得放行这个请求才算放行。所以两层各发一条的结果通常是变得更严,而不是后一条覆盖前一条。
这件事在本文的实验里意外撞见过一次:线上版本的测试页发出去的响应里有两条CSP,一条是脚本自己写的,一条是服务器配置里的。两条都生效,取的是交集。
怎么知道我的响应头是哪一层加的?
逐层剥。先直连源站绕过CDN,看头少了哪些;再关掉应用层的中间件,看还剩什么。剩下的那部分是Web服务器配置给的。样本里那些整齐划一的安全头,绝大多数是平台或者CDN统一下发的,站点自己配置文件里一行都没有。
权威参考资料
本文标题:《响应头和meta打架的时候,赢的从来不是更严的那条》
本文链接:https://zhangwenbao.com/http-header-vs-meta-layer-precedence-audit.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0