MDX-Bundler Is My New Best Friend

This post is about MDX and Kent C. Dodds' mdx-bundler. That's right. The whole thing. I'm receiving nothing in return for posting this, and I'm not sponsored by 'ol Kent. It's just a personal golden child that I'm excited to share with the world.

For this post, we'll use this blog as our example. It's built using Next.js, which I couldn't recommend more for sites with mostly static content. For my info on the ins and outs of the blog, shimmy over to this post.

Now, before we dive into the wonders of bundling, let's start from the top.

What's MDX?

You're likely familiar with the first two letters without even knowing it. All those Github . READMEs are written using Markdown or .md files. Markdown is, semi-ironically, a markup language that makes it easy to describe what a page should look like.

Essentially, it's HTML with fewer steps.

    • In Markdown an <h1> tag is just # H1 Title
    • An H2 is ## H2 Title
    • A list is as easy as - or *
    • Maybe a quote? >

This is all well and good and makes for a fantastic development experience for content creation, but its true power stems from the X. We'll call it the X-Factor (I'm sorry, I couldn't help it).

The X stands for extended, or Markdown Extended. This extension allows us to import and use custom React components within our Markdown content.

mdx
1 import TestButton from './TestButton';
2
3 # A Title, WOW!
4
5 Some content.
6
7 <TestButton />
8
9 ## Here's an H2
10
11 Some more content
12

The above would render the title as an <h1> and our button as the React component you'd expect. On its own, MDX has a ton of things going for it. Most notably, a simplified developer experience for content creators, but with the full suite of interactivity offered via React!

And we haven't even gotten to mdx-bundler and gray-matter Things are about to get spicier.

NOTE: Currently, I do not recommend MDX for CRA-based (create-react-app) applications. It takes far too much monkey-patching to get up and running, and I ran into numerous issues with race conditions from a traditional server-side setup. That isn't to say it can't be done, but alternative methods save more than a few headaches.

Using MDX with Next.js, or any framework, requires a few dependencies. The names of these imports, however, do differ slightly from framework to framework:

Enter mdx-bundler & gray-matter

The former complies all of your Markdown and its dependencies to serve at run-time rather than build-time.

    • Build-time refers to the steps to compile and prepare your code for production.
    • Run-time occurs when your application runs in response to user requests and occurs after the build and compile stages.

Say you notice a typo in a published post. With build-time solutions, correcting this error would require a rebuild and redeploy. And as each change triggers a rebuild, your build times increase the more content you serve.

On the other hand, say you have a pre-defined route for your MDX content. When a request arrives, the MDX is loaded and passed off to mdx-bundler to render. Your build times never increase, as the bundler handles each render on request. All post data can be cached to avoid unnecessary re-bundling if looking for even more speed.

And then we have gray-matter. This package parses any frontmatter, or metadata found within your content. These items can be used throughout your codebase and significantly improve your SEO.

In the following, we'll loosely utilize the handy and official Next.js guide.

So, we've covered what mdx-bundler and gray-matter are, but how do we use them? As previously mentioned, gray-matter is a dependency for mdx-bundler. Before we dive into bundling, let's take a quick dip into gray-matter.

tsx
1 ---
2 title: Post One
3 seoTitle: How I built his post using MDX and mdx-bundler
4 abstract: 10 reasons doctors HATE Post One
5 isPublished: true
6 publishedOn: 2023-08-11
7 ---
8 import React from 'react';
9 import TestButton from './TestButton';
10
11 # A Title, WOW!
12
13 Some content.
14
15 <TestButton />
16
17 ## Here's an H2
18
19 Some more content
20

Everything situated between three dashes is our frontmatter. The gray-matter package parses our content, separating its values into content and data. Content holds our parsed Markdown, and data our frontmatter.

mdx-bundler does the same but returns our values as code and frontmatter. Code contains the content of our posts, and data-- you get it--the metadata, or frontmatter. Now, let's move on to the main event, the Great Bundling.

mdx-bundler utilizes two primary functions, bundleMDX and getMDXComponent. The former handles the actual bundling, while the latter is used to populate that bundled content with React components.

