2025-07-21 by Remi Kristelijn
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.
MDX is a format that lets you write JSX in your markdown documents. This means you can:
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 MDXgray-matter: Parse frontmatter from markdown filesUpdate 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);
Organize your blog content in a dedicated directory:
mkdir -p src/content/posts
This structure keeps your content separate from your application code.
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 }>; }
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 []; } }
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}!`; }
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> ); }
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> ); }
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> ); }
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> ); }
Start your development server:
npm run dev
Visit http://localhost:3000/posts to see your blog listing
Click on a post to view the individual post page
All your MDX files should follow this frontmatter structure:
--- title: "Your Post Title" date: "YYYY-MM-DD" excerpt: "Brief description of your post" ---
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.
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.