MDX 深度指南

MDX 是 Markdown + JSX 的融合产物,它允许在 Markdown 文档中直接使用 React 组件,实现内容与交互的完美统一。本文全面解析 MDX 的核心概念、在主流框架中的使用、组件嵌入技术以及工具链生态。

目录


MDX 核心概念

什么是 MDX

MDX(Markdown + JSX)是一种可移植的文档格式,它将 Markdown 的简洁语法与 JSX 的强大组件能力结合在一起。MDX 文件本质上是一个 JavaScript 模块,导出一个 React 组件,可以在其中使用 Markdown 语法和任意 React 组件。

import { Chart } from './components/Chart';
 
# 销售报告
 
这是一个交互式图表:
 
<Chart data={salesData} type="line" />
 
上面的图表展示了 Q1 的销售趋势。

MDX 工作原理

MDX 的编译过程包含以下步骤:

MDX Source → MDX Compiler → JSX → React.createElement → DOM
  1. 解析阶段:MDX 编译器解析 Markdown 和 JSX 语法
  2. 转换阶段:将 Markdown 转换为 JSX 元素
  3. 编译阶段:生成可执行的 JavaScript 代码
  4. 渲染阶段:React 环境渲染最终组件
// MDX 编译器核心 API
import { compile } from '@mdx-js/mdx';
 
const code = await compile(`# Hello World
 
<Button>Click me</Button>
`, {
  outputFormat: 'function-body',
  development: false
});
 
// 输出 JSX 代码字符串
console.log(code);

MDX vs 标准 Markdown

特性MarkdownMDX
组件嵌入不支持完全支持
JavaScript 表达式不支持支持
导入导出不支持支持
样式系统CSS 类CSS-in-JS、组件
交互能力静态动态交互
<!-- 标准 Markdown - 静态内容 -->
# 标题
这是一段文字
 
<!-- MDX - 可嵌入组件 -->
import { Chart } from './Chart';
 
# 标题
<Chart data={data} />

MDX 2.0 新特性

MDX 2.0 带来了重大升级:

  • 性能优化:更快的编译速度和更小的产物
  • ESM 原生支持:更好的模块化
  • 增强的插件 API:更灵活的扩展能力
  • 改进的 MDX pragma:更直观的配置方式

在 Next.js 中的使用

Next.js App Router 中的 MDX

在 App Router 架构下,MDX 文件可以作为页面组件直接使用:

// app/docs/page.mdx
import { Callout } from '@/components/ui/callout';
import { TOC } from '@/components/toc';
 
# 欢迎使用文档中心
 
<Callout type="info">
  这是一个重要的提示信息
</Callout>
 
## 目录
 
<TOC items={[
  { title: '安装', href: '#安装' },
  { title: '配置', href: '#配置' }
]} />
 
## 安装
 
首先安装必要依赖...
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  experimental: {
    mdxRs: true,  // 使用 Rust 编写的 MDX 编译器
  },
};
 
export default nextConfig;

使用 next-mdx-remote

next-mdx-remote 提供了更灵活的使用方式:

// pages/blog/[slug].tsx
import { serialize } from 'next-mdx-remote/serialize';
import { MDXRemote } from 'next-mdx-remote/rsc';
import remarkGfm from 'remark-gfm';
import rehypePrism from 'rehype-prism';
import { components } from '@/components/mdx-components';
 
export async function function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}
 
export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  const mdxSource = await serialize(post.content, {
    mdxOptions: {
      remarkPlugins: [remarkGfm],
      rehypePlugins: [rehypePrism],
    },
  });
 
  return (
    <article>
      <MDXRemote {...mdxSource} components={components} />
    </article>
  );
}

MDX 内容集合

使用 Content Collections 管理 MDX:

// lib/mdx.ts
import { compileMDX } from 'next-mdx-remote/rsc';
import { mdxComponents } from '@/components/mdx-components';
 
export async function getPost(slug: string) {
  const source = await fs.readFile(`content/posts/${slug}.mdx`);
  return compileMDX({
    source,
    components: mdxComponents,
    options: { parseFrontmatter: true },
  });
}
 
// types/blog.ts
import type { MDXRemoteProps } from 'next-mdx-remote/rsc';
 
export interface Post {
  title: string;
  date: string;
  author: string;
  tags: string[];
  excerpt: string;
}
 
export type PostFrontmatter = Post;
 
export type MdxContentProps = MDXRemoteProps<PostFrontmatter>;

在 Astro 中的使用

Astro MDX 集成

Astro 提供了原生的 MDX 支持:

npx astro add mdx
---
// src/layouts/BaseLayout.astro
interface Props {
  title: string;
  description?: string;
}
 
const { title, description } = Astro.props;
---
 
<!DOCTYPE html>
<html>
  <head>
    <title>{title}</title>
    <meta name="description" content={description} />
  </head>
  <body>
    <main>
      <slot />
    </main>
  </body>
</html>
---
// src/pages/docs/getting-started.mdx
layout: '../layouts/BaseLayout.astro'
title: 快速开始
description: 学习如何快速上手
---
 
# 快速开始
 
import { Steps } from '@/components/steps.astro';
import InfoCard from '@/components/InfoCard.astro';
 
<Steps>
  1. 安装依赖
  2. 配置项目
  3. 创建第一个页面
</Steps>
 
<InfoCard client:visible>
  这是一个可交互的卡片组件
</InfoCard>

Astro 中的组件孤岛

Astro 的孤岛架构允许选择性激活交互组件:

---
// 使用 client 指令控制水合策略
---
 
<!-- 默认不激活,仅静态渲染 -->
<StaticComponent />
 
<!-- 页面加载时激活 -->
<HydratedComponent client:load />
 
<!-- 可见时激活 -->
<InteractiveChart client:visible />
 
<!-- 仅在空闲时激活 -->
<HeavyWidget client:idle />
 
<!-- 仅在特定媒体查询时激活 -->
<MobileOnly client:media="(max-width: 768px)" />
// components/InteractiveChart.tsx
import { useState, useEffect } from 'react';
 
interface ChartProps {
  data: number[];
  labels: string[];
}
 
export function InteractiveChart({ data, labels }: ChartProps) {
  const [mounted, setMounted] = useState(false);
 
  useEffect(() => {
    setMounted(true);
  }, []);
 
  if (!mounted) {
    return <div class="chart-placeholder">Loading chart...</div>;
  }
 
  return (
    <div class="chart-container">
      <canvas id="myChart" />
    </div>
  );
}

组件嵌入与交互

MDX 组件设计模式

// components/mdx-components.tsx
import type { MDXComponents } from 'mdx/types';
import { CodeBlock } from './code-block';
import { InfoBox } from './info-box';
import { ImageGrid } from './image-grid';
import { VideoPlayer } from './video-player';
 
export const mdxComponents: MDXComponents = {
  // 覆盖默认元素
  h1: (props) => <h1 class="mdx-h1" {...props} />,
  h2: (props) => <h2 class="mdx-h2" {...props} />,
  pre: (props) => <CodeBlock {...props} />,
  img: (props) => <ImageGrid images={[props]} />,
 
  // 自定义组件
  Callout: InfoBox,
  Video: VideoPlayer,
  Gallery: ImageGrid,
 
  // 代码高亮主题
  code: ({ children, className, ...props }) => {
    const language = className?.replace('language-', '');
    return (
      <CodeBlock language={language} {...props}>
        {children}
      </CodeBlock>
    );
  },
};

可交互组件示例

// components/ColorPicker.tsx
import { useState } from 'react';
 
interface ColorPickerProps {
  defaultColor?: string;
  onChange?: (color: string) => void;
}
 
export function ColorPicker({ 
  defaultColor = '#3b82f6',
  onChange 
}: ColorPickerProps) {
  const [color, setColor] = useState(defaultColor);
  const [history, setHistory] = useState<string[]>([defaultColor]);
 
  const handleChange = (newColor: string) => {
    setColor(newColor);
    setHistory(prev => [...prev.slice(-9), newColor]);
    onChange?.(newColor);
  };
 
  return (
    <div class="color-picker">
      <div 
        class="preview" 
        style={{ backgroundColor: color }}
      >
        {color}
      </div>
      
      <input
        type="color"
        value={color}
        onChange={(e) => handleChange(e.target.value)}
      />
      
      <div class="history">
        {history.map((c, i) => (
          <button
            key={i}
            onClick={() => setColor(c)}
            style={{ backgroundColor: c }}
            aria-label={`选择颜色 ${c}`}
          />
        ))}
      </div>
    </div>
  );
}
import { ColorPicker } from '@/components/color-picker';
 
