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

为什么 English 子站文章浏览量一直是 0:一次 Post Views Counter 与页面缓存兼容问题排查记录

【图 3:响应中的 rest_cookie_invalid_nonce】

作者:

WordPress 性能优化手记

如 如图1 所示,站点健康直接提示这是一个可能对性能或安全性产生重大影响的问题,需要优先解决。

(1) 从站点健康警告到全绿通关

PHP Fatal error: Uncaught RedisException: OOM command not allowed when used memory > 'maxmemory'. in /data/wwwroot/.../wp-content/plugins/w3-total-cache/Cache_Redis.php:150

(2) 解决 WordPress + Polylang 批量处理标签时遇到的 Redis OOM 错误

采用 ondemand 模式,适合 1 核小内存机器,空闲时释放进程。最终配置如下:如图4

(3) 一次WordPress站点504错误的排查与优化实录

优化前最终基准数据:历史累计504错误129条(6月3日峰值126条,为爬虫批量爬标签导致)。

(4) 从单日126次504超时到彻底稳定:WordPress 1核2G服务器极限优化全记录(附全部实操命令)

WebPageTest 核心指标

(5) WordPress性能基准测试与CDN选型实录:从国内拨测到海外WebPageTest,1核2G服务器如何面向全球

页面底部提示“此响应不是合法的 JSON 响应”。

(6) WordPress 标签保存失败?Nginx 限流规则惹的祸 —— 一次完整的 429 问题排查与解决

表示页面仍然是动态生成,没有缓存命中。

(7) WordPress + Nginx + W3 Total Cache 缓存未生效排查全过程(OneinStack 实战)

图7:带 Query String 后,W3TC 显示 Requested URI contains query

(8) WordPress CPU 再次满载:动态参数如何穿透 CDN 与 W3TC 页面缓存

图1:阿里云 ECS 在 14:33~14:42 期间 CPU 使用率持续接近 100%,而内存使用率整体较为稳定

(9) WordPress 服务器 CPU 再次满载:从 Nginx 499、PHP-FPM、Redis 到 W3TC 冷缓存的完整排查

图4:W3TC 最终使用 300 秒 × 7 页,并切换到中英文联合 Sitemap

(10) 从 CPU 告警到双域名预缓存:W3 Total Cache、EdgeOne 与 Cloudflare 96 小时缓存优化实战

WordPress 所有归档模板中使用 Language Visibility 和 WPCode Shortcode 添加中英文 Adsterra 广告

(11) WordPress 三域名架构再次踩坑:Adsterra 归档广告不生效,最终定位到 W3TC Object Cache

WordPress 多语言多域名缓存排查:修复 Polylang + W3 Total Cache 跨 Host 缓存失效问题

(12) WordPress 多语言多域名缓存排查:修复 Polylang + W3 Total Cache 跨 Host 缓存失效问题

WordPress 多域名环境下中文文章详情页未显示 Polylang 语言切换器

(13) WordPress 多域名下 Polylang 语言切换器延迟:W3TC Redis 跨 Host 缓存排查记录

图8:选择 2 核 4 GiB 后显示的实际补差价、2 Mbps 带宽和重启选项

(14) 从 CPU 再次告警到 ECS 升配:WordPress 服务器从 1 核 2G 升级到 2 核 4G 实录

**Alt:** WordPress 生产环境完成 PHP 8.5.9 升级,终端显示 OPcache、Imagick、Redis、Nginx、WordPress 版本及中英文站和后台域名均正常返回 HTTP 200

(15) OneinStack 生产环境将 PHP 8.1.19 升级到 PHP 8.5.9:Imagick 编译失败与 WordPress 多域名缓存验收

阿里云 OneinStack 服务器升级完成,终端显示 Nginx 1.30.4、OpenSSL 3.5.7、PCRE 8.45 和 PHP 8.5.9,Nginx 配置检查成功

(16) 阿里云 OneinStack 实战:将 Nginx 1.24.0 升级到 1.30.4,并同步升级 OpenSSL 3.5.7

