没有不值得去解决的问题,也没有不值得去学习的技术!

WordPress Polylang 英文站从 /en/ 迁移到 en 子域名:W3TC 与 Redis 缓存问题排查

【图 8,curl 验证 www 与 en 的 html lang、canonical 均正确】

WordPress CDN 与全球加速实战

图16:浏览器访问正常,网站全球访问成功

(1) 从阿里云 DNS 到 Cloudflare 全球加速的完整迁移实战记录

这一套规则遵循 Cloudflare + WordPress 标准 CDN 架构:

(2) Cloudflare SSL/TLS 与 Cache Rules 配置实战

图12:评论实时生效

(3) WordPress + Cloudflare 推荐配置与 Cache Rules 实战优化(SSL / API / 缓存闭环完整记录)

CDN上线后的boce数据

(4) CDN 上线前后性能对比实测:WordPress 缓存误判修正与真实加速效果验证

【图9:boce 测试 cn-test,灰云直连阿里云 ECS,平均响应 0.478s】

(5) Cloudflare 免费版适合中国大陆 WordPress 主站吗?从缓存 HIT 到灰云直连阿里云 ECS 的实测复盘

【图1:腾讯云 EdgeOne 产品页,选择“立即使用”】

(6) 从 Cloudflare Free 迁移到腾讯云 EdgeOne:一次 WordPress 主站国内外加速实战

【图2:EdgeOne 上线后 boce 第二次测试,平均响应 0.354s,不可访问 2 个】

(7) Cloudflare Free 迁移到 EdgeOne 后到底快了多少?WordPress CDN 三阶段 boce 与 WebPageTest 实测对比

[图2,EdgeOne 昨日 L7 访问流量,显示 6.56 GB]

(8) EdgeOne 流量成本排查:将 WordPress uploads 静态资源迁移到 Cloudflare media 子域名

【图3,EdgeOne 301 请求的 URL Path 排行,集中在 /wp-content/uploads/ 图片路径】

(9) WordPress 图片迁移到 media 子域名后,EdgeOne 仍然出现大量 301 的排查与处理记录

图4:Cloudflare Edge TTL 配置、Cloudflare Browser TTL 配置

(10) WordPress Media CDN 迁移后的 Cloudflare Cache Rules 调整记录

[图4:EdgeOne 按 URL path 开始于 /en/ 筛选后的流量截图]

(11) EdgeOne 流量成本控制决策:是否将英文站从 /en/ 迁移到 en.shuijingwanwq.com

[图3:执行核心升级时返回 524 No Reason Phrase]

(12) WordPress 核心升级反复 524 与“另一更新正在进行”的排查记录

【图 10:新证书 SAN 信息截图,显示已经包含 en.shuijingwanwq.com】

(13) OneinStack 现有 Nginx 虚拟主机追加域名并重签 SSL 证书的一次实操记录

【图 8,curl 验证 www 与 en 的 html lang、canonical 均正确】

(14) WordPress Polylang 英文站从 /en/ 迁移到 en 子域名:W3TC 与 Redis 缓存问题排查

【图 14,最终回归验证:en 首页和文章页 Cloudflare HIT,lang 与 canonical 正确】

(15) 将 Polylang 英文子域名接入 Cloudflare:HTML 缓存、登录绕过与后台入口收敛

背景

前面一篇文章中,我已经先完成了将 en.shuijingwanwq.com 加入源站 Nginx 虚拟主机、申请并确认 SSL 证书等前置工作。

前置文章可以参考:

这一次的目标,是把英文站从原来的 Polylang 路径模式:

Plaintext
https://www.shuijingwanwq.com/en/

迁移为英文子域名:

Plaintext
https://en.shuijingwanwq.com/

中文主站仍然保持:

Plaintext
https://www.shuijingwanwq.com/

整体目标很简单:

Plaintext
中文内容:www.shuijingwanwq.com
英文内容:en.shuijingwanwq.com
WordPress 后台:仍然以 www.shuijingwanwq.com 为主

但实际操作下来,发现这里面有几个很容易被忽略的问题。

尤其是我的网站同时使用了:

Plaintext
Polylang
W3 Total Cache
Redis Object Cache
OneinStack Nginx
tms-extensions-polylang

所以最终排查重点并不只是 Polylang 本身,而是 Polylang 与缓存插件、对象缓存、扩展插件之间的兼容问题。


迁移前的站点结构

迁移前,英文站使用的是 Polylang 默认比较常见的目录模式:

Plaintext
https://www.shuijingwanwq.com/en/

这种方式的好处是配置简单,只有一个主域名,缓存规则也比较容易统一。

但缺点也比较明显:

Plaintext
英文站仍然依附在 www 主域名下面
中英文 URL 结构不够独立
后续如果想让英文站单独走 Cloudflare,会比较麻烦

我的中文主站当前主要走 EdgeOne,媒体资源已经拆分到了 media.shuijingwanwq.com。如果英文站继续挂在 /en/ 路径下,就无法把英文前台单独交给 Cloudflare 处理。

所以这一次我准备把英文站迁移到:

Plaintext
https://en.shuijingwanwq.com/

这样后续可以形成更清晰的结构:

Plaintext
www.shuijingwanwq.com      中文主站
en.shuijingwanwq.com       英文前台
media.shuijingwanwq.com    上传媒体资源
【图 1,迁移前英文站仍然使用 /en/ 路径模式】
【图 1,迁移前英文站仍然使用 /en/ 路径模式】

第一步:确认 Nginx 已经识别 en 子域名

在修改 Polylang 之前,我先确认源站 Nginx 虚拟主机已经包含 en.shuijingwanwq.com

执行:

Bash
grep -R "server_name .*shuijingwanwq" /usr/local/nginx/conf/vhost /usr/local/nginx/conf/nginx.conf 2>/dev/null

确认当前 vhost 中已经包含:

Nginx
server_name www.shuijingwanwq.com shuijingwanwq.com en.shuijingwanwq.com;

这一步很关键。

因为 Polylang 即使配置了英文子域名,如果 Nginx 没有把这个 Host 指向同一个 WordPress 站点,请求也无法正确进入 WordPress。

【图 2,Nginx vhost 中的 server_name 已包含 en.shuijingwanwq.com】
【图 2,Nginx vhost 中的 server_name 已包含 en.shuijingwanwq.com】

第二步:Polylang URL 模式选择

进入 WordPress 后台:

Plaintext
语言 → 设置 → URL 修改

一开始我的站点使用的是:

Plaintext
根据固定链接中的目录名称设置语言

也就是:

Plaintext
/en/

这次迁移子域名时,我一开始也纠结过是否应该选择:

Plaintext
根据固定链接中的子域名称设置语言

因为从字面看,en.shuijingwanwq.com 的确是一个子域名。

但实际测试后,我最终选择的是:

Plaintext
根据不同的域名设置语言

然后分别配置:

Plaintext
中文 zh:
https://www.shuijingwanwq.com

English en:
https://en.shuijingwanwq.com

虽然 en 是子域名,但从 Polylang 配置逻辑上看,这种方式更明确:

Plaintext
zh 绑定一个完整域名
en 绑定一个完整域名

后续 canonical、hreflang、home_url 的输出也更容易判断。

【图 3,Polylang URL 修改中选择“根据不同的域名设置语言”】
【图 3,Polylang URL 修改中选择“根据不同的域名设置语言”】

第三个问题:Polylang 保存时报 500

在保存 Polylang 设置时,后台提示 Polylang 无法访问一个检测 URL,大概形式是:

Plaintext
https://www.shuijingwanwq.com/?deactivate-polylang=1

我在服务器上直接访问这个 URL:

Bash
curl -sS -L -D - -o /dev/null --max-time 15 "https://www.shuijingwanwq.com/?deactivate-polylang=1" | sed -n '1,30p'

返回的是:

Plaintext
HTTP/2 500

于是继续查看 PHP 错误日志,发现问题来自 tms-extensions-polylang 插件:

Plaintext
PHP Fatal error: Uncaught Error: Call to undefined function pll_languages_list()
in /data/wwwroot/www.shuijingwanwq.com/wp-content/plugins/tms-extensions-polylang/includes/blocks/pll-menu-by-language/register.php

这个错误比较典型。

Polylang 保存 URL 设置时,会访问:

Plaintext
?deactivate-polylang=1

用于检测在 Polylang 临时停用情况下,站点 URL 是否仍然可访问。

tms-extensions-polylang 这个扩展插件在 Polylang 函数不可用时,仍然直接调用了:

PHP
pll_languages_list()

于是就触发了 Fatal Error。

【图 4,Polylang 保存失败或 PHP Fatal Error 日志】
【图 4,Polylang 保存失败或 PHP Fatal Error 日志】

修复 tms-extensions-polylang 的兼容问题

找到出错文件:

Bash
nl -ba /data/wwwroot/www.shuijingwanwq.com/wp-content/plugins/tms-extensions-polylang/includes/blocks/pll-menu-by-language/register.php | sed -n '25,50p'

原来的逻辑类似:

PHP
$tepll_menu_by_language_editor_languages = array();
$tepll_menu_by_language_polylang_slugs   = pll_languages_list( array( 'fields' => 'slug' ) );
$tepll_menu_by_language_polylang_names   = pll_languages_list( array( 'fields' => 'name' ) );

这里的问题是:它默认 pll_languages_list() 一定存在。

但在 ?deactivate-polylang=1 这个检测场景中,这个函数可能不存在。

所以先备份文件:

Bash
cp wp-content/plugins/tms-extensions-polylang/includes/blocks/pll-menu-by-language/register.php \
wp-content/plugins/tms-extensions-polylang/includes/blocks/pll-menu-by-language/register.php.bak.$(date +%Y%m%d%H%M%S)

然后把相关逻辑改成带 function_exists() 判断:

PHP
$tepll_menu_by_language_editor_languages = array();
$tepll_menu_by_language_polylang_slugs   = array();
$tepll_menu_by_language_polylang_names   = array();

if ( function_exists( 'pll_languages_list' ) ) :
    $tepll_menu_by_language_polylang_slugs = pll_languages_list( array( 'fields' => 'slug' ) );
    $tepll_menu_by_language_polylang_names = pll_languages_list( array( 'fields' => 'name' ) );
endif;

再检查 PHP 语法:

Plaintext
php -l wp-content/plugins/tms-extensions-polylang/includes/blocks/pll-menu-by-language/register.php

输出:

Plaintext
No syntax errors detected

这样 Polylang 保存 URL 设置时,就不会再因为扩展插件调用不存在的函数而 500。

这里需要注意:这是直接修改插件文件。后续如果插件升级,这个修改可能会被覆盖。比较稳妥的方式是记录下来,后续如果再次遇到同类问题,可以快速回滚或重新修复。


第四个问题:en 子域名被 301 回 www

Polylang 设置保存后,按理说访问:

Plaintext
https://en.shuijingwanwq.com/

应该输出英文首页。

但实际测试时发现,它仍然被 301 回:

Plaintext
https://www.shuijingwanwq.com/

响应头里能看到:

Plaintext
HTTP/2 301
location: https://www.shuijingwanwq.com/
x-redirect-by: WordPress

这个现象很容易误判为 Polylang 配置没有生效,或者 WordPress canonical redirect 在强制跳转。

但后面通过临时 mu-plugin 抓取 wp_redirect 调用栈,确认真正触发跳转的是 W3 Total Cache 的 Page Cache 模块。

调用栈中能看到:

Plaintext
W3TC\PgCache_Plugin->redirect_on_foreign_domain

也就是说,不是 Polylang 主动跳转,而是 W3TC Page Cache 把 en.shuijingwanwq.com 当成了 foreign domain。

【图 5,en 子域名访问被 301 回 www 的 curl 结果】
【图 5,en 子域名访问被 301 回 www 的 curl 结果】

W3TC 为什么会把 en 判断为 foreign domain

继续查看 W3 Total Cache 源码:

Bash
grep -RIn "function redirect_on_foreign_domain\|redirect_on_foreign_domain" /data/wwwroot/www.shuijingwanwq.com/wp-content/plugins/w3-total-cache | head -n 20

找到:

Plaintext
PgCache_Plugin.php

相关代码逻辑大致是:

PHP
$request_host = Util_Environment::host();

$home_url   = get_home_url();
$parsed_url = @wp_parse_url( $home_url );

if ( isset( $parsed_url['host'] ) &&
    strtolower( $parsed_url['host'] ) !== strtolower( $request_host ) ) {
    ...
}

这里的问题是:

Plaintext
get_home_url() 返回 www.shuijingwanwq.com
当前请求 Host 是 en.shuijingwanwq.com

于是 W3TC 判断:

Plaintext
当前请求不是 home_url 所在域名

然后触发:

Plaintext
redirect_on_foreign_domain

最终把英文子域名跳回了 www

这在单域名 WordPress 站点里是合理的保护逻辑,但在 Polylang 多域名模式下,就会误伤合法的语言域名。


用 mu-plugin 修复 W3TC 与 Polylang 多域名兼容问题

我没有直接修改 W3 Total Cache 插件源码,因为插件升级很容易覆盖。

最终采用的是添加一个 mu-plugin,在 init 早期检查 Polylang 配置中的合法语言域名。如果当前 Host 属于 Polylang 的语言域名,就移除 W3TC 的 redirect_on_foreign_domain 动作。

新建文件:

Plaintext
/data/wwwroot/www.shuijingwanwq.com/wp-content/mu-plugins/w3tc-polylang-domain-fix.php

内容如下:

PHP
<?php
/*
Plugin Name: W3TC Polylang Domain Fix
Description: Prevents W3 Total Cache Page Cache from redirecting valid Polylang language domains back to the main home_url.
*/

add_action('init', function () {
    $host = $_SERVER['HTTP_HOST'] ?? '';

    if ($host === '') {
        return;
    }

    $polylang = get_option('polylang');
    $domains  = $polylang['domains'] ?? array();

    $allowed_hosts = array();

    foreach ((array) $domains as $domain) {
        if (!empty($domain)) {
            $parsed = wp_parse_url($domain);
            if (!empty($parsed['host'])) {
                $allowed_hosts[] = strtolower($parsed['host']);
            }
        }
    }

    if (!in_array(strtolower($host), $allowed_hosts, true)) {
        return;
    }

    global $wp_filter;

    if (empty($wp_filter['init']) || empty($wp_filter['init']->callbacks)) {
        return;
    }

    foreach ($wp_filter['init']->callbacks as $priority => $callbacks) {
        foreach ($callbacks as $callback) {
            $function = $callback['function'] ?? null;

            if (
                is_array($function)
                && isset($function[0], $function[1])
                && is_object($function[0])
                && $function[1] === 'redirect_on_foreign_domain'
                && get_class($function[0]) === 'W3TC\PgCache_Plugin'
            ) {
                remove_action('init', $function, $priority);
            }
        }
    }
}, 0);

检查语法:

Bash
php -l /data/wwwroot/www.shuijingwanwq.com/wp-content/mu-plugins/w3tc-polylang-domain-fix.php

输出:

Plaintext
No syntax errors detected

这个补丁的思路比较克制:

Plaintext
只处理 Polylang 已配置的合法语言域名
只移除 W3TC 的 foreign domain 跳转
不影响 W3TC Page Cache 其他功能
不影响非 Polylang 域名
【图 6,mu-plugin 文件与语法检查结果】
【图 6,mu-plugin 文件与语法检查结果】

第五个问题:Redis Object Cache 缓存了旧的 Polylang 配置

W3TC 的 301 问题解决后,en 不再跳回 www

但访问英文子域名时,又出现了另一个问题:

Plaintext
<html lang="zh-CN">
canonical: https://www.shuijingwanwq.com/

这说明请求虽然进了 en.shuijingwanwq.com,但 WordPress 运行时仍然把它识别成中文主站。

进一步检查发现,数据库里的 Polylang 配置已经是新的:

Plaintext
force_lang = 3
domains[en] = https://en.shuijingwanwq.com
domains[zh] = https://www.shuijingwanwq.com

但通过 HTTP 请求读取到的运行时配置却仍然是旧的:

