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

WordPress English Site Horizontal Scrollbar: Troubleshooting Stale CSS in W3TC Object Cache with Polylang Multi-Domain Mode

【图 7,数据库原始内容与 get_post() 返回内容不一致】

This article is a follow-up to my earlier troubleshooting notes on migrating Polylang to a multi-domain setup.

Previous article:

Plaintext
https://www.shuijingwanwq.com/2026/07/10/19280/

In the previous article, I moved the English site from the original /en/ path to a dedicated subdomain:

Plaintext
中文站:https://www.shuijingwanwq.com/
英文站:https://en.shuijingwanwq.com/

During the migration, I had already encountered two cache-related issues:

  1. W3 Total Cache Page Cache treated Polylang’s English-language domain as a foreign domain, causing en to be redirected back to www with a 301 response.
  2. After the 301 issue was fixed, W3 Total Cache Object Cache still retained the old Polylang configuration, so the English subdomain continued to be identified as the Chinese site at runtime.

This time, the initial symptom looked like an ordinary front-end issue: a horizontal scrollbar appeared at the bottom of English article pages.

By the end of the investigation, however, I found two separate causes. There was a genuine CSS layout problem, but stale objects stored by W3 Total Cache Object Cache in Redis also prevented the updated CSS from taking effect on the English site for a long time.


1. Current Website Architecture

The WordPress site currently uses Polylang in multi-domain mode:

Plaintext
中文站:www.shuijingwanwq.com
英文站:en.shuijingwanwq.com
WordPress 后台:www.shuijingwanwq.com/wp-admin/

Both domains point to the same WordPress document root and share:

Plaintext
同一套 WordPress 程序
同一个数据库
同一套 Twenty Twenty-Five 主题
同一套插件
同一个 Redis 实例

The active caching stack includes:

Plaintext
W3 Total Cache Page Cache
W3 Total Cache Object Cache
Redis
Cloudflare

One distinction is important here:

This site uses the Object Cache feature provided by W3 Total Cache, with Redis serving as the storage backend for cached objects.

For consistency, the rest of this article refers to it as:

Plaintext
W3TC Object Cache(Redis 后端)

rather than describing it as a separate Redis Object Cache plugin.


2. Symptom: Only English Articles Showed a Horizontal Scrollbar

The affected English article was:

Plaintext
https://en.shuijingwanwq.com/2026/07/11/19327/

Its corresponding Chinese article was:

Plaintext
https://www.shuijingwanwq.com/2026/07/11/19309/

The Chinese article displayed normally, while a horizontal scrollbar appeared at the bottom of the English page.

[Figure 1. A horizontal scrollbar appears at the bottom of the English article page]
[Figure 1. A horizontal scrollbar appears at the bottom of the English article page]

Because both pages used the same theme and template, my initial suspects included:

Plaintext
英文长标题
超长 URL
代码块
表格
AdSense iframe
搜索框
语言切换器
额外 CSS

Visual inspection alone made it difficult to determine which element was widening the page.


3. Using JavaScript to Find Elements Extending Beyond the Viewport

I opened the English article, pressed F12 to launch the browser developer tools, and ran the following code in the Console:

JavaScript
(() => {
    const viewportWidth = document.documentElement.clientWidth;

    const elements = [...document.querySelectorAll('body *')]
        .map(el => {
            const rect = el.getBoundingClientRect();

            return {
                element: el,
                tag: el.tagName.toLowerCase(),
                class: typeof el.className === 'string' ? el.className : '',
                left: Math.round(rect.left),
                right: Math.round(rect.right),
                width: Math.round(rect.width),
                scrollWidth: el.scrollWidth,
                clientWidth: el.clientWidth,
                text: (el.innerText || el.textContent || '')
                    .trim()
                    .replace(/\s+/g, ' ')
                    .slice(0, 100)
            };
        })
        .filter(item =>
            item.right > viewportWidth + 1 ||
            item.left < -1
        );

    elements.forEach(item => {
        item.element.style.outline = '3px solid red';
    });

    console.table(elements.map(({ element, ...item }) => item));

    console.log({
        viewportWidth,
        pageScrollWidth: document.documentElement.scrollWidth,
        overflowWidth:
            document.documentElement.scrollWidth - viewportWidth,
        overflowElements: elements.length
    });

    return elements;
})();

The results showed:

Plaintext
viewportWidth: 1405
pageScrollWidth: 1422
overflowWidth: 17

In other words, the total page width exceeded the browser viewport by 17px.

The rightmost element in the results was:

Plaintext
nav.tepll-pll-language-switcher
[Figure 2. The console detects that the language switcher extends 17px beyond the viewport]
[Figure 2. The console detects that the language switcher extends 17px beyond the viewport]

4. The Language Switcher Was the Rightmost Element, but Not Necessarily the Root Cause

To test whether the language switcher itself was causing the problem, I temporarily moved it 17px to the left in the console:

JavaScript
const el = document.querySelector(
    'nav.tepll-pll-language-switcher'
);

el.style.setProperty(
    'transform',
    'translateX(-17px)',
    'important'
);

The horizontal scrollbar disappeared immediately.

However, this only proved that the language switcher was the rightmost element on the page.

It did not prove that the language switcher itself was the root cause of the overflow.

If I had added the following rule to the production CSS:

CSS
transform: translateX(-17px);

the scrollbar would have disappeared, but the fixed offset would merely have compensated for an underlying layout error.

I therefore did not use this approach as the final fix.


5. Inspecting the Language Switcher and Its Parent Container

Next, I inspected the actual widths of the language switcher and its parent elements.

The console showed that the Flex parent containing the language switcher had:

Plaintext
clientWidth: 397px
scrollWidth: 429px

In other words:

Plaintext
父容器可用宽度:397px
内部元素所需宽度:429px
内部溢出宽度:约 32px
[Figure 3. The parent container has a clientWidth of 397 and a scrollWidth of 429]
[Figure 3. The parent container has a clientWidth of 397 and a scrollWidth of 429]

Reviewing the Additional CSS in effect at the time showed that the header search box used:

CSS
.wp-block-search {
    flex: 3 1 0;
    min-width: 250px;
    margin: 0 10px;
}

The language switcher itself required approximately:

Plaintext
160px

The minimum combined space required by the search box and language switcher was approximately:

Plaintext
搜索框最小宽度:250px
搜索框左边距:10px
搜索框右边距:10px
语言切换器宽度:160px

合计:430px

while their parent container was only:

Plaintext
397px

This closely matched the console result of:

Plaintext
scrollWidth: 429px

This was a close match.

So the actual CSS layout problem was:

The search box had min-width: 250px, which prevented it from shrinking further when the Flex container ran out of space. As a result, it pushed the English language switcher outside the parent container.

The word English is wider than the Chinese language label, so the problem was more likely to surface on English pages.


6. Changing the Header Search Box Minimum Width

I changed the original rule:

CSS
.wp-block-search {
    flex: 3 1 0;
    min-width: 250px;
    margin: 0 10px;
}

to:

CSS
@media (min-width: 769px) {
    header.wp-block-template-part .wp-block-search {
        flex: 3 1 0;
        min-width: 0;
        margin: 0 10px;
    }
}

In the saved Additional CSS, the header search box now uses a selector scoped to the Header and sets min-width to 0.

This change had two main parts.

1. Allow the Search Box to Shrink Further

The original rule was:

CSS
min-width: 250px;

It was changed to:

CSS
min-width: 0;

This allows the search box to continue shrinking when the Flex parent runs out of space instead of forcing it to occupy at least 250px.

2. Limit the Rule to the Header

The original selector:

CSS
.wp-block-search

affected every Search block on the site.

After the change:

CSS
header.wp-block-template-part .wp-block-search

the rule applies only to the search box in the header and does not interfere with Search blocks in the sidebar, article content, or other templates.

Under normal circumstances, the horizontal scrollbar should have disappeared as soon as the Additional CSS was saved.

In practice, however, the page did not change.


7. The New CSS Was Saved in the Dashboard, but the English Page Still Output the Old Rule

