Next.js static site with MDX + dynamic routes + metadata
How to use MDX + dynamic routes + metadata in your statically generated Next.js app.
So you want to build a site in React using Next.js? Excellent choice. That’s what powers this website today.
You also want to statically generate it for hosting on GitHub Pages or Netlify? Me too!
By default, pages in Next.js are written as React components. But for content like a blog, you’re writing mostly prose, not code. That’s better in a simpler format like Markdown. But sometimes you might want to sprinkle a little code into your content. MDX solves this problem:
MDX is an authorable format that lets you seamlessly write JSX in your Markdown documents.
Next.js has official support for MDX via its @next/mdx
package,
but it’s not clear how to support dynamic routes, nor how to include metadata to each post like title, description, publish date, etc.
But I can show you how! 🦸♂️
Start with a basic dynamic page for your posts:
pages/posts/[slug].tsx
export default function Post() {
return <article>My Post!</article>;
}
Suppose you want to keep your posts under content/posts/my-blog-title/index.mdx
.
Here’s an example:
content/posts/my-blog-title/index.mdx
export const metadata = {
title: "Next.js with MDX + dynamic routes + metadata",
date: "2020-07-03",
};
So you want to build a site using Next.js?
For Next.js to build our site, it has to know about our posts. Let’s write a function to grab the list:
src/fetchPostSlugs.ts
import fs from "fs";
import path from "path";
export const fetchPostSlugs = () =>
fs.promises.readdir(path.join(process.cwd(), "content/posts"));
Now, use that function in your page’s getStaticPaths
function — now Next.js will know the list of pages to generate.
pages/posts/[slug].tsx
import { fetchPostSlugs } from "../../src/fetchPostSlugs";
// ...
export async function getStaticPaths() {
const slugs = await fetchPostSlugs();
return {
paths: slugs?.map((slug) => ({ params: { slug } }));,
fallback: false, // In a static-only build, we don't need fallback rendering.
};
}
Next, define a getStaticProps
that will grab the metadata from the MDX file, and give it to your Post
component as a prop:
pages/posts/[slug].tsx
export const getStaticProps = (ctx) => {
const slug = ctx.params?.slug;
return {
props: {
slug,
metadata: require(`../content/posts/${slug}/index.mdx`).metadata,
},
};
};
For client-side rendering, use next/dynamic
to load your MDX component and render:
pages/posts/[slug].tsx
import dynamic from "next/dynamic";
import { fetchPostSlugs } from "../../src/fetchPostSlugs";
export default function Post({ slug, metadata }) {
const Mdx = dynamic(() => import(`../../../content/posts/${slug}/index.mdx`));
return (
<article>
<h1>{metadata.title}</h1>
<small>{metadata.date}</small>
<Mdx />
</article>
);
}
// ...
Unfortunately, next/dynamic
doesn’t support static rendering or server-side rendering (SSR) when used with dynamic MDX file path like this.
But we can fix that by manually rendering the content with react-dom/render
:
pages/posts/[slug].tsx
import dynamic from "next/dynamic";
import { fetchPostSlugs } from "../../src/fetchPostSlugs";
export default function Post({ slug, metadata }) {
let mdx;
if (process.browser) {
const Mdx = dynamic(() => import(`../../content/posts/${slug}/index.mdx`));
mdx = <Mdx />;
} else {
const Component = require(`../../content/posts/${slug}/index.mdx`).default;
const ReactDOMServer = require("react-dom/server");
const ssr = ReactDOMServer.renderToString(<Component />) as string;
mdx = <div dangerouslySetInnerHTML={{ __html: ssr }} />;
}
return (
<article>
<h1>{metadata.title}</h1>
<small>{metadata.date}</small>
{mdx}
</article>
);
}
// ...
And that’s all you need. Magic! ✨