CMS 内容管理与 MDX

内容是产品的灵魂。无论是技术博客、产品文档、营销落地页还是帮助中心,都需要一个高效的内容管理方案。Next.js 生态在这个领域有极其丰富的选择——从代码中直接写 MDX,到对接 Headless CMS,到构建完整的内容平台。本章从内容管理的本质需求出发,深入分析每种方案的原理、适用场景和技术权衡。

1. 内容管理的本质问题

1.1 三个核心问题

每个内容管理系统都在解决三个问题:

问题含义影响决策
谁写内容?开发者?运营?产品经理?决定编辑界面需求
内容存在哪?Git 仓库?数据库?第三方 CMS?决定架构和部署模式
内容如何渲染?构建时?请求时?边缘?决定性能和实时性

1.2 四种内容管理模式

模式内容存储编辑方式适用场景代表方案
代码即内容Git 仓库中的 MD/MDX 文件代码编辑器 + PR技术博客、文档MDX + Contentlayer
Git-based CMSGit 仓库,但有可视化编辑器在线编辑器非技术人员也要编辑Tina CMS、Decap CMS
API-first CMS第三方 SaaS 数据库专用后台营销网站、大型内容站Sanity、Contentful
自建 CMS自己的数据库自己写后台完全自定义需求Next.js + DB + Admin

选型核心原则内容的"所有权"和"编辑体验"是两个最重要的维度。开发者写的内容(博客、文档)用 Git-based 方案最自然;非技术人员维护的内容(营销页、产品更新)需要可视化编辑器。

2. MDX:开发者的内容格式

2.1 MDX 是什么

MDX = Markdown + JSX。它让你在 Markdown 中使用 React 组件:

# 我的文章
 
这是普通的 Markdown 文本。
 
<Callout type="warning">
  这是一个 React 组件,在 Markdown 中直接使用。
</Callout>
 
下面是一个交互式代码演示:
 
<CodePlayground code={`console.log('hello')`} />

2.2 MDX 的技术原理

MDX 的编译过程:

MDX 源文件
    ↓ @mdx-js/mdx 编译器
解析 Markdown 语法 → AST(抽象语法树)
    ↓ remark 插件链(处理 Markdown)
    ↓ rehype 插件链(处理 HTML)
    ↓ 生成 JavaScript 模块
React 组件(可以被 import 和渲染)

关键理解:MDX 文件编译后就是一个普通的 React 组件。这意味着它可以享受 React 生态的所有能力——props、状态、组合、懒加载等。

2.3 MDX 的两种集成方式

方式一:@next/mdx(官方插件)

Next.js 官方提供的 MDX 集成,将 .mdx 文件当作页面或组件直接使用:

  • 优点:零配置、与 App Router 无缝集成
  • 缺点:没有 frontmatter 解析、没有内容集合管理、缺少类型安全

适合:简单的内容页面(About、Privacy Policy 等),不需要列表/分类/搜索的场景。

方式二:Content Collections 方案(Contentlayer / Velite / 自建)

将 MDX 文件作为数据源,在构建时解析为类型安全的数据集合:

  • 优点:frontmatter 类型安全、自动生成目录、支持分类/标签/搜索
  • 缺点:需要额外配置、构建时处理

适合:博客、文档站、知识库等需要管理大量内容的场景。

2.4 remark 和 rehype 插件生态

MDX 的强大在于其插件生态。remark 处理 Markdown 层面的转换,rehype 处理 HTML 层面的转换:

常用 remark 插件

插件功能用途
remark-gfmGitHub Flavored Markdown表格、任务列表、删除线
remark-mathLaTeX 数学公式技术/学术文档
remark-toc自动生成目录长文章导航
remark-reading-time计算阅读时间博客文章元信息

常用 rehype 插件

插件功能用途
rehype-pretty-code代码高亮(基于 Shiki)技术博客必备
rehype-slug给标题添加 id锚点链接
rehype-autolink-headings标题自动链接文档站导航
rehype-katex渲染数学公式配合 remark-math

插件执行顺序很重要:remark 插件先执行(处理 Markdown AST),然后 rehype 插件执行(处理 HTML AST)。同类插件按数组顺序依次执行。

2.5 自定义组件映射