After checking the English page source, I found that the front end was still outputting the old rule:

CSS
.wp-block-search {
    flex: 3 1 0;
    min-width: 250px;
    margin: 0 10px;
}

while the content saved in the WordPress dashboard was already:

CSS
header.wp-block-template-part .wp-block-search {
    flex: 3 1 0;
    min-width: 0;
    margin: 0 10px;
}

In other words:

Plaintext
后台已经保存新 CSS
数据库内容已经更新
英文前台仍然输出旧 CSS
[Figure 4. Additional CSS has been updated in the dashboard, but the English page source still shows min-width: 250px]
[Figure 4. Additional CSS has been updated in the dashboard, but the English page source still shows min-width: 250px]

At this point, the issue was no longer limited to the CSS layout itself. The new question was:

Why was the WordPress front end not reading the updated CSS already stored in the database?


8. The English Page Still Did Not Update After Purging W3TC Caches in the Dashboard

The WordPress administration area is currently centralized at:

Plaintext
https://www.shuijingwanwq.com/wp-admin/

To keep a single administrative entry point, visiting:

Plaintext
https://en.shuijingwanwq.com/wp-admin/

results in a 301 redirect to the dashboard on the primary domain.

I used W3 Total Cache’s “Purge All Caches” action in the www dashboard, but the English page continued to output the old CSS.

My initial suspicion at this stage was:

After Polylang was configured to use separate domains, purging W3TC caches from the primary-domain dashboard might not have fully refreshed the cache for the English-language domain.

Later investigation showed that the stale content was not in Cloudflare’s edge cache. It was an old object in W3TC Object Cache.


9. Ruling Out Cloudflare Edge Cache

To rule out Cloudflare caching, I added a random query parameter to the English article URL and inspected the response headers:

Bash
u="https://en.shuijingwanwq.com/2026/07/11/19327/?_csscheck=$(date +%s)"

curl -sS -D - -o /dev/null "$u" \
| grep -Ei "HTTP/|server:|eo-cache-status:|cf-cache-status:|age:|cache-control:|x-cache:|via:"

The response was:

Plaintext
HTTP/2 200
server: cloudflare
cache-control: max-age=14400
cf-cache-status: MISS

Of particular relevance:

Plaintext
cf-cache-status: MISS

This indicated that the request did not directly hit stale content at a Cloudflare edge node. Cloudflare fetched the page again from the origin.

However, the returned HTML still contained:

CSS
min-width: 250px;

This confirmed that:

Plaintext
浏览器缓存不是根因
Cloudflare 边缘缓存也不是根因
源站返回的内容本身就是旧 CSS
[Figure 5. Cloudflare returns MISS, but the HTML still contains the old CSS]
[Figure 5. Cloudflare returns MISS, but the HTML still contains the old CSS]

10. Inspecting W3TC’s Multi-Domain Page Cache Directories

W3 Total Cache Page Cache stores cached pages under the following directory:

Plaintext
/data/wwwroot/www.shuijingwanwq.com/wp-content/cache/page_enhanced/

Inside that directory, I could see separate folders for the two language domains:

Plaintext
en.shuijingwanwq.com
www.shuijingwanwq.com

The English-domain directory also contained a complete cache structure:

Plaintext
2014
2018
2019
2021
2025
2026
category
series
tag
[Figure 6. Both en and www cache directories exist under page_enhanced]
[Figure 6. Both en and www cache directories exist under page_enhanced]

This showed that W3TC Page Cache stores the HTML cache for each domain separately according to the request Host.

However, I could not find a cache path directly named after article ID 19327 inside the English cache directory.

So this issue could not be resolved simply by deleting a particular 19327 directory.

Later, I bypassed Cloudflare and requested the origin directly, but the response still contained the old CSS. That moved the investigation into the WordPress runtime and object-cache layers.


11. Bypassing Cloudflare and Requesting the Origin Directly

To determine whether the old CSS was coming from the origin, I used --resolve on the server so the English domain would resolve directly to the local Nginx instance:

Bash
ts=$(date +%s)
url="/2026/07/11/19327/?origin_check=$ts"

