淘先锋技术网

首页 1 2 3 4 5 6 7

nextjs+MDX渲染md文件并生成目录

一、效果展示

线上网站:点击体验
md文件
在这里插入图片描述

网页
在这里插入图片描述

二、需要使用的模块包

包名作用
@mdx-js/react用来渲染react组件
@next-mdx-remote将字符串转换成html标签
@remark-slug为html标签添加id(锚点跳转)
@mapbox/rehype-prism添加代码高亮
@jsdevtools/rehype-toc生成目录
@remark-gfm渲染表格插件

三、使用方法

项目目录结构
1

1.在_app.js中导入mdx-js/react
import { MDXProvider } from '@mdx-js/react';
function MyApp({ Component, pageProps }) {
  // 定义 mdx 中语法的映射组件
  const components = {
    // img: props => <Image {...props}></Image>,
    h1: props => {
      return <h1>
        <div id={props.id} className='relative -top-24 invisible' ></div>
        <a href={'#' + props.id}>{props.children}</a>
      </h1>
    },
    h2: props => {
      return <h2>
        <div id={props.id} className='relative -top-24 invisible' ></div>
        <a href={'#' + props.id}>{props.children}</a>
      </h2>
    },
    h3: props => {
      return <h3>
        <div id={props.id} className='relative -top-24 invisible' ></div>
        <a href={'#' + props.id}>{props.children}</a>
      </h3>
    },
    h4: props => {
      return <h4>
        <div id={props.id} className='relative -top-24 invisible' ></div>
        <a href={'#' + props.id}>{props.children}</a>
      </h4>
    },
    wrapper: ({ children, ...props }) => {
      // console.log(children.map(child => child.props.mdxType))
      // console.log(React.Children.toArray(children), 999)
      // console.log(children)
      // console.log(props, 111)
      // if (props.layout) {
      //   return <main {...props} />
      // }
      return <>{children}</>
    }
  }

  return <MDXProvider components={components}>
    <Component {...pageProps}/>
  </MDXProvider>
}

export default MyApp
2.在blog/[name].js中

2.1通过getStaticProps在服务器使用fs模块运行读取content文件目录下的md文件内容
2.2配置MDX各种插件
2.3在通过MDXRemote将文件内容渲染出来
2.4通过插件获取到元数据和目录
2.5在组件prop中获取到内容
2.6通过自定义组件TableContents生成自己的目录

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'
import Layout from "../../components/Layout";
import slug from 'remark-slug'
import remarkGfm from 'remark-gfm'
import rehypePrism from '@mapbox/rehype-prism'
import TableContents from '../../components/TableContents';
import React from 'react';

// import { useInView } from 'react-intersection-observer';
export default function BlogPage(props) {
  const [id, setId] = React.useState(array[0])
  return (
    <div className="wrapper">
      <Layout href='/blog' full={true} theme={props.theme} setTheme={props.setTheme}>
        {/* <div>{props.source.frontmatter.title}</div> */}
        <div className='grid grid-cols-4 gap-6 relative'>
          <div className='markdown xl:col-span-3 col-span-5 p-5 rounded-lg dark:bg-[#222222] box-shadow bg-white'>
            <MDXRemote {...props.source} />
          </div>
          <div className='xl:block hidden'>
            <div className='sticky top-24 p-5 rounded-lg dark:bg-[#222222] box-shadow bg-white'>
              <h3 className="text-xl text-gray-900 dark:text-gray-100 dark:opacity-90 font-bold pb-4">目录</h3>
              <TableContents idTable={id} {...props.tocElement}></TableContents>
            </div>
          </div>
        </div>
      </Layout>
    </div>
  )
}

export async function getStaticProps(context) {
  // MDX text - can be from a local file, database, anywhere
  const fs = require('fs')
  const path = require('path')
  const toc = require("@jsdevtools/rehype-toc");
  let tocElement

  const { name } = context.params
  let source = String(fs.readFileSync(path.join(process.cwd(), 'src', 'content', name + '.md')))
  const mdxSource = await serialize(source, {
    scope: {},
    mdxOptions: {
      remarkPlugins: [slug, remarkGfm],
      rehypePlugins: [rehypePrism, [toc, {
        headings: ['h1', 'h2', 'h3', 'h4'],
        customizeTOC: (tocAll) => {
          tocElement = tocAll
          return false
        }
      }]],
      format: 'mdx'
    },
    parseFrontmatter: true
  })
  return { props: { source: mdxSource, tocElement: tocElement } }
}
export async function getStaticPaths() {
  return {
    paths: [],
    fallback: 'blocking'
  }
}

TableContents组件

//拼接字符串工具
import clsx from 'clsx'
import React from 'react'

export default function TableContents(props) {
  switch (props.tagName) {
    case 'nav': {
      return (
        <nav {...props.properties}>{props.children.map((item, index) => {
          return <TableContents {...item} key={index} idTable={props.idTable} />
        })}</nav>
      )
    };
    case 'ol': {
      return (
        <ol {...props.properties}>{props.children.map((item, index) => {
          return <TableContents {...item} key={index} idTable={props.idTable} />
        })}</ol>
      )
    };
    case 'li': {
      return (
        <li {...props.properties}>{props.children.map((item, index) => {
          return <TableContents {...item} key={index} idTable={props.idTable} />
        })}</li>
      )
    };
    case 'a': {
      return (
        <a {...props.properties} className={clsx('block py-1 text-sm font-medium hover:text-[#428dcc] focus:outline-none dark:hover:text-gray-200 focus-visible:text-gray-700 dark:opacity-90 dark:focus-visible:text-gray-200 text-gray-400', props.properties.href == '#' + props.idTable && 'text-[#428dcc] dark:text-gray-200')}>{props.children.map((item, index) => {
          return <TableContents {...item} key={index} />
        })}</a>
      )
    };
    default: return (
      <>{props.value}</>
    )
  }
}

3.引入CSS样式

在_app.js中
markdown样式可自行设计
prism官网代码高亮样式
点击链接下载

import '../styles/prism.css'
import '../styles/markdown.css'
4.项目源码链接github