How to Source MDX Content in Next.js to Dynamically Create Pages for a Blog
Markdown is a popular format for authoring. MDX takes that up a notch giving authors more tools to create interactive experiences. How can we take advantage of MDX in frameworks like Next.js to build projects with content sourced from MDX?
What is MDX?
MDX is a document format that allows people to write JavaScript and JSX components (like React) inside of a Markdown file.
This becomes super compelling, where if you’re using Markdown to author documents, you become limited in what kind of elements that Markdown can translate to based on the supported syntax.
for instance, if I wanted to add a code block, I might write:
```
const myVariable = 'value';
console.log(myVariable);
```
Note: the backticks in the above snippet are intentional which would create a code block in Markdown
While I can parse that Markdown block and add some functionality to my code blocks, I’m limited with what I can do, as I don’t have a lot of context along with it.
Instead, if I had a Code component, I could write the following in MDX:
<Code syntax="javascript" allowCopy={true} showOutput={true}>
const myVariable = 'value';
console.log(myVariable);
</Code>
Where at that point, I have the ability to add any type of interaction along with any type of context I’d like for that particular code block.
What are the challenges of using MDX with Next.js?
Next.js itself is a flexible framework that has a lot of features to build powerful web apps, but one thing that’s missing, unlike another similar framework Gatsby, is a rich ecosystem of plugins that handle the entire sourcing of content from grabbing the files to making them available inside of the project’s data layer.
While we’ll be using a Next.js plugin called Next MDX Enhanced, it still requires a bit of extra effort to make everything work seamlessly throughout the application.
What are we going to build?
We’re going to start a new Next.js project from scratch using Create React App.
To learn how to add MDX to a project, we’re going first source a few MDX files, using blog posts as an example, dynamically creating pages for each post.
Once we have our posts, we’ll then create a list of all of our available posts on the homepage, giving people who visit our project a way to navigate to each of our blog posts.
Step 0: Creating a new Next.js app with Create Next app
Starting off, we’ll need a Next.js app. To do this, we’ll use Create Next App which will scaffold a brand new project for us.
In your terminal, run:
yarn create next-app my-next-mdx
# or
npx create-next-app my-next-mdx
Note: feel free to change
my-next-mdx
to whatever name you’d like for the project.
Once that finishes running, you can navigate into that directory and start your development server:
cd my-next-mdx
yarn dev # or npm run dev
And once loaded, you should now be able to open up your new app at http://localhost:3000!
Note: I updated the title of the page and removed the description, feel free to do the same!
Step 1: Adding new blog posts with MDX
Before we actually get into the code of our project, we want to make sure we have a few documents that we’ll be able to use to create our blog.
We can start off with a “Hello, world!” post.
First, create a new folder inside of the pages
directory called posts
, and inside of that folder add a new file called hello-world.mdx
.
So inside of /pages/posts/hello-world.mdx
let’s add:
---
title: 'Hello, world!'
layout: 'post'
---
This is my first post!
What we’re doing here is:
- Creating a new MDX document
- Inside, we’re first creating our “frontmatter”, or our document’s metadata
- In our frontmatter, we include the title of our post and our layout, which will come in handy later
- We also include the body of our blog post
And with that, we have our first blog post where in the next steps of the tutorial, we’ll learn how to bring it into our Next.js project.
Before moving on to Step 2, feel free to add as many or as little additional blog posts as you want. Just be sure to follow the same format as the hello-world.mdx
document we created.
Note: if you want something to fill our some content for you, try fillerama.io which generates HTML or Markdown using Futurama episodes!
Step 2: Using Next MDX Enhanced to source MDX documents and create new pages in Next.js
Next step is to get our new blog post documents into our project.
To get started, we’ll want to install a few different packages.
yarn add next-compose-plugins next-mdx-enhanced
# or
npm install next-compose-plugins next-mdx-enhanced --save
Here’s what we’re installing:
- Next.js Compose Plugins: this plugin will allow us to more elegantly add plugins to our Next.js project
- Next.js MDX Enhanced: this plugin will help load our MDX files from the filesystem and make the data available as a prop in our page
Note: Next.js Compose Plugins isn’t strictly required but it helps if you want to set up any additional next.config.js settings or add another plugin.
With our packages installed, we’ll need to add our configuration to our Next.js config.
If you don’t have a next.config.js
file, create one in the root of your project, then add:
const composePlugins = require('next-compose-plugins');
const mdxEnhanced = require('next-mdx-enhanced');
module.exports = composePlugins([
mdxEnhanced({
layoutPath: './src/templates'
})
]);
Here we’re:
- Importing both of our Next.js plugin packages
- Using the
composePlugins
function to define our configuration - Adding an instance of
mdxEnhanced
- Where we define the path to our layout (which we’ll add next)
Now, create a new directory called templates
then create a new file inside of that directory called post
.
Inside templates/post.js
add the following:
import Head from 'next/head'
import Link from 'next/link'
import styles from '../styles/Home.module.css'
export default function Post({ children, frontMatter }) {
const { title } = frontMatter;
return (
<div className={styles.container}>
<Head>
<title>{ title }</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
{ title }
</h1>
<div>
{ children }
</div>
<p>
<Link href="/">
<a>
Back to home
</a>
</Link>
</p>
</main>
</div>
)
}
Inside of this page snippet, we’re:
- Creating a new page component called Post
- We’re defining 2 props,
children
andfrontMatter
- Our
frontMatter
prop will be an object that includes the metadata for our page, so we first destructure ourtitle
from thefrontMatter
- We then define the structure of our template, including our
h1
with the title - We also include our
children
prop in adiv
which will include our post content - And finally a link back to our homepage for easy navigation
At this point, restart your development server or start it up.
Once it’s loaded, navigate to http://localhost:3000/posts/hello-world and you should now see your first blog post!
Note: You’ll only see a page at /posts/hello-world if you followed along with me. Your post page should now be available at
/posts/[filename without extension]
.
Step 3: Adding dynamic blog post links to the homepage with Gray Matter
Our blog has it’s content, but we need a way to show all of our blog posts and give people a way to navigate to them.
To do this, we’re going to install a few more packages:
yarn add gray-matter fs path
# or
npm install gray-matter fs path --save
Here we’re adding:
- Graymatter: a parser used to scrape the frontmatter of a document from strings
- fs: Node’s filesystem module
- path: Node’s utility for working with file and directory paths
Note: technically we don’t need to add
fs
andpath
as a dependency, but I like to add them as dependencies if I use them in the project.
With our dependencies installed, first thing we need to do is import them into our homepage. At the top of pages/index.js
let’s add:
import { promises as fs } from 'fs';
import path from 'path'
import grayMatter from 'gray-matter';
import Link from 'next/link'
This imports each of the packages into our project.
You’ll also notice that we’re importing fs
a little differently, which allows us to us the API as promises instead of synchronous functions.
On top of that, you’ll notice we’re also importing Link from Next.js. We want to use the Next.js Link component so that we can take advantage of the performance benefits and clientside navigation.
Next, we’re going to use getStaticProps to find all of our blog posts to provide that data to our page. At the bottom of pages/index.js
add:
export async function getStaticProps() {
const postsDirectory = path.join(process.cwd(), 'pages/posts');
const filenames = await fs.readdir(postsDirectory);
const files = await Promise.all(filenames.map(async filename => {
const filePath = path.join(postsDirectory, filename)
const content = await fs.readFile(filePath, 'utf8')
const matter = grayMatter(content);
return {
filename,
matter
}
}));
const posts = files.map(file => {
return {
path: `/posts/${file.filename.replace('.mdx', '')}`,
title: file.matter.data.title
}
});
return {
props: {
posts
}
}
}
This is a big snippet so let’s break it down:
getStaticProps
is a Next.js API that allows us to fetch static data for our page and pass it in as props- The first thing we do is define our posts directory. We’re currently storing them in
pages/posts
, so we usepath
to create that location with our current directory - We then use
fs
to read that directory so we can find all of the filenames - Next we use map to create an array of promises that we use
Promise.all
to resolve - Inside of those promises, we look for each filename from above, read that file, use
grayMatter
to parse the string to obtain the frontmatter, then return that data - Our
files
constant ends up as an array of file data for our blog posts - We then use map to loop through all of those files, where we construct a page path for our application using the filename, additionally passing in the title so that we can use it on the page
- And finally, we return that
posts
constant as a prop that we’ll be able to use in our page component
At this point, we can test that all of that is working by adding a console log statement inside of our Home
component:
export default function Home({ posts }) {
console.log('posts', posts);
If we open up our page in the browser, we should now see all of our posts as an array in the console!
With our post data, we can now use it in our page!
To do this, we’re going to take advantage of the UI that already came with the Next.js starter. So we’re going to replace everything inside of the default .grid
element and reuse the .card
elements as our links.
Let’s replace the entire grid with:
<div className={styles.grid}>
{posts.map(post => {
const { title, path } = post;
return (
<Link key={path} href={path}>
<a className={styles.card}>
<h3>{ title }</h3>
</a>
</Link>
)
})}
</div>
With this snippet, we’re:
- Using our
posts
array to map through our available posts - We first destructure the
title
and thepath
of our post - We then return a new component for each post, where we use the
Link
component to create an internal link, passing thekey
for React’s sake and ourpath
as thehref
- Inside we add an anchor tag that represents our HTML link, passing the original
.card
classname to re-use the styles - And finally inside, we include the original
h3
and pass in our dynamictitle
Now if we reload the page, we should see all of our MDX blog posts right on our homepage!
The great thing about this is at this point, all of our posts are passed into our page component as data, so we can really do whatever we want with the UI and how we give people the ability to navigate them.
Step 4: Using React components in MDX documents with Next.js Image
At this point of the project, everything is set up and ready to go, but as one last optional step, we can see how we’re able to take advantage of MDX using React components in our documents.
To test this out, we can simply take advantage of the included Next.js Image component right inside of one of our posts.
Let’s open up our Hello World blog post (or any blog post you’d) like, then first import Next.js Image immediately after the frontmatter:
import Image from 'next/image'
Then somewhere in the post, we can use the Image component with any image we’d like:
<Image width={500} height={500} src="https://www.nasa.gov/sites/default/files/1-bluemarble_west.jpg" />
Note: MDX requires whitespace around the components, so make sure there’s a return before and after the Image reference.
If you’re following along with me we should end up with:
---
title: 'Hello, world!'
layout: 'post'
---
import Image from 'next/image'
This is my first post!
<Image width={500} height={500} src="https://www.nasa.gov/sites/default/files/1-bluemarble_west.jpg" />
As one final step, we need to include the hostname of the image URL we’re using inside of our Next.js config.
Inside of next.config.js
, we want to add a second argument to the composePlugins
function, passing in a new object, with our image config:
module.exports = composePlugins([
mdxEnhanced({
layoutPath: './templates'
})
], {
images: {
domains: ['www.nasa.gov'],
}
});
This second argument as an object will represent the standard next.config.js
options available to use, where here, we’re using the images
property, to add the host of the image we want to use.
Note: if you’re using an image from a different source, make sure to use the right hostname in the
domains
array.
Now, if we restart our server and navigate to that blog post, we should now see our image right inside of our post!
The awesome thing with MDX is we can use our React components right inside of the MDX documents, leveraging the benefits of Markdown as a format and the power of React to build interactive experiences.
What’s next?
Next.js MDX Remote
Next.js MDX Enhanced is a great easy way to load up our content, but it comes with some limitations and possible performance issues when scaling. While this option is a little more complicated, the authors themselves recommend using Next.js MDX Remote.
Try considering your use case. You may not run into any issues if you’re running a small personal project like a blog, but if you’ll need high performance out of a high number of pages, consider checking out Next.js MDX Remote.
Adding more metadata
If you try adding a console.log
statement in your blog post page to inspect your frontMatter
, you’ll notice it contains your title
and your layout
which are the same 2 properties you defined in your blog post documents.
To add more metadata to extract into your post pages, you can add any field you want to the top of each post!
More React components
There are a lot of React components that can add another level to the experience of your project.
If you’re a developer, chances are your blog posts will have code snippets. You can use libraries like React Syntax Highlighter to make your code snippets easier to read!
You can also define your own React components, such as a Quote component, giving you the ability to create a special UI for each of your quotes in your blog posts.