使用构建时数据和即时搜索重建我的静态博客
Source: Dev.to

静态站点本应快速、简洁且可靠。随着时间的推移,我的个人博客开始表现得像一个动态应用——运行时的 API 调用、到处都是分页逻辑,以及分散在各个平台的浏览量统计。
上周,我重新构建了 ravgeet.in(Nuxt.js)的博客部分,以彻底解决这些问题。最终的产出仍然是一个静态站点,但现在它感觉 活力十足:聚合的浏览量、即时搜索与排序,并且在运行时不依赖任何外部 API。
本文将详细介绍这次重构背后的思考、架构以及权衡取舍。
我的旧设置的问题
最初,我的博客是这样运作的:
- 博客内容存放在 Hashnode(权威来源)
- 部分文章也会同步发布到 Dev.to
- 页面在 运行时 使用 Hashnode 的 GraphQL API 获取博客数据
- 分页逻辑(
hasNextPage、光标)写在 UI 中
缺点
- 依赖实时 API 的静态站点感觉不妥
- 本地开发和构建速度慢且不稳定
- 添加搜索或排序等功能会需要更多的 API
我希望博客保持静态——但更智能。
构建时数据作为契约
将所有外部数据获取移至构建时,并将结果视为不可变的静态数据。
而不是在运行时获取博客,我引入了一个构建步骤,该步骤:
- 从 Hashnode 获取博客
- 从 Dev.to 获取文章
- 跨平台匹配相同的文章
- 汇总浏览次数
- 将所有内容写入单个 JSON 文件
在运行时,站点仅从该 JSON 读取。
Hashnode + Dev.to
↓
Build‑time fetch & normalize
↓
static/blogs.json
↓
Nuxt UI (search, sort, views)
Source: …
获取并聚合博客数据
Hashnode:规范内容
Hashnode 仍然是以下信息的唯一来源:
- 标题、slug、内容、标签
- 发布日期
- 封面图片
- 基础浏览次数
所有文章均通过 Hashnode 的 GraphQL API 获取,分页逻辑在 Node.js 脚本中处理。
Dev.to:分发与额外曝光
Dev.to 能带来额外的读者,因此忽略这些浏览量显得不妥。使用 Dev.to API(配合个人访问令牌),我会抓取我的所有文章并提取:
slugcanonical_urlpage_views_count
跨平台文章匹配
文章匹配采用分层策略:
- Slug 匹配
- Canonical URL 匹配
- 标题匹配
匹配成功后,最终的浏览次数计算为:
combinedViews = hashnodeViews + devtoViews;
每篇博客的输出包括:
- 合并后的浏览次数
- 各平台的浏览次数(用于调试)
- Dev.to 链接(若匹配成功)
编写静态数据合约
所有处理后的数据写入 static/blogs.json。该文件:
- 在构建时生成
- 被 Git 忽略
- 在应用中视为只读
文件中还会包含上次更新时间和博客总数等元数据,实质上取代了我原有的整个博客 API。
替换运行时 API 为静态服务
之前,services/blogs.js 会进行实时的 GraphQL 调用。重构后:
- 服务动态导入
blogs.json find、findOne和search在本地运行- 没有 Axios,没有分页状态,也没有网络故障
从 UI 的角度来看,未发生任何变化——但在内部,一切都变得可预测。
即时搜索与排序
所有博客数据均在本地,搜索变得非常简单。我添加了:
- 客户端文本搜索(标题、摘要、标签)
排序选项
- 发布日期(最近 / 最旧)
- 浏览次数(最多 / 最少)
因为数据集小且是静态的:
- 搜索结果即时返回
- 无需防抖处理
- 无加载状态
- 排序是确定性的
这大幅提升了可发现性,同时无需引入搜索服务。
权衡与经验教训
这种方法并不完美:
- 构建时间略有增加
- JSON 文件会随时间增长
- 不适用于实时分析
但对于个人博客来说,这些权衡是值得的。关键要点
- 静态并不等于无生气
- 构建时数据管道被低估了
- 一个简洁的数据契约可以简化 UI、UX 和性能
如果你感兴趣,完整实现位于 ravgeet.in repository。