阿里云 OneinStack 服务器 Redis 从 7.0.11 升级到 8.10.0 后的版本与运行状态对比截图

(17) 阿里云 OneinStack 实战:将 Redis 7.0.11 升级到 8.10.0,并完成内核优化与回滚保护

GitHub 上为 PublishPress Series 提交 PHP 8.5 SplObjectStorage 弃用警告 Issue 的页面

(18) WordPress 升级 PHP 8.5 后的插件兼容性排查:该修的修,该停的停,该等上游的等

WordPress Post Views Counter 热门文章排行榜区块及浏览量设置界面,用于排查首页查询性能问题

(19) WordPress 动态首页从 19 秒降到 1 秒以内:Post Views Counter 热门文章查询性能问题排查与 MU Plugin 优化实战

WordPress 撰写设置中已开启可能影响网站性能的 Gutenberg 实时协作功能

(20) WordPress 7.0 + PHP 8.5 服务器配置全面审计:PHP-FPM、OPcache、Redis、RDS 与 WordPress 调优实战

【图 1:8 月 6 日提交的 PublishPress Series PHP 8.5 兼容性 Issue #1163 已由上游关闭】

(21) PublishPress Series 3.1.3 升级实录:PHP 8.5 Issue 已修复,却又遇到 Gutenberg 系列编号回归

图 6:为 www 域名单独启用适中的自适应频控和流量防盗刷,处置方式均采用 JavaScript 挑战

(22) WordPress 再次出现 CPU 告警:从 EdgeOne 异常流量到自适应频控与 JavaScript 挑战

图 3:PHP-FPM 日志出现 Allowed memory size of 268435456 bytes exhausted

(23) WordPress CPU 告警继续排查:Yoast Sitemap 500、PHP 256M OOM 与 W3TC 预热失效

图 2:新的 PHP 进程通过 get_option("cron") 读取不到 Probe,但数据库中明确存在

(24) W3TC 定时任务为何创建后又消失?排查 WP-Cron 与 Redis alloptions 陈旧缓存

W3TC 缓存为什么每天失效?

(25) W3 Total Cache 明明设置了 4 天缓存,为什么每天都会失效?一次 Polylang 与 Yoast SEO 联合排查记录

W3TC Page Cache 全量失效后中英文站活跃缓存数量骤降

(26) WordPress 自定义 taxonomy 更新导致 W3TC 全量缓存失效:series 的真实生产修复

【图 3:响应中的 rest_cookie_invalid_nonce】

(27) 为什么 English 子站文章浏览量一直是 0:一次 Post Views Counter 与页面缓存兼容问题排查记录

最近我发现 WordPress 站点上的一个异常。

我的中文主站和 English 子站都使用 Post Views Counter 统计文章浏览量。中文站看起来一直正常,但 English 子站最近发布的几篇文章,后台浏览量却长时间停留在 0

刚开始我还以为只是访问量比较少,或者 WordPress 后台数字没有及时刷新。

但连续观察几篇文章以后,感觉不太对:我自己明明打开过这些页面,浏览量却仍然没有任何变化。

于是开始进一步排查。


一、最开始的现象:English 最近文章浏览量一直是 0

后台文章列表中,可以看到最近几篇 English 文章的浏览量没有增长。

【图 1:English 文章列表中最近文章浏览量为 0】
【图 1:English 文章列表中最近文章浏览量为 0】

最开始考虑过几个可能:

  • 文章实际上没有产生有效访问;
  • Post Views Counter 本身没有正常工作;
  • Cloudflare 或 W3 Total Cache 影响了计数;
  • REST API 被安全规则拦截;
  • WordPress 后台显示值和数据库实际值不同步。

为了避免继续猜测,最直接的办法还是打开浏览器开发者工具,查看一次真实的计数请求。


二、真正的异常:Post Views Counter REST API 返回 403

Post Views Counter 当前使用的是 REST API 计数模式。

访问文章时,浏览器会发送类似下面的 POST 请求:

Plaintext
/wp-json/post-views-counter/view-post/{post_id}

正常情况下,这个请求应该返回:

Plaintext
HTTP 200

但实际看到的却是:

Plaintext
HTTP 403
【图 2:Post Views Counter REST 请求返回 403】
【图 2:Post Views Counter REST 请求返回 403】

继续查看响应内容,WordPress 返回:

JSON
{
    "code": "rest_cookie_invalid_nonce",
    "message": "Cookie 检查失败",
    "data": {
        "status": 403
    }
}
【图 3:响应中的 rest_cookie_invalid_nonce】
【图 3:响应中的 rest_cookie_invalid_nonce】

后来在 www 主域上也能够看到同类 403 REST 响应。

【图 4:www 主域同样出现 REST Cookie nonce 校验失败】
【图 4:www 主域同样出现 REST Cookie nonce 校验失败】

这一步已经基本排除了“单纯是 Cloudflare WAF 拦截”的可能。

请求实际上已经到达 WordPress,真正返回 403 的是 WordPress REST API 自己。

问题开始集中到一个地方:

浏览器发送的 REST nonce 已经失效。


三、请求内容正常,真正异常的是 X-WP-Nonce

先查看 Post Views Counter 实际发送的请求内容。

请求中包含:

JSON
{
    "storage_type": "cookies",
    "storage_data": ""
}
【图 5:Post Views Counter 请求中的 storage_type 与 storage_data】
【图 5:Post Views Counter 请求中的 storage_type 与 storage_data】

也就是说,PVC 正常按照 Cookies 存储模式发送计数请求。

继续查看请求头,可以看到:

Plaintext
X-WP-Nonce: add9d7782a
【图 6:请求头中的旧 X-WP-Nonce】
【图 6:请求头中的旧 X-WP-Nonce】

随后直接在服务器上验证这个 nonce:

Bash
wp eval --allow-root '
$nonce = "add9d7782a";

echo "cached_nonce={$nonce}\n";

$result = wp_verify_nonce( $nonce, "wp_rest" );

echo "verify_result=";
var_dump( $result );

echo "current_nonce=" . wp_create_nonce( "wp_rest" ) . "\n";
'

当时得到:

Plaintext
cached_nonce=add9d7782a
verify_result=bool(false)
current_nonce=68aeb1c609

也就是说:

Plaintext
add9d7782a

这个 nonce 确实已经失效。

接下来的问题就变成了:

为什么前台页面还在继续使用这个已经失效的 nonce?


四、检查 Post Views Counter 当前配置

为了排除插件设置本身的问题,我又重新检查了 Post Views Counter。

当前主要配置为:

  • Counting Mode:REST API;
  • Data Storage:Cookies;
  • Count Interval:1 小时;
  • 统计文章、页面等正常启用。
【图 7:Post Views Counter 当前计数配置概览】
【图 7:Post Views Counter 当前计数配置概览】

继续检查计数和访客存储相关选项,也没有发现明显错误。

【图 8:Post Views Counter 计数与 Cookie 存储详细配置】
【图 8:Post Views Counter 计数与 Cookie 存储详细配置】

缓存相关设置中,还可以看到插件自身的缓存兼容功能并没有实际参与当前计数。

【图 9:Post Views Counter 缓存与性能相关设置】
【图 9:Post Views Counter 缓存与性能相关设置】

与此同时,我的网站使用 W3 Total Cache,页面缓存生命周期大约为 4 天

到这里,一个很重要的时间差开始出现:

Plaintext
页面缓存:约 4 天

WordPress REST nonce:
实际有效时间通常只有约 12~24 小时

两者显然不是同一个生命周期。


五、根因:缓存页面中保存了一个生命周期更短的 nonce

继续检查 Post Views Counter 源码,可以看到 REST API 模式会生成:

PHP
wp_create_nonce( 'wp_rest' )

然后把这个 nonce 随页面 JavaScript 参数输出给浏览器。

问题就在这里。

页面第一次生成时:

Plaintext
生成 HTML

生成一个 wp_rest nonce

nonce 被写入 HTML / JavaScript

整个页面进入 W3 Total Cache

