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?
>
- For a full list, check the Markdown Cheat Sheet
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.
1 import TestButton from './TestButton';23 # A Title, WOW!45 Some content.67 <TestButton />89 ## Here's an H21011 Some more content12
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:
esbuild
- required formdx-bundler
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 thebuild
andcompile
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
.
1 ---2 title: Post One3 seoTitle: How I built his post using MDX and mdx-bundler4 abstract: 10 reasons doctors HATE Post One5 isPublished: true6 publishedOn: 2023-08-117 ---8 import React from 'react';9 import TestButton from './TestButton';1011 # A Title, WOW!1213 Some content.1415 <TestButton />1617 ## Here's an H21819 Some more content20
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:
1 export async function getPostData(slug: string) {2 const fullPath = path.join(articlesPath, `${slug}.mdx`);3 const source = fs.readFileSync(fullPath, 'utf-8');45 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 });1516 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 viafs.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:
1 import { FiLink } from "react-icons/fi";23 type HeaderProps = {4 children: string;5 };67 /*8 Ensure each header has a unique id,9 I recommend something a little more robust10 than this.11 */1213 const generateId = (text: string) => {14 return text15 .toLowerCase()16 .replace(/[^a-z0-9]+/g, "-")17 .replace(/(^-|-$)/g, "");18 };1920 // repeat for subsequent header tags21 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.
1 export default function BlogPost({2 code,3 frontmatter4 }: BlogPostProps) {5 const Component = useMemo(() => getMDXComponent(code), [code]);67 return (8 // cut for brevity9 <article>10 <Component11 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?