Building a blog with Next.js App Router and MDX
Jan 31, 2024
Next.js and MDX are no strangers. Next is a wonderful platform to build a site if you’re using React and writing in MDX is only natural if you’re looking for the simplicity of Markdown with the flexibility of breaking out into JSX if you have to.
There are quite a few resources on how to get running with the pair and even official docs and support for MDX. But there isn’t a whole lot about how to leverage the @next/mdx package to actually build a fully functioning blog with Next’s App Router (and React server components).
I recently built out this blog with that in mind so I figured I’d share how it went about it. If you’d like to skip over the explanation and see a demo, check out the final project’s repository on GitHub or play around with it on CodeSandbox.
Why not use next-mdx-remote?
I’ve used next-mdx-remote for other projects and it’s a great option. However, it does have some limitations that I wanted to overcome for building this blog. I’m also not going to host the MDX anywhere else so collecting it from a remote source isn’t a requirement for me.
I’m planning on showcasing small experiments that would be imported as a component so being able to use import
statements is a must. I also found that not being able to specify export
statements makes it difficult to define variables within my MDX content for easy reuse of strings, etc.
As of authoring this post, next-mdx-remote is currently marked as unstable when used with React server components. I’m sure the maintainers will stabilize things soon, but I really wanted to use server components as much as possible.
Initial setup
Let’s start by installing the necessary dependencies to get MDX up and running in our Next.js project. Assuming you use npm and Typescript, you’ll want to run the following commands:
Then, you’ll need to update your next.config.js
file to use MDX:
Finally, you’ll need to create src/mdx-components.tsx
with the following:
This is where you’ll define your custom MDX components, but we’ll get into that later!
Adding posts
Now that we’ve added MDX support to our project, we can now create pages with the .mdx
extension. A convention that I decided to use was to keep my posts in a (posts)
directory. The parenthetical naming allows us to use Route Groups which helps us organize and standardize the layout of our posts without affecting the route structure.
Let’s create our first post by creating src/app/(posts)/hello-world/page.mdx
. Your directory structure should look something like this:
Our post will now be accessible at the /hello-world
route. But first, let’s add some content to our post. Add the following to page.mdx
:
Great! Now we have our first post with some content in it. Next, let’s add some metadata to the post.
Adding post metadata
Metadata allows us to encode some information with our post such as the title and publish date. You’re free to add other metadata as well and we’ll explore adding categories in another section below. For now, add the following to src/app/(posts)/hello-world/page.mdx
:
You’ll notice that the page title changed to “Hello world”. That’s because Next.js has metadata fields that are read automatically if you export a metadata object from your pages. In this case, it is reading the metadata.title
as the page title.
Customizing MDX components
There might be some cases where you want to use a custom component in place of one of the default components that MDX automatically renders. As an example, let’s replace the h1
component with our own heading component.
First, we’ll create src/components/heading/index.tsx
to house our component:
And then let’s create src/components/heading/styles.css
for our component’s CSS:
Now let’s update src/mdx-components.tsx
to add Heading
in place of h1
:
Our h1
should now render in a crimson color with an underline beneath it.
Listing posts
Listing posts is a little different than what you would do with next-mdx-remote. Since we’re using server components, we can directly access the MDX files in our (posts)
directory. This is the beauty of server components and Next.js! ✨ For the sake of this example, let’s use src/app/page.tsx
to list our posts.
We’ll create a posts helper file to house some utilities we’ll be using throughout this project. Create src/posts.ts
and then let’s define an interface for our posts so we can get some type safety:
Now we’ll create a getPosts()
function grab all of our post data from src/app/(posts)
:
Let’s break down what’s happening here. First, we’re using the readdir()
function from Node.js to get a list of the directories in src/app/(posts)
(i.e., the posts). Then we’re mapping over this list to get the metadata for each post. This is possible with the use of a dynamic import to read the exported items from each post. Hurray for async components! 🙌
Then we sort the posts by publishDate
(newest to oldest) using the sort
array method.
We’re going to be reusing this pattern so let’s create a component to list our posts in src/components/posts
:
Here we’re grabbing the slug (the name of the post route) and the post’s metadata (title
and publishDate
) and showing it in an ordered list. Since publishDate
is an ISO-8601 string, we place it in a Date
object and use the toLocaleDateString
method to format it.
The final result should look something like this:
Now we have a list of all of the posts on the homepage!
Adding categories
Let’s add some categories to our posts. In this example, posts can have multiple categories: dog
, cat
, and rock
. First, we’ll add the categories as metadata to our posts’ MDX as an array:
Then to make things easy, we’ll define a static list of categories in a categories helper in src/categories.ts
:
We’ll also create a type for our categories:
Our Post
type we defined in the last section in src/posts.ts
should be updated to this:
And finally, we can update our Posts
component to show each posts’ categories:
But wait! We also want to have individual pages that list all of the posts for a given category. To do that, we’ll need to create a dynamic route. We’ll need to filter the posts we get back by the category specified by the route. For example, the /category/dog
route should return only the posts categorized as “dog”.
Start by creating a new page at src/app/category/[category]/page.tsx
:
Let’s walk through what’s happening here. First, we’re grabbing the category from our route parameters. We’ll send a 404
with the notFound()
function provided by Next.js if the category doesn’t match one of our statically defined categories.
Now we need to get the filtered posts by category by creating a getPostsByCategory()
function in src/posts.ts
:
This is just like how we grabbed our post data on the homepage, except we filter all the posts we get back by category.
We can also take advantage of generateStaticParams()
on our category routes to statically generate them at build time:
This will return a structure that looks like this:
The final result should look like this:
Pagination
As you add more and more blog posts, it would probably be wise to add some pagination. We’ll need to add pagination to both the homepage and the category pages.
Let’s start with the homepage where we list all posts. We’ll create a new getPaginatedPosts()
function in src/posts.ts
:
This just takes our posts and returns a subset of them based on the page and limit (i.e., posts per page) defined. We also return the total number of posts to help us build our pagination controls.
We’ll also define a constant to store how many posts per page we want throughout our blog:
Now we’ll create a pagination component for our paging controls in src/components/pagination
:
We’re conditionally showing the “Previous” and “Next” controls if there are actually posts in those directions.
Let’s update our homepage to show the first page of paginated posts:
And then we can also add the Pagination
component to show the paging controls:
Since we know that the homepage will be the first page, we can hardcode the page as 1
.
Now let’s create the actual routes for the paginated pages. We want to have these live at a /page/{pageNumber}
route so we’ll need to create a dynamic route at src/app/page/[page]/page.tsx
:
Now when we go to the /page/2
route, we’ll see our paginated page. We also 404
if the page specified isn’t a positive number and we redirect to the homepage if the page is 1
(because that’s what the homepage is for!).
Let’s add the paginated posts:
There’s a bit going on here so let’s walk through it. The first part is pretty simple, rather than getPosts()
, we’re using our new function getPaginatedPosts()
which accepts the page number and a limit (our posts per page). We then render our paginated posts through the Posts
component. We’re also plugging in the page, posts per page, and total data into the Pagination
component to show the appropriate paging controls.
Lastly, we’re using generateStaticParams()
to statically generate the paginated pages at build time (just like our category pages). Here we get all of our posts with getPosts()
and then get the number of pages that we’ll need to show all of them based off of how many total posts we have and how many posts per page we’re showing. Then we iterate over that number to create an array of parameter objects that Next.js needs to generate these pages.
The structure returned would look something like this:
Now we can tackle the category pages. We’ll create a new function called getPaginatedPostsByCategory()
in src/posts.ts
:
This works similarly to getPaginatedPosts()
, except this function also takes in a category to filter on.
Just like the homepage, the category pages we created before will act as the first paginated page. We’ll update src/category/[category]/page.tsx
with:
And we’ll add our paging controls too:
Now to create the paginated category routes. We’ll need to use multiple dynamic route segments to capture both the category and page to display. The route folder structure for this looks like:
Our paginated category pages will be accessed at the /category/{category}/page/{pageNumber}
route so /category/dog/page/2
will bring us to page 2 of posts categorized with dog
.
In src/app/category/[category]/page/[page]/page.tsx
:
This is quite similar to what we created for the regular paginated pages, except we’re using getPaginatedPostsByCategory()
. We’re also doing something a little bit more hairy in generateStaticParams()
. In order to get the correct structure, we need to get how many pages each category has. To do this, we iterate over all of the categories, then apply the same technique as before to get the number of pages we’ll have. Then we iterate over each page and return the category and page parameters and flatten everything. The structure returned should look like this:
Wonderful! Now we’ve added pagination to our blog for both types of pages.
Wrapping it up
And there you have it, a fully functioning blog in Next.js powered by MDX. To recap, we learned how to set up MDX, add posts and metadata, define custom components, list all of our posts, add categories to our posts, and paginate everything!
Working through this was a huge unlock for me in terms of understanding (and appreciating) React server components. I think they really make a difference in terms of the developer experience when working with React and server-side code.
Check out the GitHub repository for the full source code or head over to CodeSandbox to play around with the example project in your browser.