随后过了一段时间:

Plaintext
wp_rest nonce 已经失效

但是:

Plaintext
W3TC 页面缓存仍然有效

于是访客继续拿到原来的缓存页面。

页面中的 JavaScript 仍然携带旧 nonce:

Plaintext
旧缓存页面

旧 X-WP-Nonce

POST /wp-json/post-views-counter/view-post/...

WordPress REST Authentication

403 rest_cookie_invalid_nonce

最终就表现为:

页面访问正常,但 Post Views Counter 浏览量不再增长。

WordPress nonce 也不是“从生成时间开始固定 24 小时有效”。

它使用时间片机制,因此实际可用时间通常处于大约 12~24 小时之间。

所以即使一篇文章看起来还没有缓存满一天,也完全可能已经携带一个无法通过验证的旧 nonce。


六、不发送旧 nonce 时,REST 计数接口本身正常

为了确认是不是 Post Views Counter REST Endpoint 本身损坏,我又直接进行测试:

Bash
curl -i -sS \
  -X POST \
  'https://en.shuijingwanwq.com/wp-json/post-views-counter/view-post/27585' \
  -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
  --data 'storage_type=cookies&storage_data='

这次不发送页面中的旧:

Plaintext
X-WP-Nonce

结果接口直接返回:

Plaintext
HTTP 200

并且:

JSON
{
    "post_id": 27585,
    "counted": true,
    "type": "post"
}

这说明:

Post Views Counter 的公开计数接口本身没有坏。

问题就是缓存页面所携带的失效 X-WP-Nonce


七、没有切换计数模式,而是增加一个很窄的兼容层

Post Views Counter 还提供 PHP、JavaScript 等计数模式。

但综合考虑以后,我没有切换。

因为我的网站使用完整页面缓存,REST API 本来就是更适合这种架构的计数方式之一。

如果为了这个问题切换计数模式,反而可能引入新的缓存兼容问题。

最终采用的方案是:

保留 REST API,保留页面缓存,只在 Post Views Counter 的 view-post 请求携带失效 nonce 时,在 WordPress Core 正式进行 REST Cookie Authentication 以前替换成一个当前有效的 nonce。

没有修改 Post Views Counter 插件源码,而是增加了一个 MU-plugin:

Plaintext
wp-content/mu-plugins/post-views-counter-rest-cache-compat.php

代码如下:

PHP
<?php
/**
 * Plugin Name: Post Views Counter REST Cache Compatibility
 * Description: Refresh stale cached REST nonces for Post Views Counter public view requests.
 */

defined( 'ABSPATH' ) || exit;

function shuijingwan_pvc_refresh_stale_rest_nonce( $result ) {
	if ( null !== $result ) {
		return $result;
	}

	if (
		! isset( $_SERVER['REQUEST_METHOD'] )
		|| strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) !== 'POST'
	) {
		return $result;
	}

	if ( ! function_exists( 'post_views_counter' ) ) {
		return $result;
	}

	$pvc = post_views_counter();

	if (
		! is_object( $pvc )
		|| ! isset( $pvc->options['general']['counter_mode'] )
		|| $pvc->options['general']['counter_mode'] !== 'rest_api'
	) {
		return $result;
	}

	$route = '';

	if (
		isset( $GLOBALS['wp'] )
		&& isset( $GLOBALS['wp']->query_vars['rest_route'] )
	) {
		$route = (string) $GLOBALS['wp']->query_vars['rest_route'];
	}

	if ( $route === '' ) {
		$request_uri = isset( $_SERVER['REQUEST_URI'] )
			? (string) $_SERVER['REQUEST_URI']
			: '';

		$path = wp_parse_url( $request_uri, PHP_URL_PATH );

		if ( is_string( $path ) ) {
			$prefix   = '/' . trim( rest_get_url_prefix(), '/' ) . '/';
			$position = strpos( $path, $prefix );

			if ( false !== $position ) {
				$route = '/' . ltrim(
					substr( $path, $position + strlen( $prefix ) ),
					'/'
				);
			}
		}
	}

	if (
		! preg_match(
			'#^/?post-views-counter/view-post(?:/\d+)?/?$#',
			$route
		)
	) {
		return $result;
	}

	if (
		! isset( $_SERVER['HTTP_X_WP_NONCE'] )
		|| $_SERVER['HTTP_X_WP_NONCE'] === ''
	) {
		return $result;
	}

	$nonce = (string) $_SERVER['HTTP_X_WP_NONCE'];

	if ( wp_verify_nonce( $nonce, 'wp_rest' ) ) {
		return $result;
	}

	$_SERVER['HTTP_X_WP_NONCE'] = wp_create_nonce( 'wp_rest' );

	return $result;
}

