给博客换了个主题:从 Hugo NexT 到自写的 Zhi

为什么换

这个博客之前用的 Hugo NexT,fork 了一份自己做修改。NexT 本身是个功能很全的主题,但在"自己改"这件事上,体验不太行。

问题集中在几件事上:

SCSS 套娃。101 个 SCSS 文件,三层目录嵌套。_common/components/post_common/components/third-party_common/outline/sidebar……改个样式得先搞清楚文件在哪、变量在哪定义、哪个 scheme 在 override。不是不能改,是改一次得翻半天。

四套布局方案。Gemini、Mist、Muse、Pisces,每套都是一套独立的 CSS。我只用 Gemini,但其他三套的代码还在那里,占了大量体积。

JS 耦合度高。9 个 JS 文件,next-boot.js 是总入口,pjax.js 做页面无刷新切换,config.js 读取主题配置。几个文件互相引用,想加个功能得理解整个启动流程。

改不动。主题是 fork 来的,上游更新了不敢合并——自己的改动散落在各个文件里,merge conflict 一堆。最后的结果就是 fork 彻底冻住,自己在一个固定版本上打补丁。

风格对不上。我有个工作室的官网,走的干净简约路线——大留白、无装饰、内容居中。NexT 是从 Hexo NexT 移植过来的,骨架里带着论坛/门户时代的审美:侧边栏信息密度高、配色选项多、组件叠加层次深。博客和工作室是同一个品牌,两个站放在一起看着像两个人做的,这事儿忍了很久。

总之,主题功能很多,但我只想改几个地方的时候,每次都要跟整个体系的复杂度搏斗。

换什么思路

想清楚了几个原则:

  • 不要 SCSS。纯 CSS + CSS Variables 足够搞定主题系统,不用多一层预处理
  • 不要构建工具。Hugo 自带 resources.Getminifyfingerprint 管线,webpack 和 vite 都不需要
  • 功能可开关。每个功能一个 feature flag,关了就不加载对应的 CSS 和 JS,不浪费字节
  • 文件要少。能一个 CSS 文件解决的事不要拆成五个

然后就开工了。整个主题从零开始,结合 AI 写代码,前前后后花了大约两周。

Zhi 主题做了什么

主题叫 Zhi。名字取自我名字中间那个字,也有几层意思:紫色的「紫」——主题的点缀色是紫色,干净但不冷淡;纸张的「纸」——博客本质就是一沓纸;文字的「字」——写的东西都在这里。「纸」和「字」,一个承载,一个表达,刚好是博客该有的样子。代码在 GitHub 上,MIT 协议。

核心数字对比

NexT (fixbyown)Zhi
总文件数285105
样式文件101 个 SCSS24 个 CSS
样式目录层级3 层嵌套1 层扁平
JS 文件913
HTML 模板7938
布局方案4 套(Gemini/Mist/Muse/Pisces)1 套
构建工具需要 SCSS 编译零构建,纯 Hugo Pipes

文件数砍了一半多,CSS 从 SCSS 预处理变成了纯 CSS,目录结构扁平化。JS 文件反而多了几个,因为 NexT 把逻辑揉在少数几个大文件里,Zhi 拆成了按功能独立的小文件——每个 JS 职责单一,改一个功能不影响其他的。

纯 Hugo Pipes,零构建

个人比较喜欢的一点是主题没有任何 npm 依赖,没有 package.json,不需要装 node_modules。

CSS 处理流程:

1
2
3
4
main.css(@import 聚合)
  → Hugo css.Build(开发时有 source map,生产时压缩)
  → fingerprint
  → 浏览器

JS 处理流程类似,resources.Getminifyfingerprint。全都是 Hugo 原生能力,不需要任何构建配置。

这意味着装完 Hugo 就能用,没有环境依赖地狱。

Feature Flags

每个功能都可以在 config.yaml 里独立开关:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
params:
  features:
    codeHighlight: true
    themeSwitch: true
    mathJax: true
    mermaid: true
    lightbox: true
    search: true
    sidebar: true
    toc: true
    readingProgress: true
    backToTop: true
    analytics: false

关掉的功能不会加载对应的 CSS 和 JS。实现方式是主题有个 features.html partial,把所有 flag 序列化成 JSON 塞到 <body>data-features 属性里,JS 读这个属性决定初始化哪些模块。

比如你博客不用数学公式,mathJax: false 一关,MathJax 的 JS 就完全不加载,省了几百 KB。

按需加载

