typescriptnextjsmdx
How to Build a Blog with Next.js and MDX
Build a blazing fast markdown blog using Next.js and MDX with this complete walkthrough.

Aayush Bharti
✨ Preface
Creating a blog with Next.js and MDX is fast and scalable. This guide walks you through building a static blog site with rich markdown content and a minimal setup.
🛠 Tech Stack
This project uses:
- Next.js for static site generation
- next-mdx-remote to handle MDX parsing
- gray-matter to extract frontmatter metadata from files
🚀 Getting Started
1. Create a Next.js App
yarn create next-app nextjs-mdx-blog
2. Install Dependencies
yarn add gray-matter next-mdx-remote
3. Create Project Structure
/components/layout.jsx # Optional layout wrapper
/data/blog/*.mdx # MDX blog articles
/lib/format-date.js # Date formatter
/lib/mdx.js # MDX processing helpers
/pages/blog/[slug].jsx # Dynamic article page
🧩 Handle Markdown Content
Start by identifying the content path and importing utilities.
import fs from "fs"
import path from "path"
import matter from "gray-matter"
import { serialize } from "next-mdx-remote/serialize"
const root = process.cwd()
const POSTS_PATH = path.join(root, "data", "blog")
🔍 Slug Utilities
export const allSlugs = fs.readdirSync(POSTS_PATH)
export const formatSlug = slug => slug.replace(/\.mdx$/, "")
📥 Load Post by Slug
export const getPostBySlug = async slug => {
const postFilePath = path.join(POSTS_PATH, `${slug}.mdx`)
const source = fs.readFileSync(postFilePath)
const { content, data } = matter(source)
const mdxSource = await serialize(content)
return {
source: mdxSource,
frontMatter: {
...data,
slug,
},
}
}
📰 Get All Posts
export const getAllPosts = () => {
return allSlugs
.map(slug => {
const source = fs.readFileSync(path.join(POSTS_PATH, slug), "utf-8")
const { data } = matter(source)
return {
...data,
slug: formatSlug(slug),
date: new Date(data.date).toISOString(),
}
})
.sort((a, b) => new Date(b.date) - new Date(a.date))
}
📆 Format Dates
export const formatDate = date =>
new Date(date).toLocaleDateString("en", {
year: "numeric",
month: "long",
day: "numeric",
})
🏠 Home Page
import Link from "next/link"
import { formatDate } from "../lib/format-date"
import { getAllPosts } from "../lib/mdx"
export default function Home({ posts }) {
return (
<>
<h1 className="mb-8 text-6xl font-bold">Blog</h1>
<hr className="my-8" />
<ul className="flex flex-col gap-3">
{posts.map(({ slug, title, summary, date }) => (
<li key={slug}>
<Link href={`/blog/${slug}`}>
<a className="block rounded-lg border border-solid border-gray-300 p-6 shadow-md">
<div className="flex justify-between">
<h2>{title}</h2>
<time dateTime={date}>{formatDate(date)}</time>
</div>
<p className="mt-4">{summary}</p>
</a>
</Link>
</li>
))}
</ul>
</>
)
}
export const getStaticProps = async () => {
const posts = getAllPosts()
return {
props: { posts },
}
}
📝 Article Page
import { MDXRemote } from "next-mdx-remote"
import { formatDate } from "../../lib/format-date"
import { allSlugs, formatSlug, getPostBySlug } from "../../lib/mdx"
export default function Blog({ post }) {
const { title, date } = post.frontMatter
return (
<div>
<h1 className="mb-2 text-6xl font-bold">{title}</h1>
<time dateTime={date} className="text-lg font-medium">
{formatDate(date)}
</time>
<hr className="my-8" />
<article className="prose max-w-none">
<MDXRemote {...post.source} />
</article>
</div>
)
}
export const getStaticProps = async ({ params }) => {
const post = await getPostBySlug(params.slug)
return {
props: { post },
}
}
export const getStaticPaths = async () => {
const paths = allSlugs.map(slug => ({
params: { slug: formatSlug(slug) },
}))
return {
paths,
fallback: false,
}
}
🔗 Useful Links
📌 Pro Tip
You can add custom components in your MDX by passing them to MDXRemote
.
Last updated on