Plaintext
force_lang = 1
domains[en] =
domains[zh] = https://www.shuijingwanwq.com
pll_home_url_en = https://www.shuijingwanwq.com/en/

这说明对象缓存里还残留着旧的 Polylang option。

我的站点使用 Redis 作为 Object Cache,所以这里其实不是 Polylang 没保存成功,而是 Redis 中的旧缓存还在影响运行时。

【图 7,运行时读取到旧 Polylang 配置,与数据库配置不一致】
【图 7,运行时读取到旧 Polylang 配置,与数据库配置不一致】

清理 Redis 后恢复正常

因为这个 Redis 主要用于当前站点缓存,我最终直接清空 Redis:

Bash
redis-cli FLUSHALL

输出:

Plaintext
OK

清理后再次检查,运行时 Polylang 配置恢复正常:

Plaintext
force_lang = 3
domains[en] = https://en.shuijingwanwq.com
domains[zh] = https://www.shuijingwanwq.com
pll_home_url_en = https://en.shuijingwanwq.com/
pll_home_url_zh = https://www.shuijingwanwq.com/

这里要特别说明一下:

如果 Redis 是多个站点或多个业务共用的,不建议直接执行 FLUSHALL。这种命令会清空整个 Redis 实例。

我这里之所以这样处理,是因为当前 Redis 主要就是给这个 WordPress 站点做对象缓存,清空后影响可控。


重新启用 W3TC Object Cache 与 Page Cache

Redis 清理后,我重新启用了:

Plaintext
W3TC Object Cache
W3TC Page Cache

然后再次验证:

Plaintext
www 首页:zh-CN + www canonical
en 首页:en-US + en canonical
en 英文文章页:en-US + en canonical

这说明:

Plaintext
W3TC Object Cache 可以继续使用
W3TC Page Cache 也可以继续使用
前提是保留前面的 w3tc-polylang-domain-fix.php 兼容补丁

验证 en 首页与英文文章页

为了避免经过 CDN 或外部缓存造成误判,我在源站上使用 --resolve 直接请求本机 Nginx:

Bash
for u in \
"https://www.shuijingwanwq.com/" \
"https://en.shuijingwanwq.com/" \
"https://en.shuijingwanwq.com/2026/07/05/18871/"
do
  echo "================ first request: $u ================"
  curl -k -sS -D - -o /tmp/w3tc-page-test.html --max-time 15 \
  --resolve www.shuijingwanwq.com:443:127.0.0.1 \
  --resolve en.shuijingwanwq.com:443:127.0.0.1 \
  "$u" | sed -n '1,18p'

  grep -o '<html[^>]*>' /tmp/w3tc-page-test.html | head -n 1
  grep -o '<link[^>]*rel=["'\''"]canonical["'\''"][^>]*>' /tmp/w3tc-page-test.html | head -n 1

  echo
done

验证结果符合预期。

中文首页:

Plaintext
HTTP/2 200
<html lang="zh-CN">
<link rel="canonical" href="https://www.shuijingwanwq.com/" />

英文首页:

Plaintext
HTTP/2 200
<html lang="en-US">
<link rel="canonical" href="https://en.shuijingwanwq.com/" />

英文文章页:

Plaintext
HTTP/2 200
<html lang="en-US">
<link rel="canonical" href="https://en.shuijingwanwq.com/2026/07/05/18871/" />
【图 8,curl 验证 www 与 en 的 html lang、canonical 均正确】
【图 8,curl 验证 www 与 en 的 html lang、canonical 均正确】

验证 hreflang 输出

英文文章页还需要确认 hreflang 是否正常。

最终输出类似:

HTML
<link rel="alternate" href="https://www.shuijingwanwq.com/2026/07/05/18860/" hreflang="zh" />
<link rel="alternate" href="https://en.shuijingwanwq.com/2026/07/05/18871/" hreflang="en" />

这说明 Polylang 已经正确识别:

Plaintext
中文版本在 www
英文版本在 en

canonical 和 hreflang 都是搜索引擎比较关注的信号,所以这一步比单纯页面能打开更重要。


验证 W3TC 是否为 en 单独生成缓存文件

