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
Published on May 25, 2021
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# oryarn 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#oryarn 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#oryarn 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 andbundleMDX
for server side(node). First we create the bundled code from mdx source by usingbundleMDX
, it will alos parse frontmatter for us . It takes source and options, we are also settingcwd
that allows us to basically include any component in the folder pointed bycwd
to out MDX. We will put any components we required in thePOSTS_PATH
directory and it can be resolved in the MDX. Then in client side we can usegetMDXComponent
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 bygetSinglePost
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 ComponentGo 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><Componentcomponents={{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.
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