How to Build a Blog with Next.js and MDX
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.

Author logo

Aayush Bharti

14 min read

Preface

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.

This blog uses:

Getting Started

1. Create a Next.js App

Terminal
yarn create next-app nextjs-mdx-blog

2. Install Required Packages

Terminal
yarn add gray-matter next-mdx-remote

3. Set Up Project Structure

Organize your content, logic, and routes like this:

layout.jsx
markdown.mdx
nextjs.mdx
react.mdx
format-date.js
mdx.js
[slug].jsx
index.jsx

Handle Markdown Content

Create utility functions to parse and load MDX content.

lib/mdx.js
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

lib/mdx.js
export const allSlugs = fs.readdirSync(POSTS_PATH) 

export const formatSlug = slug => slug.replace(/\.mdx$/, "")

Load Post by Slug

lib/mdx.js
export const getPostBySlug = async slug => {
  const postFilePath = path.join(POSTS_PATH, `${slug}.mdx`)
  const source = fs.readFileSync(postFilePath, "utf8") 
  const { content, data } = matter(source)
  const mdxSource = await serialize(content) 

  return {
    source: mdxSource,
    frontMatter: {
      ...data,
      slug,
    },
  }
}

Get All Posts

lib/mdx.js
export const getAllPosts = () => {
  return allSlugs
    .map(slug => {
      const filePath = path.join(POSTS_PATH, slug)
      const { data } = matter(fs.readFileSync(filePath, "utf8"))
      return {
        ...data,
        slug: formatSlug(slug),
        date: new Date(data.date).toISOString(), 
      }
    })
    .sort((a, b) => new Date(b.date) - new Date(a.date))
}

Format Dates

lib/format-date.js
export const formatDate = date =>
  new Date(date).toLocaleDateString("en", {
    year: "numeric",
    month: "long",
    day: "numeric",
  })

Home Page

Render a list of all posts.

pages/index.jsx
import Link from "next/link"
import { getAllPosts } from "../lib/mdx"
import { formatDate } from "../lib/format-date"

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 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

Render individual MDX articles by slug.

pages/blog/[slug].jsx
import { MDXRemote } from "next-mdx-remote"
import { getPostBySlug, allSlugs, formatSlug } from "../../lib/mdx"
import { formatDate } from "../../lib/format-date"

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} />  // [!code highlight]
      </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,
  }
}

Good to Know

You can pass custom React components to MDXRemote to enhance your markdown with interactive elements.

Last updated on

Contact Drawer

OPEN TO WORK · OPEN TO WORK ·
OPEN TO WORK · OPEN TO WORK ·

FROM CONCEPT TO CREATION

LET's MAKE IT HAPPEN!

I'm available for full-time roles & freelance projects.

I thrive on crafting dynamic web applications, and
delivering seamless user experiences.

I'm Aayush - a full-stack developer, freelancer & problem solver. Thanks for checking out my site!

© 2025 Aayush Bharti

s