最后还需要确认 W3TC Page Cache 是否真的为 en.shuijingwanwq.com 单独生成缓存文件,而不是把缓存混到 www 目录下。

执行:

Bash
find wp-content/cache/page_enhanced/en.shuijingwanwq.com -maxdepth 8 -type f 2>/dev/null | head -n 50

能看到类似:

Plaintext
wp-content/cache/page_enhanced/en.shuijingwanwq.com/2026/07/05/18871/_index_slash_ssl.html
wp-content/cache/page_enhanced/en.shuijingwanwq.com/2026/07/05/18871/_index_slash_ssl.html_gzip
wp-content/cache/page_enhanced/en.shuijingwanwq.com/_index_slash_ssl.html
wp-content/cache/page_enhanced/en.shuijingwanwq.com/_index_slash_ssl.html_gzip

这说明:

Plaintext
en 首页有独立页面缓存
en 英文文章页有独立页面缓存
www 与 en 的缓存目录已经分开
【图 9,W3TC page_enhanced 下已经生成 en.shuijingwanwq.com 缓存目录】
【图 9,W3TC page_enhanced 下已经生成 en.shuijingwanwq.com 缓存目录】

最终状态

到这里,Polylang 英文站从 /en/ 迁移到 en 子域名的源站侧配置已经完成。

当前状态是:

Plaintext
Polylang URL 模式:
根据不同的域名设置语言

中文:
https://www.shuijingwanwq.com

英文:
https://en.shuijingwanwq.com

W3TC Page Cache:
启用

W3TC Object Cache:
启用

Redis:
已清理旧 Polylang option 缓存

W3TC 兼容补丁:
wp-content/mu-plugins/w3tc-polylang-domain-fix.php 保留

最终验证结果:

Plaintext
www 首页:
200
lang = zh-CN
canonical = https://www.shuijingwanwq.com/

en 首页:
200
lang = en-US
canonical = https://en.shuijingwanwq.com/

en 英文文章页:
200
lang = en-US
canonical = https://en.shuijingwanwq.com/2026/07/05/18871/

hreflang:
zh 指向 www
en 指向 en

W3TC 页面缓存:
www 与 en 分目录生成

这次排查的几个经验

这次迁移让我比较明显地感受到,Polylang 多域名模式并不只是后台改一个设置。

真正容易出问题的地方有几个。

第一,Polylang 扩展插件必须能兼容 ?deactivate-polylang=1 这种检测场景。否则 Polylang 保存配置时,主插件还没出问题,扩展插件先 Fatal 了。

第二,W3 Total Cache 的 Page Cache 在单域名站点中没有问题,但在 Polylang 多域名模式下,可能会把合法语言域名误判为 foreign domain,然后 301 回主域名。

第三,Redis Object Cache 可能缓存旧的 Polylang option。即使数据库已经保存了新配置,即使执行了清除所有缓存的操作,运行时仍然可能读到旧值,导致页面继续输出旧的 /en/ 路径或错误的 zh-CN

第四,迁移完成后不能只看页面是否能打开,还要检查:

Plaintext
HTTP 状态码
html lang
canonical
hreflang
W3TC 缓存目录
是否仍然 301 回主域名

这些验证都通过之后,才算真正完成了源站侧迁移。

下一步,我会继续处理 en.shuijingwanwq.com 接入 Cloudflare、启用 HTML 缓存、验证登录用户绕过缓存、REST API 绕过缓存,以及将非 www 的后台入口统一收敛到 www 的问题。

OneinStack 现有 Nginx 虚拟主机追加域名并重签 SSL 证书的一次实操记录 将 Polylang 英文子域名接入 Cloudflare:HTML 缓存、登录绕过与后台入口收敛

需要长期技术维护或远程问题排查?

我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。

如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:

  • ✅ PHP / Laravel / Yii2 老项目无人维护
  • ✅ Go / Gin 后端接口需要排查或优化
  • ✅ WordPress 网站访问慢、报错或插件冲突
  • ✅ Nginx / MySQL / Redis / Linux 服务器异常
  • ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
  • ✅ 需要长期远程技术支持或兼职维护

更多介绍请查看:关于我 & 合作

微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理