本站从动态博客迁到 Hugo + Blowfish 之后,调优路上连着碰上四类「改了没反应、数字不对」的坑:中文文章只报 302 字、2 分钟;模板里新写的 Tailwind 类不生效;正文与目录列在争同一行宽度;访客那边看到的却是暗色主题。适用环境:Windows 11 本机、Hugo extended 0.166.0、Blowfish 主题,实测时间 2026-09-18。四个坑分别对应一个开关、一份预编译 CSS 的边界、一个拿宽度换宽度的取舍,以及一个跟随系统的外观开关——本文给出根因、实测数据和定稿值,照着量一遍就能复现。
一、症状与第一手数据 #
上线后首页列表显示的三条元信息:
| 文章 | 改前显示 | 人工核对的真实情况 |
|---|---|---|
| Windows 重装排错长文 | 302 字 · 2 分钟 | 2324 个汉字,非空白字符总数 4299 |
| SID 修复工具包 | 150 字 · 1 分钟 | 改后 1080 字 |
| Brave 数据复活记 | 612 字 · 3 分钟 | 改后 4470 字 |
正文宽度实测 560.6px;右侧目录列是主题默认的 lg:max-w-2xs,约 256px 宽,长章节标题反复折行。页脚想做「版权 / 声明 / 署名」三栏,模板里写了 Tailwind 的 sm:grid-cols-3,构建结果却是三段文字竖着排。
二、根因 #
字数与时长。 Hugo 的 .WordCount 默认按空白切词统计:中文不用空格,一整段话被当成一个「词」,2324 个汉字的文章只切出 302 个词;.ReadingTime 又从词数派生,所以两个数一起错。主题本身没有重写统计——它的 word-count、reading-time 两个 partial 只是把 .WordCount、.ReadingTime 原样渲染进 i18n 文案。打开 hugo.toml 的 hasCJKLanguage = true 后,Hugo 改为逐字符计数,并按更快的 CJK 阅读速率算时长。
为什么这两个数字是内容长度唯一的目测入口:本站列表页不开摘要(showSummary = false)、不用卡片视图,每个条目只有「标题 + 日期 · 字数 · 阅读时长 + 标签」。访客在列表页判断一篇文章长短,只能看这两个数,所以它们错得离谱会直接误导。
宽度。 max-w-prose 是 Tailwind 的 65ch 类,16px 汉字下约等于 561px;主题把文章头、正文容器、页脚三处都套了这个类。目录列用的是 lg:max-w-2xs,约 256px。正文和目录在同一个 flex 行里(lg:flex-row),目录每宽 1px,正文就窄 1px——这两个宽度没有第三种来源,只能互相让。实测关系也吻合这个推断:去掉 max-w-prose 后正文 728px(目录列保持默认),把目录列加到 336px 正文立刻收到 652px,收到 304px 正文回到 684px——正文宽度约等于 728 减去「目录列宽减 256」,同一行里没有凭空多出来的宽度。
样式不生效。 Blowfish 的 assets/css/compiled/main.css 是主题作者用 Tailwind JIT 预编译并提交的产物,只包含主题自身模板里出现过的类。站点模板里新写的类(比如 sm:grid-cols-3)不在这份产物里,浏览器里根本没有对应规则,于是「写了没反应」——这不是缓存问题,是规则不存在。这份产物还会随主题升级变化,站点模板里新用的类也不会自动补进去;主题倒是预留了自定义入口:head.html 会检查站点的 assets/css/custom.css,存在就加载,和主 bundle 一起加指纹(本站配置是 sha512)进产物。所以站点的正确姿势是:模板里只写自定义类名,样式全部手写进 custom.css。
配色。 Blowfish 没有配色后台或 GUI,配色等于 params.toml 里一行 colorScheme,内置 17 个方案。
三、处置过程 #
3.1 字数与阅读时长 #
先证明确实是 Hugo 的锅:主题的 word-count、reading-time 两个 partial 就是渲染 .WordCount、.ReadingTime,主题没做任何自定义统计。随后对源文件做了真实字数核算:去掉代码块内容后,纯汉字 2324、英文/数字词 205、非空白字符总数 4299——代码块和表格里的字符同样参与计数。修法只有一行:
# hugo.toml
enableEmoji = true
hasCJKLanguage = true # 中文文章按字符计数,并用更快的 CJK 阅读速率口径要同步说清楚:改后显示的 3817 不是纯汉字数(纯汉字只有 2324),它把代码、表格里的字符和英文词都算进去了,这是 Hugo 的计数方式;如果只想要「纯汉字数」,需要自定义一个统计 partial。改后的三个数据点(3817 → 8、4470 → 9、1080 → 3)恰好都等于「字数 ÷ 500 向上取整」,可以作为发布后的抽查心算。这个口径还要和「为什么这两个数重要」连起来看:列表页不显示摘要,元信息就是访客对文章体量的唯一印象,而它把代码和表格也算进字数,会系统性高估文章长度。本站接受这个误差——纯汉字统计要另写 partial 并长期维护,误差的代价比维护成本小。
3.2 阅读区与目录列:拿宽度得用宽度换 #
第一步,站点根覆盖 layouts/_default/single.html:删掉 header、正文容器、页脚三处 max-w-prose,正文实测 561px 变 728px;同时打开 smartTOC,目录在右上角 sticky 跟随。这一步只解决「加宽 + 有目录」,没动目录本身的宽度。
第二步才是目录列宽:默认 lg:max-w-2xs(约 256px)太窄。给目录外层 div 加上自定义类 toc-column(主题默认类仍留在 class 里,宽度由站点 CSS 覆盖),在 assets/css/custom.css 的媒体查询里写:
/* assets/css/custom.css(节选:目录列宽) */
@media (min-width: 1024px) {
.toc-column {
max-width: 19rem;
}
.toc-column .toc {
min-width: 19rem;
}
}先试 21rem:实测目录列 336px、每行 19 个汉字,正好落进站主偏好的 18 到 20 区间;但代价立刻显形——正文从 728px 被挤到 652px(仍比最初的 561px 宽)。把「目录收到 19rem,约 17 字/行,正文能回 680px」和「目录挪到正文上方做成可折叠块」两个选项摆出来,站主选了 19rem。定稿为什么是 19rem 而不是 21rem:21rem 虽在目标区间里,但正文被压到 652px;19rem 只让目录少两个字(每行 17 字),正文却拿回 32px,回到 684px——正文是主阅读区、目录是辅助导航,拿目录两个字换正文 32px 是划算的。定稿后实测:目录列 304px、文本区 264px、单字宽 16px、每行 17 汉字,正文 684px。这个取舍连同「21rem 会挤正文到 652px」一起记进了发布笔记,免得以后又试一遍。
这些数字怎么量:浏览器控制台里对容器调 getBoundingClientRect() 拿实际渲染宽度;再往目录里注入一个隐藏 span(固定几个汉字、white-space:nowrap),量出单字宽;每行汉字数 = 文本区宽 ÷ 单字宽。核心逻辑大致如下:
(() => {
const w = s => { const e = document.querySelector(s); return e ? Math.round(e.getBoundingClientRect().width) : null; };
const span = document.createElement('span');
span.textContent = '汉字宽测量'; // 5 个汉字
span.style.cssText = 'visibility:hidden;position:absolute;white-space:nowrap';
document.querySelector('#TableOfContents a').appendChild(span);
const charW = span.getBoundingClientRect().width / 5;
const textW = w('.toc-column .toc a');
return { 列宽: w('.toc-column'), 文本区宽: textW, 单字宽: Math.round(charW), 每行汉字: Math.floor(textW / charW) };
})()3.3 页脚三栏:别指望主题帮你生类 #
第一版页脚模板直接写 sm:grid-cols-3 等类,构建后竖排。为了坐实根因,grep 主题的编译产物 compiled/main.css:sm:grid、sm:grid-cols-3、sm:items-center、sm:text-start、sm:text-center、sm:text-end 全部 0 命中,而 text-center、flex-col 这类主题自己用过的类 1 命中——规则确实不存在。同时确认主题的 head.html 本来就会自动加载站点的 assets/css/custom.css(resources.Get "css/custom.css")并并入 bundle,自定义样式的正确落点就是它。
改法:custom.css 里手写一组类,移动端单列,640px 以上切成三列:
/* assets/css/custom.css(节选:页脚三栏) */
.site-footer-row { display: grid; gap: 0.5rem; text-align: center; }
@media (min-width: 640px) {
.site-footer-row { grid-template-columns: 1fr auto 1fr; gap: 0; align-items: center; }
.site-footer-left { justify-self: start; text-align: left; }
.site-footer-center { justify-self: center; text-align: center; }
.site-footer-right { justify-self: end; text-align: right; }
}layouts/partials/footer.html 模板换成这三个类。上线后实测三段文字的位置:中间「本站文章由 AI 执笔编写」声明的中心点 626px,视口中心也是 626px——是真正的居中,不是「看起来差不多」。
3.4 覆盖写站点根,扩展点先试试再说 #
所有改动都在站点根(config/、layouts/、assets/css/custom.css),主题内部一个文件没动;这样主题升级时不会被打回原形。这套覆盖之所以成立,是因为 Hugo 的模板查找顺序是站点 layouts/ 优先于主题 layouts/:同名文件先被找到谁生效,站点根放一份同名 partial,主题那份就整段被盖掉。同目录同名 partial 覆盖就是这套机制的具体用法:站点放了一个 layouts/partials/recent-articles/main.html 覆盖主题同名 partial,去掉首页的「最近的文章」小标题。
主题还留了官方扩展点 layouts/partials/extend-article-link.html,内容渲染在列表每个条目标题的正下方。试过用它输出 #标签 小字(第二版还修正了一个细节:标签链接要取标签页的真实地址,不能直接用 urlize 拼),但实测它和主题在日期行下方渲染的标签徽章并排重复,最终删掉这个文件,沿用主题默认徽章。结论:扩展点存在且可用,但先渲染出来看看再决定留不留。
3.5 配色:没有 GUI,就自己生成一个选色页 #
换配色只是改一行 colorScheme,但 17 个内置方案长什么样没法凭空想象。写了个生成器,读主题 assets/css/schemes/*.css 的颜色变量,输出一个自包含 HTML:每张卡片模拟一篇文章页(顶栏、标题、正文骨架、主色/次色色条),点卡片等于把「把本站配色换成 xxx」作为提示发给助手。脚本全文在文末折叠里(与助手交互、站点域名相关的字面量已做中性化改写,其余按仓库原样)。
定稿组合是 colorScheme = "blowfish"(白底)+ defaultAppearance = "light" + autoSwitchAppearance = true。这个组合的坑在 autoSwitchAppearance:它让站点跟随访客系统/浏览器的亮暗色。站主在自己暗色系统里打开看到深色调(迁移早期还试过紫色 princess,已弃用),一度以为配错,其实只是跟随系统;访客看到暗色不用慌,想锁死亮色就把 autoSwitchAppearance 改成 false。想自定义配色,主题的做法是在 custom.css 里重定义 --color-* 变量。
生成器还有个好处:它读的是主题 assets/css/schemes/ 里的文件,只要继续用 Blowfish,主题升级后脚本不用改,重跑一遍就有新方案。产物 tools/scheme-preview.html 已加进 .gitignore 不进版本库,本机保留即可。
四、验证 #
目录列宽三个方案的实测汇总:
| 方案 | 目录列宽 | 单字宽 | 每行汉字 | 正文宽度 |
|---|---|---|---|---|
主题默认 lg:max-w-2xs |
约 256px | 16px | 未量 | 728px |
| 21rem(第一版) | 336px | 16px | 19 | 652px |
| 19rem(定稿) | 304px | 16px | 17 | 684px |
其它验证点:线上文章标题下方显示「2026年9月2日 · 3817 字 · 8 分钟」,首页三篇分别是 3817/8、1080/3、4470/9;构建产物里 grep 得到 .toc-column{max-width:19rem},说明站点 CSS 确实进了主 bundle;目录的行为也要验:smartTOC 开启后目录 sticky 跟随滚动、当前章节高亮,文章页 TOC 共 8 个顶层条目;本地构建、部署、CDN 清缓存之后,浏览器实测与本地一致。
怎么判断「没解决」:线上数字还是 302 字(开关没生效或构建没重跑);构建产物里 grep 不到自定义类规则(类写错了位置);页脚三段还是竖排;量出来目录列还在 256px 上下(覆盖没写进 custom.css)。
五、留给后来者的清单 #
| 症状 | 根因 | 做法 | 验证 |
|---|---|---|---|
| 中文文章字数/时长离谱 | hasCJKLanguage 未开,按空格切词 |
hugo.toml 加 hasCJKLanguage = true |
改后数字明显变大;注意口径含代码与表格字符 |
| 模板里新 Tailwind 类不生效 | 主题 compiled CSS 是预编译产物,只含主题用过的类 | 新样式写 assets/css/custom.css,用自定义类名 |
grep 构建产物确认规则存在 |
| 正文/目录窄 | max-w-prose = 65ch 约 561px;lg:max-w-2xs 约 256px |
站点根覆盖模板去 max-w-prose;toc-column 类 + custom.css 覆盖宽度 |
浏览器量容器宽与每行汉字数 |
| 目录和正文抢宽度 | 同一 flex 行,此消彼长 | 先量后定:21rem 正文挤到 652px,19rem 回 684px | 目录每宽 1px,正文就少 1px |
| 主题升级后定制消失 | 改了主题内部文件 | 所有覆盖放站点根 | 升级后回归检查一遍 |
| 访客看到暗色 | autoSwitchAppearance = true 跟随系统 |
不是 bug;想锁亮色改 false |
系统亮/暗色各看一眼 |
速查默认值:max-w-prose = 65ch 约 561px(16px 汉字);lg:max-w-2xs 约 256px;去掉 max-w-prose 后正文 728px;定稿目录 19rem = 304px / 17 汉字每行 / 正文 684px。
六、碎碎念 #
静态站没有「配置后台」是架构选择,但选色这类纯视觉决策,反而用一个几十行脚本就换回了可视化界面——后台能做的事,脚本也能。整轮调优里最值钱的其实是「先量后改」:302、3817、561px、652px、684px,全是浏览器里量出来的,没有一个靠猜。宽度的取舍没有免费午餐:想要目录每行 19 个字,正文就得让出 76px;想要正文回到 684px,目录就少两个字。把这些数字写进发布清单,下次调的时候直接照表抄。
make_scheme_preview.py(104 行,原样代码,仅两处助手交互字面量中性化)
#!/usr/bin/env python3
"""从 Blowfish 主题的配色方案 CSS 生成一个可在浏览器里挑选的预览页。
用法:python tools/make_scheme_preview.py [输出路径]
读 themes/blowfish/assets/css/schemes/*.css,输出一个自包含 HTML(无外部依赖)。
点任意卡片会把「把站点配色换成 xxx」作为提示发给助手。
"""
import glob
import os
import re
import sys
SCHEME_DIR = 'themes/blowfish/assets/css/schemes'
OUT = sys.argv[1] if len(sys.argv) > 1 else 'tools/scheme-preview.html'
LABEL = {
'one-light': 'one-light(白底高对比)', 'github': 'github(GitHub 浅色)',
'blowfish': 'blowfish(主题默认,当前使用)', 'slate': 'slate(冷灰)',
'avocado': 'avocado(橄榄绿)', 'ocean': 'ocean(海蓝)', 'autumn': 'autumn(秋橙)',
'princess': 'princess(原紫色,已弃用)', 'neon': 'neon(霓虹)', 'noir': 'noir(黑白)',
'fire': 'fire(火红)', 'forest': 'forest(森绿)', 'bloody': 'bloody(暗红)',
'marvel': 'marvel(红黄)', 'terminal': 'terminal(终端绿)', 'congo': 'congo',
'burufugu': 'burufugu',
}
def read_scheme(path):
s = open(path, encoding='utf-8').read()
def rgb(name):
m = re.search(r'--color-' + name + r':\s*([0-9,\s]+);', s)
return 'rgb(' + m.group(1).strip() + ')' if m else None
return {
'neutral100': rgb('neutral-100'), 'neutral200': rgb('neutral-200'),
'neutral700': rgb('neutral-700'), 'neutral800': rgb('neutral-800'),
'neutral900': rgb('neutral-900'),
'primary': rgb('primary-500'), 'primary300': rgb('primary-300'),
'secondary': rgb('secondary-500'),
}
def card(name, p):
label = LABEL.get(name, name)
dark = name in {'one-light', 'github', 'blowfish', 'slate', 'avocado', 'ocean',
'autumn', 'princess', 'noir'}
bg = p['neutral100'] if dark else p['neutral900']
fg = p['neutral900'] if dark else p['neutral100']
soft = p['neutral200'] if dark else p['neutral800']
code = p['neutral800'] if dark else p['neutral700']
return f'''
<article class="card" data-send="把本站的站点配色换成 {name}" title="点一下让助手应用这个配色">
<div class="mock" style="background:{bg};color:{fg}">
<div class="bar" style="border-color:{soft}"><span class="dot" style="background:{p['primary']}"></span>
<span style="opacity:.85">本站</span><span class="sp"></span><span style="opacity:.6">首页 · 关于 · 标签</span></div>
<h3 style="color:{fg}">LTSC 精简版大坑与排错全记录</h3>
<p style="color:{soft}">从一次普通的更新失败开始,走过七轮覆盖安装、数十条命令、三次多模型会诊。</p>
<p><span style="color:{p['primary']};font-weight:600">→ 阅读全文</span>
<code style="background:{code};color:{fg}">sfc /scannow</code></p>
</div>
<footer>
<strong>{label}</strong>
<div class="sw">
<i style="background:{p['primary']}"></i><i style="background:{p['secondary']}"></i>
<i style="background:{bg};border:1px solid {soft}"></i><i style="background:{soft}"></i>
</div>
</footer>
</article>'''
def main():
files = sorted(glob.glob(os.path.join(SCHEME_DIR, '*.css')))
if not files:
sys.exit('未找到配色文件:' + SCHEME_DIR)
cards = [card(os.path.basename(f)[:-4], read_scheme(f)) for f in files]
html = f'''<!doctype html><html lang="zh-cn"><meta charset="utf-8">
<title>Blowfish 配色选择</title>
<style>
body {{ color: var(--foreground); font-family: inherit; margin: 0; }}
p.lead {{ color: var(--muted-foreground); margin: 0 0 14px; font-size: 13px; line-height: 1.6; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }}
.card {{ border: 1px solid var(--border); border-radius: 10px; overflow: hidden; cursor: pointer;
background: var(--card); transition: transform .12s ease, box-shadow .12s ease; }}
.card:hover {{ transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,.18); }}
.mock {{ padding: 12px 14px 14px; font-size: 12px; }}
.bar {{ display: flex; align-items: center; gap: 8px; padding-bottom: 8px; border-bottom: 1px solid; font-size: 11px; }}
.dot {{ width: 8px; height: 8px; border-radius: 50%; display: inline-block; }}
.sp {{ flex: 1; }}
.mock h3 {{ font-size: 14px; margin: 10px 0 6px; font-weight: 700; }}
.mock p {{ margin: 0 0 8px; line-height: 1.55; }}
.mock code {{ padding: 1px 5px; border-radius: 4px; font-size: 11px; }}
footer {{ display: flex; align-items: center; justify-content: space-between; padding: 8px 12px;
border-top: 1px solid var(--border); font-size: 12px; }}
.sw {{ display: flex; gap: 4px; }}
.sw i {{ width: 14px; height: 14px; border-radius: 3px; display: inline-block; }}
</style>
<body>
<p class="lead">点任意卡片 = 让助手把站点配色换成该方案(等价于改 <code>config/_default/params.toml</code> 里的一行)。<br>
每张卡里的色条依次是:主色 / 次色 / 页面底色 / 次级文字色。标「当前使用」的是线上生效的那个。</p>
<div class="grid">{''.join(cards)}</div>
</body></html>'''
open(OUT, 'w', encoding='utf-8', newline='\n').write(html)
print(f'已生成 {OUT}({len(cards)} 个配色方案)')
if __name__ == '__main__':
main()