Back to Blog
typescriptnextjsmdx

How to Build a Blog with Next.js and MDX

June 28, 2025
3 min read
GG

Gagan Goswami

June 28, 2025

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.

Next.js and MDX Blog Setup

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

bash
yarn create next-app nextjs-mdx-blog

2. Install Required Packages

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

Markdown Content Structure

Slug Utilities

Load Post by Slug:

javascript
export 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:

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

javascript
export 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:

jsx
import { 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:

jsx
import { 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,
  };
}

This setup gives you a powerful, flexible blog platform that's easy to customize and extend!

All Posts
Share: