最近给博客调整代码高亮样式时,遇到一个小问题:使用 Hugo 的 Chroma 高亮,并设置 noClasses: truestyle: "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

也可以换成其他组合,比如:

  • 亮色:githubtangosolarized-light
  • 暗色:draculamonokaicatppuccin-mocha

小结

在 PaperMod 里切换代码高亮明暗主题,比较稳的思路是:

  1. 关闭 Highlight.js,使用 Hugo Chroma。
  2. 设置 noClasses: false,让 Hugo 输出 Chroma class。
  3. hugo gen chromastyles 生成亮色和暗色两套 CSS。
  4. 给两套 CSS 分别加上 :root[data-theme="light"]:root[data-theme="dark"] 作用域。
  5. 把 CSS 放进 assets/css/extended/,交给 PaperMod 自动打包。
  6. 单独修一下 language-fallbacktext 代码块的文字颜色。

这样做以后,代码高亮就会跟随 PaperMod 的主题按钮一起切换,不会再出现两套 CSS 互相覆盖的问题。