站点做成 /en/、/de/、/ja/ 三份之后,最常见的麻烦不是「没收录」,而是收错了:德语关键词搜出来的是英文页,Search Console 里一批 /de/ 页面写着「重复网页,Google 选择的规范网页与用户指定的不同」。
抓一下这些页面的源码,问题通常堆在一起:canonical 全指到英文版、hreflang 只在英文页上写了一份、德语页的地址还是 ?lang=de。任意一条成立,hreflang 基本就白写了。
下面按能落地的顺序走:先定 URL 结构,再做前缀路由,然后输出 hreflang,最后用几条命令验。
做法 | 地址长这样 | 适合什么情况 | 要提前想清楚 |
|---|---|---|---|
子目录 | example.com/de/about/ | 一套 WordPress、一套主题和插件,绝大多数站点 | 前缀规则要统一;站内链接和 canonical 都得带前缀 |
子域名 | de.example.com/about/ | 不同语言由不同团队或不同服务器维护 | 两套环境各自能跑;cookie 和 CDN 缓存按域分开 |
国家域名 | example.de/about/ | 有本地实体、本地支付物流,按国家分别运营 | 每个域名单独解析、证书、备案,维护量按语言×国家翻 |
查询参数 | example.com/about/?lang=de | 不建议 | 同一页面多个地址,容易和站内搜索、广告参数混在一起,缓存键也要额外处理 |
多数情况选子目录:一个后台、一份内容、插件只装一遍。国家域名留给真有本地化运营需求的项目,别因为「看起来更专业」就上。
两种都能用,但必须二选一并坚持:全带(/en/about/、/de/about/),或者默认不带(/about/、/de/about/)。
选了「默认不带」,就要保证 /en/about/ 单向 301 到 /about/(不要互跳),而且 hreflang 里 en 那一份填的是 /about/,不是 /en/about/。
WordPress 默认不认 /de/about/ 这种地址。写一段 mu-plugin,放到 wp-content/mu-plugins/lang-prefix.php,自动生效,不用走插件启用流程:
<?php
/**
* Plugin Name: Lang Prefix Router
* Description: 语言前缀路由 + 互指 hreflang 的最小实现。
*/
function lpr_langs(): array {
return ['en', 'de', 'ja']; // URL 里用的短码
}
function lpr_hreflang_map(): array {
return ['en' => 'en', 'de' => 'de', 'ja' => 'ja']; // 短码 => BCP 47
}
add_action('init', function () {
$alt = implode('|', array_map('preg_quote', lpr_langs()));
add_rewrite_tag('%lpr_lang%', '(' . $alt . ')');
add_rewrite_rule(
'^(' . $alt . ')/(.+?)/?$',
'index.php?lpr_lang=$matches[1]&pagename=$matches[2]',
'top'
);
});
function lpr_current_lang(): string {
$lang = get_query_var('lpr_lang');
return ($lang && in_array($lang, lpr_langs(), true)) ? $lang : lpr_langs()[0];
}
/* 给任意站内 URL 换语言前缀 */
function lpr_url(string $url, string $lang): string {
$home = untrailingslashit(get_option('home'));
if (0 !== strpos($url, $home)) {
return $url; // 站外链接不动
}
$alt = implode('|', array_map('preg_quote', lpr_langs()));
$path = substr($url, strlen($home));
$path = preg_replace('#^/(' . $alt . ')(?=/|$)#', '', $path);
if ('' === $path) {
$path = '/';
}
return $home . '/' . $lang . $path;
}规则里的 pagename= 只覆盖页面。文章换成 name=,分类换成 category_name=,自定义文章类型用它自己的查询变量名,各加一条同样带 'top' 的规则。启用后去「设置 → 固定链接」点一次保存,让重写规则重建。
顺带把尾斜杠定死:如果访问 /de/about 会 301 到 /de/about/,那 hreflang 里写的就必须是带斜杠的最终地址。
不要在 home_url 上做全局过滤。后台菜单、REST 返回的 URL、邮件里的链接会被一起改,还容易递归把站点跑挂。在输出层处理:
/* canonical 自指:/de/about/ 的 canonical 就是 /de/about/ */
add_filter('get_canonical_url', function ($canonical, $post) {
return lpr_url($canonical, lpr_current_lang());
}, 10, 2);菜单项和正文链接用 lpr_url(get_permalink($id), lpr_current_lang()) 包一层,或者在模板里统一把 the_permalink() 的输出过一遍。检查办法很土但有效:切到德语版,随便点开几个页面,看地址栏里还有没有掉回无前缀的链接。
zh-Hans,繁体写 zh-Hant,要区分地区才用 en-GB、pt-BR。en-EU 这类不存在;zh-CN 是国家码,不表示「简体」翻译关系用 post meta 存一个组 ID(_lpr_group),每种语言一条内容,共用同一个组 ID:
function lpr_group_urls(int $post_id): array {
$group = get_post_meta($post_id, '_lpr_group', true);
if (!$group) {
return [];
}
$q = new WP_Query([
'post_type' => 'any',
'post_status' => 'publish',
'posts_per_page' => 20,
'no_found_rows' => true,
'meta_query' => [['key' => '_lpr_group', 'value' => $group]],
]);
$out = [];
foreach ($q->posts as $p) {
$lang = get_post_meta($p->ID, '_lpr_lang', true);
if ($lang && in_array($lang, lpr_langs(), true)) {
$out[$lang] = lpr_url(get_permalink($p), $lang);
}
}
return $out;
}
add_action('wp_head', function () {
if (!is_singular()) {
return;
}
$urls = lpr_group_urls(get_queried_object_id());
if (count($urls) < 2) {
return; // 只有一个语言版本时不要输出,半套比没有更糟
}
$map = lpr_hreflang_map();
$def = lpr_langs()[0];
foreach ($urls as $lang => $url) {
printf(
'<link rel="alternate" hreflang="%s" href="%s" />' . PHP_EOL,
esc_attr($map[$lang] ?? $lang),
esc_url($url)
);
}
if (isset($urls[$def])) {
printf(
'<link rel="alternate" hreflang="x-default" href="%s" />' . PHP_EOL,
esc_url($urls[$def])
);
}
}, 5);如果你的翻译关系存在别的地方(分类法、多站点、单独一张对照表),只换 lpr_group_urls() 里取组和取语言的那几行,输出部分不用动。
分页和归档页同样要各自成组:/de/blog/page/2/ 这一组就写三种语言的第 2 页,不要全部指到第 1 页,也不要让第 2 页的 canonical 指到第 1 页。站内搜索结果页直接 noindex,不要放进 hreflang。
hreflang 写在 HTML 里或写在 sitemap 里都行,选一个当主。写在 sitemap 时是这种形式:
<url>
<loc>https://example.com/de/about/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/about/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about/"/>
</url>两边都写、但地址对不上(一边带斜杠一边不带,一边 http 一边 https),是最难查的一类问题。
for l in en de ja; do
echo "== $l"
curl -sL "https://example.com/$l/about/" \
| grep -Eo '<link rel="(canonical|alternate)"[^>]*>' | sort
done要看到的结果:三次输出的 alternate 集合完全相同,canonical 各自指向自己那一版。再把里面每个 href 挨个抓一次,确认都是 200:
curl -s -o /dev/null -w '%{http_code} %{url_effective}\n' "https://example.com/de/about/"最后看日志:Googlebot 有没有真的抓过三种语言的地址。只抓英文版,多半是站内链接还停在无前缀形态,或者 robots.txt 把 /de/ 挡住了。
这套路由和 hreflang 输出我们自己在维护,也打包成了免费插件 AumLang,涵盖语言前缀、hreflang 和 WooCommerce 商品页翻译:https://aumcreate.com/plugins/aumlang?utm_source=tencent&utm_medium=content&utm_campaign=tencent-multilingual-url-hreflang-patterns。上面的代码可以直接拿去用;如果改用插件,记得先把自己那套撤掉,别让两套同时往页头里输出 hreflang。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。