# 颜色选择器演示
 
选择一个你喜欢的颜色:
 
<ColorPicker 
  defaultColor="#6366f1"
  onChange={(color) => console.log('选中的颜色:', color)}
/>
 
你也可以点击历史记录快速选择之前使用过的颜色。

动态内容加载

// components/AsyncContent.tsx
import { useState, useEffect } from 'react';
 
interface AsyncContentProps {
  fetchUrl: string;
  fallback?: React.ReactNode;
  render: (data: any) => React.ReactNode;
}
 
export function AsyncContent({ 
  fetchUrl, 
  fallback = <p>加载中...</p>,
  render 
}: AsyncContentProps) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  useEffect(() => {
    setLoading(true);
    fetch(fetchUrl)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [fetchUrl]);
 
  if (loading) return fallback;
  if (error) return <p>加载失败: {error.message}</p>;
  return render(data);
}

MDX 工具链

核心编译工具

# 核心包
npm install @mdx-js/mdx @mdx-js/rollup remark-mdx
 
# GFM 支持
npm install remark-gfm
 
# 代码高亮
npm install rehype-prism @shikijs/rehype
// mdx.config.js
import { mdxAnnotations } from 'mdx-annotations';
import remarkGfm from 'remark-gfm';
import rehypeShiki from '@shikijs/rehype';
 
export default {
  plugins: [
    // 代码块注释语法支持
    mdxAnnotations.remark,
    // GitHub Flavored Markdown
    remarkGfm,
    // Shiki 代码高亮
    [rehypeShiki, { theme: 'github-dark' }],
  ],
};

Velite:下一代内容框架

Velite 是一个声明式的内容处理工具:

// velite.config.ts
import { defineConfig, defineCollection, s } from 'velite';
 
const posts = defineCollection({
  name: 'Post',
  pattern: 'posts/**/*.mdx',
  schema: s.object({
    title: s.string(),
    date: s.date(),
    tags: s.array(s.string()),
    draft: s.boolean().default(false),
  }),
  transform: (post) => ({
    ...post,
    slug: post._meta.filename.replace('.mdx', ''),
  }),
});
 
export default defineConfig({
  collections: { posts },
  output: {
    path: '.velite',
    name: 'index',
  },
});

MDX 与样式系统

// Tailwind CSS 集成
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    './**/*.mdx',
    './src/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      typography: {
        DEFAULT: {
          css: {
            '--tw-prose-body': '#334155',
            '--tw-prose-headings': '#0f172a',
          },
        },
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
  ],
};
<!-- 使用 prose 样式 -->
<div class="prose prose-slate max-w-none">
  <h1>标题</h1>
  <p>正文内容...</p>
</div>

最佳实践

项目结构组织

project/
├── content/
│   ├── docs/
│   │   ├── getting-started.mdx
│   │   ├── configuration.mdx
│   │   └── api-reference.mdx
│   └── blog/
│       └── *.mdx
├── components/
│   ├── mdx/
│   │   ├── Callout.tsx
│   │   ├── CodeBlock.tsx
│   │   └── Table.tsx
│   └── ui/
├── lib/
│   └── mdx.ts
└── mdx.config.js

性能优化

MDX 性能优化建议

  • 对静态内容使用 SSG,避免不必要的客户端水合
  • 使用 next-mdx-remote 的 RSC 版本减少包体积
  • 大型组件使用 React.lazy 延迟加载
  • 利用 client:idleclient:visible 指令控制激活时机
// 延迟加载大型组件
const HeavyChart = lazy(() => import('./HeavyChart'));
 
export function MDXContent({ content }) {
  return (
    <div>
      {content}
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart />
      </Suspense>
    </div>
  );
}

相关资源


本文档由 归愚知识系统 自动生成


MDX 高级应用

MDX 编译器深度解析

MDX 的编译过程是一个复杂的多阶段转换,理解其内部机制对于优化性能和解决问题至关重要。

// MDX 编译过程详解
import { compile, run } from '@mdx-js/mdx';
import * as runtime from 'react/jsx-runtime';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
import rehypePrism from 'rehype-prism';
 
// 原始 MDX 内容
const mdxSource = `
import { Chart } from './Chart';
 
# 销售报告
 
这是一个交互式图表:
 
<Chart data={salesData} type="line" />
`;
 
// 完整编译流程
async function compileMDX(source: string) {
  // 阶段1: 解析 MDX
  // 输入: MDX 字符串
  // 输出: MDX AST (mdast)
  const mdast = await remarkParse()(source);
  
  // 阶段2: 转换 Remark 插件
  // 处理 GFM 语法、表格、脚注等
  const processedMdst = await remarkGfm()(mdast);
  
  // 阶段3: Remark to Rehype
  // 将 Markdown AST 转换为 HTML AST
  const hast = await remarkRehype()(processedMdst);
  
  // 阶段4: 转换 Rehype 插件
  // 处理代码高亮、ID 生成等
  const processedHast = await rehypePrism()(hast);
  
  // 阶段5: 生成 HTML
  const html = await rehypeStringify()(processedHast);
  
  return html;
}
 
// MDX 专用编译
async function compileToJSX(source: string) {
  // 编译为可执行的 JSX 代码
  const code = await compile(source, {
    outputFormat: 'function-body',
    development: false,
  });
  
  // code 现在是 JSX 字符串,可以:
  // 1. 使用 @mdx-js/react 的 MDXProvider 渲染
  // 2. 使用 babel 编译运行
  // 3. 使用 esbuild 编译运行
  
  return code;
}
 
// 运行时执行 MDX
async function executeMDX(source: string, scope = {}) {
  const code = await compile(source, {
    outputFormat: 'function-body',
    development: false,
  });
  
  const React = await import('react');
  const runtimeCode = `
    const { jsx: _jsx, jsxs: _jsxs, Fragment: _Fragment } = arguments[0];
  `;
  
  const fn = new Function(
    'React', 
    '_jsx', '_jsxs', '_Fragment',
    ...Object.keys(scope),
    runtimeCode + code
  );
  
  return fn(
    React.default,
    React.jsx,
    React.jsxs,
    React.Fragment,
    ...Object.values(scope)
  );
}

MDX 插件系统

// 自定义 Remark 插件
function remarkCustomDirective() {
  return (tree: mdast.Root) => {
    visit(tree, 'paragraph', (node, index, parent) => {
      const children = node.children;
      if (children.length === 1 && children[0].type === 'text') {
        const text = children[0].value;
        
        // 处理 :::warning 语法
        if (text.startsWith(':::warning')) {
          const content = text.replace(':::warning', '').trim();
          
          const container: mdast.ContainerDirective = {
            type: 'containerDirective',
            name: 'warning',
            children: [
              {
                type: 'paragraph',
                children: [{ type: 'text', value: content }],
              },
            ],
          };
          
          parent!.children.splice(index!, 1, container);
        }
      }
    });
  };
}
 
// 自定义 Rehype 插件
function rehypeAddCopyButton() {
  return (tree: hast.Root) => {
    visit(tree, 'element', (node) => {
      if (node.tagName === 'pre') {
        // 找到代码块
        const codeNode = node.children.find(
          (n): n is hast.Element => 
            (n as hast.Element).tagName === 'code'
        );
        
        if (codeNode) {
          // 添加复制按钮容器
          const wrapper: hast.Element = {
            type: 'element',
            tagName: 'div',
            properties: { className: ['code-block-wrapper'] },
            children: [
              node,
              {
                type: 'element',
                tagName: 'button',
                properties: {
                  className: ['copy-button'],
                  'data-copy': 'true',
                },
                children: [{ type: 'text', value: '复制' }],
              },
            ],
          };
          
          // 替换原节点
          Object.assign(node, {
            tagName: 'div',
            properties: { className: ['code-block-inner'] },
          });
        }
      }
    });
  };
}
 
// 组合插件
const combinedPlugins = [
  remarkCustomDirective,
  [remarkGfm, { singleLineCodeFrames: true }],
  remarkRehype,
  rehypePrism,
  [rehypeAddCopyButton],
];

MDX 作用域与上下文

