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:

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

import type { NextConfig } from "next";
import createMDX from '@next/mdx';

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
  },
});

const nextConfig: NextConfig = {
  output: "export",
  trailingSlash: true,
  images: {
    unoptimized: true
  },
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
};

export default withMDX(nextConfig);

Step 3: Create Content Structure

Organize your blog content in a dedicated directory:

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

// See src/types/index.ts for the actual Post interface
export interface Post {
  id: string;
  title: string;
  excerpt: string;
  date: string;
  author: string;  // Added in later updates
  slug: string;
  content: string;
}

export interface PostPageProps {
  params: Promise<{ slug: string }>;
}

Step 5: Create the Data Layer

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

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import type { Post } from '@/types';

const POSTS_DIRECTORY = path.join(process.cwd(), 'src/content/posts');

export function getAllPosts(): Post[] {
  try {
    const fileNames = fs.readdirSync(POSTS_DIRECTORY);
    const mdxFiles = fileNames.filter(fileName => fileName.endsWith('.mdx'));

    const posts = mdxFiles.map(fileName => {
      const slug = fileName.replace(/\.mdx$/, '');
      return getPostBySlug(slug);
    }).filter((post): post is Post => post !== undefined);

    return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
  } catch (error) {
    console.error('Error reading posts directory:', error);
    return [];
  }
}

export function getPostBySlug(slug: string): Post | undefined {
  try {
    const fullPath = path.join(POSTS_DIRECTORY, `${slug}.mdx`);

    if (!fs.existsSync(fullPath)) {
      return undefined;
    }

    const fileContents = fs.readFileSync(fullPath, 'utf8');
    const { data, content } = matter(fileContents);

    if (!data.title || !data.date || !data.excerpt) {
      console.warn(`Missing required frontmatter fields in ${slug}.mdx`);
      return undefined;
    }

    return {
      id: slug,
      slug,
      title: data.title,
      date: data.date,
      excerpt: data.excerpt,
      content
    };
  } catch (error) {
    console.error(`Error reading post ${slug}:`, error);
    return undefined;
  }
}

export function getAllPostSlugs(): string[] {
  try {
    const fileNames = fs.readdirSync(POSTS_DIRECTORY);
    return fileNames
      .filter(fileName => fileName.endsWith('.mdx'))
      .map(fileName => fileName.replace(/\.mdx$/, ''));
  } catch (error) {
    console.error('Error reading posts directory:', error);
    return [];
  }
}

Step 6: Create Your First MDX Post

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

---
title: "Hello World"
date: "2024-01-15"
excerpt: "Welcome to my first blog post!"
---

# Hello World

Welcome to my first blog post! This is written in MDX, which means I can use **markdown** syntax and even React components.

## Features

- ✅ Markdown support
- ✅ React components
- ✅ Frontmatter metadata
- ✅ TypeScript integration

## Code Example

```javascript
function greet(name) {
  return `Hello, ${name}!`;
}

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:

import { notFound } from 'next/navigation';
import { Container, Box } from '@mui/material';
import Navigation from '@/components/Navigation';
import PostContent from '@/components/PostContent';
import { getPostBySlug } from '@/lib/posts';
import type { PostPageProps } from '@/types';

export default async function PostPage({ params }: PostPageProps) {
  const { slug } = await params;
  const post = getPostBySlug(slug);

  if (!post) {
    notFound();
  }

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

      <Container maxWidth="md" sx={{ flex: 1, py: 4 }}>
        <PostContent post={post} />
      </Container>
    </Box>
  );
}

Step 9: Create Supporting Components

PostCard Component

Create src/components/PostCard.tsx:

import Link from 'next/link';
import { Card, CardContent, Typography, Box } from '@mui/material';
import type { PostCardProps } from '@/types';

export default function PostCard({ post }: PostCardProps) {
  return (
    <Card component={Link} href={`/posts/${post.slug}`} sx={{ textDecoration: 'none' }}>
      <CardContent>
        <Typography variant="h5" component="h2" gutterBottom>
          {post.title}
        </Typography>
        <Typography variant="body2" color="text.secondary" gutterBottom>
          {new Date(post.date).toLocaleDateString()}
        </Typography>
        <Typography variant="body1">
          {post.excerpt}
        </Typography>
      </CardContent>
    </Card>
  );
}

PostContent Component

Create src/components/PostContent.tsx:

import { Typography, Box } from '@mui/material';
import ReactMarkdown from 'react-markdown';
import type { PostContentProps } from '@/types';

export default function PostContent({ post }: PostContentProps) {
  return (
    <Box>
      <Typography variant="h3" component="h1" gutterBottom>
        {post.title}
      </Typography>
      <Typography variant="body2" color="text.secondary" gutterBottom>
        {new Date(post.date).toLocaleDateString()}
      </Typography>
      <Box sx={{ mt: 4 }}>
        <ReactMarkdown>{post.content}</ReactMarkdown>
      </Box>
    </Box>
  );
}

Step 10: Test Your MDX Setup

  1. Start your development server:

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

---
title: "Your Post Title"
date: "YYYY-MM-DD"
excerpt: "Brief description of your post"
---

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.