MDX 最强大的特性之一是组件映射——你可以替换任何 Markdown 元素的默认渲染:

// mdx-components.tsx
const components = {
  h1: (props) => <h1 className="text-4xl font-bold mt-8 mb-4" {...props} />,
  h2: (props) => <h2 className="text-2xl font-semibold mt-6 mb-3" {...props} />,
  code: (props) => <code className="bg-muted px-1.5 py-0.5 rounded" {...props} />,
  pre: (props) => <CodeBlock {...props} />,         // 自定义代码块组件
  img: (props) => <Image {...props} loading="lazy" />, // 自动优化图片
  a: (props) => <SmartLink {...props} />,            // 外链自动新窗口打开
  table: (props) => <div className="overflow-x-auto"><table {...props} /></div>,
  Callout,     // 自定义提示框
  Tabs,        // 自定义标签页
  Steps,       // 自定义步骤组件
}

这个映射机制意味着:你可以在不修改任何 MDX 源文件的情况下,完全改变内容的渲染样式和交互行为。迁移主题?只需要修改组件映射。

3. Content Collections:类型安全的内容管理

3.1 为什么需要 Content Collections

当你有 50 篇博客文章时,你需要:

  • 列出所有文章并按日期排序
  • 按分类/标签筛选
  • 验证每篇文章的 frontmatter(标题、日期、作者不能缺失)
  • 生成 RSS feed
  • 自动生成 sitemap

单纯的 @next/mdx 做不到这些——它把每个 MDX 文件当作独立页面,没有"集合"的概念。Content Collections 就是来解决这个问题的。

3.2 方案对比

方案状态类型安全性能维护
Contentlayer⚠️ 维护停滞⭐⭐⭐⭐⭐社区 fork 维护
Velite✅ 活跃⭐⭐⭐⭐⭐Zod 验证
自建(fs + gray-matter)✅ 永远可用⭐⭐⭐最灵活需要自己写
Fumadocs✅ 活跃⭐⭐⭐⭐文档站专用

2025 年推荐

  • 博客/通用内容 → Velite(Contentlayer 的精神继承者,基于 Zod Schema)
  • 文档站 → Fumadocs(专为文档设计,内置搜索、侧边栏、版本控制)
  • 完全控制 → 自建方案(适合对性能和行为有极致要求的场景)

3.3 Content Collections 的工作原理

无论用哪个库,核心流程一致:

构建时(build time):

1. 扫描内容目录
   blog/
   ├── hello-world.mdx
   ├── nextjs-tips.mdx
   └── react-patterns.mdx

2. 解析每个文件
   - 提取 frontmatter(标题、日期、标签等)
   - 用 Schema 验证(Zod / 自定义)
   - 编译 MDX → React 组件

3. 生成类型定义
   type Post = {
     title: string
     date: Date
     tags: string[]
     slug: string
     content: React.ReactElement
   }

4. 导出数据集合
   export const posts: Post[]
   → 可以在 Server Components 中直接 import 使用

3.4 自建 Content Collections 的核心实现思路

自建方案的核心只需要三步:

  1. 读取文件:用 fs.readdir 扫描目录,fs.readFile 读取内容
  2. 解析 frontmatter:用 gray-matter 分离 frontmatter 和 MDX 内容
  3. 编译 MDX:用 @mdx-js/mdxcompile 函数或 next-mdx-remote 的运行时编译

这种方案的优势是零外部依赖风险——不会因为某个库停止维护而影响你的项目。缺点是需要自己处理缓存、增量编译等性能优化。

4. Headless CMS:内容与展示分离

4.1 什么是 Headless CMS

传统 CMS(如 WordPress)是"有头"的——它同时管理内容和展示(前端)。Headless CMS 只管内容("无头"),通过 API 输出内容,前端可以是任何技术栈。

传统 CMS:    内容编辑 → 数据库 → 模板引擎 → HTML → 浏览器
Headless CMS: 内容编辑 → 数据库 → API → Next.js → HTML → 浏览器

4.2 主流 Headless CMS 对比

