MDX

2026年9月6日

MDX = Markdown + JSX。文档、博客既要标题列表,又要可交互的演示组件时,比「纯 md + 另开一个 tsx」省事。和上一篇的静态导出经常一起用。

本仓库笔记走的是 mdx-bundlerdocs/ 下的 .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 里覆盖 h1lipre 等,全站 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


参考文档