Next.js MDX Cheat Sheet

Next.js MDX Cheatsheet

Setup and Installation

Install Dependencies

npm install @next/mdx @mdx-js/loader @mdx-js/react
# or
yarn add @next/mdx @mdx-js/loader @mdx-js/react

Configure Next.js

// next.config.js
const withMDX = require('@next/mdx')({
    extension: /\.mdx?$/,
    options: {
        remarkPlugins: [],
        rehypePlugins: [],
    },
});

module.exports = withMDX({
    pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
});

TypeScript Configuration

// tsconfig.json
{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": ["./*"]
        }
    },
    "include": [
        "*/*.ts",
        "*/*.tsx",
        "*/*.mdx"
    ]
}

Basic MDX Usage

Create MDX File

// content/blog-post.mdx
---
title: "My First MDX Post"
date: "2024-01-15"
author: "John Doe"
---

# Welcome to MDX

This is a *markdown* file with React components!

<Callout type="info">
  This is a custom component in MDX!
</Callout>

## Code Example

```jsx
function Hello() {
  return <h1>Hello, MDX!</h1>;
}
{`function Hello() { return

Hello, MDX!

; }`}
```

Import and Render MDX

// app/blog/[slug]/page.js
import { getMDXComponent } from 'mdx-bundler/client';
import { bundleMDX } from 'mdx-bundler';
import fs from 'fs';
import path from 'path';

export default async function BlogPost({ params }) {
    const source = fs.readFileSync(
        path.join(process.cwd(), 'content', `${params.slug}.mdx`),
        'utf8'
    );

    const { code, frontmatter } = await bundleMDX({
        source,
        mdxOptions(options, frontmatter) {
            options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGfm];
            options.rehypePlugins = [...(options.rehypePlugins ?? []), rehypePrism];
            return options;
        },
    });

    const Component = getMDXComponent(code);

    return (
        <article>
            <h1>{frontmatter.title}</h1>
            <Component />
        </article>
    );
}

Custom Components

Define MDX Components

// components/mdx/index.js
import { Callout } from './Callout';
import { CodeBlock } from './CodeBlock';
import { Image } from './Image';
import { Link } from './Link';

export const mdxComponents = {
    Callout,
    CodeBlock,
    Image,
    Link,
    // Override default HTML elements
    h1: (props) => <h1 className="text-3xl font-bold mb-4" {...props} />,
    h2: (props) => <h2 className="text-2xl font-semibold mb-3" {...props} />,
    p: (props) => <p className="mb-4 leading-relaxed" {...props} />,
    code: (props) => (
        <code className="bg-gray-100 px-1 py-0.5 rounded text-sm" {...props} />
    ),
    pre: (props) => (
        <pre className="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto" {...props} />
    ),
};

Callout Component

// components/mdx/Callout.jsx
export function Callout({ children, type = 'info' }) {
    const styles = {
        info: 'bg-blue-50 border-blue-200 text-blue-800',
        warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
        error: 'bg-red-50 border-red-200 text-red-800',
        success: 'bg-green-50 border-green-200 text-green-800',
    };

    return (
        <div className={`border-l-4 p-4 mb-4 ${styles[type]}`}>
            <div className="flex">
                <div className="flex-shrink-0">
                    {type === 'info' && <InfoIcon />}
                    {type === 'warning' && <WarningIcon />}
                    {type === 'error' && <ErrorIcon />}
                    {type === 'success' && <SuccessIcon />}
                </div>
                <div className="ml-3">
                    {children}
                </div>
            </div>
        </div>
    );
}

Code Block Component

// components/mdx/CodeBlock.jsx
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';

export function CodeBlock({ children, language = 'javascript' }) {
    return (
        <div className="my-4">
            <SyntaxHighlighter
                language={language}
                style={tomorrow}
                customStyle={{
                    margin: 0,
                    borderRadius: '8px',
                }}
            >
                {children}
            </SyntaxHighlighter>
        </div>
    );
}

Image Component

// components/mdx/Image.jsx
import NextImage from 'next/image';