CMS类型特点价格适用场景
SanitySaaS实时协作、GROQ 查询、自定义 Schema免费层 + 按量需要极高灵活性
ContentfulSaaS企业级、多语言、工作流免费层有限大型企业站
Strapi自托管开源、完全可控、插件丰富免费(自托管)不想依赖 SaaS
Payload CMS自托管TypeScript 原生、Next.js 原生集成免费(自托管)与 Next.js 深度绑定
Keystatic混合Git-based + 可视化编辑免费小团队、开源项目
Tina CMS混合Git-based、实时可视编辑免费层内容实时预览

4.3 SaaS CMS vs 自托管 CMS

维度SaaS CMS (Sanity/Contentful)自托管 CMS (Strapi/Payload)
部署无需运维需要服务器和数据库
数据所有权在第三方服务器完全自己控制
自定义能力受限于平台 API完全可定制
扩展性靠平台扩容自己管理
成本内容多时可能昂贵服务器成本固定
上手速度快(注册即用)慢(需要部署)
协作功能通常内置需要自建或插件

4.4 Payload CMS:Next.js 的天然搭档

Payload CMS 值得单独介绍,因为它从 v3 开始直接运行在 Next.js 内部——不是通过 API 对接,而是作为 Next.js 的一部分:

为什么 Payload 特殊

  1. 共享同一个 Next.js 应用:Admin Panel 和前端网站在同一个 next.config 下运行
  2. 直接 import 数据:不需要 fetch API,Server Components 中直接调用 Payload 的查询方法
  3. 共享类型:Collection 的 Schema 自动生成 TypeScript 类型,前后端共用
  4. 共享认证:Payload 的 Auth 可以同时服务 Admin 和前端用户
  5. 零 API 开销:数据查询在同一个进程内完成,没有网络请求

适用场景:需要同时有内容管理后台和前端展示的项目(企业官网、电商、SaaS 落地页)。

4.5 CMS 集成模式

无论选择哪个 CMS,与 Next.js 的集成有三种模式:

模式一:构建时获取(SSG)

CMS 内容 → build 时 fetch → 静态页面
优点:最快的页面加载速度
缺点:内容更新需要重新构建
适合:更新不频繁的内容(关于页、文档)

模式二:按需重验证(ISR)

CMS 内容 → 首次请求时 fetch → 缓存 → CMS Webhook 触发 revalidate
优点:兼顾性能和实时性
缺点:有短暂的缓存延迟
适合:博客、新闻、产品更新

模式三:请求时获取(SSR)

CMS 内容 → 每次请求都 fetch → 动态渲染
优点:内容实时更新
缺点:每次请求都有延迟
适合:个性化内容、A/B 测试

最佳实践大部分内容用 ISRrevalidate: 3600),CMS 内容更新时通过 Webhook 调用 revalidatePathrevalidateTag 主动刷新缓存。

5. 博客系统架构

5.1 技术博客的核心需求

功能必要性实现方式
Markdown 渲染✅ 必须MDX + rehype-pretty-code
代码高亮✅ 必须Shiki(支持 VS Code 主题)
目录导航(TOC)✅ 必须构建时提取标题生成
分类/标签✅ 必须frontmatter 定义
搜索✅ 推荐Algolia / Flexsearch / Pagefind
RSS Feed✅ 推荐构建时生成 XML
SEO(Open Graph)✅ 推荐generateMetadata
阅读时间可选remark-reading-time
评论系统可选Giscus(GitHub Discussions)
系列/专栏可选frontmatter 中定义 series
暗色模式可选next-themes + Tailwind dark:

5.2 文档站的核心需求

文档站和博客有本质区别——博客是按时间排序的文章流,文档是按结构组织的知识树

功能博客文档站
导航结构时间线 + 分类侧边栏树形菜单
内容关系独立文章上下文关联(上一章/下一章)
版本控制不需要多版本文档
搜索权重全文搜索标题/API 优先
更新频率不频繁频繁(跟随产品版本)

文档站推荐方案

方案特点适用场景
FumadocsNext.js 原生、App Router、内置搜索Next.js 项目文档
Nextra简洁、zero-config小型文档站
DocusaurusReact 生态最成熟大型开源项目
VitePressVue 生态、极快Vue 项目文档

5.3 搜索方案对比

方案类型成本性能适用规模
Pagefind静态搜索免费极快(WASM)小中型
Flexsearch客户端搜索免费小型
AlgoliaSaaS 搜索免费层 + 付费最快大型
Orama全栈搜索免费 / 付费中大型
Meilisearch自托管搜索免费(自托管)中大型

推荐

  • 静态博客 → Pagefind(零成本、构建时自动索引、WASM 运行)
  • 中大型文档站 → Algolia DocSearch(免费给开源项目使用)
  • 需要完全控制 → Meilisearch 自托管

6. 内容工作流与协作

6.1 内容发布流程

对于 Git-based 内容管理(MDX 文件在仓库中),推荐使用 Git 工作流管理内容发布:

作者写文章(新建分支)
    ↓
提交 PR → 自动检查(Markdown lint、链接检查、构建预览)
    ↓
编辑 Review → 修改建议
    ↓
合并到 main → 自动部署
    ↓
Vercel 构建 → 文章上线

这个流程的优势:

  • 版本控制:每篇文章的每次修改都有历史记录
  • 协作 Review:编辑可以在 PR 中对具体段落提建议
  • Preview Deployments:合并前可以看到文章的真实渲染效果
  • 回滚:发现问题可以 git revert

6.2 内容质量自动化

# .github/workflows/content-check.yml — 内容质量检查
name: Content Quality
on: [pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check links         # 检查死链
        uses: lycheeverse/lychee-action@v1
      - name: Markdown lint       # Markdown 格式检查
        uses: DavidAnson/markdownlint-cli2-action@v14
      - name: Check spelling      # 拼写检查
        uses: streetsidesoftware/cspell-action@v5
      - name: Build preview       # 确保能正常构建
        run: pnpm build

6.3 内容与代码的边界

一个常见的架构问题:内容应该放在代码仓库里还是分离出去?

放在代码仓库分离到 CMS
内容和代码一起版本控制内容独立于代码部署
开发者编辑最方便非技术人员可以编辑
PR Review 流程CMS 自带工作流
构建时处理,无运行时依赖需要 API 调用(有延迟)
适合:技术博客、文档适合:营销站、新闻、频繁更新

混合模式:有些项目同时使用两种——技术文档用 MDX 在代码仓库中,营销内容用 Sanity 管理。Next.js 的 App Router 完美支持这种混合架构——不同的路由可以使用不同的数据源。

7. 性能优化

7.1 MDX 编译性能

大量 MDX 文件会影响构建时间。优化策略:

策略效果复杂度
增量编译(只编译改动的文件)显著减少构建时间中(Velite 内置)
代码高亮使用 Shiki 而非 Prism构建时高亮,零运行时 JS
图片使用 next/image 自动优化自动压缩 + WebP + 懒加载
禁用不需要的 remark/rehype 插件减少 AST 遍历

7.2 CMS 内容缓存

对接 Headless CMS 时,缓存策略至关重要:

层级          缓存位置        TTL          刷新方式
─────────────────────────────────────────────────
CDN 缓存      Vercel Edge     1h-24h       Webhook → revalidate
数据缓存      Next.js Cache   按 tag       revalidateTag
内存缓存      进程内          请求级       自动

最佳实践

  • revalidateTag 而非 revalidatePath——tag 粒度更细,可以精确控制哪些内容需要刷新
  • CMS 发布内容后通过 Webhook 调用 Next.js 的 revalidation API
  • 给 CMS 查询加 next: &#123; tags: ['posts'] &#125; 标记,方便按 tag 刷新

本章小结

  • 四种模式:代码即内容(MDX)、Git-based CMS、API-first CMS、自建 CMS——根据"谁写内容"和"存在哪"选择
  • MDX 核心:Markdown + JSX,编译后是 React 组件,通过 remark/rehype 插件扩展能力
  • Content Collections:2025 年推荐 Velite,自建方案最稳定,Contentlayer 维护停滞需注意
  • Headless CMS 选型:快速上线选 Sanity/Contentful,完全控制选 Payload/Strapi,Git-based 选 Tina
  • Payload CMS:唯一一个直接运行在 Next.js 内部的 CMS,共享类型和认证,零 API 开销
  • 集成模式:大部分场景用 ISR + Webhook 按需刷新,兼顾性能和实时性
  • 搜索方案:静态站用 Pagefind(免费 + WASM),大站用 Algolia
  • 内容工作流:Git-based 内容用 PR 流程管理,配合自动化检查(链接、拼写、构建)