// MDX 作用域管理
interface MDXScope {
  // 组件
  components: Record<string, React.ComponentType>;
  // 工具函数
  utils: Record<string, Function>;
  // 数据
  data: Record<string, unknown>;
  // 主题
  theme: ThemeConfig;
}
 
// 提供作用域给 MDX
function createMDXProvider(scope: MDXScope) {
  return function MDXContent(props: MDXContentProps) {
    return (
      <MDXProvider components={scope.components}>
        <ScopeProvider value={scope}>
          <MDXRemote {...props} />
        </ScopeProvider>
      </MDXProvider>
    );
  };
}
 
// 使用 useMDXScope 访问作用域
function useMDXScope() {
  const context = useContext(ScopeContext);
  if (!context) {
    throw new Error('useMDXScope must be used within MDXScopeProvider');
  }
  return context;
}
 
// 在 MDX 组件中使用
const CustomChart = () => {
  const { data, theme } = useMDXScope();
  
  return (
    <Chart 
      data={data.chartData} 
      theme={theme.colors}
    />
  );
};

MDX 内容管理策略

// 内容集合类型定义
interface ContentCollection<T extends Frontmatter = Frontmatter> {
  // 所有文档
  all: () => Promise<CollectionEntry<T>[]>;
  // 按 slug 查询
  getBySlug: (slug: string) => Promise<CollectionEntry<T> | null>;
  // 按标签筛选
  getByTag: (tag: string) => Promise<CollectionEntry<T>[]>;
  // 按日期范围筛选
  getByDateRange: (
    start: Date,
    end: Date
  ) => Promise<CollectionEntry<T>[]>;
  // 搜索
  search: (query: string) => Promise<SearchResult<T>[]>;
}
 
// 内容集合实现
class MDXContentCollection<T extends Frontmatter> 
  implements ContentCollection<T> {
  
  private directory: string;
  private glob: string;
  private parseFrontmatter: boolean;
  
  constructor(options: {
    directory: string;
    pattern?: string;
    parseFrontmatter?: boolean;
  }) {
    this.directory = options.directory;
    this.glob = options.pattern || '**/*.mdx';
    this.parseFrontmatter = options.parseFrontmatter ?? true;
  }
  
  async all(): Promise<CollectionEntry<T>[]> {
    const files = await glob(`${this.directory}/${this.glob}`);
    
    return Promise.all(
      files.map(async (file) => {
        const source = await fs.readFile(file, 'utf-8');
        const { content, frontmatter } = this.parseFrontmatter
          ? this.parse(source)
          : { content: source, frontmatter: {} };
        
        const slug = this.getSlugFromPath(file);
        
        return {
          slug,
          source,
          content,
          frontmatter: frontmatter as T,
          meta: {
            path: file,
            createdAt: await this.getCreatedAt(file),
            modifiedAt: await this.getModifiedAt(file),
          },
        };
      })
    );
  }
  
  async getBySlug(slug: string): Promise<CollectionEntry<T> | null> {
    const all = await this.all();
    return all.find((entry) => entry.slug === slug) || null;
  }
  
  async getByTag(tag: string): Promise<CollectionEntry<T>[]> {
    const all = await this.all();
    return all.filter((entry) =>
      entry.frontmatter.tags?.includes(tag)
    );
  }
  
  async search(query: string): Promise<SearchResult<T>[]> {
    const all = await this.all();
    const lowerQuery = query.toLowerCase();
    
    return all
      .map((entry) => {
        const titleMatch = entry.frontmatter.title
          ?.toLowerCase()
          .includes(lowerQuery);
        const contentMatch = entry.content
          .toLowerCase()
          .includes(lowerQuery);
        
        return {
          ...entry,
          score: (titleMatch ? 2 : 0) + (contentMatch ? 1 : 0),
        };
      })
      .filter((result) => result.score > 0)
      .sort((a, b) => b.score - a.score);
  }
  
  private getSlugFromPath(file: string): string {
    return file
      .replace(this.directory, '')
      .replace(/\.mdx$/, '')
      .replace(/^\//, '');
  }
  
  private parse(source: string): { 
    content: string; 
    frontmatter: Record<string, unknown>;
  } {
    const match = source.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
    
    if (!match) {
      return { content: source, frontmatter: {} };
    }
    
    const [, frontmatterYaml, content] = match;
    const frontmatter = yaml.parse(frontmatterYaml);
    
    return { content, frontmatter };
  }
}
 
// 使用示例
const posts = new MDXContentCollection<PostFrontmatter>({
  directory: './content/posts',
  pattern: '**/*.mdx',
});
 
// 获取所有文章
const allPosts = await posts.all();
 
// 获取单个文章
const post = await posts.getBySlug('getting-started-with-mdx');
 
// 按标签查询
const reactPosts = await posts.getByTag('react');
 
// 搜索
const results = await posts.search('MDX');

MDX 性能优化

编译时优化

// Vite 配置优化
// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    include: ['react', 'react-dom', '@mdx-js/react'],
  },
  
  build: {
    // 分块策略
    rollupOptions: {
      output: {
        manualChunks: {
          // 将 MDX 运行时单独打包
          'mdx-runtime': ['@mdx-js/react', '@mdx-js/mdx'],
          // 按内容类型分块
          'mdx-components': glob.sync('./components/**/*.tsx'),
        },
      },
    },
    
    // 压缩选项
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
  
  // MDX 编译选项
  mdx: {
    compileOptions: {
      providerImportSource: '@mdx-js/react',
    },
    remarkPlugins: [],
    rehypePlugins: [],
  },
});
 
// Next.js 配置优化
// next.config.js
module.exports = {
  experimental: {
    // 使用 Rust 编写的 MDX 编译器
    mdxRs: true,
  },
  
  images: {
    formats: ['image/avif', 'image/webp'],
  },
};

运行时优化

// 懒加载大型组件
const HeavyDataTable = lazy(() => import('./HeavyDataTable'));
const InteractiveMap = lazy(() => import('./InteractiveMap'));
 
function MDXContent({ source }) {
  return (
    <MDXRemote
      source={source}
      components={{
        DataTable: (props) => (
          <Suspense fallback={<TableSkeleton />}>
            <HeavyDataTable {...props} />
          </Suspense>
        ),
        Map: (props) => (
          <Suspense fallback={<MapSkeleton />}>
            <InteractiveMap {...props} />
          </Suspense>
        ),
      }}
    />
  );
}
 
// 虚拟化长列表
import { FixedSizeList } from 'react-window';
 
function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ListItem item={items[index]} />
    </div>
  );
  
  return (
    <FixedSizeList
      height={400}
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}
 
// 代码分割策略
// 方案1: 动态导入
const DynamicChart = dynamic(
  () => import('./components/Chart'),
  { 
    ssr: false,
    loading: () => <ChartSkeleton />
  }
);
 
// 方案2: 按需加载组件
function useAsyncComponent(loader: () => Promise<any>) {
  const [Component, setComponent] = useState(null);
  
  useEffect(() => {
    loader().then((mod) => {
      setComponent(() => mod.default);
    });
  }, [loader]);
  
  return Component || <Skeleton />;
}
 
// 方案3: 预加载关键组件
function preloadComponent(path: string) {
  return import(/* webpackPrefetch: true */ path);
}

缓存策略

// 静态生成缓存
export async function generateStaticParams() {
  const posts = await getAllPosts();
  
  return posts.map((post) => ({
    slug: post.slug,
  }));
}
 
// ISR (增量静态再生成)
export const revalidate = 3600; // 每小时重新验证
 
// 客户端缓存
function useMDXContent(slug: string) {
  return useSWR(
    `/api/mdx/${slug}`,
    fetcher,
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: true,
      dedupingInterval: 60000,
      fallbackData: null,
    }
  );
}
 
// SWR 配置
const swrConfig = {
  refreshInterval: 0,           // 不自动刷新
  revalidateOnFocus: false,     // 失焦时不重新验证
  revalidateOnReconnect: true,   // 断网重连时验证
  shouldRetryOnError: false,     // 错误时不重试
  dedupingInterval: 2000,        // 2秒内去重
  loadingTimeout: 3000,          // 3秒后显示 loading
};

MDX 与内容平台集成

Notion 集成

// Notion API 集成
import { Client } from '@notionhq/client';
import { notRichTextToString } from '@notionhq/client/build/src/helpers';
 
