Contents
1 前言
在 WordPress 里,实现文章阅读统计的方法有很多,常见的做法通常是借助插件,在主题文件(通常是 functions.php)中插入代码,并将阅读次数存放在本地数据库中。根据实现方式,大致可以分为两类:服务端统计 和 前端统计。
• 服务端统计:一般是通过 PHP 代码直接记录文章的访问量,但如果网站启用了 CDN 缓存,很多请求根本不会真正到达服务器,导致统计数据不准确。例如,WordPress 的 WP-PostViews 插件 就是这么做的,这种方式已经逐渐被淘汰。
• 前端统计:通过 JavaScript 在浏览器端直接向 WordPress 发送请求,调用 admin-ajax.php 记录访问量。虽然这种方式可以绕开 CDN 缓存的干扰,但 admin-ajax.php 是同步执行的,性能并不算高,流量一大就可能拖慢 WordPress 的整体响应速度。
说到底,这两种方式都不太理想:我之前一直用 WP-PostViews 插件来统计文章阅读量,但它使用的是服务端统计方式,和 Cloudflare APO 的缓存机制完全冲突,导致统计数据经常不准确。加上最近我在折腾 WordPress 瘦身计划,想尽量减少插件的使用,所以决定把这个鸡肋一样的 WP-PostViews 插件清理掉。
不过嘛,阅读次数统计这个功能本身还是有意义的,如果完全不用,又感觉少了点什么。所以,WP-PostViews 这个插件可以删掉,但最好能有一个不影响 WordPress 性能、又能绕开 APO 缓存的替代方案(如果能在云端实现,那就更完美了)!
其实,文章阅读统计的需求本质上就两步:
- 存储文章的阅读次数(比如存进数据库或 KV,每次访问 +1)。
- 从存储的地方读取数据,并显示在页面上。
这时我突然想起,之前用 Cloudflare Worker + KV 搭建随机图片 API 时,实现速率限制功能时就已经用 KV 记录过单位时间内的访问次数了,所以假如 Worker 能把文章的访问次数存进 KV,不就完美解决存储问题了? 只需要再用 JavaScript 让浏览器直接请求 Worker,从 KV 里读取数据并显示到页面上,这不就成了吗?
同时,这个方案有几个显而易见的优势:
• 不依赖 WordPress 服务器:统计逻辑全部跑在 Cloudflare 上,WordPress 只负责提供 slug,不会对服务器产生额外负担。
• 绕开 CDN 缓存干扰:Worker 直接处理请求,不管页面是否被缓存,统计数据都能正确记录。
• Cloudflare KV 读写快、几乎免费:KV 读取通常在 10ms 内,每天有 100 万次免费请求,基本等于白嫖 Cloudflare 资源。
想想这个方案还是挺有意思的,值得一试!
当删除WordPress统计文章阅读量的插件之后,原本页面上的统计数据(一般是一个眼睛后面跟着数字)往往并不会跟着消失(只是可能数字会清零),看着非常碍眼。这是因为这个功能已经作为代码插入到主题文件”functions.php”中去了,而有些插件删除时并不会主动去删除相关的代码。
根据我这次的经验,需要删除两段代码,这里记录一下,有需要的朋友可以参考。
第一段代码:
// 页面浏览量
function get_post_views(post_id){count_key = 'views';
count = get_post_meta(post_id, count_key, true);
if (count==''){
delete_post_meta(post_id,count_key);
add_post_meta(post_id,count_key, '0');
count = '0';
}
return number_format_i18n(count);
}
第二段代码:
if (type == 'views'){
if (function_exists('pvc_get_post_views')){views = pvc_get_post_views(get_the_ID());
}else{
views = get_post_views(get_the_ID());
}
return '<div class="post-meta-detail post-meta-detail-views">
<i class="fa fa-eye" aria-hidden="true"></i> ' .views .
'</div>';
}
注1:没想到在没有编程基础的前提下挑战这种以代码为主的解决方案这么难,我走了好多的弯路,改版了好多次,连文章更新时间都差点推迟了,不过好歹把骨架搭建起来了。
注2:严格意义上来说,只能说用这种方式我弄出来了,但是,并不代表这是最好的实现逻辑,所以本文只能算是打个样,希望能给有相同想法的朋友提供一个思路。
2 解决方案梳理
2.1 概述
在正式实现之前,先整体梳理方案的各个组成部分,以及每个部分的核心功能点。这样在实际操作时,逻辑会更加清晰,避免走弯路。按照当前的优化方案,整个实现过程可以拆分为三个关键步骤:
1. WordPress 获取文章 slug,并通过 JavaScript 发送到 Worker
• 在文章页面加载时,WordPress 需要提供文章的 slug(即 URL 中唯一标识文章的部分)。
• 通过 JavaScript 捕获 slug,然后向 Cloudflare Worker 发送 POST 请求,通知 Worker 该文章被访问了一次。
2. Worker 处理 slug 并更新 KV 中的阅读次数
• Worker 接收到 slug 后,先在 KV 中查找对应的存储项。
• 如果 slug 存在,则将对应的阅读次数 +1;如果 slug 不存在,则新建一个 初始值为 1 的存储项。
• 更新后的阅读次数会存回 KV,以保证数据持久化。
3. WordPress 获取文章 slug,并通过 JavaScript 向Worker查询阅读次数,最后在本地展现
• 在首页、分类页或文章详情页,JavaScript 需要 向 Worker 发送 GET 请求,查询 KV 中对应 slug 的阅读次数。
• Worker 读取 KV 中的数值,并返回 JSON 格式的数据(包含 slug 和阅读次数)。
• JavaScript 解析返回数据,将阅读次数动态插入到页面中,让用户可以看到文章的访问量。
这样,整个统计流程就变得清晰且高效:前端负责收集数据并展示,Worker 负责数据存储和处理,WordPress 仅作为桥梁,极大降低了服务器的负担,同时避免了传统方案中 CDN 缓存干扰统计 的问题。
2.2 WordPress 获取文章 slug,并通过 JavaScript 发送到 Worker
2.2.1 流程简述
在 WordPress 里,slug 是用来标识文章的唯一标识符,它通常是 URL 里用于代表这篇文章的部分。比如说,在 https://blog.tangwudi.com/technology/homedatacenter13164/
这个链接里,homedatacenter13164 就是这篇文章的 slug(当然,把technology/homedatacenter13164
当做slug也不是不可以~),这个 slug 不仅用于 SEO 友好的永久链接,也在许多插件和功能里作为文章的唯一 ID 进行引用。
以往的做法是直接在前端 JavaScript 代码里,从 window.location.pathname 里解析 slug,但这种方式有几个潜在问题:首先,WordPress 允许用户自定义永久链接的结构,比如说使用Permalink Manager Lite 这类插件,它会对 slug 进行额外的改写(/technology/homedatacenter13164/
),这样一来就改动了 URL 规则,JavaScript 解析的结果可能就会出错;其次,如果站点启用了 Cloudflare APO,前端所看到的 URL 可能是缓存的,而不一定是实际的文章路径,导致 JavaScript 获取的 slug 可能并不准确。
为了确保获取到的 slug 始终正确,不再依赖 JavaScript 解析 URL,而是改为 在 PHP 端直接生成 slug,并输出到 HTML 里。这样,无论 URL 结构如何变化,slug 都能正确地与 WordPress 文章对应。具体来说,我们在 WordPress 服务器端使用 get_permalink($post) 获取文章的完整 URL,并通过 parse_url() 和 basename() 方法提取出 slug。然后,这个 slug 会直接插入 HTML 代码里,供前端 JavaScript 读取。
当页面加载完成后,JavaScript 代码会自动读取 PHP 插入的 slug,并向 Cloudflare Worker 发送一个 POST 请求,把这个 slug 传递过去。Cloudflare Worker 在收到请求后,就能记录这个 slug,并将访问数据存入 Cloudflare KV,作为文章的访问统计数据。
这种方案的优势很明显,因为 slug 是在 PHP 里直接解析的,所以数据完全来自 WordPress 本身,保证了正确性,不会受到 JavaScript 解析错误的影响。而且,即使 Cloudflare APO 缓存了页面,JavaScript 依然可以从 HTML 里正确获取 slug,不会因为缓存导致数据不准确。同时,这个方案也兼容像 Permalink Manager Lite 这样的插件,即使 URL 规则被修改,也能确保 slug 的正确性。
最终,通过 PHP 服务器端解析 slug → 前端 JavaScript 读取并发送请求 → Cloudflare Worker 处理数据 这样的方式,成功绕过了缓存问题,同时确保了数据的准确性。这不仅适用于 Cloudflare APO 缓存环境,也不会受到 WordPress 插件或 URL 结构变动的影响,使得整套统计逻辑更加稳定可靠。
2.2.2 实操
在functions.php中插入以下代码(建议使用code snippets插件的方式来实现):
add_action('wp_footer', function() {
if (is_home() || is_archive()) { // 仅在首页或分类/标签页生效
?>
<script>
let hasFetchedViews = false; // 用于标记是否已请求过阅读次数
window.addEventListener("load", function () {
console.log("页面完全加载,开始执行脚本");
// 延时执行,确保页面完全渲染
setTimeout(function() {
if (hasFetchedViews) return; // 如果已经请求过了,直接返回
let articleLinks = document.querySelectorAll(".post-title"); // 直接选择 `.post-title`
let slugs = new Set();
if (articleLinks.length === 0) {
console.warn("未找到 .post-title,可能 DOM 还未完全渲染");
return;
}
articleLinks.forEach(link => {
let url = new URL(link.href, window.location.origin);
let slug = url.pathname.split('/').filter(Boolean).pop(); // 提取 slug
if (slug) slugs.add(slug);
});
if (slugs.size === 0) return;
console.log("即将请求以下 slug 的阅读次数:", Array.from(slugs));
slugs.forEach(slug => {
fetch(`/views-track?slug={encodeURIComponent(slug)}`, { // 改成 GET 请求 /views-track
method: "GET",
headers: { "Content-Type": "application/json" }
})
.then(response => response.json())
.then(data => {
if (data.success) {
insertViewsCount(slug, data.views);
} else {
console.error(`获取{slug} 阅读次数失败`);
}
})
.catch(error => console.error(`请求 {slug} 错误:`, error));
});
hasFetchedViews = true; // 请求完成后标记为已请求
}, 500); // 延时 500ms,确保页面 DOM 已渲染
});
function insertViewsCount(slug, views) {
let viewsText = `阅读次数:{views}`;
let targetCards = document.querySelectorAll(`.post-title[href*="${slug}"]`); // 找到匹配的文章标题
targetCards.forEach(card => {
let parent = card.closest(".post-preview"); // 找到文章的最外层容器
if (!parent) return;
// 避免重复插入
if (parent.querySelector(".views-count")) return;
let viewsElement = document.createElement("div");
viewsElement.className = "views-count"; // 便于后续查找
viewsElement.textContent = viewsText;
viewsElement.style.color = "#666";
viewsElement.style.fontSize = "12px";
viewsElement.style.marginTop = "5px";
parent.appendChild(viewsElement);
});
}
// 通过点击清除缓存并重新加载
document.addEventListener("keydown", function(event) {
if (event.ctrlKey && event.key === 'r') { // Ctrl + R 来清除缓存
hasFetchedViews = false; // 清除已请求标记
console.log("缓存已清除,准备重新加载页面统计信息...");
}
});
</script>
<?php
}
});
这段代码的作用是:每当用户访问文章详情页时,这段代码都会触发,将当前文章的 slug 发送到 Cloudflare Worker,随后Worker会对KV中该slug名称对应的数字”+1″,从而实现文章阅读统计。
- 页面加载触发:
• 当页面完全加载时(通过 window.addEventListener(“load”, …) 监听),开始执行统计流程。
- 延迟执行:
• 使用 setTimeout(function() {…}, 500) 延迟 500 毫秒执行,确保页面内容已完全渲染,包括所有的 DOM 元素。
• 延迟后,判断是否已经请求过浏览次数(hasFetchedViews 标记)。如果已请求过,跳过执行。
- 获取文章链接:
• 在页面中查找所有 .post-title 类的元素,这些元素是文章标题的链接。
• 从每个文章链接中提取出 slug(文章的唯一标识),并将其添加到一个 Set 中,确保每个 slug 只统计一次。
- 发送浏览次数请求:
• 遍历每个 slug,通过 fetch() 发起请求,获取该文章的浏览次数。请求的 URL 是 /views-track?slug=xxx。
• 如果请求成功,调用 insertViewsCount() 将浏览次数显示到页面上。
- 插入浏览次数:
• 根据 slug 找到对应的文章标题(post-title)所在的元素。
• 在该元素的最外层容器(.post-preview)中插入一个包含浏览次数的 div 元素。
- 缓存清除功能:
• 监听 Ctrl + R(浏览器刷新)按键事件,当用户按下该组合键时,重置 hasFetchedViews 标记为 false,重新加载统计信息。
总结:
• 该代码通过延迟执行确保页面完全加载时再发起浏览次数请求,避免了 AJAX 或缓存干扰。
• 通过 setTimeout 和 hasFetchedViews 标记,确保每篇文章的浏览次数只请求一次,避免重复请求。
• 添加了手动清除缓存的功能,提升了用户体验。
注:get_permalink($post) 是 WordPress 的一个函数,用于获取指定文章、页面或自定义文章类型的 固定链接(Permalink),返回的是完整的 URL。参数 $post 可以是文章 ID 或 WP_Post 对象,若省略则默认获取当前文章的链接。
刚开始的时候,我是准备用 Zaraz 中的 “自定义 HTTP REQUEST” 工具来触发统计请求。但经过测试发现,不知道什么原因,“自定义 HTTP REQUEST” 无法在浏览器中正常工作。
我怀疑是因为Zaraz 的请求机制与浏览器直接发出的 fetch() 请求不同,可能经过 Cloudflare 服务器的处理或代理,导致 Worker 无法正确识别请求来源。此外,Zaraz 的请求执行逻辑相对封闭,无法在浏览器控制台直接调试或查看请求细节,也增加了排查问题的难度。
因此,优化后的方案改为由 WordPress 页面上的 JavaScript 代码直接发起请求,确保每次访问都能正确计数,同时方便在浏览器开发者工具里调试和监控请求。
2.3 Worker 处理 slug 并更新 KV 中的阅读次数
2.3.1 流程简述
Worker(可以命名为views-track)专门用于接受WordPress发来的POST请求并提取出文章的slug,这个Worker对应的URL路由入口是:blog.tangwudi.com/views-track*
。
请求处理逻辑
Worker 在收到请求后,会直接在 KV 中操作,遵循以下规则:
- 如果 KV 里已有该 slug,则直接将其对应的阅读次数 +1。
- 如果 KV 里没有该 slug,则新建一个记录,将初始值设为 1(即从 0 开始+1)。
这样做有以下优势:逻辑简单高效,避免额外的查询操作,提高性能;不依赖 WordPress 服务器,所有统计数据存储和更新都在 Cloudflare KV 侧完成;无需 REST API 或数据库查询,不会受到 WAF 限制或 API 访问限制的影响。
这一实现逻辑确保了无论文章slug是否已经存在于 KV 中,都能顺利记录阅读次数,极大简化了统计逻辑,同时保证了性能和可扩展性。
如果要从安全性角度考虑,逻辑可以设计得更复杂一些,比如:定期同步 WordPress 文章的 slug 到 KV,每次 Worker 收到 slug 后,先检查是否是已存储的有效 slug,如果存在则 +1,否则直接丢弃。
但这样一来,还可能涉及身份验证(比如使用签名机制验证请求来源),折腾了几版后,我差点崩溃……最终还是决定不过度设计,直接依靠 WAF 的托管规则、速率限制,以及 Cloudflare 的自动程序识别 来提供基础防护。
毕竟,这只是个文章阅读次数统计,数据没什么敏感性,实在没必要为它搞一堆复杂的安全验证机制,折腾成本完全不值当~
2.3.2 实操
注:由于之前我好几篇文章中都详细记录过Worker的创建和配置过程,所以本文中就不一一记录了,只写几个创建Worker的关键点。
Worker(views-track)的代码如下:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// 处理 GET 请求 -> 查询阅读次数
if (request.method === "GET" && url.pathname === "/views-track") {
return await handleGetRequest(url, env);
}
// 处理 POST 请求 -> 记录并返回最新阅读次数
if (request.method === "POST" && url.pathname === "/views-track") {
return await handlePostRequest(request, env);
}
return new Response("Invalid request", { status: 405 });
}
};
// 处理 GET 请求 -> 查询 KV 获取阅读量
async function handleGetRequest(url, env) {
const slug = url.searchParams.get("slug");
if (!isValidSlug(slug)) {
return new Response("Invalid slug format", { status: 400 });
}
const kvKey = `views:{slug}`;
const currentViews = parseInt(await env.views_kv.get(kvKey)) || 0;
return new Response(JSON.stringify({ success: true, views: currentViews }), {
headers: jsonHeaders(),
});
}
// 处理 POST 请求 -> 记录阅读量
async function handlePostRequest(request, env) {
try {
const { slug } = await request.json();
if (!isValidSlug(slug)) {
return new Response("Invalid slug format", { status: 400 });
}
const kvKey = `views:{slug}`;
const currentViews = parseInt(await env.views_kv.get(kvKey)) || 0;
await env.views_kv.put(kvKey, (currentViews + 1).toString());
return new Response(JSON.stringify({ success: true, views: currentViews + 1 }), {
headers: jsonHeaders(),
});
} catch (error) {
console.error("Worker Error:", error);
return new Response("Internal Server Error", { status: 500 });
}
}
// 校验 slug 格式
function isValidSlug(slug) {
return slug && /^[a-z0-9-]{1,100}$/.test(slug);
}
// 统一 JSON 响应头
function jsonHeaders() {
return {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"CF-Cache-Status": "BYPASS",
};
}
需要绑定KV(名字随意取,我这里为views_kv),其对应的变量名”views_kv”(变量名在Worker绑定KV的时候设置,这个不能乱取);需要设置URL路由,以我博客为例,其路由为:”blog.tangwudi.com/views-track*
“,如下图:

