多语言 SSR SEO:hreflang、canonical、noindex 预算
以多种语言提供同一篇文章会分散您的搜索排名,除非 hreflang、规范网址和 noindex 预算保持一致。方法如下。
将一篇英文文章翻译成多种语言,并对每种语言都进行服务器端渲染,这听起来对搜索流量来说是纯粹的利好。但这也是一种快速分散自身排名、用低质量页面淹没索引、以及被标记为重复内容的捷径。对于一个依赖搜索流量的博客来说,一切都取决于保持索引的干净。因此,语言层的构建必须将 SEO 作为首要约束,而不是收尾工作。本文将详细介绍保持索引干净的三个控制方法:如何生成绝对 URL,canonical 和 hreflang 标签之间有何关联,以及 noindex 预算如何决定你实际开放哪些译文。
多语言网站中的重复内容陷阱
同一篇文章的五种语言版本就是五个 URL,如果你不小心,搜索引擎可能会将它们视为竞争对手。这会以两种不同的方式出错,而且它们会相互叠加。
第一种是语言重复。如果韩语和日语版本在结构上几乎相同,而搜索引擎无法判断它们是同一原文的翻译,它可能会选择其中一个,忽略其余的,或者将你的入站链接权重分散到所有版本中。你希望每种语言都有一个权重高的页面。结果你得到的却是同一个页面的五个权重低下的片段。
第二种是主机重复,而且它比人们想象的更容易触发。如果你的服务器根据传入的请求主机来构建绝对 URL,那么一旦相同的内容可以通过两个主机名访问,比如说一个裸域名和一个 www 子域名,或者一个泄露的暂存别名,搜索引擎就会看到每个页面的两个完整副本。现在,五个语言版本变成了十个或十五个,每一次分割都会流失排名信号。这两种问题的解决方法都始于同一个地方:网站对于自身的 URL 必须有且只有一个明确的说法。
切勿根据请求主机构建绝对 URL
最重要的一条规则很枯燥。绝对 URL 是根据一个固定的基准值生成的,绝不根据请求生成。在 Go SSR 服务器中,这意味着 canonical、hreflang、Open Graph 和站点地图的 URL 都来自配置好的 SITE_BASE_URL,并且绝不会读取 r.Host 来构建链接。
// config.SiteBaseURL is the one source of truth, e.g. "https://lucidnote.net".
// Do not read r.Host here. A request arriving on any alias still emits
// the same canonical origin, so the index sees one copy, not one per host.
func absURL(base, lang, slug string) string {
return base + "/" + lang + "/" + slug
}
如果根据 r.Host 构建链接,那么解析到你服务器的每个主机名都会在索引中成为你整个网站的一个平行宇宙。一个固定的基准值会将所有这些平行宇宙折叠成一个。这不是一个逐页的决策或模板的细节。这是整个服务器的一个属性,值得进行一项测试,如果任何处理程序试图获取请求主机来创建链接,该测试就会失败。
一旦每个页面都就其自身的绝对地址达成一致,那么管理重复内容的两个标签——canonical 和 hreflang——就有了坚实的基础。
规范标签将一个 URL 指定为原始 URL
规范标签会告知搜索引擎,对于某段内容,哪个 URL 是权威 URL。在一个构建良好的多语言网站上,每个语言页面都将自己设为规范页面。韩语页面的规范标签指向韩语 URL,日语页面的则指向日语 URL。它们不是彼此的副本,而是替代版本,hreflang 会单独表达这种关系。
规范标签发挥作用的地方是在单一语言内部。设想一个可通过跟踪参数访问的页面,或一个带有和不带末尾排序参数的分类列表。这些变体都应带有一个指向纯净 URL 的规范标签,这样带查询字符串的副本就不会与真实版本竞争。规则很简单:规范标签解决单一语言内的重复问题,并且出于与上述相同的原因,它必须始终指向一个基于固定基准构建的绝对 URL。
一个常见的错误是让每个翻译版本的规范标签都指向英语原文。这会告诉搜索引擎,其他语言是英语的副本,不应被单独索引,从而浪费了你为翻译所吸引的流量。每个语言使用自引用的规范标签几乎总是你想要的做法。跨语言关系属于 hreflang 的范畴。
hreflang 对每个语言变体进行对称映射
hreflang 是页面用来声明其在其他语言中的“兄弟”版本的方式。每个页面都会为其提供的每种语言(包括其自身)列出一个 link rel="alternate" 链接,外加一个 x-default,用于指定当用户的语言不受支持时的回退版本。搜索引擎利用这一点,向韩国搜索者显示韩语版本,向日本搜索者显示日语版本,而不是随便显示它先抓取到的那个版本。
以下是一篇文章的英文版本的头部,声明了它的四个翻译“兄弟”版本和一个默认版本:
<link rel="canonical" href="https://lucidnote.net/en/multilingual-ssr-seo">
<link rel="alternate" hreflang="en" href="https://lucidnote.net/en/multilingual-ssr-seo">
<link rel="alternate" hreflang="ko" href="https://lucidnote.net/ko/multilingual-ssr-seo">
<link rel="alternate" hreflang="ja" href="https://lucidnote.net/ja/multilingual-ssr-seo">
<link rel="alternate" hreflang="zh-Hans" href="https://lucidnote.net/zh-Hans/multilingual-ssr-seo">
<link rel="alternate" hreflang="zh-Hant" href="https://lucidnote.net/zh-Hant/multilingual-ssr-seo">
<link rel="alternate" hreflang="x-default" href="https://lucidnote.net/en/multilingual-ssr-seo">
有两个属性决定了此功能的成败。第一个是对称性。如果英文页面将韩文页面列为备用版本,那么韩文页面也必须将英文页面列回来。hreflang 是一种相互声明,搜索引擎会悄悄地丢弃单向的设置,因为它无法信任一个不指回源页面的备用版本。当服务器渲染这些标签时,它应该根据该文章的可用语言列表生成完整的备用版本集合,这样每个变体都会发出相同、完整且相互对应的块。按模板手动构建列表是导致不对称悄然出现的原因。
第二个是该集合必须反映实际提供的内容。如果某个语言版本翻译失败且页面不存在,它就不能作为 hreflang 备用版本出现。指向缺失或损坏变体的链接会毒害整个集群。干净的做法是,从针对该 slug 成功发布的语言版本集合中派生备用列表,而不是从网站理论上可以支持的静态语言列表中派生。
noindex 预算:全面翻译,选择性呈现
这是大多数指南都跳过的部分。正确设置 hreflang 和 canonical 标签可以让你安全地为所有语言版本建立索引。但这并不意味着你应该这样做。
你邀请搜索引擎索引的每一个 URL,都是一个会被引擎评判质量的页面。在一个新网站上,一个总共只有三篇文章的语言版本中,一篇新翻译的页面就是内容单薄的页面:它周围内容很少,没有同级内容的深度,也没有内部链接指向它。如果在每个语言和分类下都索引几百个这样的页面,你被索引页面的平均质量就会下降。这个平均质量至关重要。搜索排名和广告网络审核都会将网站作为一个整体来评估,大量内容单薄的页面会拖累你真正关心的那些页面的表现。
因此,我们要有策略地使用 noindex 预算。翻译和存储成本低廉,并且可以为每种语言都执行,因为你希望内容在值得呈现的那一刻就准备就绪。索引是稀缺资源,它是被授予的,而不是自动获得的。有两条规则来管理它。
第一条是语言白名单。可被索引的语言集合是所提供语言集合的一个子集。访问者访问一个已提供但未被索引的语言页面时,仍然能看到一个完整、正确的页面,只是这个页面带有 noindex 标签,并且不会出现在站点地图(sitemap)中。比如,你提供了所有五种语言,但只邀请搜索引擎索引其中两种内容足够丰富、可以独立存在的语言。
第二条是按分类设置阈值。在特定语言下,一个分类页面只有在包含至少一定数量(例如三篇)的已发布文章后,才会被索引。低于这个数量,该分类列表根据定义就是内容单薄的,因此它会被加上 noindex 标签,并从站点地图中移除,直到内容充实起来。译文依然存在,页面依然可以渲染,URL 对于访问它们的用户来说依然有效。只是它们尚未向爬虫进行展示。
这样做的效果是,你翻译并存储了所有语言和分类的完整矩阵,但只呈现那些内容足够丰富、能够带来帮助而非稀释价值的单元。
策略一览表
每个页面都属于几种状态之一,四个 SEO 控件必须在每个状态上保持一致。存储和渲染独立于索引。
| 页面状态 | 已翻译并存储 | 向访问者渲染 | canonical | hreflang 备用链接 | robots | 在站点地图中 |
|---|---|---|---|---|---|---|
| 可索引的语言,密集类别 | 是 | 是 | 自身 | 完整的相互引用集 | index, follow | 是 |
| 已提供但不可索引的语言 | 是 | 是 | 自身 | 仅由已索引的同级页面列出 | noindex, follow | 否 |
| 低于文章阈值的类别 | 是 | 是 | 自身 | 其文章的完整集合 | noindex, follow | 否 |
| 查询参数变体 | 不适用 | 是 | 纯净 URL | 继承自纯净 URL | index, follow | 否 |
| 翻译失败的语言 | 否 | 否 | 不适用 | 在所有位置省略 | 不适用 | 否 |
第二行是容易让人困惑的地方。一个不可索引的语言页面仍然会渲染,并且仍然会通过 hreflang 声明其同级页面,但它不应被您正在索引的页面列为备用链接,因为您不希望将爬虫指向一个带有 noindex 标记的目标作为真正的备用选项。让已索引的集群引用已索引的成员,并让额外的语言为直接导航到它们的用户存在。
保持 sitemap、robots、canonical 和 hreflang 的一致性
这四个控件只有作为一个整体才能发挥作用。失败模式不是一个错误的标签,而是两个标签相互矛盾,这在爬虫看来是一个混乱的网站,并会浪费你试图保护的抓取预算。
sitemap 应只列出你希望被索引的 URL,而不应包含任何带有 noindex 的内容。sitemap 中存在 noindex 页面是一种矛盾:你请求爬虫抓取它,然后又告诉它忘记抓取到的内容。robots meta 标签是决定特定页面是否被索引的权威,sitemap 应该与它保持一致。canonical 应只指向可索引的 URL,因此内容单薄的变体页面永远不应将自己指定为任何内容的权威副本。而在被索引的集群中,hreflang 应只引用那些本身也被索引的页面。
保持这些设置一致的方法是,在一个地方一次性计算出索引决策,然后让所有四个输出都读取该决策。当服务器或内容提取步骤决定某个给定的(语言、类别、文章)是可索引的时,这个单一的布尔值将驱动 robots 标签、sitemap 的包含、canonical 目标以及要发出哪些 hreflang 备用链接。如果每个输出都自行做出决定,它们就会产生偏差,而这种偏差正是你受到惩罚的原因。
权衡利弊,以及何时索引所有内容
收窄索引范围有实实在在的代价。更少的索引页面意味着第一天来自搜索的入口点更少,因此早期流量会低于将每个存根的每个翻译版本都开放给搜索引擎的情况。你是在用当前的覆盖范围换取长期的质量得分,赌的是一小部分强页面会比一大堆弱页面的排名更高,这在依靠搜索和广告盈利的网站上是确实有效的。
这个预算不是永久性的。它是一种适合新建网站或内容稀疏网站的状态,并且随着内容的积累,这种限制应该放宽。某个分类越过了其文章数量阈值,便开始自行被索引。某种语言在各个分类下都积累了足够的深度,以至于将其添加到可索引的允许列表中不再会产生内容单薄的页面。控制方式是相同的,只是阈值在变动。
当内容单薄页面的风险消失时,你就可以索引所有内容。这种情况发生在某种语言的几乎每个分类都已达到阈值、内部链接为每个页面提供了真实的同级上下文、且翻译质量足够好以至于母语搜索者在访问后不会立即跳出时。到那时,noindex 预算已经完成了它的使命,更广泛的索引将成为一种资产而非负债。在此之前,请翻译整个矩阵,保持 hreflang 和 canonical 的严格与对称,并将索引预算花在那些能够独当一面的页面上。