重复的meta标签,浏览器和搜索引擎各按各的规矩挑
本文目录
- 一个页面里把同一件事说两遍,到底有多常见?
- 这次没有算什么
- 四分之一的首页至少重了一处
- 两次快照的重复清单一条不差
- 内页比首页重得更厉害
- 顺带量到的三处缺席
- 重复和冲突不是一回事,差在哪里?
- 写重了但值一样:无害
- 写重了值还不一样:要看字段
- 七个值得单独看一眼的现场
- 验证码那16个站其实没做错
- 值不一样的时候,浏览器到底挑哪一条?
- 为什么不能在线上测
- 每一组都配了对照
- 两个字符编码声明:第一条赢
- 两个标题标签:第一条赢
- 两个规范网址:浏览器不管
- 注释掉的那条不算数
- 被忽略的那条,工具还是能看见
- 一张表看完六组实验
- 为什么是第一条赢,而不是最后一条?
- 解析器是流式的,它没有回头的习惯
- 但不是所有字段都遵守先到先得
- 五种裁决方式,没有一处写在一起
- 五种裁决方式背后是五种不同的处境
- 一个反直觉的推论
- head里放一个div,会发生什么?
- 三条声明被一个div请了出去
- 真实站点上有多少个这种div
- 还有一类声明是自己躲起来的
- 断点在第几个字节,决定了损失有多大
- 被搬进body的声明,搜索引擎还认吗
- 那些把编码声明写到十几万字节之后的站,为什么没乱码?
- 96.4%的前置字节是内联脚本
- 它们全都被响应头接住了
- 那4个声明ISO-8859-1的响应头
- 没有兜底的那19个站
- 这些重复是谁写进去的?
- 主题和插件各输出一遍
- 营销代码片段带进来的
- 建站平台自己注入的那一份
- 不同环境的配置各留了一份
- 拼错的那一类
- 怎么分辨一条重复是模板给的还是外挂给的
- 哪些重复必须今天改,哪些可以先放着?
- 第一档:会改变索引行为的
- 第二档:会改变对外展示的
- 第三档:冗余但无害的
- 一个例外:位置问题优先于重复问题
- 按这三档排,337个快照里剩多少活
- 改之前先确认这一条真的归你管
- 修不动的时候,换一条路
- 怎么在自己站上把这些问题查出来?
- 数一数关键字段各写了几遍
- 看编码声明落在第几个字节
- 查head里有没有不该有的标签
- 用浏览器给出最终答案
- 批量跑一遍全站的关键页
- 把这条检查接进上线流程
- 同一套判断,还能用到哪些地方?
- 四个问题
- 为什么这套规则不可能统一
- 把四个问题跑一遍要多久
- 能套上去的其他场景
- 同一件事换个字段再说一遍,也算重复
- 写模板的时候多做一步
- 常见问题解答
- 同一个页面写两条描述,Google会用哪一条?
- 规范网址写了两条完全一样的,需要改吗?
- 把编码声明放在head最前面,真的有必要吗?
- head里的div是怎么混进去的?
- head里的声明顺序,会影响搜索排名吗?
- 为什么七成首页没有H1也没出事?
- 动态渲染的站,这套办法还管用吗?
- 这些数据能代表中文站吗?
- 检测工具报的重复数,和实际生效的是一回事吗?
- 把重复清干净,排名会涨吗?
- 只有一条声明,是不是就一定安全?
- 权威参考资料
摘要:337份首页快照里,25.0%的页面把同一件事声明了不止一次,20.2%的页面两条声明说的还不一样。这些重复不是抓取时的偶然——间隔两天的两次快照,166个站的重复清单一条不差。真正麻烦的是裁决规则:编码取第一条,标题取第一条,机器人指令取并集里最严的那条,站点验证码几条同时有效,规范网址多写几条反而可能一条都不算。浏览器实测把这几种结果逐个跑了出来,包括一个把三条声明赶出head的div。文中给出重复率与冲突率的完整口径、七类字段各自的裁决方式、以及一条能在自己站上跑完的自查命令。
8月17日Google那边有个说法挺有意思:同一件事实,把主语和宾语调换位置写,AI给出的答案会跟着变。Search Engine Journal转述的原话是,实体出现的先后顺序会影响模型对关系方向的理解,所以文档里怎么排,机器就怎么读。
顺序影响结果,这件事在网页上比在句子里发生得早得多,也直白得多。
一个页面的head里,同一件事被写两遍是很常见的。两个站点验证码、两条主题色、两条社交分享图、偶尔还有两个标题标签。写的人多半不知道自己写重了——一条是主题模板给的,一条是插件给的,还有一条是营销部门临时插进来的代码片段带的。
浏览器不会报错,搜索引擎也不会给你发邮件。它们只是安静地挑一条用,然后把另一条丢掉。重复本身不是错误,重复是把决定权交了出去。
更麻烦的是,这个交出去的决定权,落在了一套很不统一的规则手里。有的字段取第一条,有的取最严的,有的干脆全部作废,还有的几条同时算数。这几条规则分别写在HTML规范、搜索引擎的帮助文档和各家平台的协议说明里,没有哪一份文档把它们并排列出来过。
于是就出现了一种很典型的状况:一个页面上有三处重复,其中一处完全无害,一处会让展示效果变成你没想要的样子,还有一处会改变索引行为——而做这个站的人,三处都不知道。
保哥拿两个时间点的首页快照做了一次普查,337份文档、17423条声明,把每一类声明数了一遍,又用浏览器把几种典型的重复现场跑了一遍,看它到底挑走了哪一条。
一个页面里把同一件事说两遍,到底有多常见?
样本是188个海外品牌站的首页,分两个时间点各抓一次:8月14日凌晨一次,8月16日下午一次。剔掉响应体不足2000字节的失败样本,两次分别剩169份和168份可解析文档。
统计口径只看head里那些语义上应该唯一的声明:字符编码、标题、描述、机器人指令、规范网址、各类社交属性、各家平台的站点验证码,以及html标签自己的语言属性。脚本、样式、注释、noscript和template内部的内容全部先抹掉再统计,免得把JavaScript字符串里的假标签数进来。
这次没有算什么
口径先说清楚,免得后面的数字被误读。
没有算JavaScript运行之后才插进去的声明。抓到的是服务端吐出来的原始HTML,前端框架在客户端补的那部分不在里面。这会让重复率被低估——不少站的元信息是脚本动态写进head的,跟服务端输出的那份撞车。要覆盖这一层得用无头浏览器渲染完再数,成本高一个数量级。
没有算响应头里的同名字段。头和文档谁压过谁是另一件事,本文只在编码那一节碰了一下。
没有算语义相同但字段不同的重复。标题标签、社交分享标题、结构化数据里的名称,三个字段说的是同一件事,它们互相不一致的时候不算"重复声明",但后果一样麻烦。这一类要单独量。
没有把值只差大小写或者尾部斜杠的当成冲突处理——实际上算了,但要知道这里面有噪音。fahertybrand.com的两条主题色是#fcfaf6和#FCFAF6,严格说是同一个颜色,脚本按字符串比对判成了冲突。71条冲突里这类占2条。
四分之一的首页至少重了一处
| 口径 | 8月14日快照 | 8月16日快照 |
|---|---|---|
| 可解析文档 | 169 | 168 |
| head声明总条数 | 2936 | 2919 |
| 每篇声明数中位值 | 17 | 18 |
| 单篇最多 | 43 | 43 |
| 至少一处重复的文档 | 42(24.9%) | 42(25.0%) |
| 多出来的声明条数 | 110 | 110 |
| 至少一处值冲突的文档 | 34(20.1%) | 34(20.2%) |
| 值冲突的条数 | 71 | 71 |
四分之一。这个比例比我预想的高,但更让人意外的是它有多稳。
两次快照的重复清单一条不差
两次抓取相隔约63小时,中间隔了一个周末。166个站两次都拿到了完整首页,把每个站的重复字段列成集合逐一对照,166个站全部完全一致,一个都没变。
这个百分之百有两层意思。一是这些重复不是抓取时的网络抖动或者页面随机渲染造成的,它们固化在模板里,天天如此。二是这套测量的尺子本身是准的——如果脚本的解析有随机性,两次结果不可能完全对上。
顺带一个细节:同期字符编码声明的字节偏移量有19个站变了。页面内容长度天天在变,偏移量跟着浮动很正常,但重复的字段集合纹丝不动。会动的会动,不该动的不动,这个对照本身就说明尺子没歪。
内页比首页重得更厉害
为了知道首页是不是特例,另外拿了一批内页做对照:113个站的674个内页,多数是产品页和集合页。同一套脚本跑下来,至少一处重复的文档占37.5%,值冲突占30.1%,比首页的25.0%和20.2%都高出一截。
差距的来源不难猜。首页通常是单独做的模板,改动少、盯的人多;产品页是批量生成的,一个字段写重了就是几千个页面同时重。样本里aloyoga.com的6个产品页每一个都有3条分享图声明,指向同一件商品的不同角度照片;glossier.com的产品页分享图宽高写了3组,1374和1377来回跳。
这也解释了为什么首页体检往往过关而站点整体不过关。抽样只抽首页,等于专挑班里成绩最好的那个学生去参加家长会。
顺带量到的三处缺席
数重复的时候顺手也数了缺席,有三个数字比重复率更刺眼。
规范网址:168个首页里48个完全没写,占28.6%;116个自指,4个指向别的地址。首页不写这个字段风险有限,它通常就是站点根地址。但674个内页的缺失率是11.1%,说明电商模板在内页上确实做了处理,首页反而被漏掉了。
H1标签:能完整取到body的55份首页里,39份一个H1都没有,占70.9%。现代电商首页顶部是轮播横幅,标题在图片里或者用div加样式做成大字,视觉上完全看不出少了什么。内页样本这个比例是50.5%。这件事的影响没有十年前那么大,但语义化标签对SEO的真实影响那篇量过,它在结构提取和辅助技术上仍然有用。
结构化数据:169份首页里76份一块都没有,占45.0%;48份一块,30份两块,12份三块,4份四块。在AI摘要越来越依赖实体识别的今天,这个缺口比多写一条主题色严重得多。想查自己站上有几块、字段缺什么,一次扒清页面五种格式的结构化数据那套办法可以直接套。
重复和冲突不是一回事,差在哪里?
110条多余的声明里,只有71条属于值冲突。剩下的39条是同一个值写了两遍,纯属冗余,谁赢都一样。
这两件事的严重程度差得很远,混在一起谈就没法排优先级了。
写重了但值一样:无害
marinelayer.com的首页有两条规范网址声明,指向的地址完全相同。产品页也一样,抽查的6个产品页每一个都是两条相同的规范网址。这属于模板里两个位置都输出了同一个变量,看着别扭,实际没有后果。
字符编码声明重复7个站,值全都一致。描述重复2个站,值一致。这些都归到冗余那一栏。
写重了值还不一样:要看字段
71条值冲突分布在9类字段上,密度最高的三类是站点验证码、社交分享图和主题色。
| 字段 | 有该声明的站 | 总条数 | 写重的站 | 值冲突的站 |
|---|---|---|---|---|
| Google站点验证码 | 54 | 88 | 16 | 16 |
| Facebook域名验证码 | 26 | 37 | 6 | 6 |
| 主题色 | 70 | 75 | 5 | 5 |
| 社交分享图 | 91 | 95 | 3 | 3 |
| 分享图宽高 | 52 / 50 | 55 / 53 | 2 / 2 | 1 / 1 |
| 视口设置 | 163 | 165 | 2 | 1 |
| 机器人指令 | 62 | 65 | 2 | 2 |
| 字符编码 | 141 | 169 | 7 | 0 |
| 描述 | 133 | 135 | 2 | 0 |
| 规范网址 | 120 | 121 | 1 | 0 |
七个值得单独看一眼的现场
把71条冲突里最有代表性的几个摊开,比看汇总表直观。
gymshark.com的主题色:一条纯黑,一条纯白。多半是想同时照顾深色和浅色两种系统外观,但这个字段的定义不支持这种写法——要按外观切换得用媒体查询属性单独声明。现在的结果是永远取第一条,纯黑。
laneige.com和sulwhasoo.com的分享图:同一张图,一条带www一条不带。这两个站同属一个韩国美妆集团,用的是同一套系统,同一个毛病复制了两遍。域名规范化做过了,老配置没清。
kotn.com的分享图:三条声明指向三张不同的图,配套的宽高写了800和600两组值。第一条图配的是第一组尺寸还是第二组,没人说得清。社交平台按第一条取图,尺寸信息却可能取到另一条,预览卡片的裁切就会歪。
traeger.com的视口设置:两条,一条是width=device-width, initial-scale=1,一条是initial-scale=1.0, width=device-width。顺序不同、写法不同,语义完全一样。这属于两套模板拼在一起,谁也没删谁。
cotopaxi.com的Facebook页面关联:三条,三个不同的页面ID。这个字段本身支持多值,写成一条用逗号分隔也行,写成三条也行,不算错。
innisfree.com的Naver验证码:两条,两个不同的值。韩国市场的搜索引擎验证,两个团队各验了一次。
vaude.com的状态栏样式:一条写black,一条写none。none不是这个字段的合法取值,写了等于没写;即便合法,也轮不到它,第一条已经定了。
验证码那16个站其实没做错
站点验证码是个特例。一个域名可以有多个人各自验证所有权,市场部一个、代运营一个、原来做站的外包再留一个,几条并存是设计上就允许的。bellroy.com写了3条,fromourplace.com写了4条,avocadogreenmattress.com的Facebook域名验证写了5条。这些不算病。
把这一类剔掉之后,真正需要处理的值冲突剩下18个站左右。比例一下从20.2%掉到11%上下。先分清哪些重复是设计允许的,再谈治理,能省掉一多半工作量。
值不一样的时候,浏览器到底挑哪一条?
普查告诉你有多少页面在冲突,但冲突之后谁赢,得让浏览器自己回答。
保哥在本机起了一个最小的PHP服务,每个测试地址只输出十几行HTML,专门制造一种冲突,然后用真实的Chromium内核加载,读它解析完之后的结果。为了避开线上服务器会自动补齐响应头的干扰,整套实验都在本地跑,服务端的字符集自动追加也关掉了。
为什么不能在线上测
第一版实验是直接扔到线上服务器跑的,结果一看响应头就知道不行:想制造一个不带字符集的响应头,PHP自己补了一个,nginx又补了一个,压根做不出那个场景。更热闹的是安全策略字段——我在脚本里发了一条,nginx的站点配置里本来就有一条,同一个响应里出现了两条同名的头。
这个小插曲本身是个提醒:你以为你在控制响应,实际上你只是链条上的一环,前后各有人往里加东西。所以实验挪回本机,用一个不做任何加工的最小服务,才能保证测的是浏览器的行为,不是中间层的行为。
每一组都配了对照
制造冲突之后如果结果符合预期,很容易就此收工。但这种收工方式最容易骗自己——万一浏览器根本没读那个字段,而是靠别的路径得出了相同的结论呢?
所以每一组都加了对照。测编码的那组,额外做了一个页面:不写任何编码声明,正文照样放中文。如果这个页面也能正常显示,说明浏览器在做内容自动识别,前面所有结论都得推翻。实测结果是它乱码了,返回的编码是windows-1252——浏览器没有自作主张,它只认声明。尺子成立,后面的结论才站得住。
两个字符编码声明:第一条赢
页面里先写<meta charset="iso-8859-1">,紧接着写<meta charset="utf-8">,正文放一段中文。加载之后document.characterSet返回windows-1252,中文变成了那串经典的层级è£å†³。
第一条赢了。而DOM里两个标签都在,一个没少。被忽略不等于被删除,它就摆在那儿,只是没人再看它一眼。
两个标题标签:第一条赢
同一个head里写两个标题标签,第一个是第一个标题,第二个是第二个标题,另外配两条描述。结果document.title返回第一个标题,querySelector取描述拿到的是第一条。标题标签的数量统计仍然是2。
这跟规范里的定义是对上的:文档标题这个属性的定义写的就是取文档中第一个标题元素。有意思的是这条规则不限于head——把标题标签写进body、head里一个都不留,取到的仍然是body里那个。
两个规范网址:浏览器不管
写两条指向不同地址的规范网址声明,浏览器把两条都留在head里,不做任何裁决。它本来也不需要——这个字段不是给浏览器用的。裁决权在搜索引擎那边,而Google的公开说法是多条规范网址会被当成一个混乱信号,它可能全部忽略、自己另选一个。关于这个字段本身的性质,canonical是提示不是命令这件事值得单独看一遍,它解释了为什么写对了也不一定被采纳。
注释掉的那条不算数
顺手做了个对照:把一条规范网址用HTML注释包起来,后面再写一条真的。DOM里只有真的那条。这是意料之中的结果,但对照组的意义不在于它出人意料,而在于它能证明前面几组不是脚本自己看花了眼。
被忽略的那条,工具还是能看见
这里有个容易吃亏的地方。浏览器忽略了第二条编码声明,但DOM里两个标签都在;任何一个用选择器去数标签的检测工具,都会告诉你这里有两条。工具报的是写了几条,浏览器给的是用了哪条,两者说的不是一回事。
做审计的时候这个区别很要紧:工具报出来的重复清单,要先过一遍裁决规则才知道哪些真的有后果。反过来也成立——工具报只有一条的时候,那一条也未必生效,它可能掉在了body里。
这也是为什么本文的普查和浏览器实验必须分开做。普查回答的是"页面上写了什么",用脚本数字节就够;实验回答的是"最后哪一条算数",非得让真正的解析器跑一遍不可。把两者混在一起,得到的既不是源码的事实,也不是渲染的事实,而是一个谁也对不上的中间态。
顺便说一句,浏览器给出的答案也只是三个答案之一——搜索引擎有自己的解析器,社交平台的抓取器又是另一套实现。浏览器的结论可以当作最可靠的参照,但不能当成唯一的裁决。
一张表看完六组实验
| 制造的冲突 | 浏览器给出的结果 | 能推出什么 |
|---|---|---|
| 两条编码声明,先ISO后UTF-8 | 按第一条解码,中文乱码 | 先到先得,且不可挽回 |
| 两个标题标签 | 文档标题取第一个 | 与规范定义一致 |
| 两条描述 | 选择器命中第一条 | 同上 |
| 两条不同的规范网址 | 两条都留在head,不裁决 | 裁决权不在浏览器 |
| 注释掉的一条加真的一条 | 只有真的那条进DOM | 对照组,尺子有效 |
| head里没有标题、body里有 | 文档标题取到body里那个 | 取第一个,与位置无关 |
为什么是第一条赢,而不是最后一条?
CSS里后写的规则覆盖先写的,写惯了样式表的人很容易把这个直觉带到head里来。head里恰好反过来。
解析器是流式的,它没有回头的习惯
HTML解析器从字节流的头部一路往下读,读到一个它关心的标签就当场处理。WHATWG规范里确定字符编码那一节写得很清楚:一旦确定了编码,后面再出现的编码声明就被忽略。
标题也是同一个思路。规范对文档标题的定义直接写成了取第一个标题元素,不是取最后一个,也不是取head里的那个。
说到底,先到先得是流式处理最省事的实现方式。你要让最后一条赢,解析器就得把整个文档读完才能开始渲染,那样谁也受不了。
但不是所有字段都遵守先到先得
机器人指令是个反例。一个页面里出现多条机器人指令时,Google给出的处理方式不是挑一条,而是把它们合并,冲突的部分取限制更严的那个。样本里ikea.com的产品页就是这种写法,一条写着允许索引和跟踪,另一条写着图片预览尺寸不限,两条合起来才是完整意图,谁也没被丢掉。
byredo.com的首页写了3条机器人指令,suitsupply.com写了2条,逐条看都不构成真冲突。整个样本里没有出现一例同时写着允许索引和禁止索引的页面,这算个好消息——最危险的那种冲突,大站基本不犯。
五种裁决方式,没有一处写在一起
| 字段 | 多条并存时的结果 | 依据 |
|---|---|---|
| 字符编码 | 取第一条,其余忽略 | HTML解析算法 |
| 标题 | 取文档里第一个 | HTML规范对文档标题的定义 |
| 描述 | 实现上取第一条,但搜索引擎可能整条不用 | 各家自行处理 |
| 机器人指令 | 合并,冲突处取更严的 | Google搜索中心文档 |
| 规范网址 | 可能全部忽略,由搜索引擎另选 | Google搜索中心文档 |
| 站点验证码 | 多条同时有效 | 各平台设计如此 |
| 社交分享图 | 消费方各自取第一条或自行挑选 | Open Graph协议 |
这张表是我拼出来的,不是从哪份文档里抄的。它们分散在HTML规范、Google的帮助文档、Open Graph的协议说明和各家平台的验证指引里,没有任何一个地方把它们放在一起讲过。
所以真正的风险不是你写重了,而是你以为所有字段的裁决方式都一样。
五种裁决方式背后是五种不同的处境
这几条规则看着零散,其实各有各的道理,倒不是谁拍脑袋定的。
取第一条的那两个字段,编码和标题,都必须在解析早期就定下来。编码定不下来后面的字节没法解释,标题定不下来标签栏没东西可显示。这类字段的共同点是晚了就来不及,所以规范干脆规定先到先得。
合并取严的那个字段是机器人指令。它管的是能不能收录,这是个不可逆的动作——错误地不收录还能改回来,错误地收录了,内容已经散出去了。这种场合下取更保守的那条是唯一安全的选择。
可能全部忽略的是规范网址。它本来就只是一个提示,Google要在几十个信号里综合判断哪个是主版本。你给两个互相矛盾的提示,等于什么都没提示,它当然可以自己另选。
多条同时有效的是各平台的所有权验证。这个字段的语义不是"这个页面属于谁",而是"这些人都能证明自己和这个域名有关系",本来就是多值的,写几条都不冲突。
最后一类是社交属性。协议允许结构化属性重复,具体挑哪条交给消费方,于是不同平台的行为就不一样了。这一类的裁决权已经不在页面这边,属于下一个话题。
一个反直觉的推论
既然是先到先得,那把重要的声明写在前面就行了吧——这个想法只对了一半。
对的部分:同一个字段写重的时候,你确实应该保证想要的那条排在前面。错的部分:不同字段之间没有优先级关系,把描述提前不会让它更重要。真正跟位置有关的只有两件事,一是同字段的先后,二是它有没有掉出head。除此之外,head里的顺序在SEO上没有任何额外含义。
我见过有人把整个head按"重要性"重排了一遍,指望排名跟着动。结果当然是什么都没发生。位置决定的是谁生效,不是谁更重要。
head里放一个div,会发生什么?
前面几组冲突好歹还有个裁决过程。下面这种是连参赛资格都没有。
三条声明被一个div请了出去
测试页面这样写:head里先是字符编码和标题,然后插一个div,div后面才是描述、规范网址和机器人指令,最后正常闭合head再写body。
加载之后数一遍:head里的meta只剩1条,body里多出2条meta和1条link,head里的规范网址数量是0。div之后的三条声明全部落进了body。
原因是HTML规范给head定了一份允许出现的元素清单,只有base、link、meta、noscript、script、style、template和title这八种。遇到清单外的元素,解析器认为head已经结束了,当场切到body。写在后面的那些声明不是被忽略,是被搬了家。搜索引擎读不读body里的这几条,各家说法不一,但至少你原本的意图是让它们待在head里。
这个现象之前专门量过一次,工具说在head浏览器说在body,那篇讲的是检测工具和浏览器的分歧;这里补上的是它在真实站点上的发生率。
真实站点上有多少个这种div
337份快照里踩到这个坑的有7份,涉及4个站:aliexpress.com在第2个字节的位置就出现了一个a标签,flyingtiger_com在第61300字节插了个div,temu.com直接在第6个字节开了body,vaude.com在第6405字节插了div。
比例是2.4%,不算高。但vaude.com的产品页因此掉出去了三条社交属性——分享标题、分享图和分享地址。它的商品被分享到社交平台时,抓到的信息不完整,这个损失是实打实的。
还有一类声明是自己躲起来的
noscript和template这两个标签里的内容不参与正常解析。样本里34个站的这两种容器里塞着173条声明,看上去挺吓人。
逐条拆开之后,164条是样式表链接——这是给禁用JavaScript的访客准备的降级方案,本来就该放在那儿。剩下9条里有2条Google站点验证码和几条电商平台的配置字段。又一个虚惊:数字很大,真正有问题的只有个位数。
内页那批更极端,502条藏起来的声明全部是样式表链接,一条杂质都没有。这种时候报告里那个总数就是个纯噪音,写进汇报材料只会让人白紧张一场。
断点在第几个字节,决定了损失有多大
四个站的断点位置差得很远:aliexpress.com在第2字节,temu.com在第6字节,vaude.com在第6405字节,flyingtiger.com在第61300字节。
断点越靠前,被赶出去的声明越多。但反过来说,断点在第2字节的站,它的head实际上从一开始就不存在,所有元信息都在body里——这种页面通常是单页应用的骨架,元信息本来就打算由脚本在运行时补,服务端吐出来的这份只是个壳。
断点在中间的那种才最容易被忽略:前面一半声明好端端在head里,后面一半掉了出去,从源码上看毫无异样,缩进都是对的。vaude.com就是这种,掉出去的正好是三条社交属性。
被搬进body的声明,搜索引擎还认吗
这是个没有统一答案的问题,得分开说。
Google在自己的文档里明确表示元标记应当放在head里,同时也说过它有能力处理一部分位置不规范的写法。规范网址这个字段Google讲得最直白:只有head里的那条会被采纳,body里的会被忽略。机器人指令的口径类似。至于社交平台,Facebook和Twitter的抓取器实现上通常整篇扫描,body里的属性也能被读到,所以vaude.com那几条掉出去的分享属性未必真的失效。
换句话说,同一个位置错误,对不同的读取方后果不同:搜索引擎那边可能全废,社交平台那边可能没事。这已经不是"位置决定"的问题了,是"谁在读"的问题——那是另一条线。
那些把编码声明写到十几万字节之后的站,为什么没乱码?
普查时有一项数据特别扎眼:168个首页里,字符编码声明的字节偏移量中位值是72,很靠前;但有33个站超过了1024字节,最远的一个在第521316字节。
96.4%的前置字节是内联脚本
| 站点 | 编码声明的字节位置 | 它前面的脚本字节数 | 脚本占比 |
|---|---|---|---|
| functionofbeauty.com | 521316 | 521042 | 99.9% |
| getquip.com | 156818 | 156279 | 99.7% |
| fromourplace.com | 151756 | 151249 | 99.7% |
| monos.com | 55914 | 21479 | 38.4% |
| decathlon.com | 9470 | 458 | 4.8% |
| shopify.com | 1986 | 1858 | 93.6% |
33个站的中位数是:编码声明之前有18974字节的内联脚本,占前置内容的96.4%。把编码挤到后面去的不是别的,就是那一大坨内联进来的初始化数据、同意管理平台的配置和状态预填。
它们全都被响应头接住了
这33个站,我逐个去查了同期抓到的响应头:33个全部带着字符集声明,没有一个裸奔。147个能配上响应头的首页里,143个头里写着UTF-8,4个写着ISO-8859-1,19个没写。
换句话说,那33个页面里的编码声明处在一个很尴尬的位置——它有没有生效根本无所谓,因为上一层已经把这件事定了。这个字段看起来在工作,实际上一直没在工作。
顺着这条线往下还有一串更细的机制问题:解析器到底扫多少字节、检测工具和浏览器的分界在哪里。乱码的分界不在1024字节那篇把这一层单独拆开讲过,这里就不重复了。真要排查乱码,从字节层面看一眼实际编码比猜要快得多。
那4个声明ISO-8859-1的响应头
147份配上响应头的首页里,143个头里写UTF-8,4个写ISO-8859-1。这4个站值得单独看一眼,因为响应头的优先级在文档之上——如果它们的页面里有非拉丁字符,而文档里的声明写的是UTF-8,那么头会赢,页面会乱。
本地实验里我把这个场景复现了一遍:响应头声明ISO-8859-1,页面里写UTF-8,正文放中文。浏览器返回的编码是windows-1252,中文变成了层级è£å†³那一串。反过来,头写UTF-8、页面写ISO-8859-1,结果是UTF-8,正常显示。
两个方向都测了,结论一致:头赢,而且赢得彻底,文档里的声明连争一下的机会都没有。那4个站如果内容全是英文,眼下不会出事;哪天上了一个带重音符号的法语产品名,问题就来了。
没有兜底的那19个站
还有19个首页的响应头里根本没写字符集。这19个站完全靠文档里的声明活着。它们目前也没出事——因为编码声明都写在很前面。
这就构成一个有意思的分布:编码声明写得靠后的站,全都有响应头兜底;没有响应头兜底的站,编码声明全都写得靠前。两种配置各自成立,但没有人是靠运气活着的。这个巧合本身说明,那些把编码挤到十几万字节之后的站,用的是同一类电商平台,而那类平台的服务端默认就会发字符集。
换个说法:33个站的编码声明看起来放错了地方,实际上是平台替它们兜住了。这不是它们做对了什么,是它们碰巧在一个做对了的平台上。
这些重复是谁写进去的?
知道了谁赢,还得知道这局是怎么开起来的。337份快照里的重复声明,来源基本能归成四类。
主题和插件各输出一遍
最常见的一类。建站主题自带一套元信息输出,装的SEO插件又输出一套,两边都不知道对方存在。这类冲突有个特征:两条声明在字节位置上离得很近,因为它们都在head的同一个渲染阶段被拼进去。bellroy.com那3条站点验证码分别在1717、1810、1903字节,间隔不到100字节,一看就是同一段模板循环出来的。
CMS装了SEO插件冒出两个canonical怎么归一那篇把这一类的排查和收敛路径讲全了,这里不再展开。
营销代码片段带进来的
allbirds.com两条Google站点验证码,字节位置分别是290和23763,隔了两万多字节。dreametech.com那两条在149244和162565。离得这么远,说明它们不是同一段代码生成的——一条在模板头部,另一条大概率是某个营销工具的代码块自带的。
建站平台自己注入的那一份
用SaaS建站平台的站点还有第四个来源:平台本身。样本里52个站带着同一家电商平台的三个私有字段——数字钱包标识、结账接口令牌、还有一个会话标记,站主在后台完全看不到它们。
这类字段不会跟你的内容字段冲突,但它们会把head撑得很长,把你自己的声明往后挤。前面说的33个编码声明超过1024字节的站,绝大多数是这种情况:不是站主把编码写晚了,是平台在它前面塞了十几万字节的初始化数据。
不同环境的配置各留了一份
laneige.com和sulwhasoo.com的分享图各写了两条,一条带www,一条不带。同一张图,两个域名写法。这通常是站点做过域名规范化,老配置没清干净。
gymshark.com的主题色更直白:一条是纯黑,一条是纯白。深色模式和浅色模式各留了一条,但这个字段不支持这种写法,浏览器只会取第一条,于是永远是黑的。
拼错的那一类
有一个站的兼容模式声明写成了x-ua compatible,中间的连字符掉了。这种拼错既不会报错也不会生效,静静地在那儿待着。样本里这类拼写不规范的声明有2条。
同一份样本里还有几个更奇怪的写法:bigcommerce.com把6条社交关联地址写成了name="og:see_also"——社交属性该用property而不是name,写成name之后既不属于社交协议,也不是任何一个已知的元标记,属于凭空造了个字段出来。这类东西不会有人来告诉你写错了,它就这么挂着。
怎么分辨一条重复是模板给的还是外挂给的
有个很土但很好使的判据:看两条声明的字节距离。
同一段模板循环出来的,间隔通常在几十到几百字节;两个独立来源拼进去的,间隔常常是几千甚至几万字节。前面那两个例子——bellroy间隔不到100字节,allbirds隔了两万多字节——就是这两类的典型。
知道来源之后再决定找谁改。模板里的重复找前端,一次改完;外挂带进来的得找营销或者投放那边,因为那段代码往往是从第三方后台复制过来的,改模板没用,下次发版又回来了。
还有第三种情况:两条都不是你写的。用建站平台的站点,平台会往每个页面注入自己的一套字段,你在后台看不到它们,只能在源码里看见。这类东西改不了,只能记下来,在做审计的时候排除掉,免得每次体检都被同一条告警骚扰。
哪些重复必须今天改,哪些可以先放着?
把前面的数据揉在一起,处理顺序其实很清楚。别一上来就搞大扫除,先按后果排。
第一档:会改变索引行为的
机器人指令冲突、规范网址指向不同地址、编码声明值不同——这三类直接影响页面能不能被收录、被收录成哪个地址、正文能不能被正确读出来。发现一条改一条,不用排期。
判断标准很简单:如果两条声明说的话,会让搜索引擎做出不同的动作,那它就是第一档。
这一档还有个附加动作:改完之后要验证一次,而且要用浏览器验证,不能只看源码。因为你改掉的可能是那条本来就没生效的,真正生效的那条还在原地。
第二档:会改变对外展示的
分享标题、分享图、分享地址、主题色。这些不影响收录,但影响页面被转发出去时长什么样。gymshark那个黑白冲突已经存在很久了,页面照常打开,只是标签栏的颜色永远是它没想要的那个。这一档可以攒到下个迭代一起改。
分享图有个额外的坑:写多条时消费方通常取第一条,而很多站的第一条是logo,第二条才是真正想推的主图。生成分享图这件事本身也有不少细节,顺序只是其中之一。
第三档:冗余但无害的
值相同的重复、多个站点验证码、noscript里的样式表链接。这些可以只记在技术债清单上,什么时候顺手清什么时候清。把第三档当成第一档来处理,是很多技术SEO排期表最后没人执行的原因。
一个例外:位置问题优先于重复问题
如果head里有那个div,先处理它。因为它一次性把后面所有声明的位置都毁了,改一处等于修好三条。而重复通常只影响一个字段。
按这三档排,337个快照里剩多少活
拿本文的样本自己走一遍这个排序:71条值冲突里,第一档只有机器人指令那2个站,而且逐条看下来都不构成真冲突,实际是0;第二档是分享图3个站、分享图尺寸2个站、主题色5个站、视口1个站,合计11个站;剩下的全归第三档。
也就是说,看起来20.2%的页面有冲突,真正需要动手的只有11个站,占样本的6.5%左右。如果一开始就把71条打成一张整改表发给前端,那张表大概率会躺到年底。
这个收敛过程本身比结论重要:先按后果分档,再看每一档还剩几条,最后才决定要不要立项。技术SEO的清单如果不做这一步,它的长度只会吓退执行的人。
改之前先确认这一条真的归你管
技术SEO的排期表最常见的失败方式,不是排错了顺序,是排了一堆自己改不动的事。
动手之前先分三类:模板里的、插件里的、平台注入的。第一类自己就能改;第二类要看插件有没有开关,很多SEO插件的元信息输出是可以整体关掉的,关掉之后由主题统一出;第三类基本没辙,记进白名单别再报了。
另外有个顺序上的小坑:先关插件再改模板,中间会有一段时间某些字段完全没有输出。如果站点流量不小,这个空窗期最好挑在低峰,或者干脆先在模板里补齐、确认输出正确了,再去关插件那一路。技术SEO一堆问题先修哪个那篇给的排序框架,在这种时候比凭感觉靠谱。
修不动的时候,换一条路
遇到模板改不动的情况——外包做的、发版流程长、或者干脆源码不在自己手上——可以反过来从插件那一侧收敛:把插件的元信息输出整体关掉,只留模板一路;或者反过来,让模板不输出,全交给插件。关键不是选哪一路,是只留一路。
切换的时候有个顺序上的小坑:先关掉一路再补另一路,中间会有一段时间某些字段完全没有输出。稳妥的做法是先让新的那一路把字段补齐、在预发环境确认输出正确,再去关旧的那一路,别让线上出现空窗。
怎么在自己站上把这些问题查出来?
不需要买工具。下面这些命令在任何一台能跑curl的机器上都能用。
数一数关键字段各写了几遍
curl -s https://example.com/ \
| grep -o -iE '<(meta|link)[^>]*(canonical|name="robots"|name="description"|charset)[^>]*>' \
| sort | uniq -c | sort -rn输出里每一行前面的数字就是出现次数。大于1并且内容不同的,就是要处理的对象。
看编码声明落在第几个字节
curl -s https://example.com/ | head -c 2048 | grep -bo 'charset'前面那个数字是字节偏移。查不到就说明它在2048字节之外,再配合下面这条看响应头有没有兜底。
curl -sI https://example.com/ | grep -i '^content-type'查head里有没有不该有的标签
curl -s https://example.com/ \
| sed -n '1,/<\/head>/p' \
| grep -o -iE '<(div|span|p|a|img|iframe|button|dialog)\b' \
| sort | uniq -c这条会有误报,因为内联脚本和样式里可能出现这些字符串。有输出就手工去看一眼源码,确认它是不是真的标签。
用浏览器给出最终答案
命令行看到的是字节,浏览器看到的是结果。在控制台里跑这几行,拿到的是解析之后的真实状态:
document.characterSet
document.getElementsByTagName('title').length
document.head.querySelectorAll('link[rel=canonical]').length
document.body.querySelectorAll('meta').length最后一行如果不是0,说明有声明掉进了body。这是最快的一个判断。想省事的话,现成的Meta标签检测器能一次把整页的元信息拉出来打分,适合做批量初筛;页面结构分析那一套则更偏标题层级和语义标签。
批量跑一遍全站的关键页
单页查完还要看面。把要查的地址存成一个文本文件,一行一个,然后:
while read -r u; do
n=$(curl -s -m 20 "$u" | grep -c -iE '<link[^>]*rel="?canonical')
printf '%s\t%s\n' "$n" "$u"
done < urls.txt | sort -rn | head -20把第一列大于1的挑出来看。同样的写法换个正则就能查别的字段。这段脚本的价值不在于它多聪明,在于它跑一次的成本接近零,可以每周挂一次。
把这条检查接进上线流程
比事后审计更省事的是不让它发生。在构建流水线里加一步:请求首页和三个典型内页,数几个关键字段的出现次数,超过1就让这一步失败。
#!/bin/bash
FAIL=0
for u in "$@"; do
html=$(curl -s -m 20 "$u")
for pat in 'rel="?canonical' 'name="?robots' 'name="?description'; do
n=$(printf '%s' "$html" | grep -c -iE "<(meta|link)[^>]*$pat")
[ "$n" -gt 1 ] && { echo "DUP $n $pat $u"; FAIL=1; }
done
b=$(printf '%s' "$html" | sed -n '/<\/head>/,$p' | grep -c -iE '<meta[^>]*name="?(robots|description)')
[ "$b" -gt 0 ] && { echo "INBODY $b $u"; FAIL=1; }
done
exit $FAIL不到20行,覆盖了本文说的两类问题:写重了和掉出去了。把检查接进流程,比把问题写进文档有用得多——文档会被忘掉,流水线不会。
阈值可以按站点情况调。允许多个站点验证码的话,就把那个字段从循环里摘出去单独放行;有些平台会固定注入一条自己的声明,把它加进白名单,别让流水线天天为同一件事红一次。检查项的价值来自它红的时候确实有事,天天红的检查最后都会被人加个跳过。
要注意的是这段脚本用的是纯文本匹配,遇到内联脚本里出现同样字符串会误报。真要做严谨的解析,得用带HTML解析器的语言写,或者直接在无头浏览器里跑前面那四行JavaScript。日常巡检用文本版够了,误报手工看一眼就能排除。
同一套判断,还能用到哪些地方?
重复声明只是一个具体场景。它背后那个问法能搬到很多地方去。
四个问题
面对任何一条写在页面上的声明,可以顺着问一遍:
第一,这件事在我这个页面上一共被声明了几次?不只是同一个字段写了几遍,还包括同一件事换个字段说了几遍——标题说一遍,分享标题说一遍,结构化数据里的名称再说一遍。
第二,如果它们说的不一样,谁会赢?是取第一条、取最后一条、合并、还是全部作废?这个答案每个字段都不同,不能凭直觉。
第三,决定输赢的那个依据,跟我的意图有关系吗?如果答案是位置,那就跟意图无关——先写的赢,不是更对的赢。
第四,换一个读它的人,赢家会不会换?浏览器、Google、社交平台、AI爬虫,各读各的字段。
为什么这套规则不可能统一
有人会问:既然这么乱,规范制定者为什么不统一一下?
因为这些字段根本不属于同一个体系。字符编码和标题是HTML自己的,规则写在HTML规范里;规范网址和机器人指令是搜索引擎定义的,规则写在各家的开发者文档里;社交属性是Facebook当年推的一套协议,后来被别家沿用;站点验证码是各平台自己的私有约定。
它们唯一的共同点,是都被塞进了同一个head。一个由四五拨人分别定义、又必须共处一室的空间,规则不统一是必然的,能各自跑起来已经不容易。
所以指望有朝一日出一份统一规范是不现实的。规范网址这个字段本身的跨页场景与冲突诊断就是个例子:同一个字段在不同场景下的行为都不一样,跨字段就更别提了。能做的只有一件事:碰到一个字段,就去查一次它自己的规则,别用别的字段的经验去猜。
把四个问题跑一遍要多久
对单个页面来说,前两个问题用一次curl加一次控制台就能答完,五分钟以内。第三个问题要查字段的规则文档,第一次查会慢,查过一次就记住了。第四个问题最费事,得换身份重新抓一遍。
实际排期的时候,前两个问题应该做成常规巡检,后两个问题在改版和换平台的时候各做一次就够。浏览器扩展那一类工具能把第一个问题的成本压到点一下鼠标,适合日常随手看。
能套上去的其他场景
机器人指令这件事在页面上写一遍、在站点根目录的规则文件里写一遍、还能在响应头里再写一遍,三层的关系不是覆盖而是各管一段。规则文件和页面指令什么时候用哪个是必须先分清的前置问题。
响应头自己那一层里也有同样的毛病。142个站里108个在同一份响应里写了互相矛盾的字段,那是同一层内部的冲突,跟本文这种页面内的冲突是一个道理。
对于那些压根没有head的地址——接口、订阅源、纯数据文件,声明只能写在响应头里。这类地址拿什么说自己不想被收录是另一个独立的话题。
同一件事换个字段再说一遍,也算重复
本文数的是同一个字段写了几遍。还有一种更隐蔽的重复:同一件事用不同字段各说了一遍,而且说的不一样。
一个首页可以有六个地方在自我介绍:标题标签、社交分享标题、推特卡片标题、页面上的H1、结构化数据里的名称、还有站点名称字段。它们本该指向同一个东西,但没有任何机制保证这一点,也没有哪个工具会因为它们不一致而报错。
这一类的裁决规则和本文讲的完全不同——不再是"谁排在前面",而是"读它的人取哪个字段"。搜索结果里显示的是标题标签,分享到社交平台显示的是社交标题,AI摘要里报出来的可能是结构化数据里那个名称。同一个页面,三个读者,三个名字。
这已经是另一条线了,值得单独量一次。
写模板的时候多做一步
最省事的预防办法是在模板层做一次收敛:所有元信息只从一个函数出口输出,插件想加就往这个函数里注册,不许各写各的。这样重复在生成阶段就被拦住了,不用等上线之后再拿脚本去数。
做不到的话,退一步,在构建流程里加一条检查:拉一次首页和三个典型内页,数关键字段的出现次数,超过1就让流水线报警。这条检查写完不到20行,比事后审计划算得多。
常见问题解答
同一个页面写两条描述,Google会用哪一条?
实现层面浏览器取第一条。但Google对描述这个字段本来就不保证采用——它经常根据查询词自己从正文里摘一段。写两条最坏的结果不是选错,是两条都不用。真正决定展示效果的是描述本身写得怎么样,不是写了几条。
规范网址写了两条完全一样的,需要改吗?
不急。样本里marinelayer.com就是这个情况,7个页面全是两条相同的地址。值一样的时候不构成冲突信号,属于第三档冗余。但如果你的模板会在某些页面上让这两条取到不同的值,那就变成第一档了,得回去看生成逻辑。
把编码声明放在head最前面,真的有必要吗?
有必要,但理由不是防乱码。33个把它写到很后面的站,页面全都正常显示,因为响应头替它们兜住了。放前面的真实好处是不依赖那个兜底——万一哪天CDN配置变了、或者页面被另存为本地文件,头就不在了。这是一条成本几乎为零的保险。
head里的div是怎么混进去的?
多数是第三方代码块自带的。同意管理平台、客服挂件、A/B测试工具,有些会在初始化时往当前位置插一个容器。如果这段代码被放在head里,容器就跟着进去了。排查办法是把head的源码从头看一遍,遇到第一个非法标签就是断点。
head里的声明顺序,会影响搜索排名吗?
不会。顺序只决定同一个字段的多条声明里哪一条生效,以及一条声明有没有掉出head。不同字段之间没有权重差别,把描述提到编码前面不会让它更受重视。
唯一跟顺序沾边、又确实有性能后果的是字符编码:把它写在最前面,解析器一开始就能确定怎么解释字节;写在很后面,解析器得先按默认编码往下走,遇到声明再回过头调整。这是渲染速度的事,不是排名的事。
为什么七成首页没有H1也没出事?
因为搜索引擎早就不只靠这一个标签判断页面主题了。标题标签、正文首段、面包屑、结构化数据都在提供同样的信息。H1缺席更像是一个体检指标偏低,不是急症。真正要紧的是别出现另一个极端——样本里有一个站的首页有13个H1,那才是真的把信号搅浑了。
动态渲染的站,这套办法还管用吗?
部分管用。本文的普查只看服务端吐出来的原始HTML,前端框架在客户端补进head的那些声明没算进去。对于纯客户端渲染的站,源码里可能什么都没有,元信息全靠脚本写。
这种情况下命令行那几条查不出东西,得改用浏览器控制台那四行——它们读的是渲染完之后的DOM,客户端补的那部分也在里面。两套办法配合起来看:命令行看服务端给了什么,控制台看最终变成了什么,中间的差额就是脚本干的事。
顺带一提,这个差额本身值得关注。搜索引擎的渲染有排队,脚本补的元信息不一定每次都能被及时读到;社交平台和多数AI爬虫压根不执行脚本,它们看到的只有服务端那一份。
这些数据能代表中文站吗?
样本全是海外品牌站,主流是Shopify和几家企业级电商平台。中文站的技术栈不一样,重复的具体分布肯定不同。但裁决规则是浏览器和搜索引擎那边定的,跟站点在哪儿没关系。方法可以直接套,数字要自己重新量一遍。
检测工具报的重复数,和实际生效的是一回事吗?
不是。工具数的是源码里写了几条,浏览器算的是最终用了哪条。两条编码声明在DOM里都存在,工具会报2条,而实际生效的只有第一条。反过来,掉进body的那条声明工具也可能照样计数,但搜索引擎那边可能根本不认。
做审计的时候,工具的输出应该当成线索而不是结论。拿到重复清单之后,先按字段查一遍裁决规则,再决定哪些进整改列表。
把重复清干净,排名会涨吗?
大概率不会。这类问题的性质是消除不确定性,不是增加权重。清掉一条矛盾的规范网址,收益是搜索引擎不再需要猜你想要哪个地址;清掉一条重复的主题色,收益是浏览器标签栏的颜色变成你想要的那个。
把它当成体检项而不是增长项,预期会准得多。真正会换来流量变化的,是那些改完之后索引行为跟着变的字段——收录范围、规范地址归并,这两类才有可能在数据上看出来。
只有一条声明,是不是就一定安全?
不一定。数量对了,位置还可能不对。样本里那几个站的head里插了div,后面的声明只有一条,但整条掉进了body。工具报"canonical: 1",看着完全正常。
所以查完数量还得查位置,在浏览器控制台跑一行document.head.querySelectorAll('link[rel=canonical]').length,返回0就说明那唯一的一条不在它该在的地方。
权威参考资料
本文标题:《重复的meta标签,浏览器和搜索引擎各按各的规矩挑》
本文链接:https://zhangwenbao.com/duplicate-meta-tag-first-declaration-wins-audit.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0