重复的meta标签,浏览器和搜索引擎各按各的规矩挑

重复的meta标签,浏览器和搜索引擎各按各的规矩挑
张文保 48 分钟阅读 3,657 阅读
本文目录
  1. 一个页面里把同一件事说两遍,到底有多常见?
  2. 这次没有算什么
  3. 四分之一的首页至少重了一处
  4. 两次快照的重复清单一条不差
  5. 内页比首页重得更厉害
  6. 顺带量到的三处缺席
  7. 重复和冲突不是一回事,差在哪里?
  8. 写重了但值一样:无害
  9. 写重了值还不一样:要看字段
  10. 七个值得单独看一眼的现场
  11. 验证码那16个站其实没做错
  12. 值不一样的时候,浏览器到底挑哪一条?
  13. 为什么不能在线上测
  14. 每一组都配了对照
  15. 两个字符编码声明:第一条赢
  16. 两个标题标签:第一条赢
  17. 两个规范网址:浏览器不管
  18. 注释掉的那条不算数
  19. 被忽略的那条,工具还是能看见
  20. 一张表看完六组实验
  21. 为什么是第一条赢,而不是最后一条?
  22. 解析器是流式的,它没有回头的习惯
  23. 但不是所有字段都遵守先到先得
  24. 五种裁决方式,没有一处写在一起
  25. 五种裁决方式背后是五种不同的处境
  26. 一个反直觉的推论
  27. head里放一个div,会发生什么?
  28. 三条声明被一个div请了出去
  29. 真实站点上有多少个这种div
  30. 还有一类声明是自己躲起来的
  31. 断点在第几个字节,决定了损失有多大
  32. 被搬进body的声明,搜索引擎还认吗
  33. 那些把编码声明写到十几万字节之后的站,为什么没乱码?
  34. 96.4%的前置字节是内联脚本
  35. 它们全都被响应头接住了
  36. 那4个声明ISO-8859-1的响应头
  37. 没有兜底的那19个站
  38. 这些重复是谁写进去的?
  39. 主题和插件各输出一遍
  40. 营销代码片段带进来的
  41. 建站平台自己注入的那一份
  42. 不同环境的配置各留了一份
  43. 拼错的那一类
  44. 怎么分辨一条重复是模板给的还是外挂给的
  45. 哪些重复必须今天改,哪些可以先放着?
  46. 第一档:会改变索引行为的
  47. 第二档:会改变对外展示的
  48. 第三档:冗余但无害的
  49. 一个例外:位置问题优先于重复问题
  50. 按这三档排,337个快照里剩多少活
  51. 改之前先确认这一条真的归你管
  52. 修不动的时候,换一条路
  53. 怎么在自己站上把这些问题查出来?
  54. 数一数关键字段各写了几遍
  55. 看编码声明落在第几个字节
  56. 查head里有没有不该有的标签
  57. 用浏览器给出最终答案
  58. 批量跑一遍全站的关键页
  59. 把这条检查接进上线流程
  60. 同一套判断,还能用到哪些地方?
  61. 四个问题
  62. 为什么这套规则不可能统一
  63. 把四个问题跑一遍要多久
  64. 能套上去的其他场景
  65. 同一件事换个字段再说一遍,也算重复
  66. 写模板的时候多做一步
  67. 常见问题解答
  68. 同一个页面写两条描述,Google会用哪一条?
  69. 规范网址写了两条完全一样的,需要改吗?
  70. 把编码声明放在head最前面,真的有必要吗?
  71. head里的div是怎么混进去的?
  72. head里的声明顺序,会影响搜索排名吗?
  73. 为什么七成首页没有H1也没出事?
  74. 动态渲染的站,这套办法还管用吗?
  75. 这些数据能代表中文站吗?
  76. 检测工具报的重复数,和实际生效的是一回事吗?
  77. 把重复清干净,排名会涨吗?
  78. 只有一条声明,是不是就一定安全?
  79. 权威参考资料

摘要: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日快照
可解析文档169168
head声明总条数29362919
每篇声明数中位值1718
单篇最多4343
至少一处重复的文档42(24.9%)42(25.0%)
多出来的声明条数110110
至少一处值冲突的文档34(20.1%)34(20.2%)
值冲突的条数7171

四分之一。这个比例比我预想的高,但更让人意外的是它有多稳。

两次快照的重复清单一条不差

两次抓取相隔约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站点验证码54881616
Facebook域名验证码263766
主题色707555
社交分享图919533
分享图宽高52 / 5055 / 532 / 21 / 1
视口设置16316521
机器人指令626522
字符编码14116970
描述13313520
规范网址12012110

七个值得单独看一眼的现场

把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,一条写nonenone不是这个字段的合法取值,写了等于没写;即便合法,也轮不到它,第一条已经定了。

验证码那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.com52131652104299.9%
getquip.com15681815627999.7%
fromourplace.com15175615124999.7%
monos.com559142147938.4%
decathlon.com94704584.8%
shopify.com1986185893.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

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交