散篇与系列章节在索引层分开,但正文编译只有这一条管线。章节页同样调用 MdxContent。
lib/mdx.tsx:正文如何变成 React§
import "server-only";
import { MDXRemote } from "next-mdx-remote/rsc";
// ...
export function MdxContent({ source }: { source: string }) {
return (
<MDXRemote
source={source}
components={mdxComponents}
options={{
mdxOptions: {
format: "mdx",
remarkPlugins: [remarkGfm],
rehypePlugins: [
[rehypeSlug, { prefix: "" }],
rehypeCodeGroups,
rehypePrism,
],
},
}}
/>
);
}要点:
import "server-only"——若被客户端组件误引用,构建会直接报错,防止把编译链打进浏览器包。- 插件顺序固定:先 GFM(表格等)→ 标题加 id → 合并代码组 → Prism 高亮。顺序反了,高亮或合并会作用在错误的树上。
mdxComponents来自components/mdx/index.tsx:把 Markdown 的pre、a、h2等换成站点自己的组件;并导出 MDX 里可直接写的<Callout />。
类型上有两处 as unknown as ...:unified 插件类型与 next-mdx-remote 的组件表类型不完全对齐,属于边界强转,不是业务分支。
散篇与系列章节共用同一个 MdxContent——小册不另开编译器。系列章节页同样调用 <MdxContent source={chapter.content} />。
lib/rehype.ts:作者写法 → DOM 约定§
作者在 MDX 里这样写:
```tsx group="tabs" file="Tabs.tsx"
// ...
```
```css group="tabs" file="style.css"
/* ... */
```浏览器里最终需要的是一个代码块组件,带着多个文件的原文,而不是两个互不相干的 <pre>。rehypeCodeGroups 就做这件事;随后 rehypePrism 给每个围栏内的 <code> 写入本地 Prism 高亮。
本章只记三点,便于串管线:
- meta:
group/file/preview(或live)写在围栏语言后面; - 合并:相邻且同
group的<pre>收成一个,并挂上data-files(未高亮原文); - 高亮:再遍历
<pre><code>,把 Prism HTML 解析回 hast 子节点——失败则保持原文,不走 CDN。
parseMeta 的 token 规则、同层扫描状态机、双轨契约与端到端示例,见下一章「rehype 源码」。
lib/prism.ts 与 lib/code-lang.ts§
code-lang.ts 维护语言别名(ts→typescript,html→markup 等),并声明:
export const PREVIEWABLE = new Set(["markup", "css", "javascript"]);只有这三类能进沙箱 iframe。TS/TSX 没有浏览器内置运行时,因此即使写了 preview 也不会生成可运行预览文档。
prism.ts 用副作用 import "prismjs/components/prism-..." 注册语法,再:
export function highlight(code: string, lang: string | null | undefined): string {
const name = resolveLang(lang);
const grammar = Prism.languages[name];
if (!grammar) return code;
try {
return Prism.highlight(code, grammar, name);
} catch {
return code;
}
}lib/site.ts:站点常量§
export const site = {
name: "墨栈",
latinName: "INKSTACK",
tagline: "把思考写成代码,把代码写成文章。",
description: "……",
} as const;
export function formatDate(iso: string): string {
const d = new Date(iso + "T00:00:00+08:00");
return new Intl.DateTimeFormat("zh-CN", { … }).format(d);
}日期字符串按东八区零点解析,避免服务器在 UTC 时把 2026-10-01 显示成前一天。首页、文章页、卡片共用 formatDate,避免各写一套。
下一章细拆 rehypeCodeGroups 与 rehypePrism;再往后才进入 app/ 路由组装。