在Next.js中解析MDX文件并自动生成标题锚点链接
在Next.js中自动为MDX标题添加锚点并生成目录链接
方法一:自定义MDX组件+React上下文实现
适合客户端动态收集标题的场景,步骤如下:
- 创建标题收集上下文
// components/TitleContext.js import { createContext, useContext, useState } from 'react'; const TitleContext = createContext({ titles: [], addTitle: () => {} }); export function TitleProvider({ children }) { const [titles, setTitles] = useState([]); const addTitle = (text, id) => setTitles(prev => [...prev, { text, id }]); return ( <TitleContext.Provider value={{ titles, addTitle }}> {children} </TitleContext.Provider> ); } export function useTitles() { return useContext(TitleContext); }
- 自定义H2标题组件
自动生成递增ID,并将标题信息存入上下文:
// components/H2.js import { useTitles, useRef } from 'react'; export function H2({ children }) { const { addTitle } = useTitles(); const countRef = useRef(1); const id = `heading-${countRef.current}`; countRef.current++; addTitle(children, id); return <h2 id={id}>{children}</h2>; }
- 配置MDXProvider
在_app.js里注册自定义组件并包裹上下文:
// pages/_app.js import { MDXProvider } from '@mdx-js/react'; import { TitleProvider } from '../components/TitleContext'; import { H2 } from '../components/H2'; const components = { h2: H2 }; export default function MyApp({ Component, pageProps }) { return ( <MDXProvider components={components}> <TitleProvider> <Component {...pageProps} /> </TitleProvider> </MDXProvider> ); }
- 在页面中渲染目录与MDX内容
// pages/posts/[slug].js import { useTitles } from '../../components/TitleContext'; import MyPost from './my-post.mdx'; export default function PostPage() { const { titles } = useTitles(); return ( <div className="container"> {/* 自动生成的锚点目录 */} <nav className="toc"> {titles.map((title, idx) => ( <a key={idx} href={`#${title.id}`}>{title.text}</a> ))} </nav> {/* MDX内容 */} <MyPost /> </div> ); }
方法二:Remark插件服务端解析处理
适合静态生成页面,提前在服务端收集标题并生成ID,性能更优:
- 编写自定义Remark插件
同时完成标题ID生成和标题信息收集:
// lib/remarkCollectTitles.js import { visit } from 'unist-util-visit'; export default function remarkCollectTitles() { return (tree, file) => { const titles = []; let count = 1; visit(tree, 'heading', (node) => { // 仅处理H2标题,可修改depth值适配H1-H6 if (node.depth === 2) { const text = node.children.map(child => child.value).join(''); const id = `heading-${count}`; // 给标题节点添加ID属性 node.data = node.data || {}; node.data.hProperties = node.data.hProperties || {}; node.data.hProperties.id = id; // 收集标题信息 titles.push({ text, id }); count++; } }); // 将标题列表存入文件元数据 file.data.titles = titles; }; }
- 配置Next.js与MDX
在next.config.js中启用MDX并加载插件:
// next.config.js const withMDX = require('@next/mdx')({ extension: /\.mdx?$/, options: { remarkPlugins: [require('./lib/remarkCollectTitles')], rehypePlugins: [] } }); module.exports = withMDX({ pageExtensions: ['js', 'jsx', 'md', 'mdx'] });
- 在页面中获取标题并渲染
使用next-mdx-remote在服务端解析MDX并传递标题列表:
// pages/posts/[slug].js import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; import { MDXRemote } from 'next-mdx-remote'; import { serialize } from 'next-mdx-remote/serialize'; import remarkCollectTitles from '../../lib/remarkCollectTitles'; export async function getStaticProps() { // 读取MDX文件 const filePath = path.join(process.cwd(), 'pages', 'posts', 'my-post.mdx'); const fileContent = fs.readFileSync(filePath, 'utf8'); const { content } = matter(fileContent); // 序列化MDX并获取标题列表 const mdxSource = await serialize(content, { mdxOptions: { remarkPlugins: [remarkCollectTitles] } }); const titles = mdxSource.data.titles; return { props: { mdxSource, titles } }; } export default function PostPage({ mdxSource, titles }) { return ( <div className="container"> <nav className="toc"> {titles.map((title, idx) => ( <a key={idx} href={`#${title.id}`}>{title.text}</a> ))} </nav> <MDXRemote {...mdxSource} /> </div> ); }
两种方法均可自动完成:
- 给所有H2标题添加
heading-1、heading-2格式的ID - 生成对应锚点链接的目录列表
内容的提问来源于stack exchange,提问作者Evanss
相关产品推荐
相关产品推荐