Your bundle function should look something like this:

tsx
1 export async function getPostData(slug: string) {
2 const fullPath = path.join(articlesPath, `${slug}.mdx`);
3 const source = fs.readFileSync(fullPath, 'utf-8');
4
5 const { code, frontmatter } = await bundleMDX({
6 source: source,
7 mdxOptions(options, frontmatter) {
8 options.rehypePlugins = [
9 rehypeSlug,
10 rehypeAutolinkHeadings,
11 ];
12 return options;
13 },
14 });
15
16 return {
17 slug,
18 frontmatter,
19 code,
20 };
21 };
22

To break that all down a bit:

    • Collect all MDX files in the pages directory via fs.readdirSync
    • Load and parse all my frontmatter via gray-matter
    • Bundle the MDX and parsed frontmatter with mdx-bundler
    • Pass any options or plugins that dictate the format of our content to mdx-bundler
    • Return the data, which in Next must be an object.

As for the options and plugins, mdx-bundler can handle several extensions, but remark and rehype are the two I see most commonly.

In the above snippet, we're using rehypeSlug and rehypeAutoLinkHeaders. We use the Slug for dynamic routing, AutoLinkHeaders for our table of contents, and header links. Note that in Next apps, staticProps must be returned as an object, hence the usage of return {}.

Where the bundled magic happens

It's awesome enough that mdx-bundler compiles all your content to serve run-time, but its true strength lies in how it handles your reusable components.

This blog has a handful of components that I use for each post. Take the anchor links found in the headers. I created and styled the component once, passed it to the bundler, and every single header in every single MDX post shared that universal style. It's my incumbent golden child for a reason!

But like anything in tech, this works better with a demonstration. To quickly recreate the above:

tsx
1 import { FiLink } from "react-icons/fi";
2
3 type HeaderProps = {
4 children: string;
5 };
6
7 /*
8 Ensure each header has a unique id,
9 I recommend something a little more robust
10 than this.
11 */
12
13 const generateId = (text: string) => {
14 return text
15 .toLowerCase()
16 .replace(/[^a-z0-9]+/g, "-")
17 .replace(/(^-|-$)/g, "");
18 };
19
20 // repeat for subsequent header tags
21 export const H1 = ({ children }: HeaderProps) => {
22 const id = generateId(children);
23 return (
24 <h1 id={id}>
25 {children}
26 <a href={`#${id}`} aria-hidden>
27 <FiLink />
28 </a>
29 </h1>
30 );
31 };
32

Before we wrap, once everything's bundled up, how do we pass the data to our components? That's where getMDXComponent steps in and why this bundler is an absolute workhorse.

plaintext
1 export default function BlogPost({
2 code,
3 frontmatter
4 }: BlogPostProps) {
5 const Component = useMemo(() => getMDXComponent(code), [code]);
6
7 return (
8 // cut for brevity
9 <article>
10 <Component
11 components={{
12 h1: HeaderComponents.h1,
13 h2: HeaderComponents.h2,
14 h3: HeaderComponents.h3,
15 h4: HeaderComponents.h4,
16 }}
17 />
18 </article>
19 );
20 };
21

In plain English, it automates the importing, and even usage, of React components in your blog posts. So, the only imports you need are bespoke components used exclusively in a single post. The rest? They're already bundled.

The switch made writing an absolute joy and was worth every ounce of time spent porting. Gone are the days of tedious HTML structuring for each post, and I couldn't be happier.

Wrapping up

If I had to give mdx and mdx-bundler a tagline, I'd go with, "Get back to the fun parts."

I love to build, but I also love to write. This setup allows me to do just that. Focus on the content and spend my development energy on adding fun, possibly even useful, features to the site.

For my tastes, the flow is the perfect happy medium between full-code HTML grinds and no-code auto-blog sites. So, if you're anything like me and hopelessly torn between two loves, give mdx and mdx-bundler a long look.

Once you do, you might not look back.

Why does that sound so dramatic?