class NotionToMDXConverter {
  private notion: Client;
  
  constructor(accessToken: string) {
    this.notion = new Client({ auth: accessToken });
  }
  
  async convertPage(pageId: string): Promise<string> {
    const page = await this.notion.pages.retrieve({ page_id: pageId });
    const blocks = await this.notion.blocks.children.list({
      block_id: pageId,
    });
    
    let mdxContent = `---\n`;
    mdxContent += `title: "${notRichTextToString(page.properties.Name?.title || [])}"\n`;
    mdxContent += `createdAt: "${page.created_time}"\n`;
    mdxContent += `lastEdited: "${page.last_edited_time}"\n`;
    mdxContent += `---\n\n`;
    
    for (const block of blocks.results) {
      mdxContent += this.convertBlock(block);
    }
    
    return mdxContent;
  }
  
  private convertBlock(block: BlockObjectResponse): string {
    switch (block.type) {
      case 'paragraph':
        return `${notRichTextToString(block.paragraph.rich_text)}\n\n`;
      
      case 'heading_1':
        return `## ${notRichTextToString(block.heading_1.rich_text)}\n\n`;
      
      case 'heading_2':
        return `### ${notRichTextToString(block.heading_2.rich_text)}\n\n`;
      
      case 'bulleted_list_item':
        return `- ${notRichTextToString(block.bulleted_list_item.rich_text)}\n`;
      
      case 'numbered_list_item':
        return `1. ${notRichTextToString(block.numbered_list_item.rich_text)}\n`;
      
      case 'code':
        return `\`\`\`${block.code.language}\n${notRichTextToString(block.code.rich_text)}\n\`\`\`\n\n`;
      
      case 'image':
        const imageUrl = block.image.type === 'external' 
          ? block.image.external.url 
          : block.image.file.url;
        return `![${notRichTextToString(block.image.caption)}](${imageUrl})\n\n`;
      
      case 'callout':
        return `> [!NOTE]\n> ${notRichTextToString(block.callout.rich_text)}\n\n`;
      
      default:
        return '';
    }
  }
  
  async syncDatabase(databaseId: string, outputDir: string) {
    const pages = await this.notion.databases.query({
      database_id: databaseId,
      filter: {
        property: 'Status',
        select: { equals: 'Published' },
      },
      sorts: [{ property: 'Date', direction: 'descending' }],
    });
    
    for (const page of pages.results) {
      if ('id' in page) {
        const mdxContent = await this.convertPage(page.id);
        const slug = this.generateSlug(page);
        const filename = `${outputDir}/${slug}.mdx`;
        
        await fs.writeFile(filename, mdxContent);
      }
    }
  }
}

Contentful 集成

// Contentful 集成
import { createClient } from 'contentful';
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
 
class ContentfulToMDXConverter {
  private client: ContentfulClientApi;
  
  constructor(config: {
    spaceId: string;
    accessToken: string;
  }) {
    this.client = createClient(config);
  }
  
  async fetchEntries(
    contentType: string,
    options: {
      limit?: number;
      skip?: number;
      order?: string[];
    } = {}
  ) {
    const entries = await this.client.getEntries({
      content_type: contentType,
      ...options,
    });
    
    return entries.items.map((item) => this.convertEntry(item));
  }
  
  private convertEntry(item: Entry): MDXEntry {
    const fields = item.fields as Record<string, any>;
    
    return {
      slug: fields.slug,
      title: fields.title,
      content: this.richTextToMDX(fields.content),
      publishedAt: fields.publishDate || item.sys.createdAt,
      tags: fields.tags || [],
      author: fields.author?.fields?.name,
      heroImage: fields.heroImage?.fields?.file?.url,
    };
  }
  
  private richTextToMDX(node: Document): string {
    return documentToReactComponents(
      node,
      {
        // 自定义渲染器
        BLOCKS: {
          [BLOCKS.PARAGRAPH]: (node, children) => `${children}\n\n`,
          [BLOCKS.HEADING_1]: (node, children) => `# ${children}\n\n`,
          [BLOCKS.HEADING_2]: (node, children) => `## ${children}\n\n`,
          [BLOCKS.UL_LIST]: (node, children) => `${children}\n`,
          [BLOCKS.OL_LIST]: (node, children) => `${children}\n`,
          [BLOCKS.LIST_ITEM]: (node, children) => `- ${children}\n`,
          [BLOCKS.QUOTE]: (node, children) => `> ${children}\n\n`,
          [BLOCKS.CODE]: (node) => {
            const code = node.content[0];
            return `\`\`\`${code.language}\n${code.content[0].value}\n\`\`\`\n\n`;
          },
          [BLOCKS.EMBEDDED_ASSET]: (node) => {
            const asset = node.data.target;
            return `![](${asset.fields.file.url})\n\n`;
          },
        },
        MARKS: {
          [MARKS.BOLD]: (text) => `**${text}**`,
          [MARKS.ITALIC]: (text) => `*${text}*`,
          [MARKS.CODE]: (text) => `\`${text}\``,
        },
      }
    );
  }
}

Sanity CMS 集成

// Sanity 集成
import { createClient } from '@sanity/client';
import { portableTextToMarkdown } from '@sanity/portable-text-to-markdown';
 
class SanityToMDXConverter {
  private client: SanityClient;
  
  constructor(config: {
    projectId: string;
    dataset: string;
    token?: string;
  }) {
    this.client = createClient({
      ...config,
      useCdn: false, // 开发环境不使用 CDN
      apiVersion: '2024-01-01',
    });
  }
  
  async fetchPost(slug: string): Promise<MDXPost> {
    const query = `*[_type == "post" && slug.current == $slug][0] {
      title,
      slug,
      publishedAt,
      "author": author->name,
      "categories": categories[]->title,
      body
    }`;
    
    const post = await this.client.fetch(query, { slug });
    return this.convertPost(post);
  }
  
  private async convertPost(post: SanityPost): Promise<MDXPost> {
    const body = await portableTextToMarkdown(post.body);
    
    return {
      frontmatter: {
        title: post.title,
        date: post.publishedAt,
        author: post.author,
        categories: post.categories,
      },
      content: body,
    };
  }
  
  // GROQ 查询构建器
  buildQuery(options: {
    type: string;
    filters?: string[];
    projection?: string;
  }): string {
    const { type, filters = [], projection } = options;
    
    let query = `*[_type == "${type}"`;
    
    if (filters.length > 0) {
      query += ` && [${filters.join(' && ')}]`;
    }
    
    query += `]`;
    
    if (projection) {
      query += ` { ${projection} }`;
    }
    
    return query;
  }
}

MDX 生态系统工具链

开发工具配置

// .vscode/settings.json
{
  "files.associations": {
    "*.mdx": "markdown"
  },
  "[markdown]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": true,
    "editor.wordWrap": "on"
  },
  "markdown.validate.enabled": true,
  "typescript.tsdk": "node_modules/typescript/lib"
}
// .vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "christian-kohler.path-intellisense",
    "vunguyentuan.vscode-mdx",
    "mutantdino.resourcemonitor"
  ]
}

ESLint 配置

// .eslintrc.js
module.exports = {
  extends: [
    'next/core-web-vitals',
    'plugin:mdx/recommended',
  ],
  
  plugins: ['mdx'],
  
  rules: {
    'mdx/no-unescaped-entities': 'off',
    'mdx/remark': [
      'error',
      {
        pluginPaths: [
          'remark-gfm',
          'remark-frontmatter',
          'remark-mdx-frontmatter',
        ],
      },
    ],
  },
  
  overrides: [
    {
      files: ['*.mdx'],
      parser: 'eslint-plugin-mdx/dist/parser',
      rules: {
        'no-unused-vars': 'off',
      },
    },
  ],
};

Prettier 配置

// .prettierrc.js
module.exports = {
  // MDX 文件使用 MDX 插件
  plugins: [
    require('prettier-plugin-mdx'),
    require('@prettier/plugin-sort-imports'),
  ],
  
  // MDX 特定选项
  semi: false,
  singleQuote: true,
  trailingComma: 'all',
  printWidth: 80,
  
  // MDX 格式选项
  mdxLanguageEngine: 'remark',
  mdxFileExtensions: ['mdx'],
};
// prettier-plugin-mdx 配置
const mdxConfig = {
  // 导入排序
  importOrder: [
    '^@/(.*)$',
    '^@/components/(.*)$',
    '^[./]',
  ],
  
  // MDX 代码块格式
  mdxLanguageEngine: {
    // 使用 remark 或 mdast
    parser: 'remark',
    // 保留空白
    preserveWhitespace: false,
    // 表格对齐
    tableAlignment: true,
  },
};

测试工具

// Vitest 配置
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import mdx from 'vite-plugin-mdx';
 
export default defineConfig({
  plugins: [
    mdx({
      MDXProvider: require.resolve('@mdx-js/react'),
    }),
  ],
  
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./test/setup.ts'],
    
    // MDX 特定测试配置
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['**/*.mdx'],
    },
  },
});
// 测试 MDX 组件
// __tests__/MyComponent.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MDXProvider } from '@mdx-js/react';
 
// 测试 MDX 内容
describe('MDX Content Tests', () => {
  it('should render heading', () => {
    render(<TestComponent>{"# Hello World"}</TestComponent>);
    expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Hello World');
  });
  
  it('should render interactive component', async () => {
    const handleClick = vi.fn();
    
    render(
      <MDXProvider>
        <TestComponent onClick={handleClick}>
          {"<Counter />"}
        </TestComponent>
      </MDXProvider>
    );
    
    const button = screen.getByRole('button');
    fireEvent.click(button);
    
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});
 
// 快照测试
describe('MDX Snapshot Tests', () => {
  it('should match expected output', () => {
    const { container } = render(<Article source={mdxContent} />);
    expect(container).toMatchSnapshot();
  });
  
  it('should match serialized MDX', () => {
    const result = serialize(mdxContent);
    expect(result).toMatchSnapshot();
  });
});

MDX 高级模式

MDX 与微前端

// 微前端 MDX 加载器
class MFEMDXLoader {
  private registry: Map<string, () => Promise<any>> = new Map();
  
  register(name: string, loader: () => Promise<any>) {
    this.registry.set(name, loader);
  }
  
  async loadComponent(name: string): Promise<React.ComponentType> {
    const loader = this.registry.get(name);
    if (!loader) {
      throw new Error(`Component ${name} not found in registry`);
    }
    
    const module = await loader();
    return module.default;
  }
  
  async renderMDXWithRemote(
    source: string,
    remoteComponents: Record<string, string>
  ): Promise<React.ReactElement> {
    const components: Record<string, React.ComponentType> = {};
    
    // 加载远程组件
    await Promise.all(
      Object.entries(remoteComponents).map(async ([name, url]) => {
        components[name] = await this.loadRemoteComponent(url);
      })
    );
    
    // 使用 MDX 渲染
    return <MDXRemote source={source} components={components} />;
  }
}
 
// 注册远程组件
const mdxLoader = new MFEMDXLoader();
 
mdxLoader.register('DataGrid', () => 
  import('dataGridMFE/DataGrid')
);
 
mdxLoader.register('Chart', () => 
  import('chartsMFE/Charts')
);

MDX 与 GraphQL

// GraphQL 查询集成
const POST_QUERY = gql`
  query GetPost($slug: String!) {
    post(slug: $slug) {
      title
      content
      author {
        name
        avatar
      }
      tags
      relatedPosts {
        slug
        title
      }
    }
  }
`;
 
// 带 GraphQL 的 MDX 渲染
function GraphQLMDXPage({ slug }) {
  const { data, loading, error } = useQuery(POST_QUERY, { variables: { slug } });
  
  if (loading) return <Loading />;
  if (error) return <Error error={error} />;
  
  return (
    <MDXProvider components={customComponents}>
      <article>
        <h1>{data.post.title}</h1>
        <MDXRemote source={data.post.content} />
      </article>
      
      <aside>
        <RelatedPosts posts={data.post.relatedPosts} />
      </aside>
    </MDXProvider>
  );
}
 
// MDX 中使用 GraphQL
const MDX_WITH_GRAPHQL = `
import { useQuery, gql } from '@apollo/client';
 
export const AuthorQuery = gql\`
  query GetAuthor($id: ID!) {
    author(id: $id) {
      name
      bio
    }
  }
\`;
 
# 作者介绍
 
<AuthorCard authorId="author-123" />
 
function AuthorCard({ authorId }) {
  const { data } = useQuery(AuthorQuery, { 
    variables: { id: authorId } 
  });
  
  if (!data) return <Loading />;
  
  return (
    <div className="author-card">
      <h3>{data.author.name}</h3>
      <p>{data.author.bio}</p>
    </div>
  );
}
`;

MDX 与状态管理

// MDX 上下文状态管理
const MDXContext = createContext<{
  state: Record<string, any>;
  setState: (key: string, value: any) => void;
}>({
  state: {},
  setState: () => {},
});
 
function MDXStateProvider({ 
  children, 
  initialState = {} 
}: MDXStateProviderProps) {
  const [state, setState] = useState(initialState);
  
  const contextValue = useMemo(() => ({
    state,
    setState: (key: string, value: any) => 
      setState(prev => ({ ...prev, [key]: value })),
  }), [state]);
  
  return (
    <MDXContext.Provider value={contextValue}>
      {children}
    </MDXContext.Provider>
  );
}
 
// MDX 内容中使用状态
const MDX_WITH_STATE = `
import { useContext } from 'react';
import { MDXContext } from './context';
 
# 计数器示例
 
<Counter />
 
function Counter() {
  const { state, setState } = useContext(MDXContext);
  const count = state.count || 0;
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setState('count', count + 1)}>
        增加
      </button>
      <button onClick={() => setState('count', 0)}>
        重置
      </button>
    </div>
  );
}
`;

MDX 常见问题与解决方案

问题1:MDX 在 SSR 中 hydration 不匹配

// 问题原因:服务端和客户端渲染结果不一致
// 解决方案:使用 client-only 标记
 
// ClientOnly 组件
function ClientOnly({ 
  children, 
  fallback = null 
}: ClientOnlyProps) {
  const [mounted, setMounted] = useState(false);
  
  useEffect(() => {
    setMounted(true);
  }, []);
  
  if (!mounted) {
    return fallback;
  }
  
  return children;
}
 
// 使用
<ClientOnly fallback={<Skeleton />}>
  <InteractiveComponent />
</ClientOnly>

问题2:大文件 MDX 编译缓慢

// 问题:大型 MDX 文件编译时间过长
// 解决方案:拆分为多个小文件 + 使用 content collections
 
// 原来的大文件
// posts/large-post.mdx (10000行)
 
// 拆分后
// posts/large-post/index.mdx (主文件)
// posts/large-post/part1.mdx
// posts/large-post/part2.mdx
// posts/large-post/part3.mdx
 
// 主文件使用导入
// posts/large-post/index.mdx
import Part1 from './part1.mdx';
import Part2 from './part2.mdx';
import Part3 from './part3.mdx';
 
# 大型文章
 
<Part1 />
 
<Part2 />
 
<Part3 />

问题3:MDX 中使用三方库导致 bundle 过大

// 问题:MDX 中直接 import 图表库等重型依赖
// 解决方案:动态导入 + 代码分割
 
// 方案1:动态导入
// 不要这样写:
// import { Chart } from 'heavy-chart';
 
// 而是这样:
const Chart = dynamic(() => import('./Chart'));
 
// 方案2:使用 client-only 组件
// Chart.client.tsx
export function Chart(props) {
  const [ChartComponent, setChartComponent] = useState(null);
  
  useEffect(() => {
    import('recharts').then((mod) => {
      setChartComponent(() => mod.LineChart);
    });
  }, []);
  
  if (!ChartComponent) return <Skeleton />;
  
  return <ChartComponent {...props} />;
}
 
// 方案3:使用 CDN 外链
// 在 MDX 中使用 HTML
<div id="chart-container" data-type="line" />
<script src="https://cdn.example.com/chart.js" />

问题4:MDX 中使用 useState 导致警告

// 问题:组件在服务端渲染时报错
// 原因:useState 在 SSR 时行为不一致
 
// 解决方案:始终在 useEffect 中初始化状态
 
// 错误写法
function Counter() {
  const [count, setCount] = useState(0); // SSR 时可能有警告
  return <div>{count}</div>;
}
 
// 正确写法
function Counter() {
  const [count, setCount] = useState(null);
  
  // 确保只在客户端执行
  useEffect(() => {
    setCount(0);
  }, []);
  
  if (count === null) return <Skeleton />;
  
  return <div>{count}</div>;
}
 
// 或者使用 useHydrated
function useHydrated() {
  const [hydrated, setHydrated] = useState(false);
  
  useEffect(() => {
    setHydrated(true);
  }, []);
  
  return hydrated;
}

MDX 未来趋势

MDX 3.0 预览

// MDX 3.0 的新特性(草案)
 
// 1. 更好的 TypeScript 支持
type MDXProps<T extends Frontmatter = Frontmatter> = {
  source: string;
  scope?: Record<string, unknown>;
  components?: MDXComponents;
  frontmatter?: T;
};
 
// 2. 异步插件支持
const asyncPlugin = async (tree: mdast.Root) => {
  const data = await fetchExternalData();
  // 处理数据...
  return tree;
};
 
// 3. 更好的性能
// - 增量编译
// - WebWorker 编译
// - WASM 加速
 
// 4. ESM 原生支持
// import.meta.resolve 支持

与 AI 集成的趋势

// AI 辅助 MDX 创作
 
// 1. 智能代码块生成
const AICodeBlock = ({ 
  language, 
  description 
}: AICodeBlockProps) => {
  const [code, setCode] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  
  const generate = async () => {
    setLoading(true);
    const response = await ai.generateCode({
      language,
      description,
      context: getMDXContext(),
    });
    setCode(response.code);
    setLoading(false);
  };
  
  return (
    <div>
      {loading ? <Skeleton /> : code ? (
        <CodeBlock language={language}>{code}</CodeBlock>
      ) : (
        <Button onClick={generate}>AI 生成代码</Button>
      )}
    </div>
  );
};
 
// 2. 智能内容补全
const MDXEditor = () => {
  const [suggestions, setSuggestions] = useState<string[]>([]);
  
  const handleInput = async (value: string) => {
    const suggestions = await ai.suggest({
      type: 'mdx-completion',
      content: value,
    });
    setSuggestions(suggestions);
  };
  
  return (
    <div>
      <Textarea onChange={handleInput} />
      <SuggestionList suggestions={suggestions} />
    </div>
  );
};
 
// 3. 语义搜索
const SemanticSearch = async (query: string) => {
  return ai.search({
    collection: 'posts',
    query,
    type: 'semantic',
    embeddings: true,
  });
};

MDX 高级内容管理系统

内容架构设计

大型 MDX 项目需要系统化的内容管理策略:

// 内容目录结构
// content/
// ├── docs/
// │   ├── getting-started/
// │   │   ├── index.mdx
// │   │   ├── installation.mdx
// │   │   └── configuration.mdx
// │   ├── guides/
// │   │   ├── index.mdx
// │   │   ├── beginner/
// │   │   │   ├── index.mdx
// │   │   │   └── *.mdx
// │   │   └── advanced/
// │   │       ├── index.mdx
// │   │       └── *.mdx
// │   └── api/
// │       ├── index.mdx
// │       └── *.mdx
// ├── blog/
// │   ├── index.mdx
// │   └── posts/
// │       └── *.mdx
// └── changelog/
//     └── *.mdx
 
// 内容元数据定义
interface ContentMeta {
  slug: string;
  title: string;
  description: string;
  author?: string;
  date?: string;
  tags?: string[];
  category?: string;
  draft?: boolean;
  deprecated?: boolean;
  version?: string;
}
 
// 导航配置
interface NavigationConfig {
  sidebar: {
    [path: string]: SidebarItem[];
  };
  prevNext?: boolean;
  breadcrumb?: boolean;
}
 
interface SidebarItem {
  title: string;
  path?: string;
  items?: SidebarItem[];
  collapsed?: boolean;
}
 
// 示例导航配置
const navigation: NavigationConfig = {
  sidebar: {
    '/docs/getting-started': [
      { title: '简介', path: '/docs/getting-started' },
      { title: '安装', path: '/docs/getting-started/installation' },
      { title: '快速开始', path: '/docs/getting-started/quickstart' },
    ],
    '/docs/guides': [
      {
        title: '入门指南',
        collapsed: false,
        items: [
          { title: '基础语法', path: '/docs/guides/beginner/syntax' },
          { title: '组件使用', path: '/docs/guides/beginner/components' },
        ],
      },
      {
        title: '进阶指南',
        collapsed: true,
        items: [
          { title: '插件开发', path: '/docs/guides/advanced/plugins' },
          { title: '自定义主题', path: '/docs/guides/advanced/theming' },
        ],
      },
    ],
  },
  prevNext: true,
  breadcrumb: true,
};

内容版本管理

// 版本管理配置
interface VersionConfig {
  current: string;
  versions: Version[];
}
 
interface Version {
  version: string;
  label: string;
  path: string;
  banner?: 'none' | 'unmaintained' | 'beta';
  default?: boolean;
}
 
// 版本配置文件
const versions: VersionConfig = {
  current: 'v2',
  versions: [
    { version: 'v2', label: 'v2.x (最新)', path: '/docs', default: true },
    { version: 'v1', label: 'v1.x', path: '/docs/v1', banner: 'unmaintained' },
  ],
};
 
// MDX 文件中的版本标记
// v2-content.mdx
/*
---
title: 新功能
version: v2
deprecatedIn: v3
replaces: /docs/v1/old-feature
seeAlso:
  - /docs/v2/new-feature
  - /docs/v2/migration
---
*/
 
// 版本降级提示组件
interface VersionBannerProps {
  version: string;
  message?: string;
  action?: {
    label: string;
    href: string;
  };
}
 
function VersionBanner({ version, message, action }: VersionBannerProps) {
  return (
    <div className="version-banner">
      <span>本文档适用于 {version} 版本</span>
      {message && <p>{message}</p>}
      {action && (
        <a href={action.href} className="version-action">
          {action.label}
        </a>
      )}
    </div>
  );
}

内容国际化

// i18n 配置
interface I18nConfig {
  defaultLocale: string;
  locales: LocaleConfig[];
}
 
interface LocaleConfig {
  code: string;
  name: string;
  path: string;
  rtl?: boolean;
}
 
const i18n: I18nConfig = {
  defaultLocale: 'zh-CN',
  locales: [
    { code: 'zh-CN', name: '简体中文', path: '/' },
    { code: 'en', name: 'English', path: '/en/' },
    { code: 'ja', name: '日本語', path: '/ja/' },
  ],
};
 
// MDX 文件命名规范
// content/
// ├── docs/
// │   ├── getting-started.mdx          # 默认语言
// │   ├── getting-started.zh-CN.mdx   # 中文版本
// │   ├── getting-started.en.mdx       # 英文版本
// │   ├── getting-started.ja.mdx      # 日文版本
// │   └── index.en.mdx                # 仅英文版本
 
// 本地化工具函数
function useTranslations(locale: string) {
  const translations = {
    'zh-CN': {
      'nav.docs': '文档',
      'nav.blog': '博客',
      'nav.github': 'GitHub',
      'search.placeholder': '搜索文档...',
      'search.noResults': '未找到结果',
    },
    'en': {
      'nav.docs': 'Docs',
      'nav.blog': 'Blog',
      'nav.github': 'GitHub',
      'search.placeholder': 'Search docs...',
      'search.noResults': 'No results found',
    },
  };
 
  return function t(key: string): string {
    return translations[locale]?.[key] || key;
  };
}
 
// MDX 中的国际化
/*
import { useTranslations } from '@/i18n';
 
# 欢迎使用文档
 
<Callout type="info">
  <Trans i18nKey="welcome.message" />
</Callout>
 
<Link href="/zh-CN/guide">中文指南</Link>
<Link href="/en/guide">English Guide</Link>
*/

MDX 部署与运维

构建优化

// 完整的构建配置
// next.config.js 或 vite.config.ts
 
// Next.js 配置
/** @type {import('next').NextConfig} */
const nextConfig = {
  // MDX 配置
  pageExtensions: ['ts', 'tsx', 'mdx'],
  
  experimental: {
    // 使用 Rust 编写的 MDX 编译器(更快)
    mdxRs: true,
    
    // 优化包导入
    optimizePackageImports: ['lucide-react', '@radix-ui/react-icons'],
  },
  
  // 图片优化
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [
      { protocol: 'https', hostname: 'images.unsplash.com' },
      { protocol: 'https', hostname: 'picsum.photos' },
    ],
  },
  
  // 压缩
  compress: true,
  
  // 生成 Etags
  generateEtags: true,
  
  // 静态导出(可选)
  output: 'export',
};
 
// Vite 配置
import { defineConfig } from 'vite';
import mdx from '@mdx-js/vite';
import remarkGfm from 'remark-gfm';
import rehypeShiki from '@shikijs/rehype';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
 
export default defineConfig({
  plugins: [
    mdx({
      remarkPlugins: [remarkGfm],
      rehypePlugins: [
        rehypeSlug,
        [rehypeAutolinkHeadings, { behavior: 'wrap' }],
        [rehypeShiki, { theme: 'github-dark' }],
      ],
      providerImportSource: '@mdx-js/react',
    }),
  ],
  
  build: {
    // 分块策略
    rollupOptions: {
      output: {
        manualChunks: {
          'mdx-runtime': ['@mdx-js/mdx', '@mdx-js/react'],
          'shiki': ['shiki'],
        },
      },
    },
    
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
  
  // 优化依赖预构建
  optimizeDeps: {
    include: ['react', 'react-dom', '@mdx-js/react', '@mdx-js/mdx'],
  },
});

CI/CD 配置

# .github/workflows/deploy.yml
name: Deploy Documentation
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run linter
        run: npm run lint
      
      - name: Run tests
        run: npm test
      
      - name: Build
        run: npm run build
        env:
          NODE_ENV: production
      
      - name: Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          configPath: './lighthouserc.json'
          uploadArtifacts: true
      
      - name: Deploy to GitHub Pages
        if: github.ref == 'refs/heads/main'
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./out
          cname: docs.example.com
 
  # Netlify 部署
  deploy-netlify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build
      - uses: nwtgck/actions-netlify@v3
        with:
          publish-dir: ./out
          production-deploy: true
          github-token: ${{ secrets.GITHUB_TOKEN }}
          deploy-message: "Deploy from GitHub Actions"
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

监控与分析

// 性能监控
interface PerformanceMetrics {
  // Core Web Vitals
  LCP?: number;  // Largest Contentful Paint
  FID?: number;  // First Input Delay
  CLS?: number;  // Cumulative Layout Shift
  
  // 自定义指标
  TTFB?: number; // Time to First Byte
  FCP?: number;  // First Contentful Paint
  TTMC?: number; // Time to Main Content
}
 
// 性能监控实现
class PerformanceMonitor {
  private metrics: PerformanceMetrics = {};
  
  constructor() {
    if (typeof window !== 'undefined') {
      this.initWebVitals();
    }
  }
  
  private initWebVitals() {
    // LCP
    new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();
      const lastEntry = entries[entries.length - 1] as LargestContentfulPaint;
      this.metrics.LCP = lastEntry.renderTime || lastEntry.loadTime;
    }).observe({ entryTypes: ['largest-contentful-paint'] });
    
    // FID
    new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();
      const firstEntry = entries[0] as EventTiming;
      this.metrics.FID = firstEntry.processingStart - firstEntry.startTime;
    }).observe({ entryTypes: ['first-input'] });
    
    // CLS
    new PerformanceObserver((entryList) => {
      for (const entry of entryList.getEntries()) {
        if (!(entry as any).hadRecentInput) {
          this.metrics.CLS = (this.metrics.CLS || 0) + (entry as any).value;
        }
      }
    }).observe({ entryTypes: ['layout-shift'] });
  }
  
  report() {
    // 发送到分析服务
    if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
      navigator.sendBeacon('/api/analytics', JSON.stringify({
        url: window.location.href,
        metrics: this.metrics,
        timestamp: Date.now(),
      }));
    }
  }
}
 