curl -ksS \
  --resolve en.shuijingwanwq.com:443:127.0.0.1 \
  -H "Cache-Control: no-cache" \
  "https://en.shuijingwanwq.com$url" \
  -o /tmp/en-origin.html

curl -ksS \
  -H "Cache-Control: no-cache" \
  "https://en.shuijingwanwq.com$url" \
  -o /tmp/en-public.html

I then compared the CSS returned by the origin and the public URL:

Bash
python3 - <<'PY'
import re

for name, path in [
    ("直接访问源站", "/tmp/en-origin.html"),
    ("通过公网 Cloudflare", "/tmp/en-public.html"),
]:
    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        html = f.read()

    rules = re.findall(
        r'(?:header\.wp-block-template-part\s+)?\.wp-block-search\s*\{.*?\}',
        html,
        flags=re.S
    )

    matched = [
        re.sub(r'\s+', ' ', rule).strip()
        for rule in rules
        if "flex: 3 1 0" in rule or "min-width:" in rule
    ]

    print(f"\n===== {name} =====")
    print("包含 min-width: 0:", "min-width: 0" in html)
    print("包含 min-width: 250px:", "min-width: 250px" in html)

    for rule in matched[-5:]:
        print(rule)
PY

Output:

Plaintext
===== 直接访问源站 =====
包含 min-width: 0:False
包含 min-width: 250px:True
.wp-block-search { flex: 3 1 0; min-width: 250px; margin: 0 10px; }

===== 通过公网 Cloudflare =====
包含 min-width: 0:False
包含 min-width: 250px:True
.wp-block-search { flex: 3 1 0; min-width: 250px; margin: 0 10px; }

This proved that:

Plaintext
Cloudflare 返回的是旧 CSS
直接访问源站同样返回旧 CSS

The problem therefore existed in the WordPress runtime rather than in Cloudflare.


12. Querying wp_global_styles in the Database

Twenty Twenty-Five is a block theme.

Global styles and Additional CSS saved through the Site Editor are stored in post records with the wp_global_styles post type.

I queried the database with the following PHP script:

Bash
cd /data/wwwroot/www.shuijingwanwq.com

php -d display_errors=1 <<'PHP'
<?php
require __DIR__ . '/wp-load.php';

global $wpdb;

