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
- Store MDX outside app directory
- Use frontmatter for metadata
- Add TypeScript types
- Optimize images with Next.js Image
- Use syntax highlighting plugins
Learn More: Next.js MDX Docs