外贸独立站多语言版本被判重复,先别删页面。按 Google 官方文档口径,只有网页主要内容没翻译时,本地化版本才会被当作重复网页;标注写错,则整组 hreflang 会被忽略。下面4处按顺序查。
去年接手过一个站,英文版和德语版产品页主内容是各自写的,德语版照样进不了索引,站长后台提示重复网页。前后看过七八个类似的站,现象都长得差不多,成因分两类:一类是主内容确实只做了模板翻译,另一类内容没问题、标注本身被搜索引擎丢掉了。这两类的修法完全不同,混着做会白改好几轮。
一、先分清是主内容没本地化,还是标注被忽略
Google 在本地化版本文档里写了两句需要连起来读的话。一句是:如果网页的主要内容未经翻译,则该网页的本地化版本仅会被视为它的重复网页。另一句是:如果两个网页没有互指,系统将会忽略相关标记。前一句管内容,后一句管标注。
所以动手前先回答一个问题:被判重复的那一版,主内容到底翻没翻。只翻译了导航和页脚、主体还是原文,落在前一句覆盖的范围里,先补内容;主内容是独立写的,问题在标注,再往下查。围绕外贸独立站怎么做的讨论里,这两件事经常被合成一句"多语言容易被判重复",其实是两条独立的问题线。
用 Search Console 的网址检查看具体地址,常见三种结果。
| 网址检查显示 | 说明什么 | 处理方向 |
|---|---|---|
| 重复网页,Google 选择了不同规范网址 | 组内另一版被选为对外展示版本 | 先确认它选的是不是你要的那一版,再核标注 |
| 重复网页,未编入索引 | 这一版被判与组内其他版雷同,且没被选中 | 核对主内容有没有本地化,再补标注 |
| 已编入索引 | 这一版状态正常 | 换组内下一个语言版本继续查 |
网址检查里还能展开看 Google 抓取到的 HTML。这个视图的用处是确认标注是否真的出现在服务端返回的 head 里。如果后台编辑器里能看到标注、抓取到的 HTML 里没有,那问题不在写法上,在渲染时机上,往下查标注也没有意义。
二、标注有三种落地方式,官方说它们等效
Google 官方文档列出的是三种方式:HTML 的 head 标签、HTTP 响应标头、XML 站点地图。文档的表述是这三种方法从 Google 的角度看等效,选最适合站点的那一种。同一段还提醒,可以三种同时用,但对搜索表现没有好处,管理三套实现要难得多。
这句话值得单独记下来。实操里出问题的站,很多是因为模板里写了一套 head 标注,站点地图生成器又输出了一套,两套内容对不上,等于自己给搜索引擎发了矛盾信号。选一种,写完整,语言版本增减时只改这一处。
| 落地位置 | 能否覆盖 PDF 等非 HTML 文件 | 增减一个语言版本要动什么 | 适合的站 |
|---|---|---|---|
| 页面 head 的 link 标签 | 不能 | 模板改动,全站重新发布 | 语言数少、模板统一的站 |
| HTTP 响应标头 | 能 | 服务端或 CDN 规则改动 | 有本地化 PDF、能自己控服务器的站 |
| XML 站点地图 | 能 | 重新生成一个文件 | 语言和页面数量多、希望集中管理的站 |
方式一:HTML head 里的 link 标签
三个语言版本为一组,组内每个页面的 head 里放同一套标注,包含自己、两个兄弟版本,以及兜底页。下面用螺栓产品页举例。
<link rel="alternate" hreflang="en" href="https://example.com/en/products/m8-bolt/" /> <link rel="alternate" hreflang="de" href="https://example.com/de/produkte/m8-bolt/" /> <link rel="alternate" hreflang="es" href="https://example.com/es/productos/m8-bolt/" /> <link rel="alternate" hreflang="x-default" href="https://example.com/products/m8-bolt/" />这四行要原样出现在英文版、德语版、西语版三个页面的 head 里,一行不差。德语版用的是/de/produkte/这样的本地化 URL,不是英文 URL 加语言前缀。示例域名换成真实域名之前先想清楚一件事:标注里的 href 必须和这一页的规范地址逐字符一致,协议、主机名、结尾斜杠三样都要对上,/fr和/fr/在搜索引擎眼里是两个地址。
方式二:HTTP 响应标头
这套写法对 PDF 规格书、参数表这类没有 HTML head 的文件是唯一可行的方式。响应头里用 Link 字段按同样的顺序列出来。
Link: <https://example.com/en/products/m8-bolt/>; rel="alternate"; hreflang="en", <https://example.com/de/produkte/m8-bolt/>; rel="alternate"; hreflang="de", <https://example.com/es/productos/m8-bolt/>; rel="alternate"; hreflang="es"验证不用猜,直接看线上返回的响应头。注意要取线上的,不是本地配置文件。
$ curl -sI https://example.com/en/products/m8-bolt/ | grep -i '^link:' Link: <https://example.com/en/products/m8-bolt/>; rel="alternate"; hreflang="en", ...方式三:XML 站点地图
站点地图方式把标注集中到一个文件里,语言版本增减只改一处,出错面最小。先在 urlset 上声明 xhtml 命名空间,再让每个 url 节点把组内所有版本列全,同样包含自己。
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"> <url> <loc>https://example.com/en/products/m8-bolt/</loc> <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/products/m8-bolt/"/> <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/produkte/m8-bolt/"/> <xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/productos/m8-bolt/"/> <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/products/m8-bolt/"/> </url> <!-- 德语版、西语版各写一个同样的 url 节点,四行 xhtml:link 完全相同 --> </urlset>三、4处最容易出错的地方
第一处:回指有没有断
官方原文是:如果两个网页没有互指,系统将会忽略相关标记。断掉的那一对会被丢掉,组内其余互指的对仍然处理。这句规则后面还跟着一句解释,说这么定是为了防止别的网站随意加一个标记,把自己的页面声明成你页面的备用版本。
工程上的解法不是逐对去核对,而是让同一组标注从同一个数据源生成。三个语言版本的 head 里,那一套 link 标签应当逐字节相同;走站点地图方式时,每个 url 节点的 xhtml:link 子节点也应当完全相同。手工维护九个页面,八个写对、一个漏了一行,整组的关系就断在漏掉的那一处,而且页面打开看还是正常的。
第二处:自己有没有被列进去,x-default 有没有
官方原文是:每个语言版本都必须列出其自身以及所有其他语言版本。三个语言版本加一个兜底页,每个页面的 head 里应当是四行,不是三行。少的那一行往往是自己,这类错误在 Search Console 里看不到提示,但整组标注会因此不被采纳。
x-default 是给语言和地区都与站点不匹配的访问者准备的兜底页。官方说明它专为语言选择器页面设计,所以最合适的位置就是那个让用户自己选语言的页面。这句话反过来说也成立:x-default 不要机械地指向首页。产品页所在的那一组里,兜底页指向一个与产品无关的首页,等于把找不到匹配语言的用户丢到一个不相关的页面,兜底就失去了意义。
第三处:语言和地区代码的写法
官方规则是:第一个代码为语言代码,采用 ISO 639-1 格式,后面跟一个可选的地区代码,采用 ISO 3166-1 Alpha 2 格式,中间用短划线分隔;只支持这两套标准里列出的代码。下面这几类写法都会出问题。
| 常见写法 | 问题在哪 | 应该怎么写 |
|---|---|---|
| en-UK | UK 是被预留作他用的代码,这部分注解会被忽略 | en-GB |
| es-419 | 不在 ISO 639-1 与 ISO 3166-1 标准内,不支持 | 拆成具体国家代码,或只写语言代码 es |
| en_US | 分隔符用错,标准用的是短划线 | en-US |
| be | be 是白俄罗斯语的语言代码,不是比利时 | 面向比利时按语言写 de-BE、fr-BE 或 nl-BE |
| zh-TW | 写法可用,文字由地区推断 | 需要明确文字时写 zh-Hant 或 zh-Hans |
还有一条容易被误解的官方说明:Google 不使用 hreflang 或 HTML lang 属性来判断网页的语言,它用算法判断。也就是说 hreflang 不是"告诉搜索引擎这页是什么语言"的手段,它是"告诉搜索引擎这一组页面互为语言变体"的关系声明。代码写错的后果,不是被当成别的语言,而是这条关系不被采纳。
第四处:和 canonical 打架
这一处最隐蔽,因为两个标签单独看都没写错。每个语言版本的 canonical 应当指向自己;一旦德语页的 canonical 指向英文页,就等于同时告诉搜索引擎两件相反的事:hreflang 说这几页是本地化变体、按用户语言分别展示,canonical 说忽略这一页、把它并到那个地址上。两个信号冲突时,被合并掉的那一版通常不会再出现在索引里,再补 hreflang 也救不回来。
Google 的规范网址文档对这件事的要求是:使用 hreflang 时,应为页面指定同语言的规范网址;找不到同语言版本时,才退而指向最接近的替代语言版本。落到操作上就是一句话,每个语言版本的 canonical 写自己这一版。
这类问题在"从默认语言自动生成多语言版本"的建站系统上出现频率偏高,因为规范地址常常由默认站点配置带出来,多语言版本跟着继承。查的时候逐个语言版本确认,不要只看默认语言那一版就下结论。做独立站SEO 的核对清单里,canonical 与 hreflang 这一项建议单独列一行,它和收录问题缠在一起时最难分开看。
四、把4处合成一个脚本跑完
逐页肉眼比对不现实。用 Python 标准库写个检查,输入组内任一页面的 URL,抓取该页面,输出三样东西:标注条数、自己有没有被列进去、每一对回指是否成立,另外顺带标出不符合 ISO 格式的代码写法。
# hreflang_check.py # 用法: python hreflang_check.py https://example.com/en/products/m8-bolt/ import sys, re import urllib.request from html import unescape PAIR_RE = re.compile(r'hreflang=["\']([^"\']+)["\'][^"\']*href=["\']([^"\']+)["\']', re.I) VALID = re.compile(r'^(x-default|[a-z]{2,3}(-[a-z]{4})?(-[a-z]{2})?)$', re.I) def fetch(url): req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req, timeout=20) as resp: return resp.read().decode('utf-8', 'ignore') def parse(html): return [(code, unescape(href)) for code, href in PAIR_RE.findall(html)] def main(url): anns = parse(fetch(url)) if not anns: print('未找到标注: ' + url) return codes = [code for code, _ in anns] hrefs = [href.rstrip('/') for _, href in anns] print('标注条数: %d' % len(anns)) print('自指: %s' % ('已包含' if url.rstrip('/') in hrefs else '缺失')) print('x-default: %s' % ('已包含' if 'x-default' in codes else '缺失')) for code in codes: if not VALID.match(code): print('代码格式可疑: %s' % code) for code, href in anns: if code == 'x-default': continue try: back = [h.rstrip('/') for _, h in parse(fetch(href))] except Exception as exc: print('抓取失败: %s (%s)' % (href, exc)) continue if url.rstrip('/') not in back: print('回指缺失: %s 没有指回 %s' % (href, url)) if __name__ == '__main__': main(sys.argv[1])脚本只覆盖 HTML head 这一种方式。站点走站点地图或响应头时,把抓取与解析两段换掉即可:站点地图用xml.etree.ElementTree取 xhtml:link 节点,响应头把curl -sI的输出喂给同一个正则。脚本里的域名一律写成 example.com,换成真实域名后记得确认rstrip('/')没有把两个不同的地址判成同一个。
五、Search Console 已经不提供 hreflang 报错报告了
这一点比技术细节更容易踩。网上大量教程还在教"打开 Search Console,进指定国际目标报告,看语言定位那一栏的缺少返回链接"。Google 的帮助文档已经写明:指定国际目标报告已淘汰;Google 会继续支持并使用网页上的 hreflang 标记,但这项功能不再提供。也就是说,没有替代报告,全站级的标注问题不会在 Search Console 里给你列成一张待修清单。
现在能用的三类手段:网址检查逐页看 Google 抓取到的 HTML;自己跑一遍上面的脚本,或任意能解析 hreflang 的爬虫,按组比对;官方文档在调试一节里列了两个第三方工具,并注明这些工具不由 Google 维护或检查,用之前心里有数。单靠 Search Console 一块面板,是查不出标注问题的。
六、改完之后的验证顺序
改完标注不要只看首页。按这个顺序走一遍:把组内每个 URL 在网址检查里各跑一次,确认 Google 抓到的 HTML 里标注齐全;把站点地图重新提交一次;对改动过的页面提交重新抓取;然后把每个版本当前的状态、核对日期、改了什么记进同一张表。
状态更新有它自己的节奏。改完后几天没动静,不代表没生效。隔两三周回查同一批地址,有记录才对比得出来。这是独立站搭建交付后最容易被跳过的一步,改完不记录,下一轮又得从头查一遍。
七、几个常被问到的问题
标注齐全了,某一版还是没被索引。先把两件事分开:标注决定的是哪一版展示给谁,索引由另一套判定决定。回到第一处那个问题,看这一版的主内容是否真的做了本地化,内容与其他版本是否只是换了语言。
能用 JavaScript 把标注注入进去吗。官方列出的三种方式都落在服务端可见的位置。写在客户端渲染之后才出现的 DOM 里,等于把这件事交给渲染时机。用网址检查看 Google 抓到的 HTML 里到底有没有,没有就挪到服务端输出。
能不能按访问者所在地区或浏览器语言自动跳转到对应版本。官方文档在这件事上给的是明确建议:不要自动重定向。原因写在同一页里,Googlebot 抓取时通常来自美国,请求头里也不设置 Accept-Language,自动跳转会让它只能看到一个版本,其余版本可能因此发现不了。稳妥做法是每个语言版本各有独立网址、都能直接打开,再在页面上放一组指向其他语言版本的超链接让用户自己选。用 Cookie 或浏览器设置切换同一地址上的内容语言,同样落在不被推荐的写法里。
某个语言版本要下线怎么办。不要只删自己那一页的标注。先把它从组内其他所有页面的标注里去掉,再决定这个地址是 301 到同语言的替代页,还是保留 410。让别人继续指向一个已经下线的地址,那一对被忽略,还可能连带影响同组的其他对。
只翻译了模板、主内容还是英文,要不要标。官方把"主要内容用单一语言、只翻译模板"列为建议标注的情形之一,所以标注该做;但它和"主内容未翻译时本地化版本会被视为重复网页"是两回事。标注解决的是关系声明,解决不了主内容没有本地化的问题。
改标注要逐个语言版本动 head,语言一多工作量会跟着涨。我自己的站用的是 NeoGress,多语言版本的标注可以在同一处生成,语言增减时改动少一些来回;但标注有没有断开、代码写没写对、canonical 有没有打架,仍然得按上面4处一条条查,工具不解决这件事。
你的站是标注写全了还被忽略,还是某一版从头到尾没进过索引?这两种情况的排查起点不一样,评论区说一句你落在哪一种,我按你的语言组给核对顺序。