$rows = $wpdb->get_results("
    SELECT
        ID,
        post_type,
        post_status,
        post_name,
        post_title,
        post_modified,
        post_content
    FROM {$wpdb->posts}
    WHERE post_type IN ('wp_global_styles', 'custom_css')
    ORDER BY post_modified DESC, ID DESC
");

foreach ($rows as $row) {
    $content = (string) $row->post_content;

    printf(
        "ID=%d | type=%s | status=%s | name=%s | title=%s | modified=%s | length=%d | old250=%s | new0=%s\n",
        $row->ID,
        $row->post_type,
        $row->post_status,
        $row->post_name,
        $row->post_title,
        $row->post_modified,
        strlen($content),
        strpos($content, 'min-width: 250px') !== false ? 'YES' : 'NO',
        strpos($content, 'min-width: 0') !== false ? 'YES' : 'NO'
    );
}
PHP

Output:

Plaintext
ID=17390
type=wp_global_styles
status=publish
name=wp-global-styles-twentytwentyfive
title=Custom Styles
old250=NO
new0=YES

This showed that the wp_global_styles record in the database already contained the updated version:

Plaintext
min-width: 0

There was also no second Twenty Twenty-Five global-styles record in the database that still contained min-width: 250px.

The problem was therefore not a failed CSS save, nor were there separate old and new global-styles records in the database.


13. The WordPress Runtime Was Still Generating the Old CSS

Next, I loaded WordPress directly through PHP and simulated a request to the English domain:

Bash
cd /data/wwwroot/www.shuijingwanwq.com

php -d display_errors=1 <<'PHP'
<?php

$_SERVER['HTTP_HOST']       = 'en.shuijingwanwq.com';
$_SERVER['SERVER_NAME']     = 'en.shuijingwanwq.com';
$_SERVER['REQUEST_URI']     = '/2026/07/11/19327/';
$_SERVER['HTTPS']           = 'on';
$_SERVER['SERVER_PORT']     = '443';
$_SERVER['REQUEST_SCHEME']  = 'https';

require __DIR__ . '/wp-load.php';

$global_css = function_exists('wp_get_global_stylesheet')
    ? wp_get_global_stylesheet()
    : '';

$custom_css = function_exists('wp_get_global_styles_custom_css')
    ? wp_get_global_styles_custom_css()
    : '';

$css = $global_css . "\n" . $custom_css;

echo "CSS length: " . strlen($css) . PHP_EOL;

echo "包含 min-width: 0:"
    . (strpos($css, 'min-width: 0') !== false ? 'YES' : 'NO')
    . PHP_EOL;

echo "包含 min-width: 250px:"
    . (strpos($css, 'min-width: 250px') !== false ? 'YES' : 'NO')
    . PHP_EOL;

if (preg_match(
    '/(?:header\.wp-block-template-part\s+)?\.wp-block-search\s*\{[^}]*\}/s',
    $css,
    $match
)) {
    echo PHP_EOL . "匹配到的搜索框规则:" . PHP_EOL;
    echo preg_replace('/\s+/', ' ', trim($match[0])) . PHP_EOL;
}
PHP

Output:

Plaintext
CSS length: 39474
包含 min-width: 0:NO
包含 min-width: 250px:YES

匹配到的搜索框规则:
.wp-block-search { flex: 3 1 0; min-width: 250px; margin: 0 10px; }

At this point, I could confirm that:

Plaintext
数据库里是新版 CSS
WordPress 运行时仍然生成旧版 CSS

Because the site had W3 Total Cache Object Cache enabled with Redis as its backend, I next focused on the object content returned by W3TC Object Cache.


14. Comparing the Raw Database Value with the Value Returned by get_post()

To determine whether the object cache was stale, I compared:

  1. a direct SQL read from the database;
  2. a read through WordPress get_post();
  3. whether an external object cache was currently in use.

Command:

Bash
cd /data/wwwroot/www.shuijingwanwq.com

php -d display_errors=1 <<'PHP'
<?php

$_SERVER['HTTP_HOST']      = 'en.shuijingwanwq.com';
$_SERVER['SERVER_NAME']    = 'en.shuijingwanwq.com';
$_SERVER['REQUEST_URI']    = '/2026/07/11/19327/';
$_SERVER['HTTPS']          = 'on';
$_SERVER['SERVER_PORT']    = '443';
$_SERVER['REQUEST_SCHEME'] = 'https';

require __DIR__ . '/wp-load.php';

global $wpdb;

$id = 17390;

$raw_database = (string) $wpdb->get_var(
    $wpdb->prepare(
        "SELECT post_content FROM {$wpdb->posts} WHERE ID = %d",
        $id
    )
);

$post_object = get_post($id);

$cached_post = $post_object
    ? (string) $post_object->post_content
    : '';

$stylesheet = function_exists('wp_get_global_stylesheet')
    ? (string) wp_get_global_stylesheet()
    : '';

function report_css($name, $content) {
    echo PHP_EOL . "===== {$name} =====" . PHP_EOL;
    echo "长度:" . strlen($content) . PHP_EOL;

    echo "min-width: 0:"
        . (strpos($content, 'min-width: 0') !== false ? 'YES' : 'NO')
        . PHP_EOL;

    echo "min-width: 250px:"
        . (strpos($content, 'min-width: 250px') !== false ? 'YES' : 'NO')
        . PHP_EOL;
}

echo "使用外部对象缓存:"
    . (wp_using_ext_object_cache() ? 'YES' : 'NO')
    . PHP_EOL;

report_css('数据库原始内容', $raw_database);
report_css('get_post() 返回内容', $cached_post);
report_css('wp_get_global_stylesheet() 生成结果', $stylesheet);
PHP

Output:

Plaintext
使用外部对象缓存:YES

===== 数据库原始内容 =====
长度:17407
min-width: 0:YES
min-width: 250px:NO

===== get_post() 返回内容 =====
长度:17317
min-width: 0:NO
min-width: 250px:YES

The result clearly showed that:

Plaintext
数据库中的 wp_global_styles 已经更新

W3TC Object Cache 中仍然保存旧的 post 对象

get_post(17390) 返回旧内容

WordPress 运行时继续生成旧 CSS
[Figure 7. The raw database content differs from the value returned by get_post()]
[Figure 7. The raw database content differs from the value returned by get_post()]

At this stage, there was no need to keep guessing about Cloudflare, Nginx, or browser caching.

The raw database content differed from the value returned by get_post(), and normal behavior returned after the object cache was cleared. This directly demonstrated that the problem was in the WordPress object-cache layer.


15. Precisely Clearing the wp_global_styles Object Cache

Because I had already identified the affected record as:

Plaintext
wp_global_styles ID:17390

I did not immediately run:

Bash
redis-cli FLUSHALL

Instead, I used the WordPress API to clear only that post object and the related Theme JSON caches:

Bash
cd /data/wwwroot/www.shuijingwanwq.com

php -d display_errors=1 <<'PHP'
<?php

$_SERVER['HTTP_HOST']      = 'en.shuijingwanwq.com';
$_SERVER['SERVER_NAME']    = 'en.shuijingwanwq.com';
$_SERVER['REQUEST_URI']    = '/2026/07/11/19327/';
$_SERVER['HTTPS']          = 'on';
$_SERVER['SERVER_PORT']    = '443';
$_SERVER['REQUEST_SCHEME'] = 'https';

require __DIR__ . '/wp-load.php';

$id = 17390;

/* 清理指定 wp_global_styles 文章的对象缓存 */
clean_post_cache($id);

/* 清理 WordPress Theme JSON / 全局样式缓存 */
if (function_exists('wp_clean_theme_json_cache')) {
    wp_clean_theme_json_cache();
}

/* 重新读取并验证 */
$post = get_post($id);
$content = $post ? (string) $post->post_content : '';

echo "已清理对象缓存。" . PHP_EOL;

echo "get_post() 包含 min-width: 0:"
    . (strpos($content, 'min-width: 0') !== false ? 'YES' : 'NO')
    . PHP_EOL;

echo "get_post() 包含 min-width: 250px:"
    . (strpos($content, 'min-width: 250px') !== false ? 'YES' : 'NO')
    . PHP_EOL;
PHP

Output:

Plaintext
已清理对象缓存。
get_post() 包含 min-width: 0:YES
get_post() 包含 min-width: 250px:NO

This showed that the stale wp_global_styles post object had been removed from W3TC Object Cache and that WordPress was now reading the updated content from the database.

[Figure 8. After clean_post_cache runs, get_post() reads the updated CSS]
[Figure 8. After clean_post_cache runs, get_post() reads the updated CSS]

One important caution:

Plaintext
17390

is only the ID of the relevant wp_global_styles record on my current site.

Other sites must not copy this ID directly. They need to query their own database and use the actual record ID for that installation.


16. Verifying the Public English Page Again

Finally, I requested the English page again from my local computer:

Bash
u="https://en.shuijingwanwq.com/2026/07/11/19327/?_csscheck=$(date +%s)"

curl -ksS "$u" |
python3 -c '
import sys

html = sys.stdin.read()

print("包含 min-width: 0:", "min-width: 0" in html)
print("包含 min-width: 250px:", "min-width: 250px" in html)
'

Output:

Plaintext
包含 min-width: 0:True
包含 min-width: 250px:False

After reopening the English article, the horizontal scrollbar at the bottom of the page was gone.

[Figure 9. The horizontal scrollbar is gone and the English article page is back to normal]
[Figure 9. The horizontal scrollbar is gone and the English article page is back to normal]

17. The Incident Actually Had Two Separate Causes

This incident cannot be reduced to a language-switcher styling issue, nor can it be explained simply as a cache purge failure.

It was the result of two overlapping problems.

Layer One: There Was a Real CSS Layout Problem

The header search box originally used:

CSS
min-width: 250px;

When the parent Flex container ran out of space, the search box could not shrink any further and pushed the English language switcher outside the page.

The correct fix was:

CSS
min-width: 0;

rather than using:

CSS
transform: translateX(-17px);

to force the language switcher to move left.

Layer Two: W3TC Object Cache Continued to Return the Old CSS Object

Although the new Additional CSS had been saved in the dashboard and the database’s wp_global_styles record had been updated, W3TC Object Cache still held the old post object.

As a result:

Plaintext
CSS 已经修改正确

数据库已经更新

get_post() 仍然返回旧对象

WordPress 继续生成旧 CSS

英文前台仍然存在横向滚动条

Only after running:

PHP
clean_post_cache(17390);

to clear the corresponding cached object did the actual CSS fix begin to take effect.


18. How This Relates to the Earlier Polylang Multi-Domain Migration Issue

This was not the first W3TC caching anomaly I had encountered while running Polylang in multi-domain mode.

The migration documented in the previous article actually produced two consecutive problems.

First Problem: W3TC Page Cache Treated the English Domain as a Foreign Domain

At the time, visiting:

Plaintext
https://en.shuijingwanwq.com/

resulted in a 301 redirect back to:

Plaintext
https://www.shuijingwanwq.com/

The redirect was ultimately traced to:

Plaintext
W3TC\PgCache_Plugin->redirect_on_foreign_domain

In other words, W3 Total Cache Page Cache evaluated the request against the primary WordPress site domain and did not correctly recognize the English-language domain configured in Polylang as a valid domain.

I eventually used a mu-plugin to remove this foreign-domain redirect for the language domains configured in Polylang.

After the fix:

Plaintext
en.shuijingwanwq.com

no longer redirected back to:

Plaintext
www.shuijingwanwq.com

Second Problem: W3TC Object Cache Retained the Old Polylang Configuration

After the 301 issue was resolved, another problem appeared when the English subdomain was accessed:

HTML
<html lang="zh-CN">

The canonical URL still pointed to:

Plaintext
https://www.shuijingwanwq.com/

This meant that although the request had reached:

Plaintext
en.shuijingwanwq.com

the WordPress runtime was still identifying it as the Chinese primary site.

The Polylang configuration in the database had already been updated to:

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

but the WordPress runtime was still reading the old configuration:

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

This showed that W3TC Object Cache still contained the old Polylang option.

At the time, I ran:

Bash
redis-cli FLUSHALL

Output:

Plaintext
OK

Only after the cache was cleared did the WordPress runtime return to the correct configuration:

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 websites or services, running FLUSHALL directly is not recommended.

FLUSHALL removes every database and cached value from the entire Redis instance.

I used it at the time because that Redis instance was primarily dedicated to this WordPress site, so the impact on other services was controlled.


19. The Exact Relationship Between the Two Incidents

The two incidents should therefore not be summarized simply as:

Plaintext
第一次:Page Cache 问题
第二次:Object Cache 问题

A more accurate relationship is:

Plaintext
第一次迁移过程中:

1. W3TC Page Cache
   将 Polylang 英文域名识别成 foreign domain。

2. W3TC Object Cache
   保留了旧的 Polylang option。


这一次横向滚动条问题:

3. W3TC Object Cache
   保留了旧的 wp_global_styles post 对象。

The two W3TC Object Cache failures affected different kinds of data:

Plaintext
第一次:
Polylang option

这一次:
wp_global_styles post object

but their evidence chains were highly similar:

Plaintext
数据库原始内容已经更新

WordPress API 运行时仍然返回旧内容

清理 W3TC Object Cache 后恢复正常

The first incident caused:

Plaintext
英文域名识别错误
<html lang> 错误
Canonical 错误
英文首页 URL 错误

This incident caused:

Plaintext
全局 CSS 继续使用旧版本
搜索框 min-width 修改不生效
英文文章持续出现横向滚动条

20. W3TC Cache Problems Confirmed So Far

Across the two investigations, I can now confirm three specific problems.

1. Page Cache Incorrectly Recognized a Polylang Language Domain

Plaintext
en.shuijingwanwq.com

redirect_on_foreign_domain

www.shuijingwanwq.com

This issue was ultimately resolved with a mu-plugin patch.

2. Object Cache Retained an Old Polylang Option

Plaintext
数据库:
force_lang = 3

WordPress 运行时:
force_lang = 1

清理 Redis 后:
force_lang = 3

3. Object Cache Retained an Old wp_global_styles Object

Plaintext
数据库 post_content:
min-width: 0

get_post(17390):
min-width: 250px

clean_post_cache(17390) 后:
min-width: 0

There is no longer any need to speculate about whether Redis cache keys include the Host, or to attribute the problem to a separate Redis Object Cache plugin.

The current site uses:

Plaintext
W3 Total Cache Object Cache

with Redis as its cache backend.

The shared fact in both object-cache incidents is:

The WordPress database had already been updated, but W3TC Object Cache continued to return an old option or post object.

Determining why the normal save operation did not invalidate the relevant object promptly would require further analysis of W3TC’s object-cache implementation.

For diagnosing and resolving this incident, however, the evidence is already complete enough.


21. A Recommended Troubleshooting Order for Similar Problems

If the dashboard has been updated but the front end still refuses to change, the following sequence can help narrow down the cause.

1. Confirm the Actual Front-End Problem First

Do not begin by using:

CSS
overflow-x: hidden;

or:

CSS
transform: translateX(...);

to hide the layout problem.

First use the browser console to identify the element that actually extends beyond the viewport and inspect its parent container.

2. Inspect the Source Code Actually Returned to the Front End

Confirm whether the browser is currently receiving the old CSS or the new CSS.

3. Rule Out CDN Edge Caching

Inspect:

Plaintext
cf-cache-status
age
cache-control

Add a random query parameter when necessary.

4. Request the Origin Directly

Use:

Bash
curl --resolve

to distinguish a CDN problem from an origin problem.

5. Compare Database Values with WordPress API Results

For example:

Plaintext
数据库 SQL 原始值
get_option() 返回值
get_post() 返回值
wp_get_global_stylesheet() 返回值

If the database contains the new value but the WordPress API returns the old one, Object Cache should become the primary suspect.

6. Prefer Precise Object Invalidation

When the affected post ID is known, use:

PHP
clean_post_cache($id);

instead of immediately running:

Bash
redis-cli FLUSHALL

Only consider clearing the entire Redis instance when the exact cached object cannot be identified and the instance’s scope of impact is known to be safe.


Conclusion

The complete chain behind the horizontal scrollbar on the English article was:

Plaintext
页头搜索框 min-width: 250px

搜索框和英文语言切换器
无法同时装进父级 Flex 容器

英文页面出现横向滚动条

将搜索框改为 min-width: 0

数据库中的 wp_global_styles 更新成功

W3TC Object Cache 仍然返回旧 post 对象

get_post() 继续得到 min-width: 250px

英文前台继续输出旧 CSS

clean_post_cache() 清理指定对象

WordPress 重新读取数据库中的新 CSS

英文页面横向滚动条消失

Together with the previous Polylang multi-domain migration investigation, I have now encountered:

Plaintext
W3TC Page Cache 多域名识别问题
W3TC Object Cache 缓存旧 Polylang option
W3TC Object Cache 缓存旧 wp_global_styles post

This also shows that when front-end content fails to update in a multi-domain WordPress environment, checking only the following is not enough:

Plaintext
浏览器缓存
Cloudflare 缓存
Nginx 缓存
W3TC Page Cache

You should also compare:

Plaintext
数据库原始内容
get_option() 返回内容
get_post() 返回内容
wp_get_global_stylesheet() 生成内容
W3TC Object Cache 中的对象

This is especially important in an architecture that combines:

Plaintext
后台固定使用主域名
不同语言使用独立域名
多个域名共享同一个 WordPress
多个域名共享同一个数据库
多个域名共享同一个 W3TC Object Cache

“The database has been updated” and “every language site is reading the updated data at runtime” are not the same thing.

系列导航

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

我是拥有 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 来减少垃圾评论。了解你的评论数据如何被处理