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>
<ReactMarkdown
rehypePlugins={[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

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 error
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <></>
}
return (
<div>
<ReactMarkdown
rehypePlugins={[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 ? (
<SyntaxHighlighter
style={oneDark}
language={language}
PreTag="div"
wrapLines={false}
useInlineStyles={true}
{...props}
>
{children as unknown as any}
</SyntaxHighlighter>
) : (
<code
className="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.js
import { 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

Reference#