给博客换了个主题:从 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.Get→minify→fingerprint管线,webpack 和 vite 都不需要 - 功能可开关。每个功能一个 feature flag,关了就不加载对应的 CSS 和 JS,不浪费字节
- 文件要少。能一个 CSS 文件解决的事不要拆成五个
然后就开工了。整个主题从零开始,结合 AI 写代码,前前后后花了大约两周。
Zhi 主题做了什么
主题叫 Zhi。名字取自我名字中间那个字,也有几层意思:紫色的「紫」——主题的点缀色是紫色,干净但不冷淡;纸张的「纸」——博客本质就是一沓纸;文字的「字」——写的东西都在这里。「纸」和「字」,一个承载,一个表达,刚好是博客该有的样子。代码在 GitHub 上,MIT 协议。
核心数字对比
| NexT (fixbyown) | Zhi | |
|---|---|---|
| 总文件数 | 285 | 105 |
| 样式文件 | 101 个 SCSS | 24 个 CSS |
| 样式目录层级 | 3 层嵌套 | 1 层扁平 |
| JS 文件 | 9 | 13 |
| HTML 模板 | 79 | 38 |
| 布局方案 | 4 套(Gemini/Mist/Muse/Pisces) | 1 套 |
| 构建工具 | 需要 SCSS 编译 | 零构建,纯 Hugo Pipes |
文件数砍了一半多,CSS 从 SCSS 预处理变成了纯 CSS,目录结构扁平化。JS 文件反而多了几个,因为 NexT 把逻辑揉在少数几个大文件里,Zhi 拆成了按功能独立的小文件——每个 JS 职责单一,改一个功能不影响其他的。
纯 Hugo Pipes,零构建
个人比较喜欢的一点是主题没有任何 npm 依赖,没有 package.json,不需要装 node_modules。
CSS 处理流程:
| |
JS 处理流程类似,resources.Get → minify → fingerprint。全都是 Hugo 原生能力,不需要任何构建配置。
这意味着装完 Hugo 就能用,没有环境依赖地狱。
Feature Flags
每个功能都可以在 config.yaml 里独立开关:
| |
关掉的功能不会加载对应的 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 加了复制按钮和语言标签:
| |
右上角一键复制,左上角显示语言名。Monokai 配色,行号默认开启。
公式渲染
MathJax 3 按需加载,页面里有 $...$ 或 $$...$$ 才会引入:
欧拉公式:$e^{i\pi} + 1 = 0$
贝叶斯定理:
$$ P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)} $$
支持行内和块级两种写法,暗色主题下公式颜色自动跟着变。
架构图渲染
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:文章底部版权声明
- Shortcodes:
note(提示框)、quote(引用块)、video(Bilibili/YouTube 嵌入,按时区自动切换)
国际化
i18n/en.toml 和 i18n/zh.toml,模板里用 {{ i18n "key" }} 引用。加新语言只需要加一个 toml 文件。
怎么用的
博客的 config.yaml 里改一行就行:
| |
主题作为 git submodule 引入:
| |
所有配置集中在站点的 config.yaml 里,主题自己的 hugo.toml 只提供默认值。完整的配置参考看主题目录下的 exampleSite/hugo.toml,282 行,每个参数都有注释。
AI 在其中的角色
实话说,这个主题大部分代码是 AI 写的。架构和设计是我定的——不要 SCSS、不要构建工具、feature flags、文件怎么组织——但具体的 CSS 和 Go 模板代码,AI 写了绝大部分。
整个开发流程大概是这样:
- 我定一个功能的结构和交互方式
- AI 生成 CSS 和 HTML 模板
- 我看效果,调整细节
- 遇到 CSS 变量命名冲突或 JS 初始化顺序问题,AI 帮忙排查
比较复杂的部分是暗色主题的 FOUC 防止和视频 geo-switch 逻辑,这两块 AI 的第一次生成都不完全对,来回调了几轮才搞定。
整个主题从零到可上线,大概两周时间。纯手写的话,我估计得两个月以上——CSS 细节太多了,一个人搞不动。
总结
从 NexT 换到 Zhi,核心收益就三个:
维护成本降了。文件少了,结构扁平了,改一个功能只要动一两个文件。之前改 NexT 一个小样式得翻三四层目录找 SCSS 变量定义。
没有构建依赖了。装个 Hugo 就能跑,不碰 node_modules。之前本地开发还得确认 SCSS 编译器版本对不对。
功能可控了。每个功能一个开关,不用的直接关掉,页面轻了不少。
主题还在持续改进中,有问题或建议可以到 GitHub 提 issue。