MDX 是什么

Markdown + JSX = MDX——能在 markdown 文件里直接嵌 React 组件

# My Post

This is regular markdown.

import Chart from '@/components/Chart';

<Chart data={[1, 2, 3]} />

More markdown after the component.

npm install @next/mdx @mdx-js/loader @mdx-js/react

配置 next.config.ts

import createMDX from '@next/mdx';

const withMDX = createMDX({
    extension: /\.mdx?$/,
});

const nextConfig = {
    pageExtensions: ['ts', 'tsx', 'md', 'mdx'],
};

export default withMDX(nextConfig);

在 app/ 里直接放 .mdx

src/app/
├── blog/
│   ├── page.tsx              ← 博客列表
│   └── hello/
│       └── page.mdx          ← /blog/hello

src/app/blog/hello/page.mdx:

# Hello World

Welcome to my first post.

## Code

Here's some code:

```js
console.log('hi');

访问 `/blog/hello` 看到渲染好的 markdown。

## 带 frontmatter(推荐)

但 Next.js 原生 MDX 没自带 frontmatter——用 `gray-matter` + 自定义加载:

```bash
npm install gray-matter

或更简单:把 metadata 直接 export:

export const metadata = {
    title: 'Hello',
    date: '2026-05-09',
    description: 'My first post',
};

# Hello World

Body...

列表所有博客(生成博客索引)

// src/app/blog/page.tsx
import { readdirSync } from 'fs';
import { join } from 'path';
import Link from 'next/link';

async function getPosts() {
    const dir = join(process.cwd(), 'src/app/blog');
    const entries = readdirSync(dir, { withFileTypes: true });
    const slugs = entries.filter(e => e.isDirectory()).map(e => e.name);

    const posts = await Promise.all(slugs.map(async slug => {
        const { metadata } = await import(`./${slug}/page.mdx`);
        return { slug, ...metadata };
    }));

    return posts.sort((a, b) => b.date.localeCompare(a.date));
}

export default async function BlogIndex() {
    const posts = await getPosts();
    return (
        <ul className="space-y-4">
            {posts.map(p => (
                <li key={p.slug}>
                    <Link href={`/blog/${p.slug}`} className="text-xl hover:underline">
                        {p.title}
                    </Link>
                    <p className="text-gray-500 text-sm">{p.date}</p>
                </li>
            ))}
        </ul>
    );
}

自定义组件(如更漂亮的代码块)

src/mdx-components.tsx:

import type { MDXComponents } from 'mdx/types';

export function useMDXComponents(components: MDXComponents): MDXComponents {
    return {
        h1: ({ children }) => (
            <h1 className="text-4xl font-bold mt-8 mb-4">{children}</h1>
        ),
        h2: ({ children }) => (
            <h2 className="text-2xl font-bold mt-6 mb-3">{children}</h2>
        ),
        a: ({ href, children }) => (
            <a href={href} className="text-blue-600 hover:underline">{children}</a>
        ),
        code: ({ children }) => (
            <code className="bg-gray-100 px-1 py-0.5 rounded">{children}</code>
        ),
        ...components,
    };
}

所有 .mdx 文件里的元素都用这套样式。

代码块语法高亮

npm install rehype-pretty-code shiki

next.config.ts:

import createMDX from '@next/mdx';
import rehypePrettyCode from 'rehype-pretty-code';

const withMDX = createMDX({
    extension: /\.mdx?$/,
    options: {
        rehypePlugins: [
            [rehypePrettyCode, { theme: 'github-dark' }],
        ],
    },
});

之后 .mdx 里 ```js 代码块会有漂亮高亮。

在 MDX 里嵌组件

import { LineChart } from '@/components/LineChart';
import { Callout } from '@/components/Callout';

# My Post

<Callout type="warning">
    注意:此章节假设你已读过前面。
</Callout>

数据可视化:

<LineChart data={[10, 20, 30, 25, 40]} />

继续 markdown 内容...

替代方案:fumadocs / nextra

如果只是想要"开箱即用的博客 / 文档站"——这两个框架基于 Next.js 内置了一切(侧栏 / 搜索 / TOC):

npx create-fumadocs-app

  • MDX 里 import 路径用 alias(@/)—— 不要相对路径
  • frontmatter 不是原生支持——要用 export const metadata
  • 代码块高亮构建期跑——大量代码块构建慢,考虑用 bright / @shikijs/transformers
  • MDX 里不能有 < > 的字面字符——要转义或包代码块

下一篇:Server Actions 提交表单。