
博客重构
一次从 Hexo 到 Next.js 的重构,用现代前端工程化的复杂性,换取了未来的低维护性、高性能和可扩展性。Next.js SSG + @content-collections + Cloudflare Pages 的组合,最终实现了在此前所期望的理想状态:将精力从折腾系统转移到内容输出上来。
一、从 Hexo 到 Next.js:拥抱现代 SSG
在 2017 年的博文中提到,博客的构建始终在「安装、配置、自定义、折腾、放弃」的循环中。在经历了 WordPress、Typecho 等动态系统,以及 GitHub Issues 等半成品方案后,我于 2019 年回归了 Hexo,选择了当时生态更成熟的传统静态博客生成器。
然而,随着时间推移,Hexo 在用户体验上的局限性逐渐显现——最主要的问题是页面跳转时的全局刷新。因此,在 2025 年初,开始了博客的第 N 次重构:转向基于 React 生态的 Next.js,并利用其强大的 SSG 能力,以求实现静态性能与动态体验的完美融合。
1.1 Next.js SSG 的核心优势:SPA 体验与可扩展性
选择 Next.js,意味着获得了兼顾静态部署和 SPA 体验的现代框架。
| 特性 | Next.js SSG | 传统 SSG (Hexo/Hugo) |
|---|---|---|
| 页面跳转 | 局部刷新 | 依赖主题实现,一般为全局刷新 |
| 可扩展性 | 易于集成 React 组件 | 依赖于模板引擎和插件生态 |
| 类型安全 | TypeScript & Zod | - |
1.2 托管环境升级:Cloudflare Pages 的性能与隐私
在托管服务上,也从 GitHub Pages 迁移到了 Cloudflare Pages。
相比于 GitHub Pages 必须要求公开仓库才能免费使用,Cloudflare Pages 提供了免费私有仓库托管的选择,更重要的是,Cloudflare 凭借其遍布全球的边缘网络节点,确保了博客的静态资产能够以极低延迟快速交付给全球用户,提供了卓越的访问速度。
二、数据与内容管理
重构的关键不在于前端框架,而在于如何高效、安全地管理博客内容。
2.1 内容数据源
在很早之前我就意识到备份的重要性,做到应备尽备,但是这也只是保障了数据不丢失,其实做不到如何找到一个想要的文件。
特别是针对文稿类数据,和其他富媒体混合在一起之后,因为文件体积小、可能存放在压缩包内、日期不一定对得上、文件命名无规则……
这就导致了一定需要手动去整理这些数据,因为有些内容需要手动回忆、补全,对于文稿类文件,它的创建、更新时间就一定是混乱的。
例如,数据可能来自于邮件、压缩包、富文本、电子文档、网页等,数据整理出来之后,可能就会发现,这些数据原本的时间就是和内容对不上的,需要手动修订时间。
由于 Hexo 默认读取 mtime 作为文章更新时间,导致更新时间变得一塌糊涂,在此之前必须手动指定更新时间。
现在我的文稿工具使用 Obsidian,它并不依赖文件更新时间,而且目前也没办法去保证这个时间的准确性。为了让数据在 macOS、iOS 和 Windows 上同步,同时使用了 iCloud、Git 以及自研的 Livesync 插件组合同步。这种复杂的同步机制,也会导致文件时间混乱。
2.2 数据处理
为了解决数据混乱和类型安全问题,我决定使用 Monorepo 架构,将数据处理逻辑隔离在一个单独的包中。
- 数据层隔离: 创建了一个名为
Vacuum的包,它专门负责处理数据的解析、验证和转换 - 工具选型: 尝试了对侵入性过强的
contentlayer2后,最终选择了@content-collections。它的实现更原始、更易于集成的
2.3 Vacuum 包核心实践
Vacuum 包的核心在于使用 Zod 来对 Markdown 文件的 Frontmatter 进行 Schema 定义和严格校验。
Zod Schema 定义:
import { defineCollection } from '@content-collections/core'
import { z } from 'zod'
export const post = z.object({
title: z.string(),
category: z.string(),
keywords: z.array(z.string()).optional(),
created_at: z.string(),
updated_at: z.string().optional(),
content: z.string(),
draft: z.boolean().optional(),
cover: z.string().optional(),
})通过使用 @content-collections 的钩子函数,在数据层完成了所有的数据清洗和派生,从而确保 Next.js 应用接收到的数据是即时可用且类型精确的:
transform钩子: 将created_at和updated_at从字符串类型安全地转换为Date对象,用于后续的排序和格式化add钩子: 根据文章的title和created_at字段,计算并生成唯一的slug字段,用于 Next.js 的动态路由/posts/[slug]
三、前端界面与核心功能实现
3.1 UI 框架选型:从 Atomic CSS 到 Mantine 的效率妥协
最初倾向于使用 Atomic CSS 库(如 StyleX 或 Tailwind CSS),以实现更细致的样式控制。然而,考虑到重构的首要目标是加速进度,最终还是选择了 Mantine。一方面是对它更为熟悉,另一方面它提供了一套完整的、预构建的 React 组件,虽然在 Next.js 的服务端/客户端组件边界上需要额外的处理,因为其 Context 依赖,但它极大地提高了 UI 搭建效率。
3.2 代码高亮与体积权衡:Shiki vs highlight.js
为了确保代码展示的质量,我必须权衡高亮引擎的性能与体积。
- Shiki: 提供了最高的渲染质量,但其巨大的打包体积严重拖慢了 Next.js 的构建速度和应用性能。
- highlight.js: 体积更小,性能更稳定。
最终,我选择了 highlight.js。同时,为了提高开发体验,通过实现 rehype 插件在 Markdown 解析阶段就将代码块的语言和文件名元数据提取出来,直接注入到 Mantine 的 @mantine/code-highlight 组件中。
3.3 复杂图表渲染:Mermaid 的静态预生成方案
对于流程图和时序图等,我使用 Mermaid 语法。
为了在静态博客中实现高质量的图表,我选择了 rehype-mermaid 插件,实现静态预生成 SVG。
- 挑战:
rehype-mermaid依赖playwright进行浏览器模拟渲染,而 Cloudflare Pages 的默认自动构建环境不支持。 - 解决方案: 关闭了 Cloudflare Pages 的自动构建,转而使用 GitHub Actions 接管整个 CI/CD 流程。在 Actions 中,可以自由配置
playwright环境,完成 Mermaid SVG 预生成,再将最终的静态产物部署到 Cloudflare Pages。
3.4 搜索功能:纯本地化搜索的性能与独立性
为了避免引入外部服务依赖,选择了纯本地化搜索方案。
在构建时,Vacuum 会生成一个包含文章关键信息的 JSON 索引文件。用户在访问搜索页面时,该索引会被加载到浏览器本地并缓存。所有搜索操作都直接在客户端本地进行,这在文章数量不大的情况下,能提供比网络调用后端搜索服务更快的响应速度,同时最大化了博客的独立性。