注:最初,”写 KV” 和 “查询 KV” 这两个功能是由两个独立的 Worker 处理的,一个专门负责接收 WordPress 发来的请求并增加阅读次数,另一个专门用于提供 API 接口,返回某篇文章的阅读次数。后来仔细一想,完全没必要分开,两个功能逻辑上是相辅相成的,合并在一个 Worker 里不仅能减少 Worker 的数量,简化部署和管理,还能避免重复解析请求、处理 URL 逻辑等额外开销。因此,最终的方案就是让 views-track 这个 Worker 既负责接收 POST 请求,将 slug 对应的阅读次数 +1,也负责处理 GET 请求,查询某个 slug 的阅读量并返回结果。这种合并方式不仅让代码更加集中、维护更方便,同时也减少了 Worker 的触发次数,从而优化了整体的 Cloudflare 计算资源消耗。
2.4 WordPress 获取文章 slug,并通过 JavaScript 向Worker查询阅读次数,最后在本地展现
2.4.1 流程简述
除了统计访问量,页面上还需要展示文章的阅读次数。因此,前端 JavaScript 代码需要向 Worker 发送一个 GET 请求,获取当前文章的阅读数据,并将其动态插入到页面中。
具体流程如下:
- 用户访问文章详情页时,JavaScript 代码会提取当前文章的 slug,然后向
https://blog.tangwudi.com/views-track?slug=当前文章的slug
发送 GET 请求。 - Worker 收到请求后,会先检查 slug 是否符合格式要求,然后从 Cloudflare KV 读取该文章的阅读次数,并以 JSON 格式返回,如 { “success”: true, “views”: 123 }。
- JavaScript 解析返回的数据,获取 views 的值,并将其插入到页面的合适位置,比如文章标题下方、元数据区域或其他自定义位置。
- 为了确保数据的实时性,可以在 DOMContentLoaded 事件后立即发起请求,并在异步返回后更新页面上的阅读次数显示。
这样,阅读次数的获取和展示完全由前端 JavaScript 处理,不会影响 WordPress 的 PHP 运行,也不会受到 CDN 缓存的干扰,确保数据准确、加载高效。
另外,在展现文章的阅读次数时,要分两种情况来考虑:
- 博客首页的卡片式文章列表
在首页或分类页的文章列表中,每篇文章通常会以卡片形式展示基本信息,如标题、摘要、发布日期等。如果要在这里显示阅读次数,直接使用 JavaScript 逐一请求 Worker 获取数据可能会导致大量并发请求,影响加载速度。因此,有几种优化方案可以考虑:
• 批量请求数据:如果 Worker 支持,前端可以一次性请求多个 slug 对应的阅读次数,而不是单独请求每篇文章的数据,我就是用的这种方式,如下图:

• 后端渲染缓存数据:可以让 WordPress 通过 wp-cron 定期获取阅读数据并存入数据库,这样页面渲染时就能直接显示,不需要前端额外请求。
• 本地存储优化:对于已经访问过的文章,可以利用 localStorage 缓存阅读次数,减少重复请求。
- 文章详情页
在文章详情页,获取阅读次数相对简单,只需要 JavaScript 发送 GET 请求给 Worker,获取当前 slug 对应的阅读数据并更新页面上的显示。由于请求量较小,实时性较高,因此可以直接在 DOMContentLoaded 事件触发时执行请求,确保用户进入文章时能看到最新的阅读次数。
综合来看,文章详情页可以直接从 Worker 读取实时数据,而首页或文章列表页需要优化请求方式,避免因过多请求导致加载速度变慢。
2.4.2 实操
2.4.2.1 博客首页阅读次数展现
在functions.php中插入以下代码(建议使用code snippets插件的方式来实现):
add_action('wp_footer', function() {
if (is_home() || is_archive()) { // 仅在首页或分类/标签页生效
?>
<script>
let hasFetchedViews = false; // 用于标记是否已请求过阅读次数
window.addEventListener("load", function () {
console.log("页面完全加载,开始执行脚本");
// 延时执行,确保页面完全渲染
setTimeout(function() {
if (hasFetchedViews) return; // 如果已经请求过了,直接返回
let articleLinks = document.querySelectorAll(".post-title"); // 直接选择 `.post-title`
let slugs = new Set();
if (articleLinks.length === 0) {
console.warn("未找到 .post-title,可能 DOM 还未完全渲染");
return;
}
articleLinks.forEach(link => {
let url = new URL(link.href, window.location.origin);
let slug = url.pathname.split('/').filter(Boolean).pop(); // 提取 slug
if (slug) slugs.add(slug);
});
if (slugs.size === 0) return;
console.log("即将请求以下 slug 的阅读次数:", Array.from(slugs));
slugs.forEach(slug => {
fetch(`/views-track?slug={encodeURIComponent(slug)}`, { // 改成 GET 请求 /views-track
method: "GET",
headers: { "Content-Type": "application/json",
"Cache-Control": "no-cache" // 确保请求不使用缓存
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
insertViewsCount(slug, data.views);
} else {
console.error(`获取{slug} 阅读次数失败`);
}
})
.catch(error => console.error(`请求 {slug} 错误:`, error));
});
hasFetchedViews = true; // 请求完成后标记为已请求
}, 500); // 延时 500ms,确保页面 DOM 已渲染
});
// 插入阅读次数的函数
function insertViewsCount(slug, views) {
let viewsText = `阅读次数:{views}`;
let targetCards = document.querySelectorAll(`.post-title[href*="${slug}"]`); // 找到匹配的文章标题
// 避免重复插入
targetCards.forEach(card => {
let parent = card.closest(".post-preview"); // 找到文章的最外层容器
if (!parent) return;
// 如果已经有 `.views-count`,则跳过
if (parent.querySelector(".views-count")) return;
let viewsElement = document.createElement("div");
viewsElement.className = "views-count"; // 便于后续查找
viewsElement.textContent = viewsText;
viewsElement.style.color = "#666";
viewsElement.style.fontSize = "12px";
viewsElement.style.marginTop = "5px";
parent.appendChild(viewsElement);
});
}
// 通过点击清除缓存并重新加载
document.addEventListener("keydown", function(event) {
if (event.ctrlKey && event.key === 'r') { // Ctrl + R 来清除缓存
hasFetchedViews = false; // 清除已请求标记
console.log("缓存已清除,准备重新加载页面统计信息...");
}
});
</script>
<?php
}
});
在首页展示文章阅读次数时,不同 WordPress 主题的 HTML 结构可能会有所不同,尤其是在如何获取文章的 slug 并在 JavaScript 代码中正确查询阅读数据方面,容易踩坑。
我最开始遇到的问题是,JavaScript 代码尝试使用 document.querySelectorAll(“.post-title a”) 来获取文章链接,但在实际 HTML 结构中,.post-title 本身就是 <a>
标签,而不是一个 <div>
或 <h2>
之类的容器包裹 <a>
,导致 document.querySelectorAll(“.post-title a”) 直接返回了空值。
- 主题的文章标题结构不同:
• 如果文章标题(.post-title)是 <a>
标签内的链接,使用 document.querySelectorAll(“.post-title a”) 获取所有文章链接。
• 如果 .post-title 本身就是 <a>
标签,直接使用 document.querySelectorAll(“.post-title”) 获取。
• 建议先用浏览器开发者工具(F12)检查 HTML 结构,确保选择器能准确匹配到正确的元素。
- 如何正确提取 slug:
• 使用 get_permalink() 获取完整的文章 URL,在 JavaScript 中可以通过 new URL(url).pathname 提取出 slug。
• 如果主题没有直接在首页输出 slug,可以在 functions.php 中通过 get_post_field(‘post_name’, $post->ID) 将 slug 以 data-slug=”xxx” 的方式输出到 HTML 中,这样便于 JavaScript 读取。
- 首页加载方式影响阅读次数显示:
• 懒加载和 Ajax 分页:如果主题使用了懒加载或 Ajax 分页,新的文章可能在初始页面加载后才动态出现。为确保这些文章也能被正确统计浏览次数,可以使用 setTimeout 延迟执行请求,确保页面元素完全渲染后再发起浏览次数请求。
• 在这种情况下,可以监听页面加载完毕后的事件(如 DOMContentLoaded),同时使用 setTimeout 延时执行,以确保页面完全渲染,再触发统计请求。
总结
不同 WordPress 主题的首页 HTML 结构可能不一样,在插入 JavaScript 代码获取文章阅读次数时,一定要先检查 HTML 结构,确保选择器正确。同时,slug 的获取方式也可能需要调整,以适应不同的主题实现方式。
最终效果展示:

2.4.2.2 文章详情页阅读次数展示
在functions.php中插入以下代码(建议使用code snippets插件的方式来实现):
add_action('wp_footer', function() {
if (is_single()) {
global post;slug = basename(parse_url(get_permalink(post), PHP_URL_PATH)); // 提取 slug
?>
<script>
document.addEventListener("DOMContentLoaded", function () {
// 延迟 500 毫秒后执行 fetch 请求
setTimeout(function() {
fetch("/views-track?slug=<?php echo esc_js(slug); ?>", {
method: "GET",
headers: { "Content-Type": "application/json" }
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("文章 '<?php echo esc_js(slug); ?>' 的阅读次数:", data.views);
insertViewsCount(data.views);
} else {
console.error("获取阅读次数失败:", data);
}
})
.catch(error => console.error("请求错误:", error));
}, 500); // 500 毫秒延迟执行请求
});
function insertViewsCount(views) {
let viewsText = `阅读次数:{views}`;
let targetElement = document.querySelector(".post-meta"); // 目标插入位置(示例)
if (!targetElement) {
console.warn("未找到合适的位置插入阅读次数,尝试默认插入");
targetElement = document.querySelector(".entry-content") || document.body;
}
let viewsElement = document.createElement("div");
viewsElement.textContent = viewsText;
viewsElement.style.color = "#666"; // 样式可修改
viewsElement.style.fontSize = "14px";
viewsElement.style.marginTop = "10px";
targetElement.appendChild(viewsElement);
}
</script>
<?php
}
});
相较于博客首页的代码,文章详情页的结构通常较为稳定,slug 的获取方式 (get_permalink($post)) 也不会因为主题的不同而发生较大变化,因此兼容性相对较好。不过,在不同 WordPress 主题中,仍然有一些需要注意的点:
1. JavaScript 插入阅读次数的位置可能因主题不同而有所变化
let targetElement = document.querySelector(".post-meta");
• 这段代码默认把阅读次数插入到 .post-meta(文章元信息区域)。
• 但不同主题的文章详情页可能没有 .post-meta 这个类,或者该类位于无法插入内容的位置。
解决方案:
targetElement = document.querySelector(".post-meta") || document.querySelector(".entry-content") || document.body;
• 优先选择 .post-meta,其次是 .entry-content(文章正文),最后兜底插入 body,尽可能保证阅读次数能正确显示。
• 也可以手动查看主题 HTML 结构,找到最合适的插入位置。
2. 文章 slug 提取方式的适配
$slug = basename(parse_url(get_permalink($post), PHP_URL_PATH));
• 大部分主题下,get_permalink($post) 返回的 URL 结构一致,可以直接使用 parse_url() + basename() 提取 slug。
• 但如果使用了自定义永久链接结构(如 /%category%/%postname%/),那么 basename() 可能无法准确获取 slug。
解决方案:
可以改成直接从数据库获取 post_name,以保证 slug 的正确性:
$slug = get_post_field('post_name', $post->ID);
这种方式不依赖 URL 结构,适配所有自定义链接形式。
3. 主题可能已经自带阅读次数功能
某些 WordPress 主题(如 Astra、Newspaper)可能自带阅读次数统计功能,并在文章元信息中展示,这时候需要避免重复添加阅读次数,造成 UI 冲突。
解决方案:
可以在 functions.php 里检测 post-meta 是否已有阅读次数相关的 HTML 标记:
if (!document.querySelector(".post-meta .views-count")) {
// 插入阅读次数
}
这样可以避免重复渲染。
总结
• 文章详情页代码兼容性比首页代码更好,但仍需适配 slug 提取、插入阅读次数的目标元素、以及主题自带的阅读次数统计。
• 插入位置:需确保 .post-meta 存在,否则应提供备用方案。
• slug 获取方式:如果有自定义永久链接,建议使用 get_post_field(‘post_name’, $post->ID)。
• 避免重复统计:如果主题已有阅读次数展示,可以检测 .post-meta 里是否已有相关 HTML 标记。
最终效果展现:

3 总结
WordPress 的主题实在是太多了,本文中这个解决方案总体思路上来说没问题,但是对于不同的主题来说直接照搬估计是不行的(Argon 主题除外),只能作为一个参考,最终要实现肯定是要折腾一下的。
不过,只要折腾成功了,就会有不少好处,比如可以卸载 WordPress 统计插件,减少对服务器的额外查询负担,从而加快页面的加载速度,提升整体性能。同时,由于阅读次数存放在 Cloudflare KV 这类云存储中,即使站点迁移、换服务器甚至切换主备站点,数据也不会丢失,不需要额外做本地数据库的同步或迁移。
总的来说,这个方案更适合想要减轻 WordPress 负担、优化页面性能,同时希望统计数据能长期保存的站长,但在不同主题上实现时,可能需要调整 HTML 结构、CSS 选择器以及 slug 提取逻辑,适配成本因主题而异。
这个方案开始还是有点问题,我发现一些常规操作(比如在站内点击不同文章链接去访问这些文章时),阅读次数统计并不会出现,而如果是刷新或者重新载入就会出现。这个现象好熟悉啊,感觉就像是使用了 AJAX、Instant.page 之类的”无刷新加载”功能引起的。
这类技术的核心原理是拦截浏览器的默认页面跳转行为,然后用 JavaScript 异步加载新页面的内容并动态更新 DOM,而不会真正触发 DOMContentLoaded 或 load 事件。这样虽然提升了访问速度,但同时也带来了一个问题——脚本不会像正常页面加载那样重新执行。对于我这个阅读次数统计方案来说,问题就出在这里:
• 页面切换时,WordPress 并不会重新执行 wp_footer 里的 JavaScript 代码,所以 fetch 请求不会再次发送,自然也不会更新阅读次数。
• 但是如果手动刷新页面,整个 wp_footer 代码会重新加载,统计逻辑也就能正常工作了。
如果真要从技术层面解决这个问题,可以利用 MutationObserver 或 pjax 之类的技术,监听 DOM 变化并在页面切换时手动执行统计逻辑,确保无刷新跳转的情况下也能正确获取并显示阅读次数。亦或者,针对具体的无刷新技术(比如 Instant.page),利用它提供的事件钩子,在每次页面切换后手动触发我们的统计代码。这类问题在优化 WordPress 站点时其实很常见,比如 懒加载、预加载、动态内容替换 等技术都会带来类似的副作用,所以在使用这些优化手段时,也需要同步考虑数据更新和脚本执行的兼容性。
注1:我想了一个折中的办法,使用setTimeout(function())
,直接指定在500毫秒之后进行调用显示浏览次数的功能 ,这样一来就可以不受”无刷新加载”的影响了,是不是很天才,之前的所有代码都用这个思路优化过了。
注2:不过也有代价:我又要清除APO缓存重新来过了。。。好烦,这段时间频繁清除缓存,每次清了之后都要重新缓存,这样很影响访客体验啊,关键是清除html就算了,非要把图片缓存拉着一起清,好蛋痛,看来我要研究过只清html内容的办法了~。
注3:貌似还是有点问题,只是改善了部分有问题的操作场景。
另外,再次强烈建议不要在functions.php文件中直接插入代码,而是改为使用code snippets插件来管理,那样会更方便运维,实际效果如下:

弄完之后例行用PageSpeed Insights进行测试,效果不错,最高分第一次达到90分:


注:中途历经了大量在浏览器开发者工具中的排错,不过太多了,实在是懒得写,大家有需要直接用代码去问ChatGPT如何排错吧。
code snippets?看起来是个好东西,果断回去试用一下
的确是好东西,我是真心不想在functions.php或者其他几个主题文件中加代码了,好难管理的,过一段时间就完全忘了。
流弊,佩服。
瞎搞的,不懂编程,走了好多弯路~