// 内容使用分析
interface ContentAnalytics {
  pageViews: number;
  uniqueVisitors: number;
  avgTimeOnPage: number;
  scrollDepth: number;
  completionRate: number;
}
 
// 使用分析 Hook
function useContentAnalytics(pageId: string) {
  const [analytics, setAnalytics] = useState<ContentAnalytics | null>(null);
  
  useEffect(() => {
    // 滚动深度跟踪
    let maxScroll = 0;
    const handleScroll = () => {
      const scrollPercent = Math.round(
        (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
      );
      maxScroll = Math.max(maxScroll, scrollPercent);
    };
    
    window.addEventListener('scroll', handleScroll, { passive: true });
    
    // 页面离开时报告
    const handleUnload = () => {
      fetch('/api/analytics', {
        method: 'POST',
        body: JSON.stringify({
          pageId,
          scrollDepth: maxScroll,
          timeSpent: Date.now() - startTime,
        }),
      });
    };
    
    window.addEventListener('beforeunload', handleUnload);
    
    return () => {
      window.removeEventListener('scroll', handleScroll);
      window.removeEventListener('beforeunload', handleUnload);
    };
  }, [pageId]);
  
  return analytics;
}

MDX 主题开发完整指南

主题架构设计

// 主题目录结构
// theme/
// ├── index.ts                 # 主题入口
// ├── Layout.tsx               # 根布局组件
// ├── components/
// │   ├── Header.tsx
// │   ├── Sidebar.tsx
// │   ├── Footer.tsx
// │   ├── TableOfContents.tsx
// │   ├── Search.tsx
// │   └── CopyButton.tsx
// ├── mdx/
// │   ├── Callout.tsx
// │   ├── CodeBlock.tsx
// │   ├── Tabs.tsx
// │   └── Steps.tsx
// ├── styles/
// │   ├── globals.css
// │   ├── typography.css
// │   └── syntax-theme.css
// └── utils/
//     ├── toc.ts
//     └── slug.ts
 
// 主题入口文件
// theme/index.ts
import { h } from 'vue';
import type { Theme } from 'vitepress';
import DefaultTheme from 'vitepress/theme';
import './styles/main.css';
 
import Layout from './Layout.vue';
import Home from './components/Home.vue';
import NotFound from './components/NotFound.vue';
 
export default {
  extends: DefaultTheme,
  
  Layout,
  
  enhanceApp({ app, router, siteData }) {
    // 注册全局组件
    app.component('Home', Home);
    app.component('NotFound', NotFound);
    
    // 注册 MDX 组件
    const mdxComponents = import.meta.glob('./mdx/*.tsx');
    for (const [path, component] of Object.entries(mdxComponents)) {
      const name = path.split('/').pop()?.replace('.tsx', '');
      app.component(name!, component as any);
    }
  },
} satisfies Theme;
 
// 布局组件
// theme/Layout.vue
<template>
  <DefaultTheme.Layout>
    <template #home-hero>
      <slot name="hero" />
    </template>
    
    <template #doc-before>
      <div class="doc-header">
        <Breadcrumb />
        <ArticleMeta />
      </div>
    </template>
    
    <template #doc-after>
      <ArticleFooter />
    </template>
  </DefaultTheme.Layout>
</template>
 
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme';
import Breadcrumb from './components/Breadcrumb.vue';
import ArticleMeta from './components/ArticleMeta.vue';
import ArticleFooter from './components/ArticleFooter.vue';
</script>

主题组件开发

<!-- theme/components/Callout.vue -->
<template>
  <div :class="['callout', `callout-${type}`]">
    <div class="callout-icon">
      <component :is="iconComponent" />
    </div>
    <div class="callout-content">
      <div v-if="title" class="callout-title">{{ title }}</div>
      <div class="callout-body">
        <slot />
      </div>
    </div>
  </div>
</template>
 
<script setup lang="ts">
import { computed } from 'vue';
import { InfoIcon, AlertIcon, WarningIcon, AlertCircleIcon } from 'lucide-vue-next';
 
const props = defineProps<{
  type?: 'note' | 'tip' | 'warning' | 'danger' | 'info';
  title?: string;
}>();
 
const iconComponent = computed(() => {
  const icons = {
    note: InfoIcon,
    tip: AlertIcon,
    warning: WarningIcon,
    danger: AlertCircleIcon,
    info: InfoIcon,
  };
  return icons[props.type || 'note'];
});
</script>
 
<style scoped>
.callout {
  display: flex;
  gap: 12px;
  padding: 16px;
  border-radius: 8px;
  margin: 16px 0;
}
 
.callout-note {
  background: #e0f2fe;
  border-left: 4px solid #0ea5e9;
}
 
.callout-tip {
  background: #dcfce7;
  border-left: 4px solid #22c55e;
}
 
.callout-warning {
  background: #fef9c3;
  border-left: 4px solid #eab308;
}
 
.callout-danger {
  background: #fee2e2;
  border-left: 4px solid #ef4444;
}
 
.callout-icon {
  flex-shrink: 0;
}
 
.callout-title {
  font-weight: 600;
  margin-bottom: 4px;
}
 
.callout-content {
  flex: 1;
  min-width: 0;
}
</style>
<!-- theme/components/CodeBlock.vue -->
<template>
  <div class="code-block">
    <div v-if="filename || language" class="code-header">
      <span v-if="filename" class="code-filename">{{ filename }}</span>
      <span v-if="language" class="code-language">{{ language }}</span>
      <button class="copy-button" @click="copyCode">
        <CopyIcon v-if="!copied" />
        <CheckIcon v-else />
      </button>
    </div>
    <pre class="code-pre"><code ref="codeRef" :class="`language-${language}`"><slot /></code></pre>
  </div>
</template>
 
<script setup lang="ts">
import { ref } from 'vue';
import { Copy as CopyIcon, Check as CheckIcon } from 'lucide-vue-next';
 
const props = defineProps<{
  language?: string;
  filename?: string;
  highlights?: number[];
}>();
 
const codeRef = ref<HTMLElement>();
const copied = ref(false);
 
async function copyCode() {
  const code = codeRef.value?.textContent || '';
  await navigator.clipboard.writeText(code);
  copied.value = true;
  setTimeout(() => {
    copied.value = false;
  }, 2000);
}
</script>
 
<style scoped>
.code-block {
  position: relative;
  margin: 16px 0;
  border-radius: 8px;
  overflow: hidden;
  background: #1e293b;
}
 
.code-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: #0f172a;
  border-bottom: 1px solid #334155;
}
 
