Next.js 如何优雅地渲染 Markdown 文件?

Jan 12, 2025

#Next.js

Markdown 是一种用于格式化文本的轻量级标记语言。它允许你使用纯文本语法编写,并将其转换为结构有效的 HTML。它常用于编写网站和博客的内容,在Next.js中一般通过MDXReact-markdown来实现markdown的渲染

一、MDX渲染

MDX 是 markdown 的超集,它允许你直接在 markdown 文件中编写 JSX。这是一种强大的方式,可以添加动态交互性并在你的内容中嵌入 React 组件。

Next.js 可以支持应用程序内的本地 MDX 内容,以及在服务器上动态获取的远程 MDX 文件。Next.js 插件负责将 markdown 和 React 组件转换为 HTML,包括支持在服务器组件中使用 (在 App Router 中默认使用)。

渲染效果

1.1.本地markdown渲染

1.1.1.安装依赖

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

1.1.2.配置next.config.mjs

js
/** @type {import('next').NextConfig} */ const nextConfig = { // Configure `pageExtensions`` to include MDX files pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], } const withMDX = createMDX({ extension: /\.mdx?$/, // Add markdown plugins here, as desired options: { remarkPlugins: [remarkFrontmatter], rehypePlugins: [] } }) export default withMDX(nextConfig)

插件在后面介绍

1.1.3.添加mdx-components.tsx

根目录添加 mdx-components.tsx文件,全局 MDX 组件,这里可以对markdown中的一些标签进行样式定义,如h1h2

tsx
export function useMDXComponents(components: MDXComponents): MDXComponents { return { h1: (props) => { return ( <h1> <div id={props.id} className="invisible relative -top-24"></div> <a href={"#" + props.id} id={"#" + props.id}> {props.children} </a> </h1> ) }, …… code: (info) => { const { children } = info const id = Math.random().toString(36).substr(2, 9) if (info["data-language"]) { return ( <div className="not-prose rounded-md border"> <div className="flex h-12 items-center justify-between bg-zinc-100 px-4 dark:bg-zinc-900"> <div className="flex items-center gap-2"> <span className="text-sm text-zinc-600 dark:text-zinc-400"> {/* @ts-ignore */} {info["data-language"]} </span> </div> <CopyButton1 id={id} /> </div> <div className="overflow-x-auto"> <div id={id} className="p-4"> {children} </div> </div> </div> ) } else { return ( <code {...info} className="not-prose rounded bg-gray-100 px-1 dark:bg-zinc-900" > {children} </code> ) } }, ...components } }

PS:其中props.id需要配合rehypeSlug使用

1.1.4.创建mdx布局和页面

目录如下:

 my-project
  ├── app
  │   └── mdx-page
  │       └── layout.tsx
  │       └── page.(mdx/md)
  |── mdx-components.(tsx/js)
  └── package.json

layout.tsx

tsx
export default function MdxLayout({ children }: { children: React.ReactNode }) { return <div className="markdown m-auto max-w-5xl">{children}</div> }

page.mdx

markdown
## Vue3 编译宏 ### defineProps - 父子组件传参 ```vue filename="index.js" <template> <div> <Child name="xiaoman"></Child> </div> </template> <script lang="ts" setup> import Child from "./views/child.vue" </script> <style></style> ```

mdx可以直接在 markdown 文件中编写 JSX,所以我们可以在页面直接加入我们自己的组件,比如显示一些基本信息,这里以Frontmatter 组件为例子:

tsx
type FrontmatterProps = { date: string author: string // 其它元数据,如分类、标签、来源、阅读时长等 } export default function Frontmatter({ date, author }: FrontmatterProps) { return ( <div className="my-4 rounded-r border-l-4 border-gray-200 bg-gray-50 px-4 py-3"> {date}{author} </div> ) }

在page.mdx中可以直接加入

markdown
import Frontmatter from "@/components/Frontmatter" <Frontmatter date="2024-02-22" /> ...md内容

目前可完成基本渲染,可以配置一些markdown渲染插件优化整体渲染效果在下文1.3统一介绍

1.2.Markdown-remote渲染

除了本地的md文件,通过外部获取的md可以使用next-mdx-remote来渲染

渲染效果

1.2.1.安装依赖

bash
pnpm install next-mdx-remote

1.2.2.使用

tsx
import { CopyButton1 } from "@/components/CopyButton" import { getPostById } from "@/lib/post" import { MDXRemote } from "next-mdx-remote/rsc" export default async function RemoteMdxPage() { const post = await getPostById(3) if (!post) return <div>Post not found</div> const markdown = post.content const mdxOptions = { remarkPlugins:[] rehypePlugins:[] } const components = {} return ( <MDXRemote source={markdown} options={mdxOptions} components={components} /> ) }

MDXRemote可直接导入使用,source为外部md的字符串格式,这里由于接口直接

其中componets同1.3中的一致,可设置标签渲染样式等

mdxOptions包含两类配置remarkPluginsrehypePlugins,在1.3中统一介绍

1.3.MDX渲染插件配置

完成md的基础页面渲染后,需要两类插件remarkPluginsrehypePlugins来辅助实现代码高亮表格优化

插件列表:

插件名称插件类别插件功能
remark-gfmremarkPlugins支持 GitHub Flavored Markdown 语法,如表格、任务列表、删除线等扩展功能
rehype-slugrehypePlugins为 Markdown 中的标题(h1-h6)自动生成 id 属性,便于创建锚点链接和目录
rehype-pretty-coderehypePlugins提供代码块美化功能,支持行号、高亮特定行、主题定制等
@jsdevtools/rehype-tocrehypePlugins自动生成markdown目录

这里主要介绍下**rehype-pretty-code以及@jsdevtools/rehype-toc**,根据需求自定义配置

1.3.1.代码高亮-rehype-pretty-code

rehype-pretty-code是一款由shiki语法高亮器驱动的 Rehype 插件,可为 Markdown 或 MDX 提供漂亮的代码块。它既可以在构建时在服务器上工作(避免运行时语法高亮),也可以在客户端上进行动态高亮。

安装

bash
npm install rehype-pretty-code shiki

放在rehypePlugins配置中

ts
rehypePlugins: [ () => (tree) => { visit(tree, (node) => { if (node?.type === "element" && node?.tagName === "pre") { const [codeEl] = node.children if (codeEl.tagName !== "code") return node.raw = codeEl.children?.[0].value } }) }, [ rehypePrettyCode, { theme: "material-theme-lighter" } ], () => (tree) => { visit(tree, (node) => { if (node?.type === "element") { if (!("data-rehype-pretty-code-fragment" in node.properties)) { return } for (const child of node.children) { if (child.tagName === "pre") { child.properties["raw"] = node.raw } } } }) } ]

对插件配置的解释:

代码高亮处理流程是:

第一段代码提取并保存原始代码

第二段rehype-pretty-code 处理代码高亮

第三段代码将原始代码保存到高亮后的结构中

这样做是因为代码高亮处理会改变 DOM 结构,所以需要在处理前保存原始代码,然后在处理后重新附加到新的结构上。

1.3.2.目录生成-@jsdevtools/rehype-toc

@jsdevtools/rehype-toc可以根据markdown标签直接生成目录

安装:

bash
pnpm install @jsdevtools/rehype-toc

用法:

tsx
rehypePlugins: [ ...config [ //@ts-ignore toc, { headings: ["h1", "h2", "h3", "h4", "h5"], customizeTOC: (tocAll: any) => { data = tocAll return false } } ] ]

根据heading会生成一个目录树通过customizeTOC,通过目录树自行创建目录组件markdown-nav.tsx

tsx
"use client" import clsx from "clsx" import { useEffect, useState } from "react" export default function MarkdownNav(props: any) { switch (props.tagName) { case "nav": { return ( <nav {...props.properties}> {props.children.map((item: any, index: number) => { return <MarkdownNav {...item} key={index} /> })} </nav> ) } case "ol": { return ( <ol {...props.properties}> {props.children.map((item: any, index: number) => { return <MarkdownNav {...item} key={index} /> })} </ol> ) } case "li": { return ( <li {...props.properties}> {props.children.map((item: any, index: number) => { return <MarkdownNav {...item} key={index} /> })} </li> ) } case "a": { return ( <> <a {...props.properties} > {props.children.map((item: any, index: number) => { return <MarkdownNav {...item} key={index} /> })} </a> </> ) } default: return <>{props.value}</> } }

二、React-markdown渲染

除了官方提供的mdx渲染以外,还可以使用react-markdown来渲染

这里简单介绍一个使用例子作为了解,使用还是以官方MDX优先

2.1.安装依赖

bash
pnpm install react-markdown

2.2.使用

tsx
import { type Post } from "@/lib/post" import ReactMarkdown from "react-markdown" import rehypeHighlight from "rehype-highlight" import rehypeRaw from "rehype-raw" import rehypeSlug from "rehype-slug" import remarkGfm from "remark-gfm" export default function ReactMarkdownCom({ post }: { post: Post }) { return ( <ReactMarkdown rehypePlugins={[rehypeHighlight, rehypeSlug, rehypeRaw, remarkGfm]} components={} > {post.content} </ReactMarkdown> ) }

用法与React一致,插件与1.3的使用方法相同

⚠️**rehype-pretty-code**不可在ReactMarkdown中使用,有兼容性问题,可采用rehype-highlight或者react-syntax-highlighter来实现代码高亮

三、小结

通过使用 MDX 渲染React-markdown 渲染,开发者可以在 Next.js 中优雅地渲染 Markdown 文件,满足不同需求。MDX 提供了更强大的动态渲染能力,适合复杂的页面需求;而 React-markdown 则提供了简单且高效的解决方案,适用于较为静态的内容展示。结合各类插件,开发者可以进一步提升渲染效果,优化用户体验,打造更加优雅和高效的 Markdown 渲染方案。

目录