为 Hexo Fluid 接入 Umami 浏览量统计
本文最后更新于 2026年6月5日 晚上
前言
最近给博客浏览量统计又折腾了一圈。之前用过 LeanCloud,也写过一篇把 Fluid 接到 Waline 浏览量统计的文章,后来又看过 OpenKounter。几个方案都能用,但各自都有一点小尾巴。
LeanCloud 后面要停止对外提供服务,继续用的话迟早还要迁移。Waline 虽然能做文章浏览量,但全站 PV/UV 需要自己绕一层虚拟路径。OpenKounter 更偏向轻量计数,如果只是显示数字当然够用,但想看访问来源、设备、地区这些数据,就不太像一个完整统计后台了。
所以这次换成 Umami。它是一个开源的网站访问统计工具,支持自部署,也有 API,可以同时负责后台统计和 Fluid 前台显示 PV/UV。嗯,至少不用再一边补计数逻辑一边安慰自己“能跑就行”。
这篇文章记录一下:怎么用 Vercel + Neon 自部署 Umami,然后把它接到 Hexo Fluid 主题里,用来显示博客的全站 PV/UV 和文章阅读量。
准备工作
开始之前,先把需要的东西列一下:
- 一个 GitHub 账号,用来 Fork Umami 项目。
- 一个 Vercel 账号,用来部署 Umami。
- 一个 Neon PostgreSQL 数据库,可以直接通过 Vercel Storage 创建。
- 一个使用 Hexo Fluid 主题的博客。
Fluid 的 Umami 统计展示依赖自部署 Umami 的 API。如果只是想让 Umami 后台记录访问数据,只配置 tracking script 就行;但如果还想在博客页脚和文章页显示 PV/UV,就需要额外配置 website_id、token、api_server 等字段。
Fork Umami 项目
先打开 Umami 的 GitHub 仓库:
1 | |
点击右上角 Fork,把项目 Fork 到自己的 GitHub 账号下面。后面 Vercel 和 Netlify 都会从这个 Fork 后的仓库导入项目。
创建 Neon 数据库
数据库这里不用单独跑去 Neon 官网创建,直接在 Vercel 的 Storage 里新建一个 Neon 数据库就行。
- 打开 Vercel Dashboard。
- 进入
Storage。 - 点击
Create Database。 - 选择
Neon。 - 数据库名称可以填
blog-umami,名字填其他的也行。 - 创建完成后进入数据库详情,点击
Show secret显示隐藏的数据库信息。 - 找到连接信息,复制
DATABASE_URL。
DATABASE_URL 大概长这样:
1 | |
DATABASE_URL 是数据库连接密钥,不要写到博客文章、前端代码或者公开仓库里。后面只应该把它填到 Vercel / Netlify 项目的环境变量里。
部署 Umami 到 Vercel
数据库准备好后,就可以把 Umami 部署起来了。
- 回到 Vercel Dashboard。
- 点击
Add New...。 - 选择
Project。 - 导入刚刚 Fork 的
umami仓库。 - 在环境变量里添加:
1 | |
然后点击部署。第一次部署时,Umami 会初始化数据库表。部署完成后,Vercel 会给出一个访问地址,例如:
1 | |
这个地址后面会作为 Umami 后台地址,也会作为 Fluid 配置里的 api_server。先记下来,后面会反复用到。
Umami 官方也提供了安装、Vercel 部署和 Neon 部署说明,可以配合参考:
复用数据库部署到 Netlify
如果 Vercel 部署后访问不够稳定,也可以把同一个 Umami 项目再部署到 Netlify。这里不用重新建库,直接复用前面在 Vercel Storage / Neon 中创建好的 DATABASE_URL 就行。
进入 Netlify 控制台后:
- 点击
Add new project。 - 选择
Import a Git repository。 - 选择
GitHub。 - 选择前面 Fork 的
umami仓库。 - 在
Environment variables中点击Add environment variables。 - 选择
Add key/value pairs。 - 添加环境变量:
1 | |
然后点击 Deploy。部署完成后,Netlify 会给出一个访问地址,例如:
1 | |
因为 Netlify 这边复用的是同一个 Neon 数据库,所以 Umami 后台里的用户、Team、网站、统计数据都会和前面 Vercel 部署共用。换句话说,它只是多了一个服务入口,不是新开了一个 Umami 实例。
后面配置博客时,只需要把示例里的 Vercel 地址替换成 Netlify 地址即可:
1 | |
也就是:
src改成https://your-umami.netlify.app/script.jsapi_server改成https://your-umami.netlify.app- 获取 token 时的
/api/auth/login请求地址也换成 Netlify 地址
website_id 和账号信息来自同一个数据库,不需要重新创建。这个体验还是挺舒服的,至少不用再配一遍后台。
初始化 Umami 后台
接下来打开部署好的 Umami 地址:
1 | |
首次自部署 Umami 默认账号通常是:
1 | |
登录后第一件事就是修改管理员账号的用户名和密码。默认密码不要继续用,不然这个后台等于半开门,实在有点刺激。
然后新建一个专门给博客前端读取统计数据用的账号:
- 使用管理员账号进入 Umami 后台。
- 进入
Settings。 - 找到用户管理。
- 创建一个新账号。
- 角色选择
View only。
例如:
1 | |
这里建议单独创建 View only 账号,而不是直接把管理员 token 放到博客配置里。博客前端只需要读取统计数据,不应该拥有管理权限。
创建团队并加入账号
这一步比较容易漏。我一开始也以为只要账号能登录,API 就能读到网站数据,结果实际请求 /stats 时直接给了一个 401 Unauthorized。
原因是:新建的 View only 账号虽然有效,但没有目标网站的访问权限。
比较稳妥的做法是把网站放到 Team 里,然后让 View only 账号加入这个 Team。
管理员账号操作:
- 进入
Settings。 - 找到
Teams。 - 创建一个 Team,例如
team1。 - 复制 Team 的
Access code。
然后退出管理员账号,登录刚刚创建的 View only 账号:
- 进入
Settings。 - 找到
Teams。 - 点击
Join team。 - 输入刚才复制的
Access code。
加入成功后,这个 View only 账号就可以读取 Team 里的站点统计数据了。后面用它获取 token,权限就比较收敛。
添加博客网站
重新使用管理员账号进入 Umami 后台,并切换到刚刚创建的 Team。
然后添加博客网站:
- 进入
Websites。 - 点击
Add website。 Name填博客名称,例如My Blog。Domain填博客域名,例如your-blog.example.com。- 保存。
注意这里的域名不要带 https://,只填主机名就行。
获取 Tracking Code
网站创建完成后,进入网站设置,找到 Tracking code。它大概长这样:
1 | |
这里要复制两个值,后面会填到 _config.fluid.yml:
src:例如https://your-umami.vercel.app/script.jsdata-website-id:例如the-website-id-is-this
另外还需要记住 Umami 的部署地址:
1 | |
这个地址后面要填到 api_server。注意 api_server 不要带 /script.js,不然拼出来的 API 地址会不对。
如果使用的是 Netlify 部署地址,后面的示例命令和配置同理把 https://your-umami.vercel.app 换成 https://your-umami.netlify.app。
获取 View Only Token
接下来用刚刚创建的 View only 账号获取 API token。这个 token 是给 Fluid 前端请求统计数据用的。
1 | |
正常会返回类似下面的 JSON:
1 | |
把 token 的值复制下来,后面要填到 Fluid 配置里。
先别急着填配置,可以先验证 token 是否有效:
1 | |
如果想确认这个账号能不能访问目标网站,可以查询它能看到的网站列表:
1 | |
如果返回里看不到刚才创建的网站,说明 Team 权限还没配好。这个时候访问 stats 接口大概率会返回:
1 | |
配置 Fluid
后台和 token 都准备好后,就可以回到博客这边改配置了。打开 _config.fluid.yml,找到 web_analytics 里的 umami 配置。
示例:
1 | |
然后把页脚站点统计来源改成 umami:
1 | |
再把文章浏览量来源改成 umami:
1 | |
配置完成后重新生成并部署博客。如果一切正常,页脚会显示全站 PV/UV,文章页也会显示当前文章浏览量。
临时修复 Umami API 兼容问题
这里有个小坑。当前 Fluid 主题里的 themes/fluid/source/js/umami-view.js 仍然使用旧版 Umami API 写法。我已经提交了修复 PR:fluid-dev/hexo-theme-fluid#1249。如果主题还没有合并新版 API 的兼容修复,那么只改配置可能会在浏览器控制台看到类似这样的错误:
1 | |
原因是旧代码会读取 data.visitors.value 和 data.pageviews.value,但新版 Umami API 返回的是数字形式的 data.visitors 和 data.pageviews。另外,旧版页面过滤参数是 url,新版变成了 path。
所以在 Fluid 合并修复前,可以先手动修改主题文件:
1 | |
把这个文件替换成下面的兼容版本:
1 | |
这段代码会优先按新版 Umami API 使用 path 参数请求页面统计,如果失败或检测到旧版返回结构,再回退到 v2 的 url 参数。同时读取统计值时也兼容 data.pageviews 和 data.pageviews.value 两种格式。
等 Fluid 主题正式合并这个兼容修复后,就不用再手动替换这个文件了。
测试统计接口
先别急着看页面显示,可以先用 curl 测一下 Umami stats 接口。接口能通,前端显示才有继续排查的意义。
新版 Umami API 使用 path 过滤页面:
1 | |
如果要查全站统计,不带 path 参数即可:
1 | |
新版返回结果里,pageviews 和 visitors 通常就是数字:
1 | |
常见问题
API 返回 401
如果 /api/auth/verify 能返回用户信息,但 /api/websites/:websiteId/stats 返回 401,一般不是 token 失效,而是权限问题。
重点检查:
- token 是否来自
view-only账号。 - 这个账号是否加入了包含该网站的 Team。
- 网站是否创建在正确的 Team 下。
website_id是否复制错。
可以用下面命令确认账号能看到哪些网站:
1 | |
页面显示 TypeError
如果浏览器控制台出现类似错误:
1 | |
或者:
1 | |
多半是 Umami API 版本不兼容。旧版 Umami v2 的 stats 返回结构类似:
1 | |
新版 Umami API 返回的是数字:
1 | |
同时页面过滤参数也从旧版的 url 变成了新版的 path。如果使用的 Fluid 版本还没有兼容新版 Umami API,需要更新主题或按前面的方式修改 umami-view.js,让它同时兼容 v2 和新版 API。
相关文档:
Token 不要写进公开前端代码吗
Fluid 这里的 token 会被前端请求使用,所以它确实会出现在生成后的页面配置里。这个点绕不过去,毕竟前端要直接请求 Umami API。
这也是为什么前面建议创建单独的 view-only 账号,并且只给它读取对应 Team 网站统计数据的权限。不要使用管理员账号 token。
迁移 LeanCloud 历史浏览量到 Umami
如果之前已经用 Fluid 的 LeanCloud 计数统计跑了一段时间,直接切换到 Umami 后,历史浏览量不会自动过去。啊这,旧数据丢了总觉得有点膈应,所以这里顺手把迁移方法也记一下。
LeanCloud 和 Waline 这类浏览量统计,本质上更接近聚合计数:
- LeanCloud
Counter.target表示统计目标。 - LeanCloud
Counter.time表示访问次数。 - Waline
wl_counter.url表示统计目标。 - Waline
wl_counter.time表示访问次数。
但是 Umami 不只是一个计数表。它会把访问记录写到数据库中的事件表里,核心是:
"session":访问会话和访客信息。website_event:页面访问和事件记录。
所以迁移到 Umami 不能简单做 target -> url_path 的字段映射,而是要把旧的聚合计数展开成一条条 Umami pageview 事件。听起来麻烦一点,但写个转换脚本就还好。
先检查 LeanCloud 导出的 Counter.json。如果里面有文章路径,比如 /2024/06/14/example/,就可以迁移文章历史阅读量。如果里面只有 site-pv 和 site-uv,那就只能迁移全站 PV/UV,不能恢复每篇文章的历史阅读量。
例如我这份导出数据只有站点计数:
1 | |
这种情况下,迁移脚本会把 site-pv 导入为一个合成路径:
1 | |
这样可以让 Umami 的全站 PV 包含旧数据,但不会假装这些访问属于某一篇文章。
生成迁移 SQL
这里使用一个辅助脚本 leancloud-counter-to-umami-sql.js,可以通过下面的按钮下载:
下载后,把它和 LeanCloud 导出的 Counter_20260530_005938.json 放到同一个目录,然后运行转换命令:
1 | |
参数说明:
--website-id:Umami 网站 ID,也就是 tracking code 里的data-website-id。--hostname:博客域名,例如your-blog.example.com。--created-at:给旧数据使用的导入时间。--site-pv-path:当导出文件只有site-pv时使用的合成路径。--session-count:可选,用来控制导入时构造多少个历史 session;不填时优先使用site-uv。
生成的 SQL 会使用:
tag = 'legacy-leancloud'标记导入的 pageview。distinct_id LIKE 'legacy-leancloud-%'标记导入的 session。
这样重复导入时,SQL 可以先删除旧的 legacy 数据,再重新插入。万一第一次参数填错了,也不至于把数据库弄成一团糟。
导入到 Umami 数据库
如果可以在本地连接数据库,可以直接执行:
1 | |
如果使用的是 Vercel Storage / Neon,可以进入 Neon 的 SQL Editor,把 umami-legacy-pageviews.sql 的内容粘进去运行。
生成的 SQL 会使用 gen_random_uuid() 创建 UUID,所以会先执行:
1 | |
导入完成后,最后应该能看到类似结果:
1 | |
也可以手动检查:
1 | |
导入前建议先备份 Umami 数据库。虽然脚本只会清理带有 legacy-leancloud 标记的导入数据,但数据库迁移还是值得谨慎一点。
总结
到这里,Umami 自部署、后台初始化、Team 权限、网站添加、token 获取、Fluid 配置和历史 LeanCloud 数据迁移就都串起来了。
日常使用时,博客前端会加载 Umami 的 tracking script,Fluid 再通过 Umami API 读取统计值并显示到页脚和文章页。如果后面更换 Umami 部署域名,记得同时更新 src 和 api_server。这两个地方一个负责统计脚本,一个负责 API 请求,少改一个都会让人排查半天。