How to parse Markdown in Next.js
By Flavio Copes
Learn how to parse Markdown in Next.js using marked, dompurify, and jsdom, running DOMPurify.sanitize() inside getStaticProps so it works server-side in Node.
To parse Markdown in a Next.js page, convert it to HTML with marked inside getStaticProps(), sanitize the result with DOMPurify, and render the HTML string in your component.
I had a field with markdown and I wanted to print it in a Next.js page, so that’s exactly what I did.
I used marked, dompurify and jsdom. Each one has a job. marked converts the Markdown string to HTML. DOMPurify removes anything dangerous from that HTML. jsdom gives DOMPurify a DOM to work with on the server.
By the way, if your markdown documents are long, a table of contents helps. I made a markdown TOC generator that builds one from your headings.
Why sanitize the output?
Markdown can contain raw HTML. If the content comes from users, like the item descriptions in my case, someone can put a <script> tag or an onerror attribute in there. Render that as-is and you have an XSS hole. DOMPurify strips the dangerous parts and keeps the safe markup.
There’s a catch: DOMPurify assumes a browser environment with a window object. I had to call DOMPurify.sanitize() inside getStaticProps(), and there we’re in a Node.js environment, where no window exists. That’s what jsdom solves. It creates one.
The code
Here’s an example in a Next.js dynamic page, to render an item’s description:
import prisma from 'lib/prisma'
import { getItem, getItems } from 'lib/data.js'
import marked from 'marked'
import createDOMPurify from 'dompurify'
import { JSDOM } from 'jsdom'
export default function Item({ item }) {
return <div dangerouslySetInnerHTML={{ __html: item.description }} />
}
export async function getStaticPaths() {
const items = await getItems(prisma)
return {
paths: items.map(item => ({
params: {
id: String(item.id),
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const id = String(params.id)
const item = await getItem(prisma, id)
const window = new JSDOM('').window
const DOMPurify = createDOMPurify(window)
item.description = DOMPurify.sanitize(marked(item.description))
return { props: { item } }
}
Doing the work in getStaticProps() has a nice side effect: it runs at build time, so marked, DOMPurify and jsdom never end up in the client bundle. The browser receives ready-made HTML.
Rendering the HTML
The description is now an HTML string. If you render it as {item.description} in JSX, React escapes it, and the user sees the literal tags printed on the page, like <p>Great job</p>. That’s why the component uses dangerouslySetInnerHTML.
The name is scary on purpose, but here we’re covered. The string went through DOMPurify first, and that’s the whole point of this setup.
One version note: recent releases of marked removed the default export. If you’re on a current version, import it with import { marked } from 'marked' and call marked.parse(item.description) instead.
Related posts about next: