为什么我抛弃了 Sanity CMS,转而使用 MDX(从此不再回头)
Source: Dev.to
请提供您希望翻译的具体文本内容,我会按照要求将其翻译成简体中文并保留原始的格式、Markdown 语法以及技术术语。谢谢!
设置:我最初为何选择 Sanity
当我第一次构建这个作品集时,Sanity CMS 似乎是管理博客内容的显而易见的选择:
- 可视化编辑器 – 一个友好的所见即所得写作界面
- 结构化内容 – 基于 schema 的内容建模
- 实时协作 – 虽然我只有一个作者 😅
- CDN 托管的图片 – 自动图片优化
- Webhook 重新验证 – 内容更改时按需 ISR
它运行良好。但随着时间推移,问题开始显现。
临界点:我为何决定离开
1. 单作者的额外负担
我在为……自己运行整套 CMS 基础设施。Sanity Studio 增加了路由、依赖和复杂度,感觉越来越没有必要:
src/sanity/
├── env.ts
├── lib/
│ ├── client.ts
│ ├── image.ts
│ └── queries.ts
├── schemaTypes/
│ ├── authorType.ts
│ ├── blockContentType.ts
│ ├── categoryType.ts
│ └── postType.ts
└── structure.ts
所有这些基础设施,只是为了一个可以用普通 markdown 文件实现的需求。
2. 外部依赖问题
每次想写作时,我都必须:
- 打开我的站点
- 访问
/studio - 等待 Sanity Studio 加载
- 在它们的编辑器里写作
- 希望 webhook 能正确触发以进行重新验证
我的内容托管在别人的服务器上。如果 Sanity 调整价格、出现宕机,或下线某个功能,我就会手忙脚乱。
3. 代码块很麻烦
作为一名编写技术内容的开发者,代码块是必不可少的。Sanity 的 Portable Text 格式需要自定义序列化器,而让语法高亮正常工作总是个挑战:
// Old Sanity code block serializer – verbose and fragile
const CodeBlock = ({ value }: { value: CodeBlockValue }) => {
return (
<pre>
<code>{value.code}</code>
</pre>
);
};
使用 MDX,则只需要……markdown:
```typescript
const greeting = "Hello, World!";
```
4. 版本控制?哪来的版本控制?
我的代码在 Git 中。我的内容在 Sanity 中。两个真相来源,零统一历史。我无法轻松地:
- 在 PR 中审查内容变更
- 将文章回滚到之前的版本
- 查看内容变更与代码变更的对应关系
解决方案:使用 Next.js 的 MDX
MDX 为你提供了两全其美的体验:Markdown 的简洁与 React 的强大。下面是我的搭建步骤。
第 1 步 – 安装依赖
pnpm add @next/mdx @mdx-js/loader @mdx-js/react
pnpm add remark-gfm rehype-slug rehype-prism-plus
- @next/mdx – 官方的 Next.js MDX 集成
- remark-gfm – GitHub 风格的 Markdown(表格、删除线等)
- rehype-slug – 为标题自动生成 ID(用于目录)
- rehype-prism-plus – 使用 Prism.js 进行语法高亮
第 2 步 – 配置 Next.js
// next.config.mjs
import createMDX from '@next/mdx';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
import rehypePrismPlus from 'rehype-prism-plus';
const nextConfig = {
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
// …other config
};
const withMDX = createMDX({
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypeSlug, [rehypePrismPlus, { ignoreMissing: true }]],
},
});
export default withMDX(nextConfig);
第 3 步 – 创建 MDX 组件
项目根目录下的 mdx-components.tsx 文件用于自定义 MDX 元素的渲染方式:
// mdx-components.tsx
import type { MDXComponents } from 'mdx/types';
import Image from 'next/image';
import Link from 'next/link';
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// 自动生成 ID 的自定义标题
h2: ({ children, id }) => (
<h2 id={id}>## {children}</h2>
),
// 智能链接:内部链接 vs 外部链接
a: ({ href, children }) => {
if (href?.startsWith('/')) {
return <Link href={href}>{children}</Link>;
}
return (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
},
// 可访问的代码块
pre: ({ children }) => (
<pre className="code-block">
{children}
</pre>
),
...components,
};
}
第 4 步 – 组织内容结构
每篇博客文章现在都是一个简单的 .mdx 文件,并导出元数据:
content/
└── blog/
├── my-first-post.mdx
├── another-post.mdx
└── this-post.mdx
典型文件示例:
export const metadata = {
title: 'My Blog Post',
slug: 'my-blog-post',
publishedAt: '2025-01-01',
categories: ['nextjs', 'react'],
coverImage: '/images/blog/my-post.avif',
author: {
name: 'Akshay Gupta',
avatar: '/images/blog-author.png',
},
excerpt: 'A short description of the post.',
};
TL;DR
为单个作者运行完整的 CMS 增加了不必要的复杂性。
切换到 MDX 给我带来了:
- 零外部依赖 – 所有内容都在仓库中
- 原生 Git 历史 – 内容更改是 PR 的一部分
- 代码更简洁 – 不需要自定义代码块序列化器
- 快速可靠的构建 – 不会在运行时从远程 API 拉取
如果你在维护个人博客或小型站点,考虑放弃笨重的 CMS,让 MDX 来承担重活。简约才是终极的精致。
介绍
您的 markdown 内容放在这里…
第5步:构建 MDX 实用工具
我创建了一个小型实用库来处理博客操作:
// src/lib/mdx/index.ts
import fs from 'fs';
import path from 'path';
const CONTENT_DIR = path.join(process.cwd(), 'content', 'blog');
export function getBlogSlugs(): string[] {
const files = fs.readdirSync(CONTENT_DIR);
return files
.filter((file) => file.endsWith('.mdx'))
.map((file) => file.replace(/\.mdx$/, ''));
}
export async function getBlogBySlug(slug: string) {
const { metadata } = await import(`@/content/blog/${slug}.mdx`);
const rawContent = fs.readFileSync(
path.join(CONTENT_DIR, `${slug}.mdx`),
'utf-8'
);
const readingTime = calculateReadingTime(rawContent);
return { metadata, slug, readingTime };
}
第6步:渲染博客页面
动态路由直接导入并渲染 MDX:
// src/app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await getBlogBySlug(slug);
// Dynamic import of the MDX content
const { default: MDXContent } = await import(
`@/content/blog/${slug}.mdx`
);
return (
<article>
<h1>{post.metadata.title}</h1>
<MDXContent />
</article>
);
}
export async function generateStaticParams() {
const slugs = getBlogSlugs();
return slugs.map((slug) => ({ slug }));
}
迁移概述:有哪些变化
删除的内容(约 12 000 行)
- 整个
src/sanity/目录 - Sanity Studio 路由(
/studio) - Webhook 重新验证端点
- Portable Text 序列化器
- 8 个与 Sanity 相关的 npm 包
新增的内容(约 7 500 行)
content/blog/中的 7 篇 MDX 博客文章src/lib/mdx/中的 MDX 工具- 自定义 MDX 组件
- Prism.js 语法高亮主题
public/images/blog/中的封面图片
总体结果: 代码行数减少约 4 700 行——代码更少,bug 更少,维护更简洁。
我现在正在享受的好处
-
随处编写 – 我最喜欢的 Markdown 编辑器,VS Code、Obsidian,甚至在紧急情况下使用
neovim。无需浏览器。 -
Git 原生内容 – 每篇文章都受版本控制。我可以查看完整历史,为草稿创建分支,并在 PR 中与代码一起审查内容更改。
-
极速构建 – 构建期间没有 API 调用。所有操作都是本地文件系统读取,因此构建速度明显更快。
-
真正的所有权 – 内容存放在我的仓库中。没有供应商锁定,没有意外的价格变动,也没有外部依赖。
-
更好的代码块 – 使用 Dracula 主题的 Prism.js,自动语言检测,键盘可访问的代码区域。它就是这么好用:
// Look ma, beautiful syntax highlighting! const sum = (a: number, b: number): number => a + b; -
在 Markdown 中使用 React 组件 – 需要自定义提示框?交互式演示?只需导入并使用:
import { InteractiveDemo } from '@/components/Demo'; {/* Here’s a live demo: */} <InteractiveDemo />
Source: …
注意事项与解决方案
1. OpenGraph 图片需要 Node.js 运行时
OG 图片生成器使用 fs 读取 MDX 文件,但 Next.js 的图片路由默认使用 Edge 运行时。通过强制使用 Node.js 运行时来解决:
// src/app/blog/[slug]/opengraph-image.tsx
export const runtime = 'nodejs';
2. 阅读时间计算
在 Sanity 中我可以查询计算字段。使用 MDX 时,我从原始内容中计算阅读时间:
export function calculateReadingTime(content: string) {
const text = content
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
.replace(/`[^`]*`/g, '') // Remove inline code
.replace(/]*>/g, ''); // Remove HTML
const words = text.split(/\s+/).filter(Boolean).length;
const minutes = Math.ceil(words / 200);
return { text: `${minutes} min read`, minutes, words };
}
3. 目录生成
由于没有来自 Sanity 的结构化 AST,我使用简单的正则表达式提取标题:
export function extractHeadings(content: string) {
const headingRegex = /^(#{1,4})\s+(.+)$/gm;
const headings: { id: string; text: string; level: number }[] = [];
let match;
while ((match = headingRegex.exec(content)) !== null) {
const level = match[1].length;
const text = match[2].trim();
const id = text.toLowerCase().replace(/\s+/g, '-');
headings.push({ id, text, level });
}
return headings;
}
是否应该切换?
MDX 完美适用于以下情况:
- 是单独作者或小团队
- 编写带有代码块的技术内容
- 希望内容在版本控制中
- 重视简洁胜于功能丰富
- 对 markdown 感到熟悉
如果您更适合使用 CMS,请考虑以下情况:
- 拥有非技术内容编辑者
- 需要复杂的工作流和审批
- 需要实时协作
- 想要可视化编辑体验
结论
从 Sanity 迁移到 MDX 就像清理一个杂乱的衣橱。立刻的好处显而易见:东西更少,空间更大,寻找东西更容易。真正的乐趣来自于每天仅仅…写作的体验。
没有仪表盘。没有加载旋转图标。没有“同步内容”。只有我、我的编辑器和 Markdown——这才是博客应有的方式。
这个完整博客系统的代码已开源于
github.com/gupta-akshay/portfolio-v2。欢迎随意“偷走”。 🚀
“完美的实现不是因为没有更多可添加的东西,而是因为已经没有可以再去除的东西。” – 安托万·德·圣埃克苏佩里