「静态站点」最迷人的承诺是:读者下载的页面里,没有半行为了渲染正文而存在的 JavaScript。正文在构建期变成 HTML,运行时刻表上只有你写下的交互。
墨栈的管线就是围绕这条承诺搭的。它只有四个车站:原始 md / mdx 文件、gray-matter 剥出 frontmatter、MDX 编译 + 插件流水线、MDXRemote 渲染成 React 树——整个过程全部发生在服务器端或构建期。
编译链的四个车站§
- 读取与脱壳——
fs.readFileSync读入,gray-matter把 YAML 头与正文分开; - 插件流水线——remark(AST 层)处理 GFM,rehype(HTML 层)负责 slug、代码块合并与 Prism 高亮;
- RSC 渲染——
MDXRemote的 RSC 版本把编译好的草稿变成 React 元素,不会把 MDX 运行时打进客户端包; - 静态导出——
generateStaticParams列出全部 slug,构建期逐页预渲染,输出目录里全是 HTML。
next-mdx-remote 6:为什么是它§
社区里有三派:@next/mdx(页面级编译)、contentlayer(类型安全但生态变数多)、next-mdx-remote(组件化,数据源自由)。墨栈选最后者,因为它把「编译」包装成一个可以放进 lib/mdx.tsx 的函数,而不是一堆脚手架约定:
import "server-only";
import { MDXRemote } from "next-mdx-remote/rsc";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import { rehypeCodeGroups, rehypePrism } from "@/lib/rehype";
import { mdxComponents } from "@/components/mdx";
export function MdxContent({ source }: { source: string }) {
return (
<MDXRemote
source={source}
components={mdxComponents}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
[rehypeSlug, { prefix: "" }],
[rehypeCodeGroups],
[rehypePrism],
],
format: "mdx",
},
}}
/>
);
}Turbopack 的脾气§
Next.js 16 默认走 Turbopack 构建,而 next-mdx-remote 的依赖树里有 ESM/CJS 混合导出的老包。解决方法是把包名丢进 transpilePackages,让 Turbopack 把它当源码处理:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["next-mdx-remote"],
};
export default nextConfig;六个插件,各司其职§
插件的顺序即数据流的顺序。这张表是墨栈流水线的完整分工:
| 插件 | 阶段 | 职责 | 输出物 |
|---|---|---|---|
remark-gfm | remark | 表格、删除线、任务列表 | 改动过的 mdast |
rehype-slug | rehype | 给标题生成稳定 id | h2#id |
rehypeCodeGroups | rehype | 合并同组代码围栏 | 带 data-files 的 pre |
rehypePrism | rehype | 语法高亮 | 分词后的 span 树 |
MDXRemote | 渲染 | 组件化注入 | React 元素树 |
generateStaticParams | 构建 | 枚举全部页面 | 静态 HTML |
值得点破的是自定义插件的定位:rehypeCodeGroups 和 rehypePrism 都只是三十几行的 unist 遍历器,它们把「内容作者的表达」翻译成「组件消费者的契约」——内容与代码从此解耦。
frontmatter:一张方子的纪律§
每个 md 文件的头 6 行决定它的命运。少了 date 或 title 的文件会被静默跳过——不是丢弃,而是「不出现在索引里」:
title: "文章标题"
description: "一句话摘要,出现在列表页与分享卡片"
date: "2026-09-16"
tags: [Next.js, MDX]
draft: false其中 description 不是可选装饰,它同时是列表页文案、generateMetadata 的梗概、以及链接预览卡片的全部血肉。好的摘要是一篇文章的缩印。
静态的代价与红利§
代价:没有运行时数据。评论、浏览量、最新文章列表,要么在构建期解决,要么用客户端组件后补。
红利:页面永远能打印;体积小到只剩 HTML 和一篇样式;搜索引擎读到的与你看到的是同一份文字;CDN 缓存一个文件等于缓存一个世界。
静态站点的浪漫在于:当数据库宕机、API 限流、广告脚本崩溃时,你的文章依然站得笔直。它不需要任何人在任何地方保持清醒。
下一篇,我们来聊这条管线的另一半:文字排印。代码让你被找到,排版让你被读完。