起因
第一章搭好的博客是一个"扁平流"——所有文章都在 content/posts/ 下,按时间倒序排列。这种结构很适合写日记或杂记,但凡是想要写长篇连载(比如复健笔记、菜谱、相机使用说明书)就会发现:
- 同一系列的文章被时间打散
- 读者点进单篇看不到上下文,不知道还有别的章节
- 写作者自己也容易忘了之前写到哪
我想加一个书架——和"杂记"并列的另一个内容类型,每本书内部有章、节两层结构。
整体方案
Claude 看完 dream 主题的源码之后给出的方案:
Hugo 自带 page bundle 结构能完美表达"书 → 章 → 节"三层关系。书是一个 branch bundle(带
_index.md的目录),章是嵌套在书目录下的 branch bundle,节是章目录下的普通.md文件。
具体的内容目录:
content/books/
├── _index.md (书架根,渲染时跳过)
├── rehab/ (一本书:复健笔记)
│ ├── _index.md
│ ├── chapter-01/
│ │ ├── _index.md (章简介)
│ │ ├── section-01.md
│ │ └── section-02.md
│ └── chapter-02/
└── blog/ (也可以"平结构"——章本身就是节)
├── _index.md
├── chapter-01.md (没有节,章就是叶节点)
└── chapter-02.md
dream 主题本身没有书架概念,所以需要写项目级 layout 覆盖。
自定义的 layouts
在 layouts/ 下面新建了一堆文件,每一个都覆盖主题的同路径文件。Hugo 的查找顺序是项目级优先于主题,所以放对位置就生效。
| 文件 | 干嘛的 |
|---|---|
layouts/books/list.html | 按 URL 深度分支:/books/ 渲染书架卡片、/books/<book>/ 渲染书目录、/books/<book>/<chapter>/ 渲染章节首页 |
layouts/books/single.html | 节(具体文章)页面:左侧 sticky 目录 + 中间正文 + 右侧 H2/H3 滚动目录 |
layouts/partials/book-toc.html | 侧栏目录组件,递归渲染章节 |
layouts/partials/article-meta.html | 统一的文章 meta 块(发表 / 更新 / 阅读时长 + 作者 / 分享) |
layouts/partials/share.html | 主题原版的扩展,加了复制链接按钮,HTTP fallback 也实现了 |
layouts/partials/nav.html | 顶部 nav 改:home 图标 + 关于改图标 + tooltip |
layouts/_default/single.html + zen-summary.html | 单篇文章和首页摘要的小调整 |
layouts/section/posts.html + search.html | 归档和搜索:合并杂记 + 书章节 |
每个文件平均 50-200 行 Hugo 模板。让 Claude 写 + 我看着改,几乎都是一遍过。
自定义 CSS 和 JS
主题用的是预编译的 Tailwind CSS(output.css),所以我自己加的工具类(lg:max-h-[...] 这种)根本不在 output 里。一开始踩了这个坑:脚本里加了 lg:sticky 之类的类名,浏览器里完全无效。
Claude 的解决方案是:
不依赖 Tailwind,自己写一个
static/css/book.css,在hugo.toml的[params.advanced]里通过customCSS加载。所有书架相关的样式(侧栏粘性、归档时间线、卡片样式、暗色模式微调、tooltip)都写进这一个文件。
book.css 最终大概 500 行,覆盖:
- 书侧栏 sticky 行为 + 暗色模式背景调整
- 首页书卡片 grid 布局 + 帖子卡片
- /posts/ 归档按"年 → 月"两层分组
- nav 桌面悬浮 tooltip(
::after+attr(title)) - Waline 评论框布局微调(这一节后面会讲)
- Mastodon 评论区样式
JS 只写了一个:static/js/book-scrollspy.js。节阅读时右侧的 TOC 会跟随滚动高亮当前 H2/H3,用 IntersectionObserver 实现,约 60 行。
顶部 nav:sticky → fixed
dream 主题的 <nav> 用的是 position: sticky。本地预览没问题,但部署到线上后我发现往下滚几下 nav 就消失了。Claude 排查后说:
主题的
.flip-container有一个写死的height: calc(100vh - 80px - 2rem),作为 sticky 元素的祖先打破了 sticky 的滚动上下文。靠谱的修复是把 nav 改成position: fixed,然后给 body 加一个padding-top补上。
最终 CSS:
body > nav {
position: fixed !important;
top: 0; left: 0; right: 0;
z-index: 40;
background-color: oklch(var(--b1));
border-bottom: 1px solid oklch(var(--bc) / 0.12);
}
body { padding-top: 5rem; }
@media (min-width: 1024px) {
body { padding-top: 6rem; }
}
颜色闪烁问题
日间模式下,每次切换页面会先黑一闪再变白。Claude 一眼看出问题:
系统在暗色模式时,浏览器在 JS 加载完之前默认背景是黑色,然后 Alpine.js(defer 加载)才把网页切到 emerald(亮色),出现"黑一下→白"的闪烁。修法是在
<head>顶部加一段同步执行的 inline script,比 Alpine 早一步把 class 和 data-theme 设到<html>上。
覆盖 layouts/partials/head.html,在文件开头加:
<script>
(function() {
var pref = localStorage.getItem('hugo-theme-dream-is-dark');
var prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
var isDark = pref === 'y' || (pref !== 'n' && prefersDark);
document.documentElement.classList.add(isDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', isDark ? 'forest' : 'emerald');
document.documentElement.style.backgroundColor = isDark ? '#1a1a1a' : '#ffffff';
})();
</script>
这段同步执行,挡在 paint 之前完成,黑闪消失了。
写作辅助脚本
最后让 Claude 帮我加了两个 .command 脚本(双击执行):
| 脚本 | 用途 |
|---|---|
2_new_book_section.command | 交互式选书、选章、新建节,自动生成 frontmatter,自动算 weight = max + 1,建好同名图片目录 |
5_book_images.command | 把书节配套图片目录里的原图压缩成 webp、按 <section>_pNN.webp 重命名、上传到 S3 图床、打印 centered-image shortcode 复制粘贴 |
加上之前已有的 1_new_post.command(新建杂记)、3_hugo_server.command(本地预览)、4_post_images.command(杂记图片)、6_deploy.command(部署),现在写作流程一脚本一动作,不用记 Hugo 命令。
我自己需要做的事
虽然代码大部分 Claude 写,下面这些我得亲自做:
- 在 hugo server 本地预览每个改动,盯 layout 排版(暗色模式卡片背景偏黑、字号层级、行距、章节编号
0.的怪 bug 都是边看边告诉 Claude 调) - 把已有的相机说明书、复健草稿手动整理成书的 章/节 结构(Claude 写脚本,我自己迁内容)
- 写几本书的开头介绍(
_index.md里的"为什么这本书"那段) - 移动文件夹位置(从
Hugo_Blog/blog_main搬到GitHub/blog_main统一管理,下一节讲)
现在的样子
书架功能上线后的首页是"杂记 + 书架"两栏。杂记是单篇文章流,书架是 5 本连载(复健笔记 / 相机使用说明书 / 数据化生活 / 搭建静态博客 / 菜谱)。点开书后左侧固定目录、右侧滚动高亮、章节内部上下页导航能跨章。整个改造分了几十次小迭代,每次一两个 prompt + 几行代码。