← 全部文章
ISSUE 003 · 2026/09/16

零客户端 MDX 管线:在 Next.js 16 里把笔记编译成页面

用 next-mdx-remote 6 的 RSC 模式搭一条全静态的 MDX 编译链:从 frontmatter 到语法高亮,全部发生在构建期。附带 Turbopack 的一个坑与六个插件的分工表。

⏱ 3 分钟1129 字2026/09/16Next.jsMDX

「静态站点」最迷人的承诺是:读者下载的页面里,没有半行为了渲染正文而存在的 JavaScript。正文在构建期变成 HTML,运行时刻表上只有你写下的交互。

墨栈的管线就是围绕这条承诺搭的。它只有四个车站:原始 md / mdx 文件、gray-matter 剥出 frontmatter、MDX 编译 + 插件流水线、MDXRemote 渲染成 React 树——整个过程全部发生在服务器端或构建期。

编译链的四个车站§

  1. 读取与脱壳——fs.readFileSync 读入,gray-matter 把 YAML 头与正文分开;
  2. 插件流水线——remark(AST 层)处理 GFM,rehype(HTML 层)负责 slug、代码块合并与 Prism 高亮;
  3. RSC 渲染——MDXRemote 的 RSC 版本把编译好的草稿变成 React 元素,不会把 MDX 运行时打进客户端包;
  4. 静态导出——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-gfmremark表格、删除线、任务列表改动过的 mdast
rehype-slugrehype给标题生成稳定 idh2#id
rehypeCodeGroupsrehype合并同组代码围栏带 data-files 的 pre
rehypePrismrehype语法高亮分词后的 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 限流、广告脚本崩溃时,你的文章依然站得笔直。它不需要任何人在任何地方保持清醒。

下一篇,我们来聊这条管线的另一半:文字排印。代码让你被找到,排版让你被读完。