Cloudflare Worker + KV: Building WordPress Cloud Article Reading Statistics
This article was last updated 287 days ago. The information within may have evolved or changed; if anything is outdated or broken, please feel free to leave a message in the comments section.
Article Abstract
To address the issues of traditional plugins relying on servers and being interfered with by CDN caching in WordPress post view statistics, a cloud-based solution is built using Cloudflare Worker and KV. The WordPress PHP side generates the post slug and injects it into the HTML, while the frontend JavaScript sends a POST request to the Worker to update the view count in KV, and then reads the data from KV via a GET interface to render it on the page. This solution leverages KV's high-concurrency read/write capabilities to bypass the impact of CDN caching, decoupling the statistics logic from WordPress, reducing server load, and remaining compatible with custom URL structures and caching mechanisms, thereby providing a lightweight, real-time view count statistics function for blog systems.
Qwen3-14B · 2026-06-18

1 Introduction

In WordPress, there are many ways to implement post view statistics. The common practice is usually to use plugins, insert code into the theme file (usually functions.php), and store the view count in the local database. Depending on the implementation method, they can be roughly divided into two categories:Server-side statistics 和 Frontend statistics。

• Server-side statistics: Generally, post views are recorded directly via PHP code, but if the website has enabled CDN caching, many requests will not actually reach the server at all, resulting in inaccurate statistics. For example, WordPress's WP-PostViews plugin is exactly this, and this method has gradually been phased out.

• Frontend statistics: Sends requests directly to WordPress from the browser side via JavaScript, calling admin-ajax.php to record views. Although this method can bypass the interference of CDN caching, admin-ajax.php is executed synchronously, which is not very high-performing, and heavy traffic could slow down the overall response speed of WordPress.

Ultimately, neither of these two methods is ideal: I had been using the WP-PostViews plugin to count post views, but it uses server-side statistics, which completely conflicts with Cloudflare APO's caching mechanism, causing the statistics to often be inaccurate. Plus, recently I've been working on a WordPress slimming planto minimize the use of plugins, so I decided to clean up this rather useless WP-PostViews plugin.

However, the view count statistics function itself is still meaningful; if I don't use it at all, it feels like something is missing. Therefore, the WP-PostViews plugin can be deleted, but it would be best to have an alternative solution that doesn't affect WordPress performance and can bypass APO caching (it would be even more perfect if it could be implemented in the cloud)!

Actually,the requirement for post view statistics essentially consists of just two steps:

  1. Store the post view count(for example, storing it in a database or KV, incrementing by 1 with each visit).
  2. Read the data from the storage location and display it on the page。

At this point, I suddenly remembered that when I previously built a random image API using Cloudflare Worker + KV, I had already used KV to record the number of visits per unit of time to implement a rate-limiting feature. So, if the Worker could store the post view counts in KV, wouldn't that perfectly solve the storage problem? All I would need to do is use JavaScript to have the browser directly request the Worker, read the data from KV, and display it on the page, and it's done!

At the same time, this solution has several obvious advantages:

• No reliance on the WordPress server: All statistics logic runs on Cloudflare it, and WordPress is only responsible for providing the slug, without placing any extra burden on the server.

• Bypass CDN caching interference: Worker directly handles requests, so regardless of whether the page is cached, the statistics can be recorded correctly.

• Cloudflare KV is fast to read/write and almost free: KV reads are usually within 10ms, and there are 1 million free requests per day, which is basically getting Cloudflare resources for free.

Thinking about it, this solution is quite interesting and worth a try!


After deleting the WordPress plugin for counting post views, the original statistics on the page (usually an eye icon followed by a number) often do not disappear with it (the number might just reset to zero), which looks very annoying. This is because this feature has already been inserted as code into the theme file ”functions.php”, and some plugins do not actively delete the relevant code when they are uninstalled.

Based on my experience this time, two blocks of code need to be deleted. I will record them here for reference for those who need it.

The first block of code:

// 页面浏览量
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);
}

The second block of code:

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

Note 1: I didn't expect that challenging such a code-heavy solution without any programming background would be so difficult. I took many detours, revised it many times, and almost postponed the post update time, but at least I managed to build the framework.

Note 2: Strictly speaking, I can only say that I made it work using this method, but it doesn't mean this is the best implementation logic. Therefore, this article can only be regarded as a rough template, hoping to provide some ideas for friends with similar thoughts.

2 Solution Overview

1 Overview

Before the formal implementation, let's first outline the various components of the solution and the core functional points of each part. This will make the logic clearer during actual operation and avoid detours. According to the current optimization plan, the entire implementation process can be split into three key steps:

WordPress gets the article slug and sends it to the Worker via JavaScript

• When the article page loads, WordPress needs to provide the article's slug(which is the part of the URL that uniquely identifies the article).

• Via JavaScript capture the slug, and then send a POST requestto Cloudflare Worker, notifying the Worker that the article has been visited once.

Worker processes the slug and updates the read count in KV

• Worker receives the slug, and first looks up the corresponding storage item in KV .

• If the slug exists, the corresponding read count is incremented; +1if the slug does not exist, a new storage item with an initial value of 1 is created.

• The updated read count will be saved back to KV to ensure data persistence.

WordPress gets the article slug, queries the read count from the Worker via JavaScript, and finally displays it locally

• On the homepage, category pages, or article detail pages, JavaScript needs to send a GET request to the Workerto query the read count of the corresponding slug in KV。

• The Worker reads the value in KV and returns data in JSON format(containing the slug and the read count).

• JavaScript parses the returned dataand dynamically inserts the read count into the page, allowing users to see the article's page views.

In this way, the entire statistics process becomes clear and efficient: the frontend is responsible for collecting and displaying data, the Worker is responsible for data storage and processing, and WordPress only acts as a bridge, greatly reducing the server load while avoiding the issue of CDN caching interfering with statistics in traditional solutions.

2 WordPress Gets Post Slug and Sends It to Worker via JavaScript

2.1 Process Brief

In WordPress,slug is used as a unique identifier for an article, which is usually the part of the URL that represents the article. For example, in https://blog.tangwudi.com/technology/homedatacenter13164/ this link, homedatacenter13164 is the slug of this article (of course, treatingtechnology/homedatacenter13164as the slug is also fine~). This slug is not only used for SEO-friendly permalinks, but is also referenced as the unique ID of the article in many plugins and features.

The previous approach was to directly parse the slug from window.location.pathname in the frontend JavaScript code, but this method has several potential issues: First, WordPress allows users to customize the permalink structure, for example, usingPermalink Manager Lite and similar plugins, which perform additional rewriting on the slug (/technology/homedatacenter13164/), thereby changing the URL rules, which might cause errors in the JavaScript parsing results; second, if the site has Cloudflare APO enabled, the URL seen by the frontend might be cached and not necessarily the actual article path, making the slug obtained by JavaScript potentially inaccurate.

To ensure that the obtained slug is always correct and no longer relies on JavaScript parsing the URL, we instead change to directly generating the slug on the PHP side and outputting it into the HTML. This way, no matter how the URL structure changes, the slug can correctly correspond to the WordPress article. Specifically, we use get_permalink($post) on the WordPress server side to get the full URL of the article, and extract the slug using the parse_url() and basename() methods. Then, this slug is directly inserted into the HTML code for the frontend JavaScript to read.

When the page finishes loading, the JavaScript code will automatically read the slug inserted by PHP and send a POST request to the Cloudflare Worker, passing this slug along. After receiving the request, the Cloudflare Worker can record this slug and store the access data in Cloudflare KV as the post's visit statistics.

The advantage of this solution is obvious: because the slug is parsed directly in PHP, the data comes entirely from WordPress itself, ensuring accuracy and avoiding any impact from JavaScript parsing errors. Moreover, even if Cloudflare APO caches the page, JavaScript can still correctly retrieve the slug from the HTML, preventing inaccurate data caused by caching. At the same time, this solution is also compatible with plugins like Permalink Manager Lite such plugins, ensuring the correctness of the slug even if the URL rules are modified.

Ultimately, through PHP server-side parsing of the slug → Frontend JavaScript reading and sending the request → Cloudflare Worker processing the data this approach successfully bypasses caching issues while ensuring data accuracy. This not only applies to Cloudflare APO caching environments but is also unaffected by changes in WordPress plugins or URL structures, making the entire statistics logic more stable and reliable.

2.2 Practical Operation

Insert the following code into functions.php (it is recommended to use a code snippets plugin to implement this):

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

The purpose of this code is: whenever a user visits a post details page, this code is triggered to send the current post's slug to the Cloudflare Worker. The Worker then increments the number corresponding to that slug name in KV by ”+1", thereby achieving post view statistics.

  1. Page Load Trigger:

• When the page is fully loaded (monitored via window.addEventListener(“load”, ...)), the statistics process begins execution.

  1. Delayed Execution:

• 使用 setTimeout(function() {…}, 500) 延迟 500 毫秒执行,确保页面内容已完全渲染,包括所有的 DOM 元素。

• After the delay, determine whether the view count has already been requested (hasFetchedViews flag). If it has been requested, skip execution.

  1. Get Post Links:

• Find all elements with the .post-title class on the page, which are links to the post titles.

• Extract the slug (the unique identifier of the post) from each post link and add it to a Set to ensure each slug is counted only once.

  1. Send View Count Request:

• Iterate through each slug and initiate a request via fetch() to get the view count of the post. The requested URL is /views-track?slug=xxx.

• If the request is successful, call insertViewsCount() to display the view count on the page.

  1. Insert View Count:

• Find the element where the corresponding post title (post-title) is located based on the slug.

• Insert a div element containing the view count into the outermost container (.post-preview) of that element.

  1. Cache Clearing Feature:

• Listen for the Ctrl + R (browser refresh) key event. When the user presses this key combination, reset the hasFetchedViews flag to false and reload the statistics.

Summary:

• This code ensures that the view count request is initiated only when the page is fully loaded through delayed execution, avoiding AJAX or cache interference.

• Through setTimeout and the hasFetchedViews flag, it ensures that the view count for each post is requested only once, avoiding duplicate requests.

• Added a manual cache clearing feature to improve user experience.

Note: get_permalink($post) is a WordPress function used to retrieve the specified post, page, or custom post type's Permalink, returning the full URL. The parameter $post can be a post ID or a WP_Post object; if omitted, it defaults to retrieving the link of the current post.


In the beginning, I was planning to use Zaraz's “Custom HTTP REQUEST” tool to trigger the statistics request. However, after testing, I found that for some unknown reason,“Custom HTTP REQUEST” does not work properly in the browser。

I suspect this is because Zaraz's request mechanism differs from a fetch() request sent directly by the browser. It might go through Cloudflare server processing or proxying, preventing the Worker from correctly identifying the request source. In addition, Zaraz's request execution logic is relatively closed, making it impossible to debug directly in the browser console or view request details, which also increases the difficulty of troubleshooting.

Therefore, the optimized solution was changed to have the JavaScript code on the WordPress page initiate the request directly, ensuring that every visit is counted correctly while making it easy to debug and monitor requests in the browser developer tools.


3 Worker Processes Slug and Updates View Count in KV

3.1 Process Brief

Worker (which can be namedviews-track) is specifically used to receive POST requests sent from WordPress and extract the post's slug. The URL route entry corresponding to this Worker is:blog.tangwudi.com/views-track* 。

Request Processing Logic

After receiving the request, the Worker willoperate directly in KV, following these rules:

  • If the slug already exists in KV, directly increment its corresponding view count by +1.
  • If the slug does not exist in KV, create a new record and set the initial value to 1 (i.e., starting from 0 and adding 1).

