How to Build a Blog with Next.js and MDX
Creating a blog with Next.js and MDX is a fast, scalable solution for developers who want full control over content and styling. This guide walks you through building a statically-generated blog with markdown content and dynamic routingβusing a clean, modular structure.
Preface
This blog uses:
- Next.js for static site generation
- next-mdx-remote to parse MDX content
- gray-matter to extract frontmatter
Getting Started
1. Create a Next.js App
bashyarn create next-app nextjs-mdx-blog
2. Install Required Packages
bashyarn add gray-matter next-mdx-remote
3. Set Up Project Structure
Organize your content, logic, and routes like this:
π components
π layout.jsx
π data
π blog
π markdown.mdx
π nextjs.mdx
π react.mdx
π lib
π format-date.js
π mdx.js
π pages
π blog
π [slug].jsx
π index.jsx
Handle Markdown Content
Slug Utilities
Load Post by Slug:
javascriptexport function loadPostBySlug(slug) { const postPath = path.join(process.cwd(), 'data/blog', `${slug}.mdx`); const source = fs.readFileSync(postPath); const { content, data } = matter(source); return { content, meta: { slug, excerpt: data.excerpt ?? "", title: data.title ?? slug, tags: (data.tags ?? []).sort(), date: (data.date ?? new Date()).toString(), }, }; }
Get All Posts:
javascriptexport function getAllPosts() { const postFilenames = fs .readdirSync(path.join(process.cwd(), 'data/blog')) .filter((name) => name.endsWith('.mdx')); const posts = postFilenames .map((name) => { const slug = name.replace(/\.mdx$/, ''); return loadPostBySlug(slug); }) .sort((a, b) => new Date(b.meta.date) - new Date(a.meta.date)); return posts; }
Format Dates
javascriptexport function formatDate(date) { return new Intl.DateTimeFormat('en-US', { month: 'long', day: 'numeric', year: 'numeric' }).format(new Date(date)); }
Home Page
Display all blog posts with metadata:
jsximport { getAllPosts } from '../lib/mdx'; import { formatDate } from '../lib/format-date'; export default function Home({ posts }) { return ( <div> <h1>My Blog</h1> {posts.map((post) => ( <article key={post.meta.slug}> <h2> <a href={`/blog/${post.meta.slug}`}> {post.meta.title} </a> </h2> <p>{formatDate(post.meta.date)}</p> <p>{post.meta.excerpt}</p> </article> ))} </div> ); } export async function getStaticProps() { const posts = getAllPosts(); return { props: { posts } }; }
Article Page
Render individual blog posts:
jsximport { serialize } from 'next-mdx-remote/serialize'; import { MDXRemote } from 'next-mdx-remote'; import { getAllPosts, loadPostBySlug } from '../../lib/mdx'; export default function PostPage({ post }) { return ( <article> <h1>{post.meta.title}</h1> <MDXRemote {...post.source} /> </article> ); } export async function getStaticProps({ params }) { const { slug } = params; const { content, meta } = loadPostBySlug(slug); const mdxSource = await serialize(content); return { props: { post: { source: mdxSource, meta, }, }, }; } export async function getStaticPaths() { const posts = getAllPosts(); const paths = posts.map((post) => `/blog/${post.meta.slug}`); return { paths, fallback: false, }; }
Useful Links
This setup gives you a powerful, flexible blog platform that's easy to customize and extend!