WordPress hreflang 的建议大多是"安装一个多语言插件,它会处理它。"插件拥有你的翻译就好了。在我最常打到的情况下,这是错误的答案:一个网站,有两个或三个语言版本构建为单独的子目录,单独的安装或一组手动翻译的页面,没有人想要一个完整的翻译框架及其数据库模式,四页。
我以这种形式维护 WordPress 属性,包括手写翻译并生活的文档网站 /de/ 並 /es/。hreflang 在一个主题文件中有四十行,它从一个数组中读取,它从未漂移过。本指南是那个代码,加上插件在你的主题旁边发出 hreflang 时出错的部分。
它位于下面 完整的 hreflang 指南的,旁边 下一个。js 版本。
TL;博士: WordPress 不会自行发出 hreflang。钩子
wp_head循环到区域设置地图上,并打印完整的倒数集,包括x-default在组中的每个页面上。构建 URLhome_url()or一个全局域的起源而不是硬编码,保留每个页面's规范的自我参照,并检查没有seo插件已经在发出竞争块,对于多站点设置,get_sites()为您提供地图;对于具有翻译页面的单个站点,后元字段映射对应项就足够了。
WordPress 是否自行添加 hreflang?
No。 Core 输出规范链接、短链接以及各种 feed 和 REST 链接 wp_head的,但关于语言的交替却没有。 get_locale() 知道该网站使用哪种语言,核心的任何部分都不会将其转化为注释。
Plugins填补空白,Polylang和WPML为他们管理的翻译发出hreflang,而大多数SEO插件会发出,如果你配置了他们能看到的翻译关系,如果你已经在使用其中一个,它覆盖了你的整个站点,使用它,跳过这篇文章,手卷的原因是你的翻译存在于插件知道的任何外部,并且一个发出部分集的插件比一个不发出一个更糟糕。
最小版本:一个数组,一个钩子
对于翻译为已知的一小组 URL 前缀的网站:
<?php
/**
* Hreflang annotations for a hand-translated multilingual site.
* Drop in the theme's functions.php or a small mu-plugin.
*/
function mysite_hreflang_map(): array {
return [
'en-US' => 'https://example.com',
'de-DE' => 'https://example.com/de',
'es-ES' => 'https://example.com/es',
'x-default' => 'https://example.com',
];
}
function mysite_print_hreflang(): void {
$path = mysite_current_path(); // e.g. '/pricing/'
foreach ( mysite_hreflang_map() as $hreflang => $origin ) {
printf(
'<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
esc_attr( $hreflang ),
esc_url( untrailingslashit( $origin ) . $path )
);
}
}
add_action( 'wp_head', 'mysite_print_hreflang', 1 );
这可以正确地理解手写块通常不会做到的三件事。
它在每一页上打印全套面,包括页面'自己的语言,互惠在结构上得到满足:没有一个分支可以省略一个条目,因为根本没有分支。
x-default 是同一数组中的一行的,所以它是由同一个循环发出的,不能在一个模板上忘记。
URL 转义出来。 esc_url 在一小时内 wp_head 是不可选的,并且 hreflang 值会经过 esc_attr 出于同样的原因。
找出当前的路径
路径是需要照顾的部分,因为它必须是路径 内 语言,而不是完整的请求 URI。
function mysite_current_path(): string {
$home = wp_parse_url( home_url(), PHP_URL_PATH ) ?: '';
$req = wp_parse_url( $_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH ) ?: '/';
// Strip the language prefix this install is served under.
if ( $home && str_starts_with( $req, $home ) ) {
$req = substr( $req, strlen( $home ) );
}
return user_trailingslashit( '/' . ltrim( $req, '/' ) );
}
如果您的德国安装有效 example.com/de 访客正在开启 example.com/de/preise/,这又回来了 /preise/(which then get fixed) 与每个区域设置's 起源。尾随斜线处理事项比它看起来更重要: /preise 並 /preise/ 是不同的 URL,将注释指向非规范形式是 尾部斜杠不匹配 这会在看起来完美的集合上产生返回标签错误。 user_trailingslashit 尊重网站和#39;的永久链接设置,因此它与 WordPress 所服务的任何内容相匹配。
这里的假设是翻译后的 URL 在结构上是平行的: /pricing/ 地图到 /de/pricing/.如果你的德语slugs也被翻译了, /de/preise/的,这个函数是不够的,你需要一个显式映射,也就是下一节。
slugs不同时:后元映射
slugs翻译对用户和排名更好, 并且意味着路径无法导出, 存储对应的代替:
/**
* Meta key holds a JSON map: {"de-DE": 123, "es-ES": 456} of post IDs,
* or absolute URLs for external installs.
*/
function mysite_alternates_for( int $post_id ): array {
$raw = get_post_meta( $post_id, '_hreflang_alternates', true );
$map = $raw ? json_decode( $raw, true ) : [];
if ( ! is_array( $map ) ) {
return [];
}
$out = [];
foreach ( $map as $hreflang => $target ) {
$url = is_numeric( $target ) ? get_permalink( (int) $target ) : $target;
if ( $url ) {
$out[ $hreflang ] = $url;
}
}
return $out;
}
然后是 wp_head callback 合并当前帖子's 自己在打印之前进入该地图, 因此该集合是完整的, 自我参照:
function mysite_print_hreflang_mapped(): void {
if ( ! is_singular() ) {
return;
}
$post_id = get_queried_object_id();
$alts = mysite_alternates_for( $post_id );
if ( count( $alts ) < 1 ) {
return; // Nothing to declare: a one-entry cluster is not a cluster.
}
$alts[ mysite_hreflang_for_locale( get_locale() ) ] = get_permalink( $post_id );
$alts['x-default'] = $alts['en-US'] ?? get_permalink( $post_id );
foreach ( $alts as $hreflang => $url ) {
printf(
'<link rel="alternate" hreflang="%s" href="%s" />' . "\n",
esc_attr( $hreflang ),
esc_url( $url )
);
}
}
维护成本是元必须写在两侧,带有对应选择器的管理 UI 是诚实的解决方案;从 CSV 填充地图的 WP-CLI 命令是快速的,你不能做的就是仅填充在一侧,因为单向声明正是 缺少返回标签 失败。
多站点:从网络导出地图
如果每种语言都是多站点网络中的站点,则地图已经存在:
function mysite_network_hreflang_map(): array {
$map = [];
foreach ( get_sites( [ 'number' => 50 ] ) as $site ) {
switch_to_blog( (int) $site->blog_id );
$map[ mysite_hreflang_for_locale( get_locale() ) ] = home_url();
restore_current_blog();
}
return $map;
}
将结果缓存为瞬态; switch_to_blog 在a wp_head 在任何规模的网络上,每个请求的挂钩都是一个真正的成本。并小心 get_locale() 下 switch_to_blog:它仅当站点's WPLANG 设置选项,这是通常情况,但在通过常量配置的网络上无法保证。
哪种方法适合您的网站?
| 设置 | 方法 | 努力 |
|---|---|---|
| slugs平行,局部少 | 静态数组加上派生路径 | 最低;一个功能 |
| 翻译为slugs | 后元对应地图 | 中等;需要编辑工作流程 |
| 多站点,每种语言一个站点 | 衍生自 get_sites(),缓存 |
低代码,需要缓存纪律 |
| Polylang 或 WPML 管理翻译 | 用插件's输出 | 验证它是否发出 x 默认值 |
| SEO插件与手动翻译字段 | 使用它,然后检查渲染的头部 | 注意重复的块 |
将 hreflang 放入站点地图中
如果您的页面标题已经很拥挤,或者您有足够的位置使块比标题的其余部分更长,则站点地图是替代方案。 WordPress 5.5 及更高版本 核心站点地图并且它们是可过滤的,尽管过滤器的设计并未考虑替代方案和命名空间声明 <urlset> 不是核心让你干净利落地触摸的东西。
务实的路线是一个独立的站点地图,由重写规则而不是战斗提供 wp_sitemaps:
add_action( 'init', function () {
add_rewrite_rule( '^sitemap-i18n\.xml$', 'index.php?mysite_i18n_sitemap=1', 'top' );
} );
add_filter( 'query_vars', fn( $vars ) => array_merge( $vars, [ 'mysite_i18n_sitemap' ] ) );
add_action( 'template_redirect', function () {
if ( ! get_query_var( 'mysite_i18n_sitemap' ) ) {
return;
}
header( 'Content-Type: application/xml; charset=UTF-8' );
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ';
echo 'xmlns:xhtml="http://www.w3.org/1999/xhtml">' . "\n";
foreach ( mysite_translated_paths() as $path ) {
echo " <url>\n <loc>" . esc_url( home_url( $path ) ) . "</loc>\n";
foreach ( mysite_hreflang_map() as $hreflang => $origin ) {
printf(
' <xhtml:link rel="alternate" hreflang="%s" href="%s"/>' . "\n",
esc_attr( $hreflang ),
esc_url( untrailingslashit( $origin ) . $path )
);
}
echo " </url>\n";
}
echo '</urlset>';
exit;
} );
要做对的两件事。 xmlns:xhtml 声明于 <urlset> 是必需的;没有它,替代条目将被忽略,文件仍然作为站点地图进行验证,这又是静默失败模式。并在添加规则后,在激活而不是在每个请求上刷新重写规则一次。
完成此操作后,删除头输出。两者都发射 混合实施方法,这给你两个漂移的来源。
重复块问题
WordPress特有的故障,也是你写任何代码前值得检查的,几个插件发出hreflang,不止一个可以一次活动,一个SEO插件配置翻译关系,一个多语种插件,你新的主题功能,都会很开心地打印到 wp_head并且结果是两个或三个具有不同内容的块,并且可能有两个 x-default 参赛作品。
Google's处理一页内矛盾注释,就是不信任他们,所以一个没有hreflang,现在有三个区块的站点,可能比以前更糟糕。
检查渲染的输出而不是插件设置:
curl -s https://example.com/de/preise/ | grep 'rel="alternate"' | grep hreflang
数行数不同的代码。每个区域设置一个条目加一个 x-default并且没有重复。如果插件正在发出无法配置的集合,则通常的转义舱口是将其操作从中删除 wp_head 而不是在上面添加您的:
// Example shape: find the real hook and callback with `has_action`.
add_action( 'init', function () {
remove_action( 'wp_head', 'some_plugin_hreflang_output', 10 );
} );
规范标签,而不是打破它们
WordPress 发射 rel="canonical" 供奇异的视图通过 rel_canonical()并且它指向帖子's自己的永久链接,这是你想要的,大多数SEO插件都用自己的替换,默认情况下也是自我参照的。
规则来自 hreflang 与规范 apply unchanged:每个页面都对自己进行规范化,hreflang 描述了那些自我规范页面之间的关系。WordPress 风格的破解方式是一种插件设置,可以将翻译的帖子规范化为 "primary" 语言版本,读起来像一个明智的重复数据删除选项,并悄悄地从索引中删除您的翻译。如果设置提供跨语言规范化,请将其保留。
验证结果
取两个兄弟页并比较它们的注释块。它们应该相同,只有规范不同。然后检查您命名的每个 URL 返回 200 而没有重定向,因为退役的局部区域和更改的永久链接是正确的集合如何腐烂成破碎的。
将套装粘贴到 免费 hreflang 生成器 要验证代码:它根据 ISO 形状检查每个值,标记未识别的子标签,例如 en-UK、抓重复、拒绝相对URL,并在集合没有后备时发出警告,它完全在浏览器中运行,当URL描述一个尚未公开的客户端站点时,这很重要。然后使用搜索控制台's国际目标报告进行持续检查,以及 hreflang 工具指南 为围绕它的工作流程。
常见问题
WordPress 是否会自动添加 hreflang 标签?
No。Core发出一个规范链接和各种头部链接但没有任何关于语言的交替,Hreflang来自一个插件或来自你自己的 wp_head 输出。
如何在没有插件的情况下在 WordPress 中添加 hreflang?
钩 wp_head 并打印a <link rel="alternate" hreflang> 适用于您控制的地图中每个区域设置的行,包括 x-default entry,在群组中的每个页面。与 逃避值 esc_attr 並 esc_url。
WordPress 主题中的 hreflang 标签去哪儿?
在一个挂钩的功能中 wp_head理想情况下,使用小型 mu 插件而不是主题,因此切换主题不会默默地丢弃注释。
如果我的翻译页面有不同的slugs怎么办?
Deriving nothing。将对应的post ID或URL存储在post元中,并在渲染时间读取地图。将其填充到两侧,或者创建缺少的返回标签。
Polylang 和 WPML 处理 hreflang 吗?
是的,对于他们管理的翻译。验证输出包括 x-default 没有第二个插件会发出竞争块。
为什么我的一页上有两个 hreflang 块?
有两件事是打印 wp_head:通常SEO插件加多语种插件,或者插件加自己的功能,一页的矛盾注释是不信任的,所以去掉一个而不是分层。
每个 WordPress 语言版本都应该有自己的规范吗?
是的,自我指涉。请勿使用将翻译的帖子规范化为主要语言的插件设置,因为这会将它们从索引中删除并取消 hreflang 设置。
如何通过 WordPress 多站点网络添加 hreflang?
循环 get_sites()、切换到每个博客来阅读其区域设置和 home_url()(cache) 所得到的映射, 并在一个瞬态中运行 switch_to_blog 根据每个请求 wp_head 在大型网络上价格昂贵。



