← Back to portfolio

Blog

Thoughts on development, design, and technology

← All posts
4 min readby Zakkarija Micallef

Setting Up MDX for Content-Rich Blogging

How I set up MDX with Next.js to create a flexible blogging system that combines markdown simplicity with React component power.

MDXBloggingNext.jsReact
Setting Up MDX for Content-Rich Blogging

Setting Up MDX for Content-Rich Blogging

When building this blog, I wanted the simplicity of Markdown with the power of React components. MDX (Markdown + JSX) provides exactly that - here's how I set it up.

Why MDX?

Traditional Markdown is great for simple content, but sometimes you need more:

  • Interactive components within content
  • Custom styling for specific elements
  • Rich media embeds
  • Complex layouts within posts

MDX lets you write Markdown while seamlessly embedding React components:

# Regular Markdown Heading

This is normal text with **bold** and *italic* formatting.

<CustomComponent data="some-value" />

Back to regular markdown!

The Technical Setup

My MDX setup uses several key packages:

{
  "@next/mdx": "^15.4.6",
  "@mdx-js/loader": "^3.1.0", 
  "@mdx-js/react": "^3.1.0",
  "next-mdx-remote": "^5.0.0",
  "gray-matter": "^4.0.3"
}

Configuration

The Next.js config enables MDX processing:

import createMDX from '@next/mdx';

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

export default withMDX(config);

File Structure

Posts live in src/content/posts/ as .mdx files:

src/content/posts/
├── welcome-to-my-blog.mdx
├── building-with-nextjs-15.mdx
└── mdx-blogging-setup.mdx

Each post starts with frontmatter:

---
title: "Post Title"
date: "2025-01-15"
excerpt: "Brief description"
readTime: "5 min read"
---

# Post content starts here

Content Processing

I use gray-matter to parse frontmatter and next-mdx-remote for server-side rendering:

export async function getBlogPost(slug: string) {
  const fullPath = path.join(postsDirectory, `${slug}.mdx`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');
  const { data, content } = matter(fileContents);

  return {
    slug,
    title: data.title,
    date: data.date,
    excerpt: data.excerpt,
    content, // Raw MDX content
  };
}

Custom Components

The power of MDX shines with custom components. I created an MDX components file:

export function useMDXComponents(components: MDXComponents) {
  return {
    h1: ({ children }) => (
      <h1 className="text-3xl font-bold mb-4 border-b-2 border-gray-900 pb-2">
        {children}
      </h1>
    ),
    blockquote: ({ children }) => (
      <blockquote className="border-l-4 border-gray-900 pl-6 my-6 italic">
        {children}
      </blockquote>
    ),
    // ... more custom components
  };
}

Rendering Posts

In the blog post page, I render MDX with custom components:

import { MDXRemote } from 'next-mdx-remote/rsc';

export default async function BlogPost({ params }) {
  const post = await getBlogPost(params.slug);
  const components = useMDXComponents({});

  return (
    <div className="prose">
      <MDXRemote source={post.content} components={components} />
    </div>
  );
}

Benefits of This Approach

  1. Performance - Static generation of all posts
  2. Flexibility - Can embed any React component
  3. Developer Experience - Write in familiar Markdown
  4. SEO - Server-rendered content with proper metadata
  5. Maintainability - Clean separation of content and code

Future Enhancements

Some ideas I'm considering:

  • Syntax highlighting for code blocks
  • Reading time calculation
  • Table of contents generation
  • Search functionality
  • Comment system

Conclusion

MDX strikes the perfect balance between simplicity and power. For developers who want more than basic Markdown but don't need a full CMS, it's an excellent choice.

The setup takes a bit of work upfront, but the authoring experience is smooth, and the flexibility is worth it.


Have you used MDX in your projects? What components have you found most useful to embed in your content?