Doing so has the following advantages:Simple and efficient logic, avoiding extra query operations and improving performance;No dependency on the WordPress server, with all statistical data storage and updates completed on the Cloudflare KV side;No REST API or database queries required, and will not be affected by WAF restrictions or API access limits.

This implementation logic ensures thatregardless of whether the article slug already exists in KV, the view count can be successfully recorded, greatly simplifying the statistics logic while ensuring performance and scalability.


If considering it from a security perspective, the logic could be designed to be more complex, for example: regularly syncing WordPress article slugs to KV, and each time the Worker receives a slug, it first checks if it is a stored valid slug; if it exists, it increments by 1, otherwise it is discarded directly.

But in doing so, it might also involveAuthentication(such as using a signature mechanism to verify the request source). After tossing and turning through several versions, I almost broke down... and finally decidednot to over-engineer, and rely directly on WAF's managed rules, rate limiting, and Cloudflare's bot management to provide basic protection.

After all, this is just anarticle view count statistic, the data is not sensitive at all, and there is really no need to set up a bunch of complex security verification mechanisms for it. The effort is completely not worth it~


3.2 Practical Operation

Note: Since I have detailed the creation and configuration process of Workers in several of my previous articles, I won't record them one by one in this article, but only write down a few key points for creating a Worker.

The code for the Worker (views-track) is as follows:

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",
  };
}

You need to bind a KV (choose any name, here I use views_kv), and its corresponding variable name is ”views_kv” (the variable name is set when the Worker binds the KV, this cannot be chosen randomly); you need to set up a URL route, taking my blog as an example, its route is: ”blog.tangwudi.com/views-track*“, as shown in the figure below:

image.png

Note: Initially, the two functions of ”writing to KV” and “querying KV” were handled by two independent Workers. One was specifically responsible for receiving requests sent from WordPress and increasing the view count, while the other was dedicated to providing an API interface to return the view count of a certain article. Later, on second thought, there was absolutely no need to separate them. The two functional logics complement each other. Merging them into a single Worker not only reduces the number of Workers, simplifying deployment and management, but also avoids extra overhead such as repeatedly parsing requests and processing URL logic. Therefore, the final solution is to let the views-track Worker be responsible for both receiving POST requests to increment the view count corresponding to the slug by 1, and also responsible for handling GET requests to query the view count of a certain slug and return the result. This merging method not only makes the code more centralized and easier to maintain, but also reduces the number of Worker triggers, thereby optimizing the overall Cloudflare computing resource consumption.

4 WordPress Gets Post Slug, Queries View Count from Worker via JavaScript, and Displays It Locally

4.1 Process Brief

In addition to counting visits, the page also needs to display the article's view count. Therefore, the frontend JavaScript code needs to send a GET request to the Worker to retrieve the view data of the current article and dynamically insert it into the page.

The specific process is as follows:

  1. When a user visits the article detail page, the JavaScript code will extract the slug of the current article and then send a GET request tohttps://blog.tangwudi.com/views-track?slug=当前文章的slug .
  2. After the Worker receives the request, it will first check if the slug meets the format requirements, then read the article's view count from Cloudflare KV, and return it in JSON format, such as { “success”: true, “views”: 123 }.
  3. JavaScript parses the returned data, retrieves the value of views, and inserts it into an appropriate position on the page, such as below the article title, in the metadata area, or other custom locations.
  4. To ensure the real-time nature of the data, the request can be initiated immediately after the DOMContentLoaded event, and the view count display on the page can be updated after the asynchronous return.

In this way, the retrieval and display of the view count are completely handled by the frontend JavaScript, which will not affect the PHP execution of WordPress, nor will it be interfered with by CDN caching, ensuring accurate data and efficient loading.

In addition, when displaying the view count of articles, two situations need to be considered:

  1. Card-style article list on the blog homepage

