Dipesh Wagle's Logo
BlogLibrary

MDX in Next.js using mdx-bundler

Guide on creating a simple mdx-powered blog in Next.js using mdx-bundler

Written By

Dipesh Wagle

Introduction

Starting a new dev blog these days can be a daunting task, there are so many choices to make. While deciding to build this blog I knew I wanted to use Next.js and MDX to author content. Even for integrating MDX with Next.js as outlined by joshwcomeau's great article here, there are plenty of choices. Upon some research, I decided to go with mdx-bundlers because it has great features like

  • Component import in mdx
  • Ability to use frontmatter and meta and constants in mdx
  • Bundle image in mdx

and many more and the library is actively developed and has great perfomance.

mdx-bundler is great but when I went about integrating it to my blog I was little confused as there were no resources about it and it was my first time getting dirty and doing it myself ( previously used mdx with Gatsby previously). So in this tutorial I'm going to show you how to use mdx-bundler with Next.js.

Getting Started

First let's create a new Next.js project.

npx create-next-app
# or
yarn create next-app

For this guide we are going to build a simple app that reads mdx files from a folder and creates post for each file. We will show list of posts in the index page. I'm starting from a new Next.js app but you can your existing Next.js project and follow along.

Now let's add mdx-bundler to our app.

npm install mdx-bundler
#or
yarn add mdx-bundler

We are also going to use gray-matter, which is a library that parses frontmatter from out mdx files.

npm install gray-matter
#or
yarn add gray-matter

When you create a Markdown file, you can include a set of key/value pairs that can be used to provide additional data relevant to specific pages in the GraphQL data layer. This data is called “frontmatter” and is denoted by the triple dashes at the start and end of the block.

Reading mdx files

Now we will create a folder where we will store all our posts. I will use data/posts on the root of the projects. To store all mdx related functions let's also new file utils/mdx.js .

import fs from "fs";
import path from "path";
import matter from "gray-matter";
import { bundleMDX } from "mdx-bundler";
export const POSTS_PATH = path.join(process.cwd(), "data/_posts");
export const getSourceOfFile = (fileName) => {
return fs.readFileSync(path.join(POSTS_PATH, fileName));
};
export const getAllPosts = () => {
return fs
.readdirSync(POSTS_PATH)
.filter((path) => /\.mdx?$/.test(path))
.map((fileName) => {
const source = getSourceOfFile(fileName);
const slug = fileName.replace(/\.mdx?$/, "");
const { data } = matter(source);
return {
frontmatter: data,
slug: slug,
};
});
};
export const getSinglePost = async (slug) => {
const source = getSourceOfFile(slug + ".mdx");
const { code, frontmatter } = await bundleMDX(source, {
cwd: POSTS_PATH,
});
return {
frontmatter,
code,
};
};

We define POSTS_PATH that has all the posts. Let's dig into detail about the other three functions.

  • getSourceOfFile : Given a fileName it get's the source of the file
  • getAllPosts : It reads the POSTS_PATH for mdx and processes the source for frontmatter data with gray-matter and creates slug for the post by removing mdx extension so we can have clean url.
  • getSinglePost: This is were it gets interesting mxd-bundler has two methods getMDXComponent for client side and bundleMDX for server side(node). First we create the bundled code from mdx source by using bundleMDX, it will alos parse frontmatter for us . It takes source and options, we are also setting cwd that allows us to basically include any component in the folder pointed by cwd to out MDX. We will put any components we required in the POSTS_PATH directory and it can be resolved in the MDX. Then in client side we can use getMDXComponent to render the mdx, we will use this later in the post page.

Creating list of posts

Now after setting up the helper function to work with the mdx file it's now time to create a page that shows all the list of posts. Let's change the pages/index.js

import Link from "next/link";
import { getAllPosts } from "../utils/mdx";
export default function BlogList({ posts }) {
return (
<div className="wrapper">
<h1>All Posts</h1>
<p>
Click the link below to navigate to a page generated by{" "}
<code>mdx-bundler</code>.
</p>
<ul>
{posts.map((post, index) => (
<li key={index}>
<Link href={`posts/${post.slug}`}>{post.frontmatter.title}</Link>
</li>
))}
</ul>
</div>
);
}
export const getStaticProps = async () => {
const posts = getAllPosts();
return {
props: { posts },
};
};

