MDX + Next.js: Build a Blog in Minutes

MDX lets you write JSX in Markdown, perfect for blogs and documentation. Here's how to set it up in Next.js.

Setup

npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

Configure Next.js:

// next.config.mjs
import createMDX from '@next/mdx'

const nextConfig = {
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}

export default createMDX()(nextConfig)

Custom Components

Create mdx-components.tsx at project root:

import type { MDXComponents } from 'mdx/types'
import Link from 'next/link'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h1: ({ children }) => ({ children }),
    a: ({ href, children }) => ({ children }),
    ...components,
  }
}

File-Based MDX Pages

app/
├── blog/
   ├── first-post/
      └── page.mdx
   └── page.tsx

app/blog/first-post/page.mdx:

export const metadata = {
  title: 'My First Post',
  publishDate: '2025-11-25',
}

import { CustomButton } from '@/components/CustomButton'

# My First Blog Post

Regular **markdown** works great!

<CustomButton>Interactive Component</CustomButton>

## Dynamic MDX with Remote Content

```bash
npm install next-mdx-remote gray-matter
```

app/blog/[slug]/page.tsx:

import { MDXRemote } from 'next-mdx-remote/rsc'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const filePath = path.join(process.cwd(), 'content/posts', `${params.slug}.mdx`)
  const { content, data } = matter(fs.readFileSync(filePath, 'utf8'))

  return (
      {data.title}
      {data.publishDate}
  )
}

content/posts/first-post.mdx:

---
title: 'Getting Started'
publishDate: '2025-11-25'
tags: ['nextjs', 'tutorial']
---

# Getting Started

Your content here with **markdown** support!

Blog Index

app/blog/page.tsx:

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import Link from 'next/link'

async function getPosts() {
  const postsDirectory = path.join(process.cwd(), 'content/posts')
  const filenames = fs.readdirSync(postsDirectory)

  return filenames.map((filename) => {
    const { data } = matter(
      fs.readFileSync(path.join(postsDirectory, filename), 'utf8')
    )
    return { slug: filename.replace(/\.mdx$/, ''), ...data }
  })
}

export default async function BlogIndex() {
  const posts = await getPosts()

  return (
      Blog
      {posts.map((post) => (
          {post.title}
          {post.publishDate}

      ))}
  )
}

Custom Components Example

components/Callout.tsx:

export function Callout({
  type = 'info',
  children,
}: {
  type?: 'info' | 'warning'
  children: React.ReactNode
}) {
  const styles = {
    info: 'bg-blue-50 border-blue-500',
    warning: 'bg-yellow-50 border-yellow-500',
  }
  return { children }
}

Use in MDX:

<Callout type="warning">Make sure to backup your data!</Callout>

Best Practices

  1. Store MDX outside app directory
  2. Use frontmatter for metadata
  3. Add TypeScript types
  4. Optimize images with Next.js Image
  5. Use syntax highlighting plugins

Learn More: Next.js MDX Docs