In the article list on the homepage or category pages, each article is usually displayed in a card format with basic information such as title, summary, and publication date. If you want to display the view count here, directly using JavaScript to request data from the Worker one by one may lead to a large number of concurrent requests, affecting the loading speed. Therefore, several optimization schemes can be considered:

• Batch request data: If the Worker supports it, the frontend can request the view counts corresponding to multiple slugs at once, instead of requesting the data for each article individually. This is the method I used, as shown in the figure below:

image.png

• Backend rendering of cached data: You can have WordPress regularly retrieve view data via wp-cron and store it in the database, so that it can be displayed directly when the page renders, without requiring additional frontend requests.

• Local storage optimization: For articles that have already been visited, localStorage can be used to cache the view count, reducing duplicate requests.

  1. Article detail page

On the article detail page, obtaining the view count is relatively simple. It only requires JavaScript to send a GET request to the Worker to retrieve the view data corresponding to the current slug and update the display on the page. Since the request volume is small and real-time requirement is high, the request can be executed directly when the DOMContentLoaded event is triggered, ensuring that users can see the latest view count when they enter the article.

Overall, the article detail page can directly read real-time data from the Worker, while the homepage or article list page needs to optimize the request method to avoid slow loading speeds caused by too many requests.

4.2 Practical Operation

4.2.1 Displaying View Count on Blog Homepage

Insert the following code into functions.php (it is recommended to use a code snippets plugin to implement this):

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

When displaying article view counts on the homepage, the HTML structure of different WordPress themes may vary, especially inHow to get the post's slug and correctly query reading data in JavaScript codeaspects, where it is easy to run into pitfalls.

The problem I initially encountered was that the JavaScript code tried to use document.querySelectorAll(“.post-title a”) to get post links, but in the actual HTML structure, .post-title itself is a <a> tag, rather than a <div> 或 <h2> container wrapper like <a>, causing document.querySelectorAll(“.post-title a”) to directly return a null value.

  1. The post title structure of themes is different:

• If the post title (.post-title) is a <a> link inside the tag, use document.querySelectorAll(“.post-title a”) to get all post links.

• If .post-title itself is a <a> tag, directly use document.querySelectorAll(“.post-title”) to get it.

• It is recommended to first use the browser developer tools (F12) to inspect the HTML structureto ensure that the selector can accurately match the correct element.

  1. How to correctly extract the slug:

• Use get_permalink() to get the full post URL, and in JavaScript, you can extract the slug via new URL(url).pathname.

• If the theme does not directly output the slug on the homepage, you can output the slug to the HTML as data-slug=‘xxx’ via get_post_field(”post_name”, $post->ID) in functions.php, making it easier for JavaScript to read.

  1. Homepage loading methods affect the display of reading counts:

• Lazy loading and Ajax pagination: If the theme uses lazy loading or Ajax pagination, new posts may only appear dynamically after the initial page load. To ensure these posts are also correctly counted for views, you can use setTimeout to delay the execution of the request, ensuring the page elements are fully rendered before initiating the view count request.

• In this case, you can listen for events after the page has finished loading (such as DOMContentLoaded), while using setTimeout to delay execution to ensure the page is fully rendered before triggering the statistics request.

Summary

The homepage HTML structure of different WordPress themes may vary. When inserting JavaScript code to get post view counts,be sure to check the HTML structure first to ensure the selector is correct. Meanwhile,the way to get the slug may also need to be adjustedto adapt to different theme implementations.

Final effect display:

image.png

4.2.2 Displaying View Count on Post Detail Page

Insert the following code into functions.php (it is recommended to use a code snippets plugin to implement this):

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); ?>", {slug); ?>' 的阅读次数:", data.views);
                        insertViewsCount(data.views);
                    } else {
                        console.error("获取阅读次数失败:", data);
                    }
                })
                .catch(error => console.error("请求错误:", error));
            }, 500); // 500 毫秒延迟执行请求
        });

        function insertViewsCount(views) {
            let viewsText = `阅读次数:method: "GET",;

Compared to the code of the blog homepage, the structure of the post detail page is usually relatively stable, and the method of obtaining the slug (get_permalink($post)) will not change significantly due to different themes, sothe compatibility is relatively good. However, in different WordPress themes, there are still some points to note:


The position where JavaScript inserts the reading count may vary depending on the theme

let targetElement = document.querySelector(".post-meta");

• This code inserts the reading count into .post-meta (post meta information area) by default.

• Butthe post detail pages of different themes may not have the .post-meta class, or the class is located in a position where content cannot be inserted.

Solution:

targetElement = document.querySelector(".post-meta") || document.querySelector(".entry-content") || document.body;

• Prioritize .post-meta, followed by .entry-content (post content), and finally fall back to inserting into the body, to ensure the reading count is displayed correctly as much as possible.

• You can also manually check the theme's HTML structure to find the most suitable insertion position.


Adaptation of the post slug extraction method

$slug = basename(parse_url(get_permalink($post), PHP_URL_PATH));

• Under most themes, the URL structure returned by get_permalink($post) is consistent, and you can directly use parse_url() + basename() to extract the slug.

• But ifa custom permalink structure is used(如 /%category%/%postname%/),那么 basename() 可能无法准确获取 slug。

Solution:

You can change it to get post_name directly from the database to ensure the correctness of the slug:

$slug = get_post_field('post_name', $post->ID);

This methodDoes not depend on URL structure, compatible with all custom link formats。


The theme may already have a built-in view count function

Some WordPress themes (such as Astra, Newspaper) may come with a built-in view count statistics function and display it in the post meta information. In this case,it is necessary to avoid repeatedly addingthe view count, which causes UI conflicts.

Solution:

You can detect in functions.php whether the post-meta already has HTML markup related to the view count:

if (!document.querySelector(".post-meta .views-count")) {
    // 插入阅读次数
}

This avoids duplicate rendering.


Summary

• The code for the post detail page has better compatibility than the homepage code, but it still needs toadapt to slug extraction, the target element for inserting the view count, and the theme's built-in view count statistics。

• Insertion position: Ensure that .post-meta exists, otherwise an alternative solution should be provided.

• Method to get slug: If there are custom permalinks, it is recommended to use get_post_field(‘post_name’, $post->ID).

• Avoid duplicate statistics: If the theme already displays the view count, you can detect whether there is already relevant HTML markup in .post-meta.

Final effect display:

image.png

3 Summary

There are really too many WordPress themes. The general idea of the solution in this article is fine, but directly copying it for different themes probably won't work (except for the Argon theme). It can only serve as a reference, and achieving the final result will definitely require some tinkering.

However, once you succeed in tinkering with it, there will be many benefits, such asbeing able to uninstall WordPress statistics plugins, reducing the extra query burden on the server, thereby speeding up page loading and improving overall performance. Meanwhile, becausethe view count is stored in cloud storage like Cloudflare KV, even if the site is migrated, servers are changed, or even if you switch between primary and backup sites, the data will not be lost, and there is no need for additional local database synchronization or migration.

Overall, this solution is more suitable forwebmasters who want to reduce the burden on WordPress, optimize page performance, and hope that statistical data can be preserved for a long time, but when implementing it on different themes, you may need to adjust the HTML structure, CSS selectors, and slug extraction logic. The adaptation cost varies depending on the theme.


There were still some issues with this solution at the beginning. I found that during some routine operations (such as clicking links to different posts within the site to visit them), the view count statistics would not appear, but they would appear if the page was refreshed or reloaded. This phenomenon feels so familiar, almost as if AJAX、Instant.page or similar ”no-refresh loading” functions were used.

The core principle of this type of technology is tointercept the browser's default page navigation behavior, and then use JavaScript to asynchronously load the content of the new page and dynamically update the DOM, without actually triggering DOMContentLoaded or load events. Although this improves access speed, it also brings a problem—scripts will not execute again like they do during a normal page loadFor my view count statistics solution, this is exactly where the problem lies:

• When switching pages, WordPress does not re-execute the JavaScript code in wp_footer, so the fetch request will not be sent again, and naturally, the view count will not be updated.

• However, if you manually refresh the page, the entire wp_footer code will reload, and the statistics logic will work normally.

If you really want to solve this problem from a technical level, you can use MutationObserver 或 pjax or similar technologies to listen for DOM changes and manually execute the statistics logic when switching pages, ensuring that the view count can be correctly retrieved and displayed even under no-refresh transitions. Alternatively, for specific no-refresh technologies (such as Instant.page), you can use the event hooks they provide to manually trigger our statistics code after each page switch. These kinds of issues are actually very common when optimizing WordPress sites, such as lazy loading, preloading, and dynamic content replacement and other technologies will bring similar side effects. Therefore, when using these optimization methods, you also need to simultaneously consider the compatibility of data updates and script execution.

Note 1: I thought of a compromise, usingsetTimeout(function())to directly specify calling the function to display the view count after 500 milliseconds. This way, it won't be affected by ”no-refresh loading”. Isn't that genius? All the previous code has been optimized using this approach.

Note 2: But there is a price: I have to clear the APO cache and start over again... So annoying. I've been clearing the cache frequently lately, and every time I clear it, it has to be cached again, which really affects the visitor experience. The key point is, clearing HTML is fine, but why must it drag the image cache along to be cleared too? So painful. It seems I need to research a way to clear only the HTML content~.

Note 3: There still seems to be a bit of a problem, it only improved some of the problematic operation scenarios.


In addition, I strongly recommend again not to insert code directly into the functions.php file, but instead use a code snippets plugin to manage it, which will be more convenient for maintenance. The actual effect is as follows:

image.png

After finishing, I routinely tested it with PageSpeed Insights. The result was good, with the highest score reaching 90 for the first time:

image.png

image.png

Note: There was a lot of debugging in the browser developer tools along the way, but there's too much of it and I'm really too lazy to write it down. If you need to, just take the code and ask ChatGPT how to debug it.

View Related Categories · 3 Matches
📌 Content Structure Prompt:
This content is part of the following knowledge map, where you can view the complete content path: Cloudflare Learning Map · WordPress Learning Roadmap。
📎 Related Articles
Share this article
The blog content is original, please indicate the source when reposting! The RSS address of the blog is:https://blog.tangwudi.com/feed, welcome to subscribe; if needed, you can join theTelegram Groupto discuss questions together.

Comments

    • Owner
      ", it is recommended to disable the automatic plugin update feature on nodes other than the primary node. This is because after the primary node automatically upgrades and updates the plugins, the other nodes can actually upgrade the plugins through Syncthing's synchronization feature, although this synchronization might not be immediately perceived by local plugins (restarting WordPress on the other nodes will allow them to recognize it normally).
      Macintosh Chrome 134.0.0.0
      2 years ago
      2025-3-18 9:53:08

      It is indeed a good thing. I really don't want to add code to functions.php or other theme files anymore. It's so hard to manage, and I completely forget about it after a while.

  1. Jottings
    Windows Edge 134.0.0.0
    2 years ago
    2025-3-18 9:05:43

    Awesome, respect.

    • Owner
      Jottings
      Macintosh Chrome 134.0.0.0
      2 years ago
      2025-3-18 9:09:56

      Just messing around, I don't know programming and took a lot of detours~

Send Comment Edit Comment


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗ
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
Kaomoji
Emoji
Little Dinosaur
Flower!
Previous
Next
       

👋 Welcome to “Wudi's Personal Blog”

Here, long-term exploration is mainly carried out around the following directions:

🧱 Personal Digital Infrastructure and Blog System Construction
☁️ Cloudflare and Network Architecture Practice
🧠 AI and Knowledge System Exploration
🛡️ Network Security and Access Optimization
🎵 Music and Sound Cognition
👁️ Cognitive Perspectives and Worldviews