export function Image({ src, alt, width = 800, height = 400 }) {
    return (
        <div className="my-6">
            <NextImage
                src={src}
                alt={alt}
                width={width}
                height={height}
                className="rounded-lg shadow-lg"
            />
            {alt && (
                <p className="text-center text-gray-600 text-sm mt-2">{alt}</p>
            )}
        </div>
    );
}

Dynamic Content

Interactive Components in MDX

// content/interactive-post.mdx
---
title: "Interactive MDX Post"
---

# Interactive Content

<Counter initialValue={0} />

<ToggleButton>
  Click me to toggle content!
</ToggleButton>

<Chart data={[10, 20, 30, 40, 50]} />

Interactive Components

// components/mdx/Counter.jsx
'use client';

import { useState } from 'react';

export function Counter({ initialValue = 0 }) {
    const [count, setCount] = useState(initialValue);

    return (
        <div className="my-4 p-4 border rounded-lg">
            <p>Count: {count}</p>
            <button
                onClick={() => setCount(count + 1)}
                className="bg-blue-500 text-white px-4 py-2 rounded"
            >
                Increment
            </button>
        </div>
    );
}

Toggle Component

// components/mdx/ToggleButton.jsx
'use client';

import { useState } from 'react';

export function ToggleButton({ children }) {
    const [isOpen, setIsOpen] = useState(false);

    return (
        <div className="my-4">
            <button
                onClick={() => setIsOpen(!isOpen)}
                className="bg-gray-200 px-4 py-2 rounded"
            >
                {children}
            </button>
            {isOpen && (
                <div className="mt-2 p-4 bg-gray-100 rounded">
                    This content is now visible!
                </div>
            )}
        </div>
    );
}

Frontmatter and Metadata

Extract Frontmatter

// lib/mdx.js
import matter from 'gray-matter';
import fs from 'fs';
import path from 'path';

export function getPostBySlug(slug) {
    const filePath = path.join(process.cwd(), 'content', `${slug}.mdx`);
    const fileContents = fs.readFileSync(filePath, 'utf8');
    const { data, content } = matter(fileContents);

    return {
        slug,
        frontmatter: data,
        content,
    };
}

export function getAllPosts() {
    const postsDirectory = path.join(process.cwd(), 'content');
    const filenames = fs.readdirSync(postsDirectory);

    return filenames
        .filter(filename => filename.endsWith('.mdx'))
        .map(filename => {
            const slug = filename.replace(/\.mdx$/, '');
            return getPostBySlug(slug);
        })
        .sort((a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date));
}

Generate Static Pages

// app/blog/[slug]/page.js
import { getAllPosts, getPostBySlug } from '@/lib/mdx';

export async function generateStaticParams() {
    const posts = getAllPosts();
    
    return posts.map((post) => ({
        slug: post.slug,
    }));
}

export async function generateMetadata({ params }) {
    const post = getPostBySlug(params.slug);
    
    return {
        title: post.frontmatter.title,
        description: post.frontmatter.description,
        openGraph: {
            title: post.frontmatter.title,
            description: post.frontmatter.description,
        },
    };
}

Advanced Features

Custom Hooks in MDX

// hooks/useMDX.js
import { useMemo } from 'react';
import { getMDXComponent } from 'mdx-bundler/client';

export function useMDX(code) {
    return useMemo(() => getMDXComponent(code), [code]);
}

MDX with Layouts

// components/mdx/MDXLayout.jsx
import { mdxComponents } from './index';

export function MDXLayout({ children, frontmatter }) {
    return (
        <article className="max-w-4xl mx-auto px-4 py-8">
            <header className="mb-8">
                <h1 className="text-4xl font-bold mb-2">{frontmatter.title}</h1>
                <div className="text-gray-600">
                    <time dateTime={frontmatter.date}>
                        {new Date(frontmatter.date).toLocaleDateString()}
                    </time>
                    {frontmatter.author && (
                        <span className="ml-4">by {frontmatter.author}</span>
                    )}
                </div>
            </header>
            
            <div className="prose prose-lg max-w-none">
                {children}
            </div>
        </article>
    );
}

MDX with Search

// components/mdx/SearchableMDX.jsx
'use client';

import { useState, useMemo } from 'react';
import { useMDX } from '@/hooks/useMDX';

export function SearchableMDX({ posts }) {
    const [searchTerm, setSearchTerm] = useState('');
    
    const filteredPosts = useMemo(() => {
        return posts.filter(post => 
            post.frontmatter.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
            post.frontmatter.description.toLowerCase().includes(searchTerm.toLowerCase())
        );
    }, [posts, searchTerm]);

    return (
        <div>
            <input
                type="text"
                placeholder="Search posts..."
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
                className="w-full p-2 border rounded"
            />
            
            <div className="mt-4">
                {filteredPosts.map(post => (
                    <div key={post.slug} className="mb-4 p-4 border rounded">
                        <h3>{post.frontmatter.title}</h3>
                        <p>{post.frontmatter.description}</p>
                    </div>
                ))}
            </div>
        </div>
    );
}

Styling and Theming

Tailwind CSS Integration

// tailwind.config.js
module.exports = {
    content: [
        './app/*/*.{js,ts,jsx,tsx,mdx}',
        './components/*/*.{js,ts,jsx,tsx,mdx}',
    ],
    theme: {
        extend: {},
    },
    plugins: [
        require('@tailwindcss/typography'),
    ],
};

Custom MDX Styles

/* styles/mdx.css */
.mdx-content {
    @apply prose prose-lg max-w-none;
}

.mdx-content h1 {
    @apply text-4xl font-bold mb-6;
}

.mdx-content h2 {
    @apply text-3xl font-semibold mb-4 mt-8;
}

.mdx-content h3 {
    @apply text-2xl font-medium mb-3 mt-6;
}

.mdx-content p {
    @apply mb-4 leading-relaxed;
}

.mdx-content ul {
    @apply list-disc list-inside mb-4;
}

.mdx-content ol {
    @apply list-decimal list-inside mb-4;
}

.mdx-content blockquote {
    @apply border-l-4 border-gray-300 pl-4 italic my-4;
}

.mdx-content code {
    @apply bg-gray-100 px-1 py-0.5 rounded text-sm;
}

.mdx-content pre {
    @apply bg-gray-900 text-white p-4 rounded-lg overflow-x-auto;
}

Best Practices

Performance Optimization

// Optimize MDX loading
export async function generateStaticParams() {
    const posts = getAllPosts();
    
    return posts.map((post) => ({
        slug: post.slug,
    }));
}

// Use dynamic imports for heavy components
const HeavyChart = dynamic(() => import('./HeavyChart'), {
    ssr: false,
    loading: () => <div>Loading chart...</div>
});

Error Handling

// components/mdx/MDXErrorBoundary.jsx
import { Component } from 'react';

export class MDXErrorBoundary extends Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    static getDerivedStateFromError(error) {
        return { hasError: true };
    }

    render() {
        if (this.state.hasError) {
            return (
                <div className="p-4 bg-red-50 border border-red-200 rounded">
                    <h3>Error rendering MDX content</h3>
                    <p>Please check the markdown syntax and try again.</p>
                </div>
            );
        }

        return this.props.children;
    }
}

SEO Optimization

// lib/mdx-seo.js
export function generateSEOMetadata(post) {
    return {
        title: post.frontmatter.title,
        description: post.frontmatter.description,
        keywords: post.frontmatter.tags?.join(', '),
        openGraph: {
            title: post.frontmatter.title,
            description: post.frontmatter.description,
            type: 'article',
            publishedTime: post.frontmatter.date,
            authors: [post.frontmatter.author],
        },
        twitter: {
            card: 'summary_large_image',
            title: post.frontmatter.title,
            description: post.frontmatter.description,
        },
    };
}

Content Validation

// lib/validate-mdx.js
import { validateFrontmatter } from './schemas';

export function validateMDXContent(content, frontmatter) {
    const errors = [];
    
    // Validate frontmatter
    try {
        validateFrontmatter(frontmatter);
    } catch (error) {
        errors.push(`Frontmatter error: ${error.message}`);
    }
    
    // Check for broken links
    const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
    const links = [...content.matchAll(linkRegex)];
    
    links.forEach(([match, text, url]) => {
        if (!url.startsWith('http') && !url.startsWith('/')) {
            errors.push(`Broken link: ${url}`);
        }
    });
    
    return errors;
}