当代码块悄悄失声

为 Shiki 不认识的语言加上语法高亮

15.04.2026 | 27 Shawwal 1447
9 min read

بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ

你不会注意到的问题

这个站点上有两篇关于 Caddy 的博客文章。两篇都用 ```caddy 这种带语言标识的围栏代码块来展示 Caddyfile 配置。Expressive Code 负责渲染,Shiki 负责分词,页面构建一路无报错。

只是每一段 Caddy 代码块都被渲染成了纯文本:没有语法高亮,没有颜色,没有视觉结构。只有暗色背景上单调一色的字符。

那些代码块看起来像代码。它们有行号、有外框、有复制按钮。Expressive Code 该加的都加上了。缺的是 Shiki 该加的那一层:真正的高亮。指令、字符串、注释、值,全是一个颜色。

是把博客和 VS Code 一对比才注意到的。同一份 Caddy 配置,在编辑器里色彩齐全,在页面上却是一片单色。

为什么会这样

Expressive Code 用 Shiki 做语法高亮,而 Shiki 内置了 250 多种语言的语法。JavaScript、Python、TypeScript、Bash、HTML、CSS、Go、Rust。也就是大多数技术博客会写到的那些语言。

外部链接 github.com/shikijs/textmate-grammars-themes/tree/main/packages/tm-grammars

Caddy 不在其中。Traefik、HAProxy、Docker Compose 也不在,这些都是自托管基础设施里常见的工具。当 Shiki 遇到一个它不认识的语言标识时,它会回退到纯文本。没有报错,构建也不会失败。只在构建输出里埋着一行你大概率不会去读的警告:

[astro-expressive-code] Error while highlighting code block using language "caddy".
The language could not be found. Using "txt" instead.

这里的 「error」 用得宽容了。什么也没坏。页面照样渲染。代码块看起来还是代码块。它只是没在做一个语法高亮器本来该做的那一件事。

术语说明
Expressive Code 是什么?

给本站代码示例做样式的工具:加上行号、复制按钮和一圈干净的外框。关键字和字符串的实际着色工作交给 Shiki。

Shiki 是什么?

给代码上色的组件,让关键字、字符串和注释一眼就能分辨开。它内置支持 250 多种编程语言。遇到不认识的语言时,会把代码当作纯文本显示出来,不会报错。

TextMate 语法是什么?

一种告诉工具在一段代码里该找什么的文件:关键字、字符串、注释等等。VS Code 用这类文件在编辑器里给代码着色。Shiki 使用同一种格式,这意味着 VS Code 能着色的语言,Shiki 也都能着色。

语法文件从哪里来

Shiki 用的是 TextMate 语法,和 VS Code 用的是同一种格式。每一个 VS Code 的语言扩展都会带一份 .tmLanguage.json 文件,定义这门语言要如何被分词:什么是关键字,什么是字符串,什么是注释。

如果 VS Code 能正确高亮你这门语言,那这份语法文件就已经存在了。它就躺在你的扩展目录里。

对 Caddy 来说,官方扩展是 caddyserver/vscode-caddyfile,发布名为 matthewpi.caddyfile-support。MIT 协议。

外部链接 github.com/caddyserver/vscode-caddyfile

语法文件的位置是:

~/.vscode/extensions/matthewpi.caddyfile-support-0.4.0/syntaxes/caddyfile.tmLanguage.json

让 VS Code 能够高亮 Caddyfile 的,就是这一份文件。同一份文件也能让 Expressive Code 高亮起来。把它拷贝进项目,把配置指向它,就完事了。

五行配置

Expressive Code 通过 ec.config.mjs 里的 shiki.langs 选项接收自定义语法:

import { defineEcConfig } from 'astro-expressive-code'
import fs from 'node:fs'
const caddyfile_Grammar = {
...JSON.parse(fs.readFileSync('./src/Scripts/Build/Grammars/caddyfile.tmLanguage.json', 'utf-8')),
name: 'caddy',
}
export default defineEcConfig({
shiki: {
langs: [caddyfile_Grammar],
langAlias: { caddyfile: 'caddy' },
},
})

加载语法,把它加到 langs 里,按需把别的名字别名过去。配置的其余部分原封不动。

把站点构建一遍。警告没了。代码块亮起来了。

为什么它还是没起作用

只是它并没有亮起来。第一次没有。

语法文件加载的时候没有报错。构建跑得干干净净。代码块依然被渲染成纯文本。没有警告,也没有任何迹象表明哪里出了问题。

问题出在语法文件的 name 字段上。Caddyfile 的语法把自己声明为 name: "Caddyfile",大写 C,大写 F。可是 MDX 文件里围栏代码块写的是 ```caddy,全小写。Shiki 在匹配语言名时区分大小写。"Caddyfile" 匹配不上 "caddy"

