CMS 内容管理与 MDX
内容是产品的灵魂。无论是技术博客、产品文档、营销落地页还是帮助中心,都需要一个高效的内容管理方案。Next.js 生态在这个领域有极其丰富的选择——从代码中直接写 MDX,到对接 Headless CMS,到构建完整的内容平台。本章从内容管理的本质需求出发,深入分析每种方案的原理、适用场景和技术权衡。
1. 内容管理的本质问题
1.1 三个核心问题
每个内容管理系统都在解决三个问题:
| 问题 | 含义 | 影响决策 |
|---|---|---|
| 谁写内容? | 开发者?运营?产品经理? | 决定编辑界面需求 |
| 内容存在哪? | Git 仓库?数据库?第三方 CMS? | 决定架构和部署模式 |
| 内容如何渲染? | 构建时?请求时?边缘? | 决定性能和实时性 |
1.2 四种内容管理模式
| 模式 | 内容存储 | 编辑方式 | 适用场景 | 代表方案 |
|---|---|---|---|---|
| 代码即内容 | Git 仓库中的 MD/MDX 文件 | 代码编辑器 + PR | 技术博客、文档 | MDX + Contentlayer |
| Git-based CMS | Git 仓库,但有可视化编辑器 | 在线编辑器 | 非技术人员也要编辑 | 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-gfm | GitHub Flavored Markdown | 表格、任务列表、删除线 |
remark-math | LaTeX 数学公式 | 技术/学术文档 |
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 的核心实现思路
自建方案的核心只需要三步:
- 读取文件:用
fs.readdir扫描目录,fs.readFile读取内容 - 解析 frontmatter:用
gray-matter分离 frontmatter 和 MDX 内容 - 编译 MDX:用
@mdx-js/mdx的compile函数或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 | 类型 | 特点 | 价格 | 适用场景 |
|---|---|---|---|---|
| Sanity | SaaS | 实时协作、GROQ 查询、自定义 Schema | 免费层 + 按量 | 需要极高灵活性 |
| Contentful | SaaS | 企业级、多语言、工作流 | 免费层有限 | 大型企业站 |
| 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 特殊:
- 共享同一个 Next.js 应用:Admin Panel 和前端网站在同一个
next.config下运行 - 直接 import 数据:不需要 fetch API,Server Components 中直接调用 Payload 的查询方法
- 共享类型:Collection 的 Schema 自动生成 TypeScript 类型,前后端共用
- 共享认证:Payload 的 Auth 可以同时服务 Admin 和前端用户
- 零 API 开销:数据查询在同一个进程内完成,没有网络请求
适用场景:需要同时有内容管理后台和前端展示的项目(企业官网、电商、SaaS 落地页)。
4.5 CMS 集成模式
无论选择哪个 CMS,与 Next.js 的集成有三种模式:
模式一:构建时获取(SSG)
CMS 内容 → build 时 fetch → 静态页面
优点:最快的页面加载速度
缺点:内容更新需要重新构建
适合:更新不频繁的内容(关于页、文档)
模式二:按需重验证(ISR)
CMS 内容 → 首次请求时 fetch → 缓存 → CMS Webhook 触发 revalidate
优点:兼顾性能和实时性
缺点:有短暂的缓存延迟
适合:博客、新闻、产品更新
模式三:请求时获取(SSR)
CMS 内容 → 每次请求都 fetch → 动态渲染
优点:内容实时更新
缺点:每次请求都有延迟
适合:个性化内容、A/B 测试
最佳实践:大部分内容用 ISR(revalidate: 3600),CMS 内容更新时通过 Webhook 调用 revalidatePath 或 revalidateTag 主动刷新缓存。
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 优先 |
| 更新频率 | 不频繁 | 频繁(跟随产品版本) |
文档站推荐方案:
| 方案 | 特点 | 适用场景 |
|---|---|---|
| Fumadocs | Next.js 原生、App Router、内置搜索 | Next.js 项目文档 |
| Nextra | 简洁、zero-config | 小型文档站 |
| Docusaurus | React 生态最成熟 | 大型开源项目 |
| VitePress | Vue 生态、极快 | Vue 项目文档 |
5.3 搜索方案对比
| 方案 | 类型 | 成本 | 性能 | 适用规模 |
|---|---|---|---|---|
| Pagefind | 静态搜索 | 免费 | 极快(WASM) | 小中型 |
| Flexsearch | 客户端搜索 | 免费 | 快 | 小型 |
| Algolia | SaaS 搜索 | 免费层 + 付费 | 最快 | 大型 |
| 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 build6.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: { tags: ['posts'] }标记,方便按 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 流程管理,配合自动化检查(链接、拼写、构建)