I recently decided to move all my blog content to a CMS (I'm using Contentful) and statically render it on Next.js. Previously, I was separately hosting my blog on a VPS running Ghost because it provides such a wonderful editing experience out of the box. But, it also has a lot of features that I never wanted to use. I just wanted to be able to quickly write things down to help me remember, with the added benefit of potentially helping someone else who may stumble upon it as well.
I knew that I wanted to use Markdown to format my content and that I also wanted syntax highlighting. So, I started to look into the available tooling for rendering Markdown in React and Next.js and was quickly overwhelmed by how many options there are. Ultimately, I decided on using Unified (AKA remark and rehype) for rendering the Markdown, and Shiki for highlighting the code.
After a while of trial-and-error, I came up with a straightforward React Server Component:
import rehypeShiki from "@shikijs/rehype";
import rehypeStringify from "rehype-stringify";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
interface Props {
children: string;
}
export async function Markdown(props: Props) {
const file = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeShiki, {
inline: "tailing-curly-colon",
theme: "material-theme-darker",
})
.use(rehypeStringify)
.process(props.children);
// If you use eslint, you'll probably need to add an ignore here
// biome-ignore lint/security/noDangerouslySetInnerHtml: Yeehaw
return <div dangerouslySetInnerHTML={{ __html: String(file) }} />;
}
This works great and is the setup I ended up going with, but I can't help but be a little dismayed that it takes five imports to get to this point. Four of which are strictly for handling the Markdown. To be fair, there is react-markdown which gives you a nicely packaged component:
import Markdown from 'react-markdown'
<Markdown>{markdown}</Markdown>
But unfortunately using this with the Shiki syntax highlighter does not work when rendering the content on the server, and I really didn't want to use Prism.
So, this is how I'm rendering the markdown on my personal site for now. I did notice that some of the pages would take ~15s to render when deployed to Vercel as dynamic pages. But, I overcame this problem by statically rendering them all at build time by using generateStaticParams on Next.js, which makes sense to do anyways.
If I missed something, feel free to reach out!