.code-filename {
  color: #e2e8f0;
  font-size: 13px;
}
 
.code-language {
  color: #94a3b8;
  font-size: 12px;
  margin-left: auto;
}
 
.copy-button {
  background: transparent;
  border: none;
  color: #94a3b8;
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
}
 
.copy-button:hover {
  color: #e2e8f0;
  background: #334155;
}
 
.code-pre {
  margin: 0;
  padding: 16px;
  overflow-x: auto;
}
 
.code-pre code {
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px;
  line-height: 1.6;
}
</style>

暗色模式实现

// 主题切换逻辑
// theme/composables/useTheme.ts
import { ref, watch, computed } from 'vue';
 
type Theme = 'light' | 'dark' | 'auto';
 
const theme = ref<Theme>('auto');
const prefersDark = ref(false);
 
export function useTheme() {
  // 监听系统主题变化
  if (typeof window !== 'undefined') {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    prefersDark.value = mediaQuery.matches;
    
    mediaQuery.addEventListener('change', (e) => {
      prefersDark.value = e.matches;
    });
  }
  
  // 计算实际生效的主题
  const isDark = computed(() => {
    if (theme.value === 'auto') {
      return prefersDark.value;
    }
    return theme.value === 'dark';
  });
  
  // 应用主题到文档
  function applyTheme() {
    if (typeof document !== 'undefined') {
      document.documentElement.classList.toggle('dark', isDark.value);
    }
  }
  
  // 监听变化
  watch([theme, prefersDark], applyTheme, { immediate: true });
  
  // 切换主题
  function setTheme(newTheme: Theme) {
    theme.value = newTheme;
    localStorage.setItem('theme', newTheme);
  }
  
  // 初始化
  function initTheme() {
    const stored = localStorage.getItem('theme') as Theme | null;
    if (stored) {
      theme.value = stored;
    }
  }
  
  return {
    theme,
    isDark,
    setTheme,
    initTheme,
  };
}
 
