HomeBlog Posts
Adding MDX Functionality to Your Next.js Blog

Adding MDX Functionality to Your Next.js Blog

2025-07-21 by Remi Kristelijn

Adding MDX Functionality to Your Next.js Blog

In this third post of our series, I'll show you how to add MDX (Markdown + JSX) functionality to your Next.js blog. MDX allows you to use React components within your markdown content, making your blog posts more dynamic and interactive.

What is MDX?

MDX is a format that lets you write JSX in your markdown documents. This means you can:

  • Use React components in your blog posts
  • Create interactive content
  • Maintain the simplicity of markdown while adding React's power
  • Build custom components for your content

Step 1: Install MDX Dependencies

Add the necessary MDX packages to your project:

1npm install @next/mdx @mdx-js/loader @mdx-js/react gray-matter

Package explanations:

  • @next/mdx: Next.js MDX integration
  • @mdx-js/loader: Webpack loader for MDX files
  • @mdx-js/react: React components for MDX
  • gray-matter: Parse frontmatter from markdown files

Step 2: Configure Next.js for MDX

Update your next.config.ts to include MDX support:

1import type { NextConfig } from "next"; 2import createMDX from '@next/mdx'; 3 4const withMDX = createMDX({ 5 options: { 6 remarkPlugins: [], 7 rehypePlugins: [], 8 }, 9}); 10 11const nextConfig: NextConfig = { 12 output: "export", 13 trailingSlash: true, 14 images: { 15 unoptimized: true 16 }, 17 pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], 18}; 19 20export default withMDX(nextConfig);

Step 3: Create Content Structure

Organize your blog content in a dedicated directory:

1mkdir -p src/content/posts

This structure keeps your content separate from your application code.

Step 4: Define Content Types

Create TypeScript interfaces for your blog posts in src/types/index.ts:

1// See src/types/index.ts for the actual Post interface 2export interface Post { 3 id: string; 4 title: string; 5 excerpt: string; 6 date: string; 7 author: string; // Added in later updates 8 slug: string; 9 content: string; 10} 11 12export interface PostPageProps { 13 params: Promise<{ slug: string }>; 14}

Step 5: Create the Data Layer

Build src/lib/posts.ts to handle content operations:

1import fs from 'fs'; 2import path from 'path'; 3import matter from 'gray-matter'; 4import type { Post } from '@/types'; 5 6const POSTS_DIRECTORY = path.join(process.cwd(), 'src/content/posts'); 7 8export function getAllPosts(): Post[] { 9 try { 10 const fileNames = fs.readdirSync(POSTS_DIRECTORY); 11 const mdxFiles = fileNames.filter(fileName => fileName.endsWith('.mdx')); 12 13 const posts = mdxFiles.map(fileName => { 14 const slug = fileName.replace(/\.mdx$/, ''); 15 return getPostBySlug(slug); 16 }).filter((post): post is Post => post !== undefined); 17 18 return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); 19 } catch (error) { 20 console.error('Error reading posts directory:', error); 21 return []; 22 } 23} 24 25export function getPostBySlug(slug: string): Post | undefined { 26 try { 27 const fullPath = path.join(POSTS_DIRECTORY, `${slug}.mdx`); 28 29 if (!fs.existsSync(fullPath)) { 30 return undefined; 31 } 32 33 const fileContents = fs.readFileSync(fullPath, 'utf8'); 34 const { data, content } = matter(fileContents); 35 36 if (!data.title || !data.date || !data.excerpt) { 37 console.warn(`Missing required frontmatter fields in ${slug}.mdx`); 38 return undefined; 39 } 40 41 return { 42 id: slug, 43 slug, 44 title: data.title, 45 date: data.date, 46 excerpt: data.excerpt, 47 content 48 }; 49 } catch (error) { 50 console.error(`Error reading post ${slug}:`, error); 51 return undefined; 52 } 53} 54 55export function getAllPostSlugs(): string[] { 56 try { 57 const fileNames = fs.readdirSync(POSTS_DIRECTORY); 58 return fileNames 59 .filter(fileName => fileName.endsWith('.mdx')) 60 .map(fileName => fileName.replace(/\.mdx$/, '')); 61 } catch (error) { 62 console.error('Error reading posts directory:', error); 63 return []; 64 } 65}

Step 6: Create Your First MDX Post

Create src/content/posts/hello-world.mdx:

1--- 2title: "Hello World" 3date: "2024-01-15" 4excerpt: "Welcome to my first blog post!" 5--- 6 7# Hello World 8 9Welcome to my first blog post! This is written in MDX, which means I can use **markdown** syntax and even React components. 10 11## Features 12 13- ✅ Markdown support 14- ✅ React components 15- ✅ Frontmatter metadata 16- ✅ TypeScript integration 17 18## Code Example 19 20```javascript 21function greet(name) { 22 return `Hello, ${name}!`; 23}

