最近给博客调整代码高亮样式时,遇到一个小问题:使用 Hugo 的 Chroma 高亮,并设置 noClasses: true、style: "tango" 后,一些代码块看起来颜色特别浅。尤其是生成出来带有 class="language-fallback" 的代码块,浅灰背景配浅灰文字,阅读体验不太好。
折腾了一圈后,发现更适合 PaperMod 的方式是:关闭内联样式,使用 Chroma 生成 CSS 类,然后自己准备亮色和暗色两套高亮样式,让它们跟随 PaperMod 的主题切换。
问题原因#
PaperMod 支持 Hugo 内置的 Chroma 语法高亮。配置里一般会先关闭 Highlight.js:
1
2
3
|
params:
assets:
disableHLJS: true
|
如果使用下面这种配置:
1
2
3
4
5
6
7
|
markup:
highlight:
noClasses: true
codeFences: true
guessSyntax: true
lineNos: true
style: "tango"
|
Hugo 会把 Chroma 的样式直接写到 HTML 里,也就是内联样式。比如 tango 的代码块背景就是浅灰色:
1
|
background-color: #f8f8f8;
|
这本身没有问题,但当某些代码块没有写语言,或者 Chroma 无法判断语言时,生成的 HTML 里会出现:
1
|
<code class="language-fallback" data-lang="fallback">
|
这种 fallback 代码块没有具体语言的 token 颜色,容易吃到主题默认的代码文字颜色。如果背景又是浅灰色,就会显得很淡。
所以问题不是 tango 没生效,而是 fallback 代码块没有语法 token,只剩下普通文字颜色和代码块背景在互相配合。
推荐配置#
我最后选择把 Chroma 改成 class 模式:
1
2
3
4
5
6
7
|
markup:
highlight:
noClasses: false
codeFences: true
guessSyntax: true
lineNos: true
style: ""
|
这里最关键的是:
noClasses: false 表示让 Hugo 输出 Chroma 的 CSS class。
style: "" 基本不用再管,因为颜色交给外部 CSS 控制。
disableHLJS: true 仍然保留,避免 highlight.js 参与。
这样生成出来的代码结构会带 .chroma、.k、.s、.c 等 class,具体颜色由 CSS 文件决定。
PaperMod如何加载自定义CSS#
PaperMod 官方 FAQ 里提到,可以把自定义 CSS 放到站点根目录:
1
2
3
4
5
6
|
assets/
└── css/
└── extended/
├── custom.css
├── syntax-light.css
└── syntax-dark.css
|
这个目录下所有 CSS 文件都会被 PaperMod 自动打包进最终样式文件,而且加载顺序在主题核心 CSS 后面,所以很适合覆盖代码高亮样式。
生成亮色和暗色高亮#
先生成两套 Chroma 样式。比如亮色使用 github,暗色使用 dracula:
1
2
|
hugo gen chromastyles --style=github > assets/css/extended/syntax-light.css
hugo gen chromastyles --style=dracula > assets/css/extended/syntax-dark.css
|
但这样还不能直接用。因为两个文件里都会生成类似这样的选择器:
1
2
3
4
5
6
7
|
.chroma {
background-color: #f8f8f8;
}
.chroma .k {
color: #cf222e;
}
|
如果亮色和暗色 CSS 都是裸的 .chroma,那它们会互相覆盖,最后加载的文件会赢。结果就是无论切到亮色还是暗色,都只剩一套高亮。
PaperMod 切换主题时,会修改 <html> 上的 data-theme 属性:
1
2
|
<html data-theme="light">
<html data-theme="dark">
|
所以正确做法是给两套 Chroma CSS 分别加上作用域。
亮色 CSS 应该类似这样:
1
2
3
4
5
6
7
|
:root[data-theme="light"] .chroma {
background-color: #f8f8f8;
}
:root[data-theme="light"] .chroma .k {
color: #cf222e;
}
|
暗色 CSS 应该类似这样:
1
2
3
4
5
6
7
|
:root[data-theme="dark"] .chroma {
background-color: #282a36;
}
:root[data-theme="dark"] .chroma .k {
color: #ff79c6;
}
|
这样切换主题时,浏览器会自动命中对应的 CSS。
用PowerShell自动加作用域#
手工给每一行加前缀太麻烦,可以直接用 PowerShell 处理。
生成亮色主题:
1
2
3
|
hugo gen chromastyles --style=github |
ForEach-Object { $_ -replace '^(/\*.*?\*/\s*)?(\.bg|\.chroma)', '${1}:root[data-theme="light"] ${2}' } |
Set-Content -Encoding utf8 assets/css/extended/syntax-light.css
|
生成暗色主题:
1
2
3
|
hugo gen chromastyles --style=dracula |
ForEach-Object { $_ -replace '^(/\*.*?\*/\s*)?(\.bg|\.chroma)', '${1}:root[data-theme="dark"] ${2}' } |
Set-Content -Encoding utf8 assets/css/extended/syntax-dark.css
|
这段命令会把 Chroma 生成的选择器从:
1
2
3
|
.chroma .k {
color: #cf222e;
}
|
变成:
1
2
3
|
:root[data-theme="light"] .chroma .k {
color: #cf222e;
}
|
修复fallback代码块#
有些代码块本来就不是程序代码,比如示例输入、示例输出、命令输出等。这类内容即使写成 text 也不会有什么 token 高亮。
为了避免 fallback 或纯文本代码块颜色太浅,可以在 assets/css/extended/custom.css 里补一段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
:root[data-theme="light"] .chroma code.language-fallback,
:root[data-theme="light"] .chroma code.language-text,
:root[data-theme="light"] .chroma code.language-bash,
:root[data-theme="light"] .chroma code[data-lang="fallback"],
:root[data-theme="light"] .chroma code[data-lang="text"],
:root[data-theme="light"] .chroma code[data-lang="bash"] {
color: #24292f;
}
:root[data-theme="dark"] .chroma code.language-fallback,
:root[data-theme="dark"] .chroma code.language-text,
:root[data-theme="dark"] .chroma code[data-lang="fallback"],
:root[data-theme="dark"] .chroma code[data-lang="text"] {
color: #f8f8f2;
}
|
如果还想让普通代码块背景也跟主题一致,可以补充:
1
2
3
4
5
6
7
|
:root[data-theme="light"] {
--code-block-bg: #f8f8f8;
}
:root[data-theme="dark"] {
--code-block-bg: #2e2e33;
}
|
注意 PaperMod 使用的是 data-theme="dark",不是 .dark class。因此不要写成:
1
2
3
|
.dark {
--code-block-bg: #2e2e33;
}
|
这样不会命中 PaperMod 的主题切换。
给代码块写清楚语言#
虽然 guessSyntax: true 可以自动猜语言,但它并不总是可靠。尤其是算法题里的输入输出示例,很容易被当成 fallback。
程序代码建议明确写语言:
1
2
3
4
5
|
```c
int main(void) {
return 0;
}
```
|
普通文本或运行结果建议明确写 text:
1
2
3
4
|
```text
Case 1:
1 + 2 = 3
```
|
这样生成结果更稳定,也方便以后统一调整样式。
最终结构#
最后相关文件大概是这样:
1
2
3
4
5
6
|
assets/
└── css/
└── extended/
├── custom.css
├── syntax-light.css
└── syntax-dark.css
|
hugo.yaml 里保留:
1
2
3
4
5
6
7
8
9
10
11
|
params:
assets:
disableHLJS: true
markup:
highlight:
noClasses: false
codeFences: true
guessSyntax: true
lineNos: true
style: ""
|
之后如果想换风格,只要重新生成两份 CSS:
1
2
|
hugo gen chromastyles --style=github
hugo gen chromastyles --style=dracula
|
也可以换成其他组合,比如:
- 亮色:
github、tango、solarized-light
- 暗色:
dracula、monokai、catppuccin-mocha
在 PaperMod 里切换代码高亮明暗主题,比较稳的思路是:
- 关闭 Highlight.js,使用 Hugo Chroma。
- 设置
noClasses: false,让 Hugo 输出 Chroma class。
- 用
hugo gen chromastyles 生成亮色和暗色两套 CSS。
- 给两套 CSS 分别加上
:root[data-theme="light"] 和 :root[data-theme="dark"] 作用域。
- 把 CSS 放进
assets/css/extended/,交给 PaperMod 自动打包。
- 单独修一下
language-fallback 或 text 代码块的文字颜色。
这样做以后,代码高亮就会跟随 PaperMod 的主题按钮一起切换,不会再出现两套 CSS 互相覆盖的问题。