langAlias 这个选项乍一看就是答案。把 caddy 映射到 caddyfile。但是别名是在检查已加载的语言之前就解析的,而那份语法文件是以 "Caddyfile" 注册的,不是 "caddyfile"。别名指向了一个不存在的名字。

修复方式:在加载语法文件时直接把它的 name 属性覆盖掉。

const caddyfile_Grammar = {
...JSON.parse(fs.readFileSync('./path/to/caddyfile.tmLanguage.json', 'utf-8')),
name: 'caddy', // override "Caddyfile" to match ```caddy fences
}

一行代码。语法文件现在以 "caddy" 注册,围栏写的是 caddy,Shiki 找到了匹配。一切就跑起来了。

这是那种不会主动昭告自己的 bug:没有堆栈,没有错误信息,构建也不会失败。语法文件加载成功。Shiki 只是从来没有把它和你的代码块对上号。你得知道 Shiki 的名字匹配是大小写敏感的,还得知道语法文件内部的 name 未必和你在 Markdown 里用的标识符一致。这两件事在 Expressive Code 的接入指南里都没有写。

外部链接 expressive-code.com/key-features/syntax-highlighting
langAlias approachname overridefence: caddyalias: caddy to caddyfilelook up caddyfile in grammarsgrammar name is Caddyfilecase mismatch: no matchfalls back to plain textfence: caddygrammar.name overridden to caddylook up caddy in grammarsexact matchsyntax highlighting

套路

给 Expressive Code 加一门自定义语言,是一个三步流程:

  1. **找到语法文件。**到你的 VS Code 扩展目录里翻一翻。如果你的编辑器能高亮这门语言,那 .tmLanguage.json 就存在。把它拷贝进项目,这样构建就不再依赖你本机的编辑器配置。检查一下许可证。这份内置进项目的副本是你自己要维护的:上游的修复不会自动到你的构建里,除非你再拷贝一次。

  2. **注册它。**把它加到 Expressive Code 配置里的 shiki.langs。如果想让多个标识符都解析到同一份语法,就用 langAlias

  3. **对齐名字。**把语法文件的 name 属性覆盖成你在围栏代码块里实际用的那个名字。不要假设语法内部的 name 和你的围栏标识符一致。

整个流程,一旦你知道了,是很短的。真正费功夫的,是发现你需要走这套流程,以及搞清楚为什么第一次尝试会悄无声息地失败。

现在的样子

之前:

handle_errors {
rewrite * /404.html
file_server
}

之后:

handle_errors {
rewrite * /404.html
file_server
}

内容是一样的。差别在于读者的眼睛是能一眼看清结构,还是要逐词去读。对一篇想要教别人配 Caddy 的文章来说,这个差别因沙安拉就是 「有用」 和 「让人挫败」 之间的距离。

技术博客上的代码块是一种安静的可信度信号。当它们带着完整的高亮渲染出来,没有人会注意。当它们没有,会注意到的人,恰恰就是你最希望读你东西的那一群人。

常见问题
这个方法对 Caddy 以外的语言也适用吗?

适用。Shiki 没有原生支持的语言,都可以用同样的方式添加。需要的只是一份 TextMate 语法文件。除 Caddy 之外,常见例子还有 Traefik、HAProxy 和 Docker Compose,都是自托管环境里常见的工具。无论添加哪种语言,三个步骤都是一样的。

如果我的语言没有 VS Code 扩展怎么办?

语法文件不一定要来自 VS Code 扩展。有些项目会直接在自己的 GitHub 仓库里发布语法文件。如果什么都找不到,从头手写一份语法也是可能的,但那超出了本文的范围。

VS Code 扩展更新时,我需要手动更新语法文件吗?

需要,得手动来。项目里的那份副本不会自动跟踪原始来源的变更。当 VS Code 扩展发布语法修复或改进时,需要自己把新文件复制过来。对于像 Caddyfile 这样稳定的语言,这种情况很少发生。对于还在活跃开发中的语言,偶尔检查一下是值得的。

航空航天工程师

公开透明的道德创业者

你专注你的事业

我负责数字化的部分

与我合作
  • 诚实的 AI
  • 私有基础设施
  • 高性能网站

告诉我您的情况:

javed@javedab.com 了解更多