add_filter(
	'rest_authentication_errors',
	'shuijingwan_pvc_refresh_stale_rest_nonce',
	99
);

这里使用:

Plaintext
rest_authentication_errors

99 优先级,是因为 WordPress Core 的:

Plaintext
rest_cookie_check_errors

默认在 100 优先级执行。

因此兼容代码可以在 Core 正式验证 REST nonce 以前完成处理。

它的作用范围也非常有限:

Plaintext
POST
+
Post Views Counter
+
REST API 计数模式
+
view-post Endpoint
+
请求存在 X-WP-Nonce
+
nonce 已经失效

如果 nonce 本身有效,它不会做任何修改。

如果未来不用 REST API 模式,它也会自动停止介入。


八、修复后,English 浏览量开始发生变化

部署兼容代码以后,很快就可以在后台看到 English 文章的浏览量开始发生变化。

【图 10:修复以后 English 文章浏览量开始变化】
【图 10:修复以后 English 文章浏览量开始变化】

不过这还不足以证明一定是 MU-plugin 生效。

因为还有一种可能:

页面缓存刚好重新生成了,于是页面自然获得了新的 nonce。

所以还需要更严格的验证。


九、关键验证:故意不清页面缓存

部署 MU-plugin 以后,我没有立即清理 W3 Total Cache 页面缓存。

这是故意的。

因为如果把缓存清掉,再访问页面就会生成新的 nonce,那就无法判断究竟是兼容层修复成功,还是单纯因为缓存刷新。

继续访问原来的缓存页面后,REST 请求已经变成:

Plaintext
HTTP 200
【图 11:保留旧页面缓存的情况下 REST 请求恢复 HTTP 200】
【图 11:保留旧页面缓存的情况下 REST 请求恢复 HTTP 200】

更重要的是,检查请求头以后发现:

浏览器发送的仍然是原来那个旧 nonce。

【图 12:修复后浏览器仍然发送旧 X-WP-Nonce】
【图 12:修复后浏览器仍然发送旧 X-WP-Nonce】

也就是说:

Plaintext
页面缓存没有换
旧 nonce 也没有换

但服务端已经能够正常接受请求。

随后响应内容显示:

JSON
{
    "post_id": 27585,
    "counted": true,
    "type": "post"
}
【图 13:修复以后 Post Views Counter 返回 counted:true】
【图 13:修复以后 Post Views Counter 返回 counted:true】

这一步基本构成了完整闭环:

Plaintext
旧缓存页面仍存在
+
浏览器仍发送失效 nonce
+
MU-plugin 将其替换为当前有效 nonce
+
WordPress REST Authentication 通过
+
PVC counted:true

因此可以确认:

不是页面缓存碰巧刷新了,而是服务端兼容代码真正解决了问题。


十、期间出现过一次 Cloudflare 520,但属于另外一个问题

测试过程中还碰到过一次:

Plaintext
Cloudflare Error 520
【图 14:测试过程中偶发的一次 Cloudflare 520】
【图 14:测试过程中偶发的一次 Cloudflare 520】

一开始也担心会不会和刚部署的 MU-plugin 有关系。

不过立即重新测试同一篇文章以后,请求又恢复了正常:

Plaintext
HTTP 200
counted:true

而且后续没有持续重现。

因此目前把这次 520 当作一次偶发的源站/CDN 请求异常,没有把它和 nonce 修复直接关联起来。


