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

WordPress Polylang Migration from /en/ to an English Subdomain: W3TC and Redis Troubleshooting

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

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:

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

to a dedicated English subdomain:

Plaintext
https://en.shuijingwanwq.com/

The Chinese main site would remain at:

Plaintext
https://www.shuijingwanwq.com/

The overall target architecture was straightforward:

Plaintext
中文内容: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:

Plaintext
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:

Plaintext
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:

Plaintext
英文站仍然依附在 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:

Plaintext
https://en.shuijingwanwq.com/

This would create a cleaner architecture going forward:

Plaintext
www.shuijingwanwq.com      中文主站
en.shuijingwanwq.com       英文前台
media.shuijingwanwq.com    上传媒体资源
Figure 1. Before the migration, the English site still used the /en/ path structure
Figure 1. Before the migration, the English site still used the /en/ path structure

Step 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:

Bash
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:

Nginx
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.

Figure 2. The Nginx virtual host server_name already includes en.shuijingwanwq.com
Figure 2. The Nginx virtual host server_name already includes en.shuijingwanwq.com

Step 2: Choose the Polylang URL Mode

In the WordPress dashboard, go to:

Plaintext
语言 → 设置 → URL 修改

Initially, my site used:

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

In other words:

Plaintext
/en/

When planning the subdomain migration, I initially considered whether I should select:

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

After all, en.shuijingwanwq.com is literally a subdomain.

After testing the available options, however, I ultimately chose:

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

I then configured the domains separately:

Plaintext
中文 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:

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

It also makes the resulting canonical URLs, hreflang tags, and home_url values easier to verify.

Figure 3. Selecting the Polylang option to assign languages by domain
Figure 3. Selecting the Polylang option to assign languages by domain

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:

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

I requested that URL directly from the server:

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

The response was:

Plaintext
HTTP/2 500

I then checked the PHP error log and found that the failure originated in the tms-extensions-polylang plugin:

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

This is a typical compatibility failure.

When Polylang saves its URL settings, it requests:

Plaintext
?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:

PHP
pll_languages_list()

That triggered a fatal error.

Figure 4. Polylang save failure and the related PHP fatal error log
Figure 4. Polylang save failure and the related PHP fatal error log

Fixing the tms-extensions-polylang Compatibility Issue

I located the file responsible for the error:

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'

The original logic was similar to this:

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' ) );

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:

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)

I then updated the relevant logic to guard the call with 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;

Next, I checked the PHP syntax:

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

Output:

Plaintext
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:

Plaintext
https://en.shuijingwanwq.com/

should have returned the English homepage.

In practice, however, it was still redirected with a 301 response to:

Plaintext
https://www.shuijingwanwq.com/

The response headers showed:

Plaintext
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:

Plaintext
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.

Figure 5. curl output showing the en subdomain being redirected back to www
Figure 5. curl output showing the en subdomain being redirected back to www

Why W3TC Classified en as a Foreign Domain

I continued by reviewing the W3 Total Cache source code:

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

I found:

Plaintext
PgCache_Plugin.php

The relevant logic was approximately:

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 ) ) {
    ...
}

The problem was:

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

W3TC therefore concluded:

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

It then triggered:

Plaintext
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:

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

Its contents were:

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);

I checked its syntax:

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

Output:

Plaintext
No syntax errors detected

The patch is deliberately narrow in scope:

Plaintext
只处理 Polylang 已配置的合法语言域名
只移除 W3TC 的 foreign domain 跳转
不影响 W3TC Page Cache 其他功能
不影响非 Polylang 域名
Figure 6. The mu-plugin file and PHP syntax-check result
Figure 6. The mu-plugin file and PHP syntax-check result

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:

Plaintext
<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:

Plaintext
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:

Plaintext
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.

Figure 7. The runtime loaded an old Polylang configuration that did not match the database
Figure 7. The runtime loaded an old Polylang configuration that did not match the database

Clearing Redis Restored the Correct Configuration

Because this Redis instance was used primarily for caching this site, I ultimately cleared it directly:

Bash
redis-cli FLUSHALL

Output:

Plaintext
OK

After clearing Redis, I checked again and confirmed that the runtime Polylang configuration was now correct:

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/

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:

Plaintext
W3TC Object Cache
W3TC Page Cache

I then verified the site again:

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

This confirmed that:

Plaintext
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:

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

The results matched the expected configuration.

Chinese homepage:

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

English homepage:

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

English post:

Plaintext
HTTP/2 200
<html lang="en-US">
<link rel="canonical" href="https://en.shuijingwanwq.com/2026/07/05/18871/" />
Figure 8. curl verification confirms correct html lang and canonical values for www and en
Figure 8. curl verification confirms correct html lang and canonical values for www and en

Verifying hreflang Output

The English post also needed to be checked for correct hreflang output.

The final output looked similar to:

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" />

This confirmed that Polylang correctly recognized:

Plaintext
中文版本在 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:

Bash
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:

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

This confirmed that:

Plaintext
en 首页有独立页面缓存
en 英文文章页有独立页面缓存
www 与 en 的缓存目录已经分开
Figure 9. W3TC has created a dedicated en.shuijingwanwq.com directory under page_enhanced
Figure 9. W3TC has created a dedicated en.shuijingwanwq.com directory under page_enhanced

Final 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:

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 保留

Final verification results:

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 分目录生成

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:

Plaintext
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

评论

发表回复

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

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