两千访问量全是自己刷的:静态博客怎么彻底防住站长自己
两千访问量全是自己刷的:静态博客怎么彻底防住站长自己
起因:在给博客「关于我」页面增加阅读量与字数统计后,偶然注意到主站页脚的总访问量数字跳动极快。自己在几个主页面和文章之间来回点击切换调试,总访问量便以肉眼可见的速度不断自增——粗略估算,全站 6000 多的访问量中,至少有两千多次是站长自己日常高频调试与切页刷出来的“虚假繁荣”。 涉及系统:Vercount(
events.vercount.one全站统计)、Waline(Supabase PostgreSQL 文章阅读量)、Umami Cloud(客户端行为分析) 技术栈:Astro 5.x · ClientRouter (View Transitions) · TypeScript · Web Storage API (localStorage) · History API 核心结论:单纯依靠传统服务端的“IP 黑名单”在现代代理/梯子频繁漂移的环境下完全失效。通过拆解三套统计系统的底层通信协议,利用 Vercount 的隐藏GET只读接口、Waline 只读查询参数与 Umami 官方原生免追踪规范,配合浏览器本地同源持久化存储与动态 URL 抹除,构建了一套与网络 IP 彻底解绑、对普通访客 0 影响、对搜索引擎 SEO 零干扰的**「站长免记账模式(Zero-Tracking Mode)」**。
💡 速览:三套统计系统机制与改造方案对照
| 统计系统 | 统计作用域 | 默认累加机制 | 传统 IP 封禁为何失效 | 本地免记账改造方案 |
|---|---|---|---|---|
| Vercount | 全站级 site_pv 与 site_uv |
凡发送 POST 请求,服务端无条件对 site_pv 递增 +1 |
科学上网出口 IP 随节点漂移,宽带 CGNAT 动态分配导致 IP 随时变更 | 将请求降级为 GET 查询,只读取 site_pv/site_uv,不触发数据库累加 |
| Waline | 单文章/页面阅读量(time 字段) |
前端发送 POST /api/article 并携带 { action: 'inc' } |
每次新开标签页或不同会话均会发起递增请求 | 识别站长身份后跳过 action: 'inc',直接走 GET /api/article?type=time 纯只读拉取 |
| Umami | 访问趋势、来源与访客行为 | 客户端加载脚本后自动向云端分析上报每次 Pageview 事件 | IP 变动导致被识别为全新 Session 访客,产生脏数据 | 写入 Umami 官方标准标记 localStorage.setItem('umami.disabled', '1'),脚本主动休眠 |
一、 问题还原:两千次虚假访问量的诞生
1.1 从「关于我」增加阅读量与字数说起
在近期的博客功能完善中,作者对「关于我」(/about/)页面进行了重构。原本该页面为一个独立的一级页面,为了保持与全站博客文章一致的视觉规范与元数据展示,页面改用 src/layouts/BlogPost.astro 作为通用布局模板:
---
// src/pages/about.astro
import Layout from '../layouts/BlogPost.astro';
const wordCount = 2531;
const readingTime = Math.max(1, Math.ceil(wordCount / 350));
---
<Layout
title="关于我"
description="关于 eryuemu(二月木)..."
pubDate={new Date('2026-07-18T16:42:00+08:00')}
id="about"
wordCount={wordCount}
readingTime={readingTime}
>
<!-- 正文内容 -->
</Layout>
在接入之后,布局组件自带的字数统计、阅读时长和 Waline 阅读量计数器顺利生效。
1.2 异常现象:导航栏单页切页导致页脚计数疯狂自增
在完成上述页面改造后,作者在本地与生产环境反复测试页面跳转与布局表现。然而在测试过程中,注意到了一个反直觉的现象:
“就是 我自己本人在主页面的时候 切换导航栏 这个站最下面的总访问量一直在增加() 就是说 我单纯想着自己浏览调试的时候 不刷量 目前看来这得有两千是我刷的”
现象具体表现为:
- 打开博客首页,页脚显示总访问量例如为
6,450; - 点击顶部导航栏的「关于我」,页面平滑过渡,底部数字变为
6,451; - 点击导航栏的「项目」,数字变为
6,452; - 点击回「首页」,数字再次变为
6,453。
只要站长在站内进行正常的页面巡检、排版校对或文章编写预览,总访问量就会以极高的频率线性上涨。博客总访问量中充斥着站长本人制造的大量无效统计。
二、 为什么常规思路行不通?(三套机制的底层剖析)
面对自身流量污染统计数据的问题,初学者往往会第一时间想到“把站长自己的 IP 加进黑名单”或者“在统计后台过滤”。但在现代开发与网络环境下,这些常规方案无一例外会彻底失效。
2.1 【疑问/情况】:为什么不能直接在后台把自己的 IP 封禁/排除?
对话复盘:“话说应该不应该把自己的ip禁止统计了 但是这个代理ip平常一直变 不禁也没啥吧”
【底层技术原理】:IP 动态漂移与出口拓扑的不确定性
在服务端或统计面板中配置基于客户端 IP 的黑名单,存在两大无法克服的技术缺陷:
- 代理节点轮询与出口 IP 高频漂移: 开发与折腾过程中普遍依赖 Clash / 梯子等网络代理工具。无论是开启负载均衡(Load Balance)还是根据规则自动分流,网络请求经过的代理服务器出口 IP 随时都在香港、日本、美国、新加坡之间跳变。甚至短短 10 分钟内,同一个浏览器的出口 IP 就会更换数次。任何静态的 IP 黑名单规则在此场景下都毫无意义。
- 运营商级 NAT(CGNAT)与蜂窝网络动态分配: 在手机移动网络(4G/5G)或普通家用宽带环境下,宽带拨号每隔 24~48 小时被运营商强制重置分配新 IP;更关键的是,大量宽带处于运营商 CGNAT 网关内,数千个家庭共享同一个公网出口 IP。若强行封禁某个公网 IP,极有可能将处于同一网段的真实潜在访客一同误杀。
因此,试图在网络层(L3/L4 IP)解决应用层身份标识的问题,从网络拓扑上就是不可行的。
2.2 【疑问/情况】:为什么感觉 Umami、Waline 和 Vercount 的计数逻辑完全不一样?
对话复盘:“这个umami和网页文章不是一个计数方法好像”
【底层技术原理】:三套统计系统的数据模型与防抖边界
博客系统中同时存在三套统计体系,各自的职责边界、去重策略与数据模型截然不同:
┌────────────────────────────────────────────────────────────────────────┐
│ 博客前端 (Browser Client) │
└──────┬─────────────────────────┬───────────────────────────────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Vercount API │ │ Waline API │ │ Umami Cloud │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
[整站 PV/UV 累加] [单文章阅读量累加] [全站多维会话分析]
无条件 POST 递增 sessionStorage 防抖 客户端指纹 + Session
- Vercount(全站级简单计数器):
- 定位:公益极简访问量服务,类似经典的卜算子计数器。
- 机制:前端发送
POST https://events.vercount.one/api/v2/log,携带{ url, isNewUv }。只要服务端收到POST请求,其后端数据库中的site_pv计数器就会无脑递增 +1;仅在isNewUv: true时才累加site_uv。
- Waline(基于 Supabase PostgreSQL 的单页计数器):
- 定位:伴随评论系统的文章阅读计数。
- 机制:针对具体文章路径(如
/posts/my-post/),向服务端发送带有{ path, type: 'time', action: 'inc' }的POST请求。前端在BlogPost.astro中通过sessionStorage.getItem('waline_pv_' + path)做了会话级拦截,单次会话内反复刷新不会重复递增,但重新打开浏览器或不同标签页会再次递增。
- Umami(专业无 Cookie 隐私分析平台):
- 定位:全功能网站流量与转化率分析。
- 机制:不依赖 Cookie,而是根据客户端请求头中的 IP、User-Agent 以及主机名生成临时的每日哈希指纹,用于识别独立访客和会话(Session)。同一次会话中的连续点击会被归并为一个完整的用户访问旅程,而不是粗暴地累加独立访客数。
2.3 【疑问/情况】:为什么仅仅是在主页面切换导航栏,页脚的访问量就会不停上涨?
【底层技术原理】:Astro View Transitions 与无状态 POST 的叠加效应
该问题的根源在于现代前端静态站点生成器(SSG)的单页应用(SPA)路由导航事件与传统统计脚本之间的冲突:
Astro 在启用 <ClientRouter /> 后,站内所有链接的点击均被浏览器端 JavaScript 劫持,转由 HTML5 Fetch 与 DOM 交换完成页面无刷新过渡,并向全局 document 派发 astro:page-load 自定义生命周期事件。
在旧版 Footer.astro 的脚本中,为了确保页面跳转后页脚统计能够动态刷新,绑定了该事件:
// 旧版逻辑缺陷演示
function setupFooterStats() {
async function fetchStats() {
const statUrl = window.location.href.replace(/^https?:\/\/eryuemu\.com/, 'https://www.eryuemu.com');
// 缺陷所在:只要切页就无脑发起 POST 请求
const res = await fetch('https://events.vercount.one/api/v2/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: statUrl, isNewUv: isNewUv })
});
// ...
}
fetchStats();
}
// 每次点击导航栏切页,均会触发一次全量 POST 上报
document.addEventListener('astro:page-load', setupFooterStats);
由于 Vercount 后端的逻辑是**“见 POST 即累加”**,当站长在导航栏中从 首页 -> 归档 -> 项目 -> 关于我 点击一圈时,浏览器在几十秒内连续发出了 4 次 POST 请求,Vercount 忠实地将全站 site_pv 增加了 4。
三、 架构设计:构建「站长免记账模式」
既然网络层 IP 无法识别站长,服务端又缺乏传统后台的登录鉴权 Session,最佳的工程解决方案必须下沉到**客户端持久化存储(Browser Storage)**层。
实现一套合格的“免记账模式”,必须同时满足以下四大严苛要求:
- 网络无感:不受梯子、代理节点切换、断网重连与宽带变动的影响;
- 零权限泄露:不引入复杂的账号密码系统,不暴露任何私有数据库或管理权限;
- 零游客干扰:对普通读者 100% 透明,完全保留真实的访客统计能力;
- 零 SEO 风险:不破坏页面语义,不产生重复 URL,杜绝被搜索引擎判定为作弊伪装(Cloaking)。
flowchart TD
A["浏览器访问博客网址"] --> B{"URL 是否携带专属暗号?"}
B -- "携带 ?eryuemu0721" --> C["1. 写入 localStorage 标记"]
C --> D["2. history.replaceState 静默擦除 URL 参数"]
D --> E["3. 激活站长免记账上下文"]
B -- "不携带参数" --> F{"localStorage 是否存在站长标记?"}
E --> G["站长分支 (纯只读不增量)"]
F -- "存在 (站长本人设备)" --> G
F -- "不存在 (普通真实访客)" --> H["访客分支 (正常上报统计)"]
G --> I["Vercount: 发送 GET 请求纯读数据"]
G --> J["Waline: 跳过 POST inc, 仅 GET 当前阅读量"]
G --> K["Umami: 命中 umami.disabled, 脚本自动休眠"]
H --> L["Vercount: POST 递增 site_pv/site_uv"]
H --> M["Waline: POST inc 递增阅读量"]
H --> N["Umami: 正常采集访客与页面行为"]
3.1 突破点:挖掘 Vercount API 的隐藏 GET 只读接口
要让页脚能够正常显示当前的访问量与访客数,同时又绝不增加统计,核心在于统计接口是否支持纯读模式。
通过对 Vercount 服务端接口协议进行逆向抓包与终端验证测试:
# 1. 验证 POST 请求(默认行为):返回数据,且后端内部执行 site_pv = site_pv + 1
curl -X POST https://events.vercount.one/api/v2/log \
-H "Content-Type: application/json" \
-d '{"url":"https://www.eryuemu.com","isNewUv":false}'
# 2. 验证 GET 请求(隐藏只读模式):
curl -s "https://events.vercount.one/api/v2/log?url=https%3A%2F%2Fwww.eryuemu.com"
终端返回 JSON 数据:
{
"status": "success",
"message": "Data retrieved successfully",
"data": {
"site_uv": 1023,
"site_pv": 6458,
"page_pv": 2433
}
}
多次连续执行上述 GET 请求,返回的 site_pv 稳定维持在 6458 不动,未发生任何增量!这证明了:当使用 HTTP GET 查询带参 URL 时,Vercount 仅执行只读查询,完美契合站长免记账需求。
3.2 Waline 与 Umami 的免追踪对接
- Waline 只读查询:
在
BlogPost.astro中,普通访客走的是POST /api/article(携带action: 'inc');而站长身份被识别后,直接绕过该分支,仅调用GET /api/article?path=...&type=time,只拉取当前存储的计数值展示在文章侧边栏与头部。 - Umami 官方原生免追踪规范:
查阅 Umami 官方开发者文档,Umami 追踪脚本内置了对本地存储的检测规则:只要宿主环境的
localStorage中存在名为umami.disabled且值为'1'的项,追踪脚本就会在初始化时直接退出,不发送任何事件上报。
四、 用户核心疑问深度技术解析
在方案设计与实现过程中,围绕该机制的底层原理、安全性与边界条件,产生了一系列极具技术代表性的关键探讨。以下按照真实提炼出的疑问展开逐一解析。
4.1 【疑问/情况】:这套机制到底在我的电脑上做了什么?修改了系统文件吗?
对话复盘:“啥意思 ? 没听懂 只在我这个电脑上做了什么”
【底层技术原理】:操作系统零侵入与客户端运行时沙箱
该方案没有对本地电脑操作系统进行任何修改:
- 无系统与网络改动:没有修改
/etc/hosts,没有修改注册表,没有安装任何后台进程,也没有修改 Clash 代理规则。 - 纯代码逻辑部署:所有代码变更均位于博客源代码仓库中,经构建后部署在 Vercel 静态 CDN 边缘节点。
- 数据存储宿主为浏览器沙箱:当且仅当站长在浏览器中输入特定参数时,页面中运行的内联 JavaScript 脚本调用了 W3C 标准的浏览器本地存储接口(
window.localStorage.setItem()),在浏览器的用户配置文件(Profile)目录下写入了一条键值对数据。数据完全受限于浏览器的应用沙箱之内。
4.2 【疑问/情况】:这会影响正常游客的计数与浏览吗?
对话复盘:“我草这是什么原理 不影响游客计数浏览吧”
【底层技术原理】:同源数据隔离与条件分支短路
对于互联网上的普通读者,其访问链路完全不受影响:
- 数据状态隔离:普通访客访问的是干净的官方域名(
https://eryuemu.com/),其本地环境从未写入过eryuemu_is_owner标识; - 运行时分支命中:前端代码在执行统计上报前执行布尔运算判定:
对普通访客而言,该表达式恒为const isOwner = typeof localStorage !== 'undefined' && localStorage.getItem('eryuemu_is_owner') === '1';false; - 统计链路原样执行:访客的浏览器继续向 Vercount 发送
POST增量请求,向 Waline 发送inc增量请求,Umami 脚本正常工作。整个渲染树与 DOM 结构对普通读者无任何区别。
4.3 【疑问/情况】:这会对网站的 SEO 和搜索引擎爬虫产生负面影响吗?
对话复盘:“再就是 不影响seo吧”
【底层技术原理】:爬虫无态性、标准规范链接与杜绝 Cloaking
对搜索引擎优化(SEO)的影响为 0,原因由底层爬虫运行机制决定:
- 搜索引擎爬虫(Googlebot / Bingbot)不带参数:
搜索引擎蜘蛛在抓取网页时,遵循 Sitemap 与全站内链抓取,请求的 URL 始终为标准的规范路径(如
/about/),绝不会拼接站长的暗号参数。 - 爬虫无持久化会话(Stateless Client):
即使现代 Googlebot 具备无头浏览器(Headless Chromium)渲染能力,爬虫在每次抓取请求后都会销毁沙箱环境,绝不跨请求持久化存储
localStorage数据。 - 规范链接(Canonical URL)强约束:
在 HTML 头部,
<link rel="canonical" href="..." />永远指向服务端计算出的纯净路径。 - 拒绝作弊伪装(No Cloaking): 搜索引擎重点惩罚的是“向爬虫返回一套内容、向人类返回另一套欺诈内容”的 Cloaking 作弊手法。而本方案中,服务端向所有人下发的 HTML 源码、Meta 标签、结构化数据(JSON-LD)100% 保持严格同构,仅在客户端执行异步请求时做只读/写入路由,完全符合搜索引擎质量准则。
4.4 【疑问/情况】:如果别人也知道了这个暗号,他们也能复刻吗?会不会有权限安全风险?
对话复盘:“也就是说存浏览器里面了什么东西 如果其他人知道的话 按理说他们也能复刻”
【底层技术原理】:鉴权认证(Authentication)与客户端免追踪(Opt-out)的本质区别
站长敏锐地指出了客户端标记的共享风险,但该机制在安全层面上是绝对安全的:
- 零权限赋予:
eryuemu_is_owner只是一个客户端自愿放弃被统计的开关(Client-side Opt-out Flag),而非服务端授权令牌(如 JWT Token、OAuth 凭据或 Admin Password)。它不赋予持有者任何管理后台权限、无法篡改任何文章数据、无法删除评论,更无法访问 Supabase 数据库。 - 复刻的唯一后果: 若有其他访客好奇地在浏览器中输入了该暗号,其产生的唯一影响是:该访客今后浏览本博客时,也不会被计入总访问量。这与用户在浏览器中安装了 uBlock Origin、AdGuard 或开启隐私浏览模式产生的效果完全一致,对站长与网站系统没有任何破坏性。
4.5 【疑问/情况】:暗号名字有什么要求?能存在多久?底层存储原理是什么?
对话复盘:“可以怎么设置? 名字有什么要求 再就是 这个可以持续存在多久 再就是 这个是什么存储的原理 简单说说”
【底层技术原理】:URL 规范、存储生命周期与 HTML5 Web Storage 机制
针对这四个连环技术发问,从底层规范展开解析:
1. URL Query 参数命名规范
URL 问号后面的参数属于 RFC 3986 规范中的 Query 组件:
- 安全字符集:推荐使用英文字母(
a-z,A-Z)、阿拉伯数字(0-9)、下划线_和连字符-; - 禁止字符:避免包含空格、中文以及保留控制符(如
#代表锚点,&代表参数分隔符,?代表查询开始),若包含必须经过 URL Percent-Encoding(百分号编码)。
2. 生命周期与持久化时效对比
在现代浏览器中,主要存储方案的时效边界如下:
| 存储类型 | 数据持久化位置 | 过期时间(Lifecycle) | 跨标签页共享 | 随网络请求传输 |
|---|---|---|---|---|
| SessionStorage | 内存 | 标签页关闭即销毁 | 否(仅限同标签页) | 否 |
| Cookie | 磁盘文件 | 由 Expires / Max-Age 指定(通常有期) |
是 | 是(每次 HTTP 均携带) |
| LocalStorage | 磁盘数据库 | 永久有效(除非主动清除) | 是 | 否(纯本地静默) |
因为选用了 localStorage,一旦存入,即便电脑关机、浏览器重启、节点反复切换,标记依然永久驻留。只有在以下三种特定场景下失效:
- 站长在浏览器设置中执行了“清除浏览数据 / 清理 Cookie 与网站数据”;
- 运行在无痕窗口 / 隐私模式下(窗口关闭后沙箱全量丢弃);
- 更换了全新的物理设备或启用了新的独立浏览器配置文件。
3. 底层存储与安全隔离机制
- 磁盘物理载体:在 Chromium 内核(Chrome、Edge)中,
localStorage物理保存在用户数据目录下的Local Storage/leveldb/数据库中;在 Gecko 内核(Firefox)中,保存在webappsstore.sqlite文件中。 - 同源策略(Same-Origin Policy)强隔离:浏览器按照协议、域名、端口三元组(
https+eryuemu.com+443)开辟沙箱。任何第三方网站(例如百度、Google)的前端 JavaScript 脚本均绝对无法跨域读取eryuemu.com沙箱内的任何键值数据。 - 零网络开销:传统 Cookie 无论资源是否需要,每次请求静态图片、CSS 都会在 Request Header 中携带,浪费上行带宽;
localStorage纯粹存放在本地,除非脚本主动读取,否则不会产生任何多余的网络字节。
五、 落地实装:定制专属暗号 eryuemu0721 与完整代码
根据需求,将通用触发参数收敛为站长专属暗号 eryuemu0721,并在全站组件中完成闭环适配。
5.1 全局检测与静默抹除:src/components/BaseHead.astro
在全站通用的 <head> 区域注入一段轻量自执行脚本,负责暗号捕获、状态存取与 URL 痕迹清理:
<!-- src/components/BaseHead.astro -->
<!-- Admin / Owner zero-tracking mode detector -->
<script is:inline>
try {
const params = new URLSearchParams(window.location.search);
const val = params.get('eryuemu0721');
const isTurnOff = val === '0' || val === 'off';
const isTurnOn = (params.has('eryuemu0721') && !isTurnOff) ||
params.get('token') === 'eryuemu0721' ||
params.get('secret') === 'eryuemu0721';
if (isTurnOn) {
localStorage.setItem('eryuemu_is_owner', '1');
localStorage.setItem('umami.disabled', '1');
console.log('🛡️ [站长免记账模式] 已开启:全站统计(Vercount/Umami/Waline)均处于纯只读状态,绝不增加自身访问!');
params.delete('eryuemu0721');
params.delete('token');
params.delete('secret');
const cleanSearch = params.toString() ? '?' + params.toString() : '';
// 利用 HTML5 History API 在不刷新页面的前提下抹除地址栏中的暗号参数
window.history.replaceState({}, '', window.location.pathname + cleanSearch + window.location.hash);
} else if (isTurnOff) {
localStorage.removeItem('eryuemu_is_owner');
localStorage.removeItem('umami.disabled');
console.log('🔄 [站长免记账模式] 已关闭:恢复正常访客统计。');
params.delete('eryuemu0721');
const cleanSearch = params.toString() ? '?' + params.toString() : '';
window.history.replaceState({}, '', window.location.pathname + cleanSearch + window.location.hash);
}
} catch (e) {}
</script>
设计亮点:通过
window.history.replaceState,当站长在地址栏键入https://eryuemu.com/?eryuemu0721并回车后,参数在毫秒内被读取并写入磁盘,随后地址栏中的?eryuemu0721瞬间被擦除,保持 URL 干净,避免站长在后续复制分享链接时误将暗号泄露给他人。
5.2 页脚访问量纯只读改造:src/components/Footer.astro
在页脚统计中引入只读分支逻辑。当检测到站长身份时,将原本的 POST 上报切换为隐藏的带参 GET 接口:
// src/components/Footer.astro (片段)
async function fetchStats() {
const hostname = window.location.hostname;
// 本地开发环境不统计,避免污染生产数据
if (hostname === 'localhost' || hostname === '127.0.0.1') {
const visitorsEl = document.getElementById('visitors-count');
const viewsEl = document.getElementById('views-count');
if (visitorsEl) visitorsEl.innerText = '1 (本地)';
if (viewsEl) viewsEl.innerText = '1 (本地)';
return;
}
let isOwner = false;
try {
isOwner = typeof localStorage !== 'undefined' && localStorage.getItem('eryuemu_is_owner') === '1';
} catch (e) {}
const hasVisited = checkHasVisited();
// 若为站长,强行将 isNewUv 置为 false,避免消耗 UV
const isNewUv = !hasVisited && !isOwner;
if (isNewUv) {
markAsVisited();
}
const statUrl = window.location.href.replace(/^https?:\/\/eryuemu\.com/, 'https://www.eryuemu.com');
try {
let res: Response;
if (isOwner) {
// 站长免记账模式:使用 GET 请求仅获取当前数据,完全不增加 site_pv 和 site_uv
res = await fetch(`${API_URL}?url=${encodeURIComponent(statUrl)}`);
} else {
// 真实访客模式:正常 POST 累加
res = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: statUrl,
isNewUv: isNewUv
})
});
}
if (res.ok) {
const json = await res.json();
const data = json.data || json;
const siteUv = Number(data.site_uv || 0);
const sitePv = Number(data.site_pv || 0);
const visitorsEl = document.getElementById('visitors-count');
const viewsEl = document.getElementById('views-count');
if (visitorsEl) visitorsEl.innerText = siteUv.toLocaleString();
if (viewsEl) {
viewsEl.innerText = sitePv.toLocaleString();
// 站长模式下赋予原生 title 提示,不破坏既有 UI 视觉
if (isOwner) {
viewsEl.title = '站长免记账模式 (仅读取不增量)';
}
}
}
} catch (err) {
console.error('Failed to fetch stats:', err);
}
}
5.3 文章阅读量纯只读改造:src/layouts/BlogPost.astro
在文章详情与「关于我」通用的布局模板中,对 Waline 阅读量计数逻辑实施同样的只读降级:
// src/layouts/BlogPost.astro (片段)
async function updatePageviews() {
const els = document.querySelectorAll<HTMLElement>('.waline-pageview-count');
if (els.length === 0 || !WALINE_SERVER_URL) return;
const rawPath = window.location.pathname;
const currentPath = rawPath.replace(/\/+$/, '') || '/';
const sessionKey = `waline_pv_${currentPath}`;
const alreadyViewed = sessionStorage.getItem(sessionKey);
let isOwner = false;
try {
isOwner = typeof localStorage !== 'undefined' && localStorage.getItem('eryuemu_is_owner') === '1';
} catch (e) {}
try {
let count: number | null = null;
// 仅当非站长且未在当前会话浏览过时,才发起 POST inc 递增请求
if (!alreadyViewed && !isOwner) {
const res = await fetch(`${WALINE_SERVER_URL.replace(/\/+$/, '')}/api/article`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentPath, type: 'time', action: 'inc' }),
});
if (res.ok) {
const data = await res.json();
if (data?.data?.[0]?.time !== undefined) {
count = data.data[0].time;
sessionStorage.setItem(sessionKey, '1');
}
}
}
// 站长模式、重复切页或降级读取:执行纯 GET 接口获取当前阅读量
if (count === null) {
const res = await fetch(
`${WALINE_SERVER_URL.replace(/\/+$/, '')}/api/article?path=${encodeURIComponent(currentPath)}&type=time`
);
if (res.ok) {
const data = await res.json();
if (data?.data?.[0]?.time !== undefined) {
count = data.data[0].time;
}
}
}
if (count !== null) {
const formatted = count.toLocaleString();
els.forEach((el) => {
const elPath = el.getAttribute('data-path');
if (!elPath || elPath === currentPath) {
el.textContent = formatted;
}
});
}
} catch (err) {
console.warn('Failed to load pageviews from Waline:', err);
}
}
六、 验证与日常使用指南
系统构建完成并推送到 GitHub 触发 Vercel 自动部署后,整套机制在生产环境中完全闭环。
1. 站长日常启用方式
在个人主力设备(PC 端 Chrome / Edge / Firefox 以及手机端移动浏览器)的地址栏中访问一次:
https://eryuemu.com/?eryuemu0721
直接访问带值参数 https://eryuemu.com/?eryuemu0721=1 也完全支持。
- 预期验证现象:
- 页面加载完成瞬间,地址栏后缀自动还原为
https://eryuemu.com/; - 打开浏览器 DevTools 控制台,可看到日志输出:
🛡️ [站长免记账模式] 已开启:全站统计(Vercount/Umami/Waline)均处于纯只读状态,绝不增加自身访问!; - 在站内高频切换各个页面,页脚总访问量数字与网络面板中 Waline 请求均保持恒定只读,数字不再异常跳动;
- 鼠标悬停在页脚总访问量数字上,浮现浏览器原生气泡:
站长免记账模式 (仅读取不增量)。
- 页面加载完成瞬间,地址栏后缀自动还原为
2. 模式重置/退出(备用)
若后续需要测试真实访客的上报链路,可在浏览器中访问:
https://eryuemu.com/?eryuemu0721=0
也可以访问 https://eryuemu.com/?eryuemu0721=off,控制台将输出恢复提示,并清除磁盘中的站长标记。