十一、继续测试其他 English 文章

随后又选择另外一篇 English 文章继续验证。

同样可以看到:

Plaintext
HTTP 200
counted:true
【图 15:另一篇 English 文章同样成功计数】
【图 15:另一篇 English 文章同样成功计数】

这说明修复并不是只对某一个具体 Post ID 有效。


十二、中文文章计数也保持正常

为了确认兼容层没有对中文主站产生副作用,又测试了一篇中文文章。

结果同样:

Plaintext
HTTP 200
counted:true
【图 16:中文文章的 Post Views Counter 计数仍然正常】
【图 16:中文文章的 Post Views Counter 计数仍然正常】

因此没有发现兼容代码破坏其他正常计数路径。


十三、后台文章列表中的浏览量已经恢复

经过几篇文章连续测试以后,再次回到 WordPress 后台文章列表。

之前异常的浏览量已经开始正常增加。

【图 17:修复后后台文章列表中的浏览量恢复增长】
【图 17:修复后后台文章列表中的浏览量恢复增长】

到这里,最开始的 stale nonce 问题实际上已经基本解决。


顺手发现的第二个低频问题

在继续观察的过程中,我又遇到了一件有点奇怪的事情。

一篇刚发布的中文文章,我记得自己已经访问过几次,但在后台重新刷新文章列表以后,它的浏览量仍然显示为:

Plaintext
0
【图 18:另一篇已经访问过的文章后台浏览量仍显示为 0】
【图 18:另一篇已经访问过的文章后台浏览量仍显示为 0】

这让我一度怀疑:

前面的修复是不是还有遗漏?

于是继续检查。


十四、这一次不是 nonce,而是 Cookie 去重

打开文章并查看 REST 请求。

这一次:

Plaintext
HTTP 200

没有再出现:

Plaintext
rest_cookie_invalid_nonce

但是响应为:

JSON
{
    "post_id": 27587,
    "counted": false,
    "storage": [],
    "type": "post"
}
【图 19:文章 REST 请求返回 counted:false】
【图 19:文章 REST 请求返回 counted:false】

继续查看浏览器发出的 storage data:

JSON
{
    "version": 1,
    "session_id": "71075757-e541-4cb8-8f90-7c268cab8768",
    "started_at": 1789646679,
    "expires_at": 1789650279,
    "visited": {
        "post": [
            27587
        ]
    }
}

其中:

Plaintext
started_at
→ 20:04:39

expires_at
→ 21:04:39

正好相差 1 小时。

这与我的 Post Views Counter:

Plaintext
Count Interval = 1 hour

完全一致。

所以当前这次:

Plaintext
counted:false

本身并不是 Bug。

它只是说明:

当前浏览器在一小时内已经成功统计过这篇文章,因此重复访问不会再次增加浏览量。


十五、但为什么之前明明访问过,后台却还是 0?

真正奇怪的是:

如果浏览器已经认为这篇文章统计过一次,为什么后台此前还会显示 0?

于是继续检查 nginx access log:

Bash
grep -RniE \
  'post-views-counter/view-post/27587' \
  /data/wwwlogs /var/log/nginx \
  2>/dev/null

可以看到:

Plaintext
18:58:06  POST /view-post/27587 → 200
18:59:48  POST /view-post/27587 → 200
19:05:11  POST /view-post/27587 → 200
19:10:29  POST /view-post/27587 → 200
19:20:07  POST /view-post/27587 → 200
19:20:28  POST /view-post/27587 → 200
20:04:39  POST /view-post/27587 → 200
20:04:59  POST /view-post/27587 → 200

根据响应大小和后续实际请求内容,可以看到比较明显的模式:

Plaintext
215 / 216 bytes
≈ counted:true + storage

85 bytes
≈ counted:false

也就是说,18:58 左右其实已经成功产生过浏览量。

问题并不是访问没有发生。


十六、数据库暴露出了真正的异常

直接查询 Post Views Counter 使用的:

Plaintext
wp_post_views

表以后,发现 27587 的状态是:

Plaintext
day   = 3
week  = 3
month = 3
year  = 3
total = 1

正常一次访问,会同时增加:

Plaintext
day
week
month
year
total

所以正常应该类似:

Plaintext
3 / 3 / 3 / 3 / 3

但现在却是:

Plaintext
3 / 3 / 3 / 3 / 1

说明:

中间曾经有某个操作只修改了 total,但没有修改 day/week/month/year。

继续查看插件源码以后,正好发现了这样的代码路径:

PHP
pvc_update_post_views()

它只会直接修改:

Plaintext
type = 4
period = total

而不会同步 day/week/month/year。


十七、最终定位到 Gutenberg 编辑器中的旧浏览量覆盖

继续检查 Post Views Counter 的 Block Editor 代码。

Gutenberg 编辑器打开时,插件会读取当时的浏览量:

PHP
$count = pvc_get_post_views( $id );

并保存到:

Plaintext
pvcEditorArgs.postViews

假设刚发布时:

Plaintext
total = 0

那么已经打开的 Gutenberg 编辑器就一直保存着:

Plaintext
postViews = 0

随后,文章前台产生了两个新的真实访问:

Plaintext
0 → 1 → 2

数据库已经变成:

Plaintext
2

但一直没有刷新的 Gutenberg 页面仍然保存:

Plaintext
postViews = 0

而插件的 block-editor.js 在文章保存时会调用:

Plaintext
/post-views-counter/update-post-views/

并提交当前编辑器 state 中的:

Plaintext
post_views

于是就可能形成:

Plaintext
刚发布:
0 / 0 / 0 / 0 / 0

两次真实访问:
2 / 2 / 2 / 2 / 2

编辑器仍保存旧值:
postViews = 0

再次保存文章:
2 / 2 / 2 / 2 / 0

后来再产生一次真实访问:
3 / 3 / 3 / 3 / 1

最终正好和数据库实际情况一致。


十八、admin 子域日志也找到了对应请求

因为 WordPress 后台使用的是:

Plaintext
admin.shuijingwanwq.com

所以继续检查 admin 子域 nginx 日志。

果然找到了:

Plaintext
18:57:51
POST /wp-json/post-views-counter/update-post-views/?id=27587

18:57:55
POST /wp-json/post-views-counter/update-post-views/?id=27587

19:04:57
POST /wp-json/post-views-counter/update-post-views/?id=27587

而文章的:

Plaintext
post_modified

恰好为:

Plaintext
2026-09-17 19:05:00

两者时间完全吻合。

所以这个低频问题基本也已经解释清楚:

Gutenberg 编辑器打开以后,如果前台浏览量发生变化,而编辑器没有重新加载,那么再次保存文章时,Post Views Counter 可能使用编辑器最初读取到的旧浏览量覆盖最新的 total


十九、为什么这个问题暂时没有继续修?

因为这个问题的触发条件比较特殊:

Plaintext
打开 Gutenberg 编辑器

编辑页面一直保持打开

期间前台产生新的浏览量

随后再次保存文章

而我自己发布文章以后再次修改的频率并不高。

即使偶尔损失一两次浏览量,对整体统计影响也比较有限。

相比之下,前面的 stale nonce 问题会持续影响正常访客的计数,所以那个必须修。

这个 Gutenberg 边界问题目前只记录下来,没有继续增加新的生产兼容代码。

后来重新产生有效访问以后,后台浏览量也恢复为了 1

【图 20:后续新的有效访问产生后,后台浏览量恢复为 1】
【图 20:后续新的有效访问产生后,后台浏览量恢复为 1】

二十、第二天再次确认 English 浏览量正常变化

修复以后没有马上结束观察。

第二天再次查看 English 文章列表时,可以看到最近发布文章的浏览量已经继续变化。

例如最近几篇分别已经出现:

Plaintext
5
5
7
8
4
4
2
9
……

已经不存在之前连续多篇文章长期保持:

Plaintext
0

的情况。