What's Next?

In future posts, we'll explore how to add custom React components to make our content even more interactive.


## Step 7: Create Blog Listing Page

Update `src/app/posts/page.tsx` to display all posts:

```typescript
import { Container, Typography, Box } from '@mui/material';
import Navigation from '@/components/Navigation';
import PostCard from '@/components/PostCard';
import { getAllPosts } from '@/lib/posts';

export default function PostsPage() {
  const posts = getAllPosts();

  return (
    <Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
      <Navigation title="Blog Posts" showHome={true} showBack={false} />

      <Container maxWidth="md" sx={{ flex: 1, py: 4 }}>
        <Typography variant="h3" component="h1" gutterBottom sx={{ mb: 4 }}>
          Blog Posts
        </Typography>

        <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
          {posts.map((post) => (
            <PostCard key={post.id} post={post} />
          ))}
        </Box>
      </Container>
    </Box>
  );
}

Step 8: Create Individual Post Pages

Create src/app/posts/[slug]/page.tsx for dynamic post routes:

1import { notFound } from 'next/navigation'; 2import { Container, Box } from '@mui/material'; 3import Navigation from '@/components/Navigation'; 4import PostContent from '@/components/PostContent'; 5import { getPostBySlug } from '@/lib/posts'; 6import type { PostPageProps } from '@/types'; 7 8export default async function PostPage({ params }: PostPageProps) { 9 const { slug } = await params; 10 const post = getPostBySlug(slug); 11 12 if (!post) { 13 notFound(); 14 } 15 16 return ( 17 <Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}> 18 <Navigation title={post.title} showHome={true} showBack={true} /> 19 20 <Container maxWidth="md" sx={{ flex: 1, py: 4 }}> 21 <PostContent post={post} /> 22 </Container> 23 </Box> 24 ); 25}

Step 9: Create Supporting Components

PostCard Component

Create src/components/PostCard.tsx:

1import Link from 'next/link'; 2import { Card, CardContent, Typography, Box } from '@mui/material'; 3import type { PostCardProps } from '@/types'; 4 5export default function PostCard({ post }: PostCardProps) { 6 return ( 7 <Card component={Link} href={`/posts/${post.slug}`} sx={{ textDecoration: 'none' }}> 8 <CardContent> 9 <Typography variant="h5" component="h2" gutterBottom> 10 {post.title} 11 </Typography> 12 <Typography variant="body2" color="text.secondary" gutterBottom> 13 {new Date(post.date).toLocaleDateString()} 14 </Typography> 15 <Typography variant="body1"> 16 {post.excerpt} 17 </Typography> 18 </CardContent> 19 </Card> 20 ); 21}

PostContent Component

Create src/components/PostContent.tsx:

1import { Typography, Box } from '@mui/material'; 2import ReactMarkdown from 'react-markdown'; 3import type { PostContentProps } from '@/types'; 4 5export default function PostContent({ post }: PostContentProps) { 6 return ( 7 <Box> 8 <Typography variant="h3" component="h1" gutterBottom> 9 {post.title} 10 </Typography> 11 <Typography variant="body2" color="text.secondary" gutterBottom> 12 {new Date(post.date).toLocaleDateString()} 13 </Typography> 14 <Box sx={{ mt: 4 }}> 15 <ReactMarkdown>{post.content}</ReactMarkdown> 16 </Box> 17 </Box> 18 ); 19}

Step 10: Test Your MDX Setup

  1. Start your development server:

    1npm run dev
  2. Visit http://localhost:3000/posts to see your blog listing

  3. Click on a post to view the individual post page

MDX Frontmatter Structure

All your MDX files should follow this frontmatter structure:

1--- 2title: "Your Post Title" 3date: "YYYY-MM-DD" 4excerpt: "Brief description of your post" 5---

Benefits of MDX

  1. Enhanced Content: Use React components in your markdown
  2. Interactive Elements: Add charts, forms, or custom widgets
  3. Consistent Styling: Apply your design system to content
  4. Type Safety: Full TypeScript support
  5. Developer Experience: Familiar markdown syntax with React power

What's Next?

In the next post, we'll integrate Material-UI to replace the basic HTML with beautiful, consistent UI components. This will give your blog a professional, modern appearance.

Troubleshooting

Common Issues

  1. Build Errors: Ensure all MDX dependencies are installed
  2. TypeScript Errors: Check that your type definitions are correct
  3. Missing Posts: Verify your MDX files have proper frontmatter
  4. Styling Issues: Make sure your components are properly styled

Resources


Your Next.js blog now supports MDX! You can create rich, interactive content using markdown and React components. In the next post, we'll enhance the visual design with Material-UI.