博客重构

一次从 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 定义:

typescript
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 应用接收到的数据是即时可用且类型精确的:

  1. transform 钩子: 将 created_at 和 updated_at 从字符串类型安全地转换为 Date 对象,用于后续的排序和格式化
  2. 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 语法。

Markdown SourceContent CollectionsVacuumType-safe Post DataNext.js BuildGitHub Actions CI/CDMermaid SVGPre-generationCloudflare PagesDeployment

为了在静态博客中实现高质量的图表,我选择了 rehype-mermaid 插件,实现静态预生成 SVG。

  • 挑战: rehype-mermaid 依赖 playwright 进行浏览器模拟渲染,而 Cloudflare Pages 的默认自动构建环境不支持。
  • 解决方案: 关闭了 Cloudflare Pages 的自动构建,转而使用 GitHub Actions 接管整个 CI/CD 流程。在 Actions 中,可以自由配置 playwright 环境,完成 Mermaid SVG 预生成,再将最终的静态产物部署到 Cloudflare Pages。

3.4 搜索功能:纯本地化搜索的性能与独立性

为了避免引入外部服务依赖,选择了纯本地化搜索方案。

在构建时,Vacuum 会生成一个包含文章关键信息的 JSON 索引文件。用户在访问搜索页面时,该索引会被加载到浏览器本地并缓存。所有搜索操作都直接在客户端本地进行,这在文章数量不大的情况下,能提供比网络调用后端搜索服务更快的响应速度,同时最大化了博客的独立性。

作者Aoang
发布于2024-04-01
更新于2025-12-16
许可协议CC BY-NC-SA 4.0
转载或引用本文时请遵守许可协议,并注明出处。