【图 21:第二天再次确认 English 最近文章浏览量持续正常变化】
【图 21:第二天再次确认 English 最近文章浏览量持续正常变化】

这一步也比较重要。

因为它证明:

前面的成功并不只是人工测试过程中偶然计数了一两次,而是在后续真实访问中持续正常工作。

因此目前可以确认,最主要的 Post Views Counter stale nonce 问题已经解决。


二十一、最终结论

这次最主要的问题可以总结为:

Plaintext
Post Views Counter REST API
+
WordPress wp_rest nonce
+
长生命周期页面缓存

三者之间发生了生命周期不匹配。

页面缓存可以存在几天,但是页面中嵌入的 REST nonce 在更早的时候就会失效。

于是:

Plaintext
缓存页面

携带旧 nonce

PVC REST 请求

WordPress REST Cookie Authentication

403 rest_cookie_invalid_nonce

文章浏览量停止增长

增加 MU-plugin 兼容层以后:

Plaintext
缓存页面

浏览器仍发送旧 nonce

确认这是 PVC view-post 请求

确认 nonce 已失效

生成当前有效 wp_rest nonce

WordPress Core 正常完成 Authentication

Post Views Counter 正常执行计数

HTTP 200

counted:true

整个过程中没有:

  • 关闭 W3 Total Cache;
  • 缩短整个网站的页面缓存时间;
  • 修改 Post Views Counter 插件源码;
  • 切换 Post Views Counter 的计数模式;
  • 修改 Cloudflare 配置。

只增加了一层作用范围非常有限的兼容处理。


二十二、这次排查中值得记录的几个经验

第一个是:

HTTP 403 并不一定意味着 CDN 或 WAF 拦截。

这一次 Cloudflare 工作正常,请求也已经到达 WordPress。

真正返回 403 的是:

Plaintext
WordPress REST Cookie Authentication

所以遇到 REST API 403 时,首先还是应该检查具体响应内容。

如果看到:

Plaintext
rest_cookie_invalid_nonce

排查方向就已经和普通 WAF 拦截完全不同。

第二个是:

动态认证信息进入长期静态缓存时,需要格外小心。

这一次真正的问题并不是:

Plaintext
页面缓存坏了

也不是:

Plaintext
REST API 坏了

而是:

Plaintext
生命周期较短的 nonce

被放入:

Plaintext
生命周期明显更长的缓存页面

两个生命周期不匹配,最终自然会产生问题。

第三个则是这次顺手发现的 Gutenberg 边界问题:

后台编辑器里缓存的旧状态,也有可能覆盖前台已经发生变化的实时数据。

它影响不大,所以目前没有专门修复,但至少以后再遇到类似现象,就已经知道从哪里开始排查。


目前这个问题就先记录到这里。

主要的浏览量计数故障已经解决。

经过后续继续观察,English 子站最近文章的浏览量都能够正常变化,也没有再出现之前连续多篇新文章长期保持 0 的情况。

WordPress 自定义 taxonomy 更新导致 W3TC 全量缓存失效:series 的真实生产修复

WordPress 网站维护、性能优化与博客运营咨询

本站已持续运营超过 10 年,累计发布 1000+ 篇原创技术文章,长期实践 WordPress 网站建设、CDN / Cloudflare 配置、缓存优化、Google SEO、广告变现和多语言网站运营。

如果你的 WordPress 网站遇到访问慢、缓存异常、插件冲突、广告不显示、SEO 基础结构混乱、CDN 配置不确定等问题,可以联系我做一次远程技术排查。

适合以下用户:
✅ 个人博客站长
✅ WordPress 网站运营者
✅ 独立开发者与内容创作者
✅ SaaS 产品官网运营团队
✅ 希望优化网站速度与稳定性的站点

服务内容:
✅ WordPress 速度优化
✅ Cloudflare / CDN / 缓存配置排查
✅ 插件冲突与页面异常排查
✅ AdSense 广告显示问题排查
✅ SEO 基础结构检查
✅ 博客运营与商业化咨询

如需了解方案或交流相关问题,请直接联系我,并注明:WordPress 维护咨询

联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com