Introduction#
GitHub this note shows how to use react-markdown to render markdown content. The end goal is to create a technical blog
- ContentLayer to convert mdx to json
- React markdown to render the content json to HTML with styles
- Prism or others for syntax highlighting
Project Setup#
Create a new next.js project
npx create-next-app@latest
Add react-markdown and plugins
npm install react-markdown, remark-gfm
Add Tailwind typography
npm install -D @tailwindcss/typography
Update tailwind.config.js
module.exports = {content: ['./pages/**/*.{js,ts,jsx,tsx,mdx}','./components/**/*.{js,ts,jsx,tsx,mdx}','./app/**/*.{js,ts,jsx,tsx,mdx}'],theme: {extend: {}},plugins: [require('@tailwindcss/typography')]}
Project structure
|--app|--global.css|--layout.css|--page.tsx|--blog|--page.tsx|--public|--blog.mdx|--posts|--post-1.mdx|--post-2.mdx|--contentlayer|--tailwind.config.js|--package.json|--next.config.js|--contentlayer.config.js
React Markdown#
Let use react-markdown to render a markdown content either from file or code inline. Please take note
- Install and setup the Tailwind typography
- Install and setup remark-gfm plugin
- Custom style heading, imag, etc.
import React from 'react'import { ReactMarkdown } from 'react-markdown/lib/react-markdown'import remarkGfm from 'remark-gfm'import * as fs from 'fs'import * as path from 'path'const data = fs.readFileSync(path.resolve('./public', 'blog.mdx'), {encoding: 'utf-8'})export default function Home() {return (<main className="flex min-h-screen flex-col items-center justify-between p-24"><div><ReactMarkdownrehypePlugins={[remarkGfm]}className="prose"components={{h1: ({ ...props }) => {return <h1 className="text-4xl font-semibold" {...props}></h1>},img: ({ node: _node, src, placeholder, alt, ...props }) => {return <img src={src} {...props}></img>}}}>{data}</ReactMarkdown></div></main>)}
Syntax Highligher#
There are some options
- Option 1. prism
- Option 2. react syntax highlighter
- Option 2. rehype-pretty-code
Let try the react syntax highlighter by updating the MDXComponent as below
'use client'SyntaxHighlighter.registerLanguage('javascript', javascript)SyntaxHighlighter.registerLanguage('jsx', jsx)SyntaxHighlighter.registerLanguage('tsx', tsx)SyntaxHighlighter.registerLanguage('typescript', typescript)SyntaxHighlighter.registerLanguage('json', json)SyntaxHighlighter.registerLanguage('bash', bash)const MDXComponent = ({ content }: { content: string }) => {const [mounted, setMounted] = useState(false)// prevent some wierd erroruseEffect(() => {setMounted(true)}, [])if (!mounted) {return <></>}return (<div><ReactMarkdownrehypePlugins={[remarkGfm]}className="prose"components={{h1: ({ ...props }) => {return <h1 className="text-4xl font-semibold" {...props}></h1>},img: ({ node: _node, src, placeholder, alt, ...props }) => {return <img src={src} {...props}></img>},code({node,inline,className,children,style,...props}: CodeProps) {const languages = /language-(\w+)/.exec(className || '')const language = languages ? languages[1] : null// const language = "javascript";return language ? (<SyntaxHighlighterstyle={oneDark}language={language}PreTag="div"wrapLines={false}useInlineStyles={true}{...props}>{children as unknown as any}</SyntaxHighlighter>) : (<codeclassName="rounded bg-gray-800 p-1 text-white before:content-[''] after:content-none"{...props}/>)}}}>{content}</ReactMarkdown></div>)}export default MDXComponent
ContentLayer#
Let setup contentlayer
npm install contentlayer next-contentlayer
Create and update the contentlayer.config.js as below
// contentlayer.config.jsimport { defineDocumentType, makeSource } from 'contentlayer/source-files'export const Post = defineDocumentType(() => ({name: 'Post',filePathPattern: `**/*.mdx`,contentType: 'mdx',fields: {title: {type: 'string',description: 'The title of the post',required: true},description: {type: 'string'},date: {type: 'date',description: 'The date of the post',required: true}},computedFields: {url: {type: 'string',resolve: post => `/posts/${post._raw.flattenedPath}`}}}))export default makeSource({contentDirPath: 'posts',documentTypes: [Post]})
Update the next.config.js with baseUrl as
const { withContentlayer } = require('next-contentlayer')module.exports = withContentlayer({})
Now we can use contentlayer to transform the mdx file into json by npm run build and check the generated things in .contentlayer
npm run build
Then just pass the generated content in to the MDXComponent and provide styles via components as above section
import React from 'react'import MDXComponent from '@/components/mdx-component'import { allPosts } from '.contentlayer/generated'const getPost = async () => {return allPosts[0].body.raw}const Blog = async () => {const data = await getPost()return (<main className="flex min-h-screen flex-col items-center justify-between p-24"><div><MDXComponent content={data}></MDXComponent></div></main>)}export default Blog