Background
In the previous article, I completed the prerequisite work, including adding en.shuijingwanwq.com to the origin server’s Nginx virtual host and issuing and verifying its SSL certificate.
For the prerequisite setup, see:
The goal this time was to move the English site from its original Polylang path-based URL:
https://www.shuijingwanwq.com/en/
to a dedicated English subdomain:
https://en.shuijingwanwq.com/
The Chinese main site would remain at:
https://www.shuijingwanwq.com/
The overall target architecture was straightforward:
中文内容:www.shuijingwanwq.com
英文内容:en.shuijingwanwq.com
WordPress 后台:仍然以 www.shuijingwanwq.com 为主
In practice, however, several easy-to-miss issues surfaced during the migration.
This was particularly relevant because my site uses all of the following components:
Polylang
W3 Total Cache
Redis Object Cache
OneinStack Nginx
tms-extensions-polylang
As a result, the investigation was not limited to Polylang itself. The real focus became the compatibility between Polylang, the page-caching plugin, the object cache, and a Polylang extension plugin.
Site Architecture Before the Migration
Before the migration, the English site used Polylang’s common directory-based URL structure:
https://www.shuijingwanwq.com/en/
This approach is easy to configure: there is only one primary domain, and the caching rules are relatively simple to keep consistent.
Its limitations were also clear:
英文站仍然依附在 www 主域名下面
中英文 URL 结构不够独立
后续如果想让英文站单独走 Cloudflare,会比较麻烦
My Chinese main site currently runs primarily through EdgeOne, while uploaded media has already been separated onto media.shuijingwanwq.com. If the English site remained under the /en/ path, I would not be able to route the English frontend through Cloudflare independently.
For that reason, I decided to move the English site to:
https://en.shuijingwanwq.com/
This would create a cleaner architecture going forward:
www.shuijingwanwq.com 中文主站
en.shuijingwanwq.com 英文前台
media.shuijingwanwq.com 上传媒体资源

/en/ path structureStep 1: Confirm That Nginx Recognizes the en Subdomain
Before changing the Polylang settings, I first confirmed that the origin server’s Nginx virtual host already included en.shuijingwanwq.com.
Run:
grep -R "server_name .*shuijingwanwq" /usr/local/nginx/conf/vhost /usr/local/nginx/conf/nginx.conf 2>/dev/null
The current virtual host already contained:
server_name www.shuijingwanwq.com shuijingwanwq.com en.shuijingwanwq.com;
This check is essential.
Even if Polylang is configured to use the English subdomain, requests cannot reach the correct WordPress installation unless Nginx maps that Host header to the same site.

server_name already includes en.shuijingwanwq.comStep 2: Choose the Polylang URL Mode
In the WordPress dashboard, go to:
语言 → 设置 → URL 修改
Initially, my site used:
根据固定链接中的目录名称设置语言
In other words:
/en/
When planning the subdomain migration, I initially considered whether I should select:
根据固定链接中的子域名称设置语言
After all, en.shuijingwanwq.com is literally a subdomain.
After testing the available options, however, I ultimately chose:
根据不同的域名设置语言
I then configured the domains separately:
中文 zh:
https://www.shuijingwanwq.com
English en:
https://en.shuijingwanwq.com
Although en is a subdomain, this option is more explicit within Polylang’s configuration model:
zh 绑定一个完整域名
en 绑定一个完整域名
It also makes the resulting canonical URLs, hreflang tags, and home_url values easier to verify.

Issue 3: Polylang Returns a 500 Error When Saving
When I tried to save the Polylang settings, the dashboard reported that Polylang could not access a validation URL in roughly the following form:
https://www.shuijingwanwq.com/?deactivate-polylang=1
I requested that URL directly from the server:
curl -sS -L -D - -o /dev/null --max-time 15 "https://www.shuijingwanwq.com/?deactivate-polylang=1" | sed -n '1,30p'
The response was:
HTTP/2 500
I then checked the PHP error log and found that the failure originated in the tms-extensions-polylang plugin:
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
This is a typical compatibility failure.
When Polylang saves its URL settings, it requests:
?deactivate-polylang=1
This request checks whether the site URL remains accessible while Polylang is temporarily deactivated.
However, the tms-extensions-polylang extension still called the following function directly even when Polylang’s functions were unavailable:
pll_languages_list()
That triggered a fatal error.

Fixing the tms-extensions-polylang Compatibility Issue
I located the file responsible for the error:
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'
The original logic was similar to this:
$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' ) );
The problem is that this code assumes pll_languages_list() is always available.
During the ?deactivate-polylang=1 validation request, however, that function may not exist.
I first backed up the file:
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)
I then updated the relevant logic to guard the call with function_exists():
$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;
Next, I checked the PHP syntax:
php -l wp-content/plugins/tms-extensions-polylang/includes/blocks/pll-menu-by-language/register.php
Output:
No syntax errors detected
With this change, saving the Polylang URL settings no longer produces a 500 error when the extension plugin attempts to call an unavailable function.
One important caveat is that this is a direct modification to a plugin file. A future plugin update may overwrite it. The safest practical approach is to document the change so it can be restored or reapplied quickly if the same issue returns.
Issue 4: The en Subdomain Is Redirected Back to www
After the Polylang settings were saved, requesting:
https://en.shuijingwanwq.com/
should have returned the English homepage.
In practice, however, it was still redirected with a 301 response to:
https://www.shuijingwanwq.com/
The response headers showed:
HTTP/2 301
location: https://www.shuijingwanwq.com/
x-redirect-by: WordPress
It would be easy to misinterpret this behavior as a failed Polylang configuration or a forced WordPress canonical redirect.
To identify the source, I used a temporary mu-plugin to capture the wp_redirect call stack. This confirmed that the redirect was actually triggered by the W3 Total Cache Page Cache module.
The call stack included:
W3TC\PgCache_Plugin->redirect_on_foreign_domain
In other words, Polylang was not issuing the redirect. W3TC Page Cache was treating en.shuijingwanwq.com as a foreign domain.

curl output showing the en subdomain being redirected back to wwwWhy W3TC Classified en as a Foreign Domain
I continued by reviewing the W3 Total Cache source code:
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
I found:
PgCache_Plugin.php
The relevant logic was approximately:
$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 ) ) {
...
}
The problem was:
get_home_url() 返回 www.shuijingwanwq.com
当前请求 Host 是 en.shuijingwanwq.com
W3TC therefore concluded:
当前请求不是 home_url 所在域名
It then triggered:
redirect_on_foreign_domain
As a result, the English subdomain was redirected back to www.
This is a reasonable safeguard for a single-domain WordPress site, but in Polylang’s multi-domain mode it can incorrectly block a legitimate language domain.
Using an mu-plugin to Fix W3TC and Polylang Multi-Domain Compatibility
I did not modify the W3 Total Cache plugin source directly, because an update could easily overwrite the change.
Instead, I added an mu-plugin that checks the valid language domains in the Polylang configuration early during init. If the current Host matches one of Polylang’s configured language domains, the plugin removes W3TC’s redirect_on_foreign_domain action.
I created the following file:
/data/wwwroot/www.shuijingwanwq.com/wp-content/mu-plugins/w3tc-polylang-domain-fix.php
Its contents were:
<?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);
I checked its syntax:
php -l /data/wwwroot/www.shuijingwanwq.com/wp-content/mu-plugins/w3tc-polylang-domain-fix.php
Output:
No syntax errors detected
The patch is deliberately narrow in scope:
只处理 Polylang 已配置的合法语言域名
只移除 W3TC 的 foreign domain 跳转
不影响 W3TC Page Cache 其他功能
不影响非 Polylang 域名

Issue 5: Redis Object Cache Retained the Old Polylang Configuration
After the W3TC 301 issue was fixed, en no longer redirected back to www.
However, another problem appeared when I accessed the English subdomain:
<html lang="zh-CN">
canonical: https://www.shuijingwanwq.com/
This showed that the request was reaching en.shuijingwanwq.com, but the WordPress runtime was still identifying it as the Chinese main site.
Further inspection showed that the Polylang configuration stored in the database was already correct:
force_lang = 3
domains[en] = https://en.shuijingwanwq.com
domains[zh] = https://www.shuijingwanwq.com
Yet the runtime configuration returned through an HTTP request was still the old version:
force_lang = 1
domains[en] =
domains[zh] = https://www.shuijingwanwq.com
pll_home_url_en = https://www.shuijingwanwq.com/en/
This indicated that the object cache still contained a stale Polylang option value.
My site uses Redis for object caching, so Polylang had not actually failed to save. Instead, stale data in Redis was still influencing the WordPress runtime.

Clearing Redis Restored the Correct Configuration
Because this Redis instance was used primarily for caching this site, I ultimately cleared it directly:
redis-cli FLUSHALL
Output:
OK
After clearing Redis, I checked again and confirmed that the runtime Polylang configuration was now correct:
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/
An important warning:
If a Redis instance is shared by multiple sites or applications, do not run FLUSHALL casually. It clears the entire Redis instance.
I used it here because this Redis instance primarily served as the object cache for this WordPress site, so the impact of clearing it was controlled.
Re-enabling W3TC Object Cache and Page Cache
After clearing Redis, I re-enabled:
W3TC Object Cache
W3TC Page Cache
I then verified the site again:
www 首页:zh-CN + www canonical
en 首页:en-US + en canonical
en 英文文章页:en-US + en canonical
This confirmed that:
W3TC Object Cache 可以继续使用
W3TC Page Cache 也可以继续使用
前提是保留前面的 w3tc-polylang-domain-fix.php 兼容补丁
Verifying the en Homepage and an English Post
To avoid misleading results from a CDN or another external cache, I used --resolve on the origin server to send requests directly to the local Nginx instance:
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
The results matched the expected configuration.
Chinese homepage:
HTTP/2 200
<html lang="zh-CN">
<link rel="canonical" href="https://www.shuijingwanwq.com/" />
English homepage:
HTTP/2 200
<html lang="en-US">
<link rel="canonical" href="https://en.shuijingwanwq.com/" />
English post:
HTTP/2 200
<html lang="en-US">
<link rel="canonical" href="https://en.shuijingwanwq.com/2026/07/05/18871/" />

curl verification confirms correct html lang and canonical values for www and enVerifying hreflang Output
The English post also needed to be checked for correct hreflang output.
The final output looked similar to:
<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" />
This confirmed that Polylang correctly recognized:
中文版本在 www
英文版本在 en
Both canonical URLs and hreflang annotations are important search-engine signals, so this verification matters more than simply confirming that the page opens.
Verifying That W3TC Generates Separate Cache Files for en
Finally, I needed to confirm that W3TC Page Cache was creating separate cache files for en.shuijingwanwq.com rather than mixing them into the www cache directory.
Run:
find wp-content/cache/page_enhanced/en.shuijingwanwq.com -maxdepth 8 -type f 2>/dev/null | head -n 50
The output included entries similar to:
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
This confirmed that:
en 首页有独立页面缓存
en 英文文章页有独立页面缓存
www 与 en 的缓存目录已经分开

en.shuijingwanwq.com directory under page_enhancedFinal Status
At this point, the origin-side migration of the Polylang English site from /en/ to the en subdomain was complete.
The current architecture is:
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 保留
Final verification results:
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 分目录生成
Lessons from This Troubleshooting Process
This migration made it clear that enabling Polylang’s multi-domain mode involves much more than changing a single dashboard setting.
Several areas are particularly prone to failure.
First, Polylang extension plugins must handle the ?deactivate-polylang=1 validation scenario. Otherwise, an extension may trigger a fatal error before the core Polylang plugin itself encounters any problem.
Second, W3 Total Cache Page Cache works normally on a single-domain site, but under Polylang’s multi-domain mode it may misclassify a valid language domain as a foreign domain and redirect it to the primary domain with a 301 response.
Third, Redis Object Cache may retain an outdated Polylang option. Even after the new configuration has been saved to the database—and even after a general cache purge—the runtime may still read the stale value, causing pages to continue outputting the old /en/ path or the incorrect zh-CN language value.
Fourth, after the migration, it is not enough to confirm that the pages load. You also need to verify:
HTTP 状态码
html lang
canonical
hreflang
W3TC 缓存目录
是否仍然 301 回主域名
The origin-side migration should only be considered complete after all of these checks pass.
Next, I will connect en.shuijingwanwq.com to Cloudflare, enable HTML caching, verify cache bypass behavior for logged-in users and the REST API, and consolidate all non-www admin entry points onto www.
需要长期技术维护或远程问题排查?
我是拥有 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

发表回复