MDX
2026年9月6日
MDX = Markdown + JSX。文档、博客既要标题列表,又要可交互的演示组件时,比「纯 md + 另开一个 tsx」省事。和上一篇的静态导出经常一起用。
本仓库笔记走的是 mdx-bundler 读 docs/ 下的 .md,和官方 @next/mdx 是另一条路。下面按官方脚手架写法记。
安装和配置
pnpm add @next/mdx @mdx-js/loader @mdx-js/react
pnpm add -D @types/mdx
bash
import type { NextConfig } from "next";
import createMDX from "@next/mdx";
const withMDX = createMDX({
// extension: /\.(md|mdx)$/,
});
const nextConfig: NextConfig = {
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};
export default withMDX(nextConfig);
ts
默认只认 .mdx。连 .md 当页面用,把上面注释打开。
项目根放 mdx-components.tsx(或 src/ 下,看你的目录):
import type { MDXComponents } from "mdx/types";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return components;
}
tsx
当页面用
app/notes/page.mdx
text
里面可以混 Markdown、HTML、JSX:
# 欢迎
这段是 **粗体**,这段是 code。
- 一项
- 二项
div.bg-red-500 自定义块
text
编辑器装 MDX 插件,高亮和校验会好很多。
引入自己的组件
自定义组件的 import 语句和后面的 Markdown 空一行,贴在一起容易解析失败:
import Counter from "./counter";
# 欢迎
Counter 组件
text
复杂交互写在客户端组件里:
"use client";
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN((x) => x + 1)}>{n}</button>;
}
tsx
MDX 文件本身更适合排版,别把整页状态机塞进去。
全局换标签样式
useMDXComponents 里覆盖 h1、li、pre 等,全站 MDX 一处改:
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
h1: ({ children }) => (
<h1 className="text-2xl font-bold">{children}</h1>
),
li: ({ children }) => <li className="ml-4 list-disc">{children}</li>,
...components,
};
}
tsx
远程 MDX
内容在 CMS / 对象存储时,页面仍是 tsx,把字符串喂给远程渲染器:
pnpm add next-mdx-remote-client
bash
import { MDXRemote } from "next-mdx-remote-client/rsc";
export default async function Page() {
const source = await fetch("https://example.com/note.mdx").then((r) =>
r.text()
);
return <MDXRemote source={source} />;
}
tsx
远程内容等于在跑别人的 JSX,来源必须可信;生产环境建议白名单组件,不要裸跑任意标签。
下一篇:Server Actions,表单提交可以不先开 route.ts。