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

Terminal
yarn create next-app nextjs-mdx-blog

2. Install Dependencies

Terminal
yarn add gray-matter next-mdx-remote

3. Create Project Structure

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

📌 Pro Tip

You can add custom components in your MDX by passing them to MDXRemote.

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