统计页展示的是站点聚合数据,不展示访客个人信息,也不把第三方分析服务的任意接口暴露成公共代理。
页面分成两类数据
当前统计页主要展示:
- 概览 今日 PV、近 24 小时访客、近 24 小时 PV 和累计 PV。
- 访问趋势 PV、访客和会话的时间序列。
- 来源与内容 入口页面、来源网站、设备和浏览器等聚合排行。
- 访问热力图 按北京时间展示一天中的访问分布。
- 推送节奏 Vercel 生产部署历史的次数、间隔、连续推送和日期分布。
服务端统一调用固定的 Umami 分享接口,前端只请求博客自己的 /api/analytics:
浏览器 → /api/analytics → stats.moodlog.cn
这样做比让浏览器直接请求 Umami 更容易控制字段、错误和缓存。
默认显示近 24 小时
访问趋势现在默认使用 days=1,但这里的“1 天”不是自然日,而是:
当前时间 - 24 小时 ~ 当前时间
数据按北京时间的小时边界分桶,因此通常会有 25 个时间点:第一个和最后一个点可能是不完整小时。
24 小时范围跨越 25 个小时边界。保留首尾两个部分小时,可以让用户准确看到完整的滚动时间窗口,而不是悄悄丢掉边界数据。
接口返回粒度信息:
{
"days": 1,
"trendUnit": "hour",
"startAt": 1791967877075,
"endAt": 1792054277075,
"trend": []
}
选择 7 天、30 天或 90 天后,接口切换回按日统计。
按小时绘制趋势
趋势图的每个点包含三个值:
interface TrendPoint {
date: string
pageviews: number
visitors: number
sessions: number
}
PV 和会话可以从 Umami 的时间序列接口得到;访客需要按每个时间桶分别请求聚合统计,再合并到相同的小时桶中。
为了避免一次请求过多,访客统计按小批次执行:
for (let i = 0; i < buckets.length; i += 6) {
const rows = await Promise.all(
buckets.slice(i, i + 6).map(bucket => getStats(bucket)),
)
counts.push(...rows.map(row => row.visitors))
}
每小时访客是每个小时桶内独立去重的结果。一个访客在多个小时访问,可能在多个小时分别计数;它不能简单相加得到全天独立访客。
让坐标文字不跟着 SVG 缩小
早期版本把曲线、网格线和坐标文字全部放在固定 viewBox 中。手机上 SVG 等比例缩小时,坐标文字也会变小,日期还容易相互重叠。
现在的处理方式是:
- 曲线和网格线继续使用 SVG
- Y 轴文字改成普通 HTML 元素
- X 轴时间文字也改成 HTML 元素
- 坐标字号保持 13px
- 首尾刻度分别左对齐和右对齐
- 根据时间跨度减少中间刻度
.axis-x,
.axis-y {
font-size: 13px;
font-variant-numeric: tabular-nums;
}
这样文字不受 SVG viewBox 缩放影响,移动端更容易阅读。
推送节奏组件的移动端处理
推送组件包含三个指标:最近推送、累计推送和平均间隔。手机宽度有限时,如果三项强制并排,最近推送日期就会被省略。
响应式布局改成:
- 桌面端:三列
- 移动端:最近推送独占一行
- 移动端:累计推送和平均间隔放在下面两列
- 日期与时间拆成两行,不使用省略号
@container (max-width: 520px) {
.rhythm-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.rhythm-latest {
grid-column: 1 / -1;
}
}
不要把 Vercel Token 返回给浏览器
推送节奏使用 Vercel API 时,Token 只能在服务端环境变量中读取:
const token = process.env.VERCEL_TOKEN
接口只返回经过处理的公开数据,例如:
const release = {
total: 30,
latest: '2026-10-14T10:55:32.778Z',
recentInterval: 310037,
averageInterval: 500000,
streak: 3,
days: [],
}
永远不要把 Token 写进:
app/前端代码public/静态文件- Git 提交
- 浏览器 URL
- 错误响应
本地测试
node scripts/test-analytics.mjs node scripts/test-releases.mjs pnpm exec eslint app/pages/stats.vue app/components/analytics/Trend.vue pnpm build
接口验收可以直接检查默认响应:
curl -s https://your-domain.example/api/analytics | jq '{days, trendUnit, points: (.trend | length)}'
预期结果类似:
{
"days": 1,
"trendUnit": "hour",
"points": 25
}
小结
一个实用的统计页不只是把数字画出来,还要处理数据范围、时区、隐私、失败状态和小屏可读性。对个人博客来说,聚合数据已经足够回答几个重要问题:最近有没有人访问、哪些文章被看到、博客更新是否规律。
统计不是为了追逐数字,而是帮助我们知道哪些内容真的被读到,也帮助博客作者更有意识地持续写下去。

评论区
评论加载中...