article

MDX 2 with Next.js and Embed Components (ESM version)

1 Apr 2022 | 5 min read

markdown for the component era

Motivation and Introduction

We've crafted quite a few websites now with Next.js and we're really loving the experience and outcomes. Thanks to Opstrace we were able to Open Source a huge component to render product documentation on your own Next.js page: Next Product Docs.

The entire Markdown ecosystem is evolving though and we're happy to welcome the release of MDX 2 which brings a set of really cool new features, such as:

  • πŸ“ Improved syntax makes it easier to use markdown in JSX
  • πŸ§‘β€πŸ’» JavaScript expressions turn {2 \* Math.PI} into 6.283185307179586
  • πŸ”Œ New esbuild, Rollup, and Node.js integrations
  • and much more. You can read the announcement here.

As it is in an ecosystem, some parts are moving faster or slower than others, so moving to MDX 2 isn't without a few hoops to jump.

We're going to integrate unified/remark/rehype for improved Markdown handling and extensibility with useful features such as GitHub Flavored Markdown, external link handling etc. and we're also integrating embeds to show off your work on 3rd party platforms such as Twitter, Instagram etc. rehype is a tool that transforms HTML with plugins, while remark transforms Markdown.

In this article we're using two different kind of embed mechanisms:

remark-embedder is awesome because it converts simple links into embeds based on the oEmbed format. Everything is done during build/render time, so the embed code is rendered directly into the compiled html without any client-side JavaScript. You can find a list of supported oEmbed providers here.

mdx-embed takes a different approach and offers components that can be imported in mdx. They have a different set of components/embeds.

The good news: you can use both approaches in the same file if you wish to. Here's an example rendered with remark-embedder:

Bonus: it works with ESM.

Getting Started

You can either start from scratch or upgrade your existing Next.js/MDX project. As a reference, we created a repository on GitHub with a working example.

Setup

We're listing the required packages, use your preferred package manager to install those.

The basics (MDX loader for Next.js):

@mdx-js/loader @next/mdx

remark/rehype plugins

rehype-external-links remark-gfm remark-frontmatter

remark-embedder

If you want to include oEmbeds during build time:

@remark-embedder/core @remark-embedder/transformer-oembed

mdx-embed

And last but not least "mdx-embed" for embedding components:

mdx-embed

Next.js Config

We're integrating mdx handling via webpack and the @mdx-js/loader. You'll need to add the pageExtensions and webpack config. Feel free to add / remove remark/rehype plugins here.

import remarkFrontmatter from 'remark-frontmatter'
import remarkGfm from 'remark-gfm'
import rehypeExternalLinks from 'rehype-external-links'
import fauxRemarkEmbedder from '@remark-embedder/core'
import fauxOembedTransformer from '@remark-embedder/transformer-oembed'
const remarkEmbedder = fauxRemarkEmbedder.default
const oembedTransformer = fauxOembedTransformer.default
const nextConfig = {
reactStrictMode: true,
experimental: { esmExternals: true },
pageExtensions: ['md', 'mdx', 'jsx', 'js'],
webpack: function (config) {
config.module.rules.push({
test: /\.mdx?$/,
use: [
{
loader: '@mdx-js/loader',
options: {
rehypePlugins: [rehypeExternalLinks],
remarkPlugins: [
remarkGfm,
remarkFrontmatter,
[remarkEmbedder, { transformers: [oembedTransformer] }]
]
}
}
]
})
return config
}
}
export default nextConfig

JSX Page

You can create a new page (ie [post].jsx). We're able to use a dynamic import to load the contents of the MDX file as a regular component. We're also dynamically importing mdx-embed, as the module is not ESM compatible yet.

import dynamic from 'next/dynamic'
import { getSlugs, getMeta } from 'utils/posts.js'
import { MDXProvider } from '@mdx-js/react'
export default function Post({ post, meta }) {
// import mdx
const Post = dynamic(import(`content/posts/${post}.mdx`))
// dynamic import because not ESM compatible
const embeds = dynamic(() => import('mdx-embed'))
const { CodePen, Gist } = embeds
const components = {
CodePen,
Gist
}
return (
<div>
<main>
<h1>{meta.title}</h1>
<article className="prose">
<MDXProvider components={components}>
<Post />
</MDXProvider>
</article>
</main>
</div>
)
}

MDX Content

This is the file that's being rendered. It's a simple MDX file using regular Markdown YML frontmatter, rather than an export. You can change that to your liking. The components need to be imported again, otherwise there's an error, you can find the full list of supported components on the mdx-embed page.

---
title: hello world
date: '2022-04-01'
preview: showing off twitter embeds
---
import { CodePen, Gist } from 'mdx-embed'
This is a demo post.
### Twitter Embed
https://twitter.com/PatrickHeneise/status/1508503730295037954
### The Gist ...
<Gist gistLink="PatrickHeneise/bbca1a8c4816f92aa3796db41a4a6203" />
### And the Pen
<CodePen codePenId="PNaGbb" />

In addition to those embed components, you can also add your own JSX components. As an exmaple, we created a small <Gallery> component taking an array of images and displaying that in a grid:

The Markdown for this is very simple:

### Component
<Gallery images={['cyprus1.jpeg', 'cyprus2.jpeg', 'cyprus3.jpeg']} caption="This
is a caption" />

This opens up a whole new world of possibilities when it comes to Markdown content.

Summary & Feedback

In this article we explored the basics of and upgrade to MDX2. We also learned how to add rehype/remark plugins to add features to our Markdown content during render/build time and how to use the mdx-embeds to create an easy and rich experience with 3rd party platforms. You can find the minimal example blog on GitHub.

We hope you enjoyed this article and it'll help you on your journey with Next.js and MDX2. If you have any feedback, comments, ideas about this article, please share them here or let us know on Twitter. We'd love to hear from you.

Patrick Heneise

Chief Problem Solver and Founder at Zentered

I'm a Software Enginer focusing on web applications and cloud native solutions. In 1990 I started playing on x386 PCs and crafted my first β€œwebsite” around 1996. Since then I've worked with dozens of programming languages, frameworks, databases and things long forgotten. I have over 20 years professional experience in software solutions and have helped many people accomplish their projects.