Here we use getStaticProps method from Next.js to statically generate the index page. We first get all the posts from getAllPosts and pass it to the BlogList component and render link to post in a list.

You can learn more about data fetching in Next.js here

Creating single post

Now let's create a file pages/posts/[slug].js . The filename may look different because it's a way to define dynamic routing in Next.js. We need dynamic routing because we want to generate different page for each blog post.

You can learn more about dynamic routes in Next.js here

import React from "react";
import { getMDXComponent } from "mdx-bundler/client";
import { getAllPosts, getSinglePost } from "../../utils/mdx";
const Post = ({ code, frontmatter }) => {
const Component = React.useMemo(() => getMDXComponent(code), [code]);
return (
<div className="wrapper">
<h1>{frontmatter.title}</h1>
<Component />
</div>
);
};
export const getStaticProps = async ({ params }) => {
const post = await getSinglePost(params.slug);
return {
props: { ...post },
};
};
export const getStaticPaths = async () => {
const paths = getAllPosts().map(({ slug }) => ({ params: { slug } }));
return {
paths,
fallback: false,
};
};
export default Post;

Here we are making use of two Next.js function.

  • getStaticPaths : Since this page is dynamic page we need to define all the pages that can be generated, for us that's page for all posts. So we grab all the posts and create paths with slug as param. The params is used to create the url for the post.
  • getStaticProps : It is used to pass different required props to the page component. For us we want the code and frontmatter from bundleMDX, which is retreived by getSinglePost we defined earlier.

Now we will make use of the getMDXComponent from mdx-bundler to get the component that can be rendered in the post page.

Adding posts

So we've build a simple blog now let's add some data into data/_posts folder.

first-post.mdx

---
title: First post
---
This is an my first post. There's another one [here](/posts/second-post).

post-with-cool-component.mdx

---
title: Post with cool component
---
import Cool from "./cool"
This is an my second post.
The title and description are pulled from the MDX file and processed using `mdx-bundler`.
Now I will render a cool component from the same folder.
<Cool/>
I'm also using custom Link Component
Go back [home](/).

In the second post note that we are importing a component named cool and it its being resolved because we used cwd option with the folder path in bundleMDX function. So let's create the cool component too.

cool.jsx

import React from "react";
const Cool = () => {
return <div className="cool">😎</div>;
};
export default Cool;

Custom component

We can pass custom component that replaces the one in MDX like link, heading tags and others. It may be useful in styling or extending the functionality of default components. For example we may want to use Next.js link in our mdx rather than plain link tag rendered by MDX. So let's see how to do that. First let's create a new component CustomLink, for now we will keep it in the same file as [slug].js

import Link from "next/link";
const CustomLink = ({ as, href, ...otherProps }) => {
return (
<>
<Link as={as} href={href} className="custom-link">
<a {...otherProps} />
</Link>
</>
);
};

This component is nothing special we are passing the props received by MDX link to Next.js link. No we need to tell mdx-bundler to use CustomLink we can do that by passing components props to the Component returned by getMDXComponent.

So our final [slug].js will look like this.

import React from "react";
import Link from "next/link";
import { getMDXComponent } from "mdx-bundler/client";
import { getAllPosts, getSinglePost } from "../../utils/mdx";
const CustomLink = ({ as, href, ...otherProps }) => {
return (
<Link as={as} href={href}>
<a {...otherProps} className="custom-link" />
</Link>
);
};
const Post = ({ code, frontmatter }) => {
const Component = React.useMemo(() => getMDXComponent(code), [code]);
return (
<div className="wrapper">
<h1>{frontmatter.title}</h1>
<Component
components={{
a: CustomLink,
}}
/>
</div>
);
};
export const getStaticProps = async ({ params }) => {
const post = await getSinglePost(params.slug);
return {
props: { ...post },
};
};
export const getStaticPaths = async () => {
const paths = getAllPosts().map(({ slug }) => ({ params: { slug } }));
return {
paths,
fallback: false,
};
};
export default Post;

Here is our final result.

result

Conclusion

We just created a simple blog, but our main goal was to use mdx-bundler. mdx-bundler looks simple and efficient but we can do really cool stuffs with it and I think it the best way to integrate MDX in Next.js

2021 - dw