两个比较重的外部库做了懒加载:

  • MathJax 3:只有页面内容包含 $...$$$...$$ 时才加载
  • Mermaid:只有存在 ```mermaid 代码块时才加载

大部分博客文章不需要这两个库,所以大部分页面不会加载它们。

暗/亮主题

CSS Variables 实现。theme.css 里定义了 :root(亮色)和 [data-theme="dark"](暗色)两套变量,其他所有 CSS 文件用变量引用颜色,不硬编码。

有个小细节:为了防止页面加载时闪烁(FOUC),在 <head> 里用一段内联 JS 在渲染之前就设置好 data-theme 属性。theme-toggle.js 负责切换,状态存在 localStorage 里,刷新不丢。

默认设置为 auto,跟随系统偏好。

代码渲染

代码块用 Hugo 内置的 Chroma 做语法高亮,通过 render hook 加了复制按钮和语言标签:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def fibonacci(n):
    """Generate Fibonacci sequence up to n terms."""
    a, b = 0, 1
    result = []
    for _ in range(n):
        result.append(a)
        a, b = b, a + b
    return result

print(fibonacci(10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

右上角一键复制,左上角显示语言名。Monokai 配色,行号默认开启。

公式渲染

MathJax 3 按需加载,页面里有 $...$$$...$$ 才会引入:

欧拉公式:$e^{i\pi} + 1 = 0$

贝叶斯定理:

$$ P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)} $$

支持行内和块级两种写法,暗色主题下公式颜色自动跟着变。

架构图渲染

Mermaid 也是按需加载的,用代码块写就行:

mermaid
graph LR
    A[用户请求] --> B{Nginx}
    B -->|静态资源| C[CDN]
    B -->|API 调用| D[Go 服务]
    D --> E[(PostgreSQL)]
    D --> F[(Redis)]
    C --> G[用户浏览器]

不用截图,不用画图工具,Markdown 里直接写。暗色模式下 Mermaid 自动切换为深色主题。

其他功能

  • 图片灯箱:点击文章图片全屏查看
  • 本地搜索:XML 索引 + 前端搜索,不依赖外部服务
  • 目录:支持自动编号和折叠,深度可配置
  • 阅读进度条:顶部细条,显示阅读进度
  • 回到顶部:浮动按钮,显示百分比
  • 捐赠:微信/支付宝二维码弹窗
  • 友链:独立的友链页面模板
  • 归档:按时间线展示所有文章
  • Creative Commons:文章底部版权声明
  • Shortcodesnote(提示框)、quote(引用块)、video(Bilibili/YouTube 嵌入,按时区自动切换)

国际化

i18n/en.tomli18n/zh.toml,模板里用 {{ i18n "key" }} 引用。加新语言只需要加一个 toml 文件。

怎么用的

博客的 config.yaml 里改一行就行:

yaml
1
theme: hugo-themes-zhi

主题作为 git submodule 引入:

bash
1
git submodule add https://github.com/mickeyzzc/hugo-theme-zhi.git themes/hugo-themes-zhi

所有配置集中在站点的 config.yaml 里,主题自己的 hugo.toml 只提供默认值。完整的配置参考看主题目录下的 exampleSite/hugo.toml,282 行,每个参数都有注释。

AI 在其中的角色

实话说,这个主题大部分代码是 AI 写的。架构和设计是我定的——不要 SCSS、不要构建工具、feature flags、文件怎么组织——但具体的 CSS 和 Go 模板代码,AI 写了绝大部分。

整个开发流程大概是这样:

  1. 我定一个功能的结构和交互方式
  2. AI 生成 CSS 和 HTML 模板
  3. 我看效果,调整细节
  4. 遇到 CSS 变量命名冲突或 JS 初始化顺序问题,AI 帮忙排查

比较复杂的部分是暗色主题的 FOUC 防止和视频 geo-switch 逻辑,这两块 AI 的第一次生成都不完全对,来回调了几轮才搞定。

整个主题从零到可上线,大概两周时间。纯手写的话,我估计得两个月以上——CSS 细节太多了,一个人搞不动。

总结

从 NexT 换到 Zhi,核心收益就三个:

维护成本降了。文件少了,结构扁平了,改一个功能只要动一两个文件。之前改 NexT 一个小样式得翻三四层目录找 SCSS 变量定义。

没有构建依赖了。装个 Hugo 就能跑,不碰 node_modules。之前本地开发还得确认 SCSS 编译器版本对不对。

功能可控了。每个功能一个开关,不用的直接关掉,页面轻了不少。

主题还在持续改进中,有问题或建议可以到 GitHub 提 issue。