Setting up MDX on Next.js 14
It's comically nontrivial to set up an ergonomic, performant MDX Next.js app, with all the bells & whistles like parsing YAML frontmatter. Here's how I did it.
My criteria
I set out to check a few boxes:
- A natural authoring experience: it should feel intuitive enough to edit as actual Markdown, such as in editors like Obsidian.
- As little dependencies & acrobatics as possible: many of the solutions in guides I read while trying to build this used either multiple packages (like
gray-matter
+next-mdx-remote
) or unmaintained ones like Contentlayer. It should be elegant — enough ;). - Flexibility: I don't want to be confined to something like a
/blog
route to use MDX. I want to be able to use what I want, where I want it, based on my needs.
I'm happy to say I managed to check all of the above, with some bells & whistles, including:
- YAML frontmatter support
- Dynamic metadata & OG image generation
- Static generation of MDX pages at build time
Ecosystem
@next/mdx
While very fast when using SSR, @next/mdx
is not very flexible: you can't name files anything other than <slug>/page.mdx
, which does not get prettier over time. Also, YAML frontmatter is not natively supported.
gray-matter
If the @next/mdx
tradeoffs don't bother you, gray-matter
is a great package for parsing frontmatter. In addition to standard YAML, it supports JSON and TOML as well.
mdx-bundler
mdx-bundler
is nice for apps with a lot of components used sparsely in different places, as well as for non-Next.js apps, but it does come with a developer experience tradeoff in my opinion.
next-mdx-remote
This is it. next-mdx-remote
allows you to write full-fat Markdown — you don't even need to use an .mdx
extension — and just use your components, no import required. That does mean you have to package all of them for every page. But, since I use just a few small ones I wrote for convenience — in almost all of my pages anyway — this isn't a concern for me personally.
Setup
In this guide, I'll walk you through creating an MVP for serving MDX content on your Next.js app. I've scaffolded a complete template repository you can use to follow along.
If you'd prefer to start from scratch, let's get going with a clean install of Next.js:
For the reasons outlined above, let's roll with next-mdx-remote
:
At this point, you get to decide the scope of rendering Markdown for your app. For this site, any .md
file in the app/
directory will be rendered. If this is what you're looking for, go ahead and create a new file: app/[...slug]/page.tsx
.
Our implementation can be boiled down into five steps:
- Fetch all
.md
files - Statically generate routes for each file
- Read each file's content
- Compile Markdown → HTML
- Render the generated page
Fetching files & generating routes
A "slug" is an identifier for a file we want to make available as a route. For example, to generate the route /notes/mdx-nextjs-14
, we want to have a file mdx-nextjs-14.md
in the app/notes/
directory. Let's write a function to return all the slugs for our app:
An aside on generateStaticParams()
You might be wondering why we're returning an array of objects. That's because to statically generate a catch-all dynamic segment:
- Next.js expects an array of objects,
- Where the value of each key is an array,
- And each element in that array maps to a segment in the route to be generated.
For example:
Three routes will be statically generated:
/roadmap
/notes/mdx-nextjs-14
/notes/vs-code
End aside
Let's set up the logic for traversing each folder:
Great! In order to generate each route at build time, let's wrap all of this in generateStaticParams()
:
Reading files & compiling content
Great, we're generating all Markdown files as routes at build time! But, there's no page content to generate yet. Now, we need each route to render its respective page's content and frontmatter (metadata about the page).
For frontmatter, you can add anything that might be useful for you and/or your users, such as date_published
, tags
, and so on. For now, let's keep it simple:
Let's now compile the Markdown content of each page using next-mdx-remote
's compileMDX()
:
Rendering generated routes
We can define the page as such:
There we have it — we've just implemented a basic MDX on Next.js 14 app. But we're not done just yet.
Bells & whistles
Before we go any further, now's a good time to get an example running. Create app/hello-world.md
:
You can verify that it works by going to localhost:3000/hello-world
.
Now, let's implement a couple ✨ additions ✨:
- Dynamic metadata
- OG image generation
The complete template repository has way more, including syntax highlighting, the <Breadcrumbs />
component you see here on my site, and some styles to get you started.
Dynamic metadata
Having rich metadata for sharing across the web greatly contributes to SEO, and it just looks good too. Let's dynamically generate metadata to go along with each route. The best part? It stays in sync with each Markdown file, thanks to your frontmatter.
We can take it a step further, and generate a new OG image on the fly if you don't have a static one linked in your frontmatter.
OG image generation
Next.js has a library for generating dynamic images using JSX and CSS: next/og
.
Create a new file app/api/og/route.tsx
:
I'll admit, the syntax isn't that pretty, and the library has its limitations, but in both concept and practice this is very cool.
Back in app/[...slug]/page.tsx
:
And that's it! This was my first technical post; if you liked it, please let me know! I learned a lot building my site, and I hope this guide helped you soar off the runway in building your awesome Next.js app with MDX.
Resources
- The complete template repository fit with this setup and more
- Josh Comeau's (the 🐐) post on how he built his blog
- The fantastic plugins I'm using:
- The
generateStaticParams()
docs - The Vercel OG Playground