在nextjs中渲染Markdown
- mdx 可以作为组件引入到.tsx 中使用,作为组件引入到.tsx 使用时,可以通过 mdx-components.tsx 设置 mdx 样式,也可以使用 mdx 组件的 components 属性设置样式
- 也可以将组件引入到.mdx 中使用。
- 也可以使用<div dangerouslySetInnerHTML={{ __html: content }} />或 import { MDXRemote } from "next-mdx-remote-client/rsc 渲染,content 为内容部分。
一、基础配置
1.安装依赖
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
2.配置 next.config.ts
import createMDX from '@next/mdx'
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}
const withMDX = createMDX({
extension: /\.(md|mdx)$/,
})
export default withMDX(nextConfig)
3.在项目根目录新建 mdx-components.tsx 文件并添加以下内容
import type { MDXComponents } from "mdx/types";
import Image, { ImageProps } from "next/image";
const generateId = (text: string) => text + new Date().getTime();
// markdown 解析文件,用于直接导入组件渲染的方式
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// 自定义h1的样式,并设置一个id,此处设置id的作用是用作锚点定位到相应位置。
h1: ({ children }) => (
<h1 id={generateId(children as string)}>{children}</h1>
),
img: (props) => (
<Image // 使用next/image的Image组件性能更好
sizes="100vw"
style={{ width: "100%", height: "auto" }}
{...(props as ImageProps)}
/>
),
...components,
};
}
二、使用
1.在.tsx 组件或页面中使用
{/* 引入.mdx组件 */}
import Me from "./_comp/me.mdx";
function CustomH2({ children }: { children: React.ReactNode }) {
return (
<h2 id="test" style={{ color: "red" }}>
{children}
</h2>
);
}
{/* 设置样式 */}
const overrideComponents = {
h2: CustomH2,
};
{/* 使用 */}
<div className="mx-auto bg-white">
{/* 通过 components={overrideComponents} 设置样式 */}
<Me components={overrideComponents} />
</div>
2.在<div dangerouslySetInnerHTML={{ __html: content }} />或 中使用
解析文件内容, 得到文档目录(toc)和内容(content)
import path from "path"
import { notFound } from "next/navigation";
import matter from "gray-matter";
import fs from "fs";
import { MdxFiles, TocItem } from "@/types/note";
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import remarkParse from 'remark-parse';
import { visit } from 'unist-util-visit';
import { unified } from 'unified'
// import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeSlug from 'rehype-slug' // 给h1~h6设置随机id
import { toString } from 'mdast-util-to-string';
import { generateHash } from "./utils";
import { h } from 'hastscript';
import type { Plugin } from 'unified';
import type { Node, Element, ElementContent } from 'hast';
import type { Heading } from 'mdast';
/**
* 读取MDX文件内容
* @param folder - 文件夹路径
* @param filename - 文件名
* @returns 处理后的内容
*/
export const readMdxContent = async (folder = "src/noteContent/zh-CN", filename = "readme") => {
const filePath = path.join(process.cwd(), folder, `${filename}.mdx`);
let fileContent: string;
try {
fileContent = fs.readFileSync(filePath, "utf8");
} catch (err) {
console.error(`MDX文件未找到: ${filePath}, ${err}`);
return notFound();
}
const { data, content } = matter(fileContent);
const toc: TocItem[] = [];
try {
const processedContent = await unified()
.use(remarkParse)
.use(() => (tree) => {
visit(tree, 'heading', (node: Heading) => {
const headingText = toString(node);
const id = generateHash(headingText, 8);
let uniqueId = id;
let counter = 1;
while (toc.some(item => item.id === uniqueId)) {
uniqueId = `${id}-${counter}`;
counter++;
}
toc.push({
depth: node.depth,
value: headingText,
id: uniqueId
});
if (!node.data) node.data = {};
if (!node.data.hProperties) node.data.hProperties = {};
node.data.hProperties.id = uniqueId;
});
})
.use(remarkRehype)
.use(rehypeSlug)
// .use(rehypeAutolinkHeadings, { // 给每个标题添加锚点
// behavior: 'wrap',
// properties: {
// class: 'heading-link',
// ariaHidden: 'true',
// tabIndex: -1
// }
// })
.use(rehypeCodeWithCopy) // 添加我们的自定义插件
.use(rehypeStringify)
.process(content);
// 添加复制功能的JavaScript代码
const copyScript = `
<script>
function copyToClipboard(button, text) {
navigator.clipboard.writeText(text).then(() => {
const originalText = button.textContent;
button.textContent = '已复制!';
button.classList.add('copied');
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('copied');
}, 2000);
}).catch(err => {
console.error('复制失败:', err);
button.textContent = '复制失败';
});
}
</script>
`;
return {
data,
content: String(processedContent) + copyScript,
toc
};
} catch (error) {
console.error('处理MDX内容时出错:', error);
return {
data,
content,
toc: []
};
}
}
读取.mdx 文件的“---”中的内容
---
title: '标题'
description: '描述'
date: '2023-08-29 12:15:00'
category: 'javascript'
tags: ['javascript', 'json']
---
获取函数
/**
* 获取所有mdx文件列表头部内容
* @param dirPath
* @param arrayOfFiles
* @returns
*/
export const getMdxFiles = (dirPath: string = "src/noteContent", arrayOfFiles: MdxFiles[] = []) => {
// 拿到文件及文件夹数组
const files = fs.readdirSync(dirPath);
files.forEach(file => {
// 判断是否为文件夹
const fullPath = path.join(dirPath, file);
if (fs.statSync(fullPath).isDirectory()) {
getMdxFiles(fullPath, arrayOfFiles);
} else if (path.extname(file) === '.mdx') {
const fileContent = fs.readFileSync(fullPath, 'utf8');
const { data } = matter(fileContent);
arrayOfFiles.push({
path: dirPath,
category: path.dirname(fullPath).split(path.sep).pop(),
slug: path.basename(file, '.mdx'),
title: data.title || path.basename(file, '.mdx'),
description: data.description || '',
date: data.date || '',
tags: data.tags || []
});
}
});
return arrayOfFiles;
}
在页面中使用
// import { MDXRemote } from "next-mdx-remote-client/rsc";
const { content, toc } = await readMdxContent(post.path, slug);
return (
<article className="flex">
<main className="prose mx-auto">
{/* 渲染内容 可远程获取 */}
{/* <MDXRemote source={content} /> */}
{/* 渲染内容 */}
<div dangerouslySetInnerHTML={{ __html: content }} />
</main>
<aside className="w-68 p-4">
<Catalog toc={toc} />
</aside>
</article>
);