技技术学习庭院建造日志2026.09.26

Shiki 与 KaTeX:暗夜里的代码与公式

把代码块交给 Shiki,把公式交给 KaTeX,让技术笔记里的每一行都拥有正确的颜色与呼吸。

4 MIN READ1219 字

关于这篇笔记的排版

你看到的提示框、代码高亮与公式,都不是在浏览器里临时拼出来的。正文保存在 SQLite,每次请求由 Astro SSR 渲染成 HTML,再交给浏览器。

技术笔记里最难排版的两样东西,是代码和公式。

代码需要语义高亮、行内外的区分、可复制的按钮;公式需要正确的字体、基线、上下标与求和符号。把这两件事都交给浏览器端的 JavaScript,会让一篇本可以很轻的文章背上沉重的包。

在这套 Astro SSR 架构里,它们都可以在服务端完成,并在浏览器抵达前变成干净的 HTML。

Shiki:在服务端把代码染上颜色

Astro 内置了 Shiki 作为语法高亮器。不需要安装额外的 remark 插件,只要在配置里选一个主题:

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: 'rose-pine',
      wrap: true,
    },
  },
});

rose-pine 是 Rosé Pine 主题家族里最暗的一支:底色是带紫的墨色,关键字是玫瑰粉,字符串是低饱和的薄荷绿。它和这座影之庭院几乎像是为彼此准备的。

Shiki 的工作方式是在服务端生成带内联样式的 HTML:

<pre class="astro-code rose-pine" style="background-color:#191724">
  <code>
    <span style="color:#c4a7e7">const</span>
    <span style="color:#e0def4"> garden </span>
    <span style="color:#c4a7e7">=</span>
    <span style="color:#9ccfd8">'bloom'</span>
  </code>
</pre>

浏览器不需要加载任何高亮库。页面仍然由服务端动态渲染,但 HTML 可以压缩、缓存,这笔交易依然非常划算。

一行代码的三种状态

行内代码、代码块、以及代码块里的复制按钮,需要分开设计:

.prose code {
  padding: 0.18em 0.4em;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: color-mix(in srgb, var(--surface-3) 55%, transparent);
  color: var(--sakura-soft);
  font-family: var(--font-mono);
  font-size: 0.84em;
}

.prose pre {
  position: relative;
  padding: 22px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: #0a0712 !important;
  overflow-x: auto;
}

!important 在这里是必要的,因为 Shiki 会把背景色写成内联样式。把代码块统一压到比页面更深的墨色,相当于给代码挖出一块安静的凹槽。

复制按钮由一小段脚本注入:

document.querySelectorAll('.prose pre').forEach((pre) => {
  const button = document.createElement('button');
  button.className = 'code-copy';
  button.type = 'button';
  button.textContent = 'COPY';

  button.addEventListener('click', async () => {
    await navigator.clipboard.writeText(pre.innerText);
    button.textContent = 'COPIED ✿';
    button.classList.add('is-copied');
    setTimeout(() => {
      button.textContent = 'COPY';
      button.classList.remove('is-copied');
    }, 1800);
  });

  pre.appendChild(button);
});

按钮默认透明,悬停代码块时才浮现。它不应抢走代码的注意力,只需要在需要时被找到。

KaTeX:公式的排版应该像印刷品

Markdown 本身不理解数学。Astro 通过两个插件把这件事接上:

npm install remark-math rehype-katex katex
// astro.config.mjs
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';

export default defineConfig({
  markdown: {
    remarkPlugins: [remarkMath],
    rehypePlugins: [rehypeKatex],
  },
});

还需要在全局样式里引入 KaTeX 的字体表:

import 'katex/dist/katex.min.css';

然后,行内公式写作 $E = mc^2$,独立公式用一对美元符号包住整段:

a2+b2=c2a^2 + b^2 = c^2

现在正文直接以 Markdown 形式保存。像 \frac{a}{b} 这样的公式可以自然出现,不再需要担心花括号被别的解析器抢走。

Markdown 与数学

KaTeX 只负责渲染公式;Markdown 只负责把内容组织成段落、列表和代码块。两层各司其职,公式里的花括号可以安心待着。

让公式融入夜色

KaTeX 默认的黑色文字在暗色背景上会显得突兀。最简单的做法是让它继承正文颜色:

.katex {
  color: inherit;
  font-size: 1.04em;
}

.katex-display {
  overflow-x: auto;
  padding-block: 0.5rem;
}

overflow-x: auto 很重要。移动端遇到超宽公式时,横向滚动比强行缩小或撑破布局都更体面。

代码与公式之间的节奏

技术文章真正的难点不是「能不能渲染」,而是阅读节奏。

我现在会刻意控制三件事:

  • 代码块之前先用一句话说明「这段代码解决什么问题」。
  • 长代码不超过 40 行;超过时拆成两个片段,中间用文字搭桥。
  • 公式只保留能帮助理解的,不为炫技而推导。

一条私人的写作准则

如果一段代码或公式删掉之后,读者仍然能理解结论,那它大概不该出现在正文里,而应该放进折叠块或附录。

服务端渲染的美,是一种安静的美

Shiki 和 KaTeX 都选择了同一条路:把昂贵的工作留在服务端。

页面抵达浏览器时,代码已经是带颜色的 HTML,公式已经排好版。没有闪烁,没有布局偏移,也没有等待客户端脚本执行后才突然出现的公式。

这种美不太容易被察觉。它不像樱花粒子那样从屏幕上飘过,也不会在滚动时弹出来。它只是让每一行代码、每一个积分号,都安安静静地待在它该在的位置。

而一座庭院,本来就该是这样的。

MARGINALIA

留言0

路过的话,留下一枚脚印吧。
BE KIND, BLOOM SOFTLY.

    还没有留言。做第一个在这里留下脚印的人吧。

    登录后会把留言记在你的名字下;不注册也可以直接留言。
    支持换行 · 最多 2000 字0 / 2000