// 使用示例
/*
<script setup>
import { onMounted } from 'vue';
import { useTheme } from '../composables/useTheme';
 
const { initTheme, isDark, setTheme } = useTheme();
 
onMounted(() => {
  initTheme();
});
</script>
 
<template>
  <button @click="setTheme(isDark ? 'light' : 'dark')">
    {{ isDark ? '切换到浅色模式' : '切换到深色模式' }}
  </button>
</template>
*/

MDX 编辑器与创作工具

VS Code 配置

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[markdown]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.wordWrap": "on",
    "editor.quickSuggestions": {
      "other": true,
      "comments": false,
      "strings": true
    }
  },
  "[mdx]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "files.associations": {
    "*.mdx": "mdx"
  },
  "markdown.styles": [],
  "markdown.validate.enabled": true,
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "emmet.includeLanguages": {
    "mdx": "html"
  },
  "cSpell.language": "en,zh-CN",
  "cSpell.words": [
    "MDX",
    "VitePress",
    "frontmatter",
    "codeblock"
  ]
}
// .vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "christian-kohler.path-intellisense",
    "formulahendry.auto-rename-tag",
    "ms-vscode.vscode-typescript-next",
    "styled-components.vscode-styled-components",
    "bradlc.vscode-tailwindcss",
    "davidanson.vscode-markdownlint",
    "bierner.emojisense",
    "oderwat.indent-rainbow",
    "usernamehw.errorlens",
    "contextusdc.theme-dracula-at-night"
  ]
}

MDX 预览工具

// 本地预览服务器
// scripts/preview.ts
import { createServer } from 'vite';
import mdx from '@mdx-js/vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
 
async function startPreview() {
  const server = await createServer({
    plugins: [
      react(),
      mdx(),
    ],
    resolve: {
      alias: {
        '@': resolve(__dirname, '../src'),
        '@components': resolve(__dirname, '../src/components'),
        '@content': resolve(__dirname, '../content'),
      },
    },
    server: {
      port: 3000,
      open: true,
    },
  });
  
  await server.listen();
  server.printUrls();
}
 
startPreview();
# 预览脚本
# package.json
{
  "scripts": {
    "preview": "tsx scripts/preview.ts",
    "preview:build": "npm run build && npm run preview:static",
    "preview:static": "npx serve dist"
  }
}

内容质量检查

// scripts/lint-content.ts
import { glob } from 'glob';
import matter from 'gray-matter';
import { z } from 'zod';
 
const frontmatterSchema = z.object({
  title: z.string().min(1),
  description: z.string().optional(),
  date: z.string().optional(),
  author: z.string().optional(),
  tags: z.array(z.string()).optional(),
  draft: z.boolean().optional(),
});
 
async function lintContent() {
  const files = await glob('content/**/*.mdx');
  const errors: string[] = [];
  
  for (const file of files) {
    const content = await Bun.file(file).text();
    const { data, content: body } = matter(content);
    
    // 验证 frontmatter
    const result = frontmatterSchema.safeParse(data);
    if (!result.success) {
      errors.push(`${file}: Invalid frontmatter - ${result.error.message}`);
    }
    
    // 检查标题存在性
    if (!body.includes('# ')) {
      errors.push(`${file}: Missing main heading`);
    }
    
    // 检查代码块语言标注
    const codeBlocks = body.match(/```(\w+)/g);
    if (codeBlocks) {
      const withoutLang = codeBlocks.filter(block => {
        const lang = block.replace('```', '');
        return !lang || lang === 'text';
      });
      if (withoutLang.length > 0) {
        errors.push(`${file}: Found ${withoutLang.length} code blocks without language`);
      }
    }
    
    // 检查链接有效性(可选)
    const links = body.match(/\[([^\]]+)\]\(([^)]+)\)/g);
    // ... 验证链接
  }
  
  if (errors.length > 0) {
    console.error('Content lint errors:');
    errors.forEach(e => console.error(`  - ${e}`));
    process.exit(1);
  }
  
  console.log(`✓ Linted ${files.length} files`);
}
 
lintContent();

MDX 生态系统资源

官方资源

插件生态

插件功能
remark-gfmGitHub Flavored Markdown 支持
remark-frontmatterFrontmatter 解析
remark-mdx-frontmatterFrontmatter 导出为变量
rehype-prismPrism 代码高亮
@shikijs/rehypeShiki 代码高亮(支持更多主题)
rehype-slug自动添加标题 ID
rehype-autolink-headings标题自动链接
remark-toc目录生成
rehype-katexLaTeX 数学公式
rehype-mathjaxMathJax 数学渲染

学习资源


相关资源


本文档由 归愚知识系统 自动生成