Next.js MDX Cheatsheet
Install Dependencies
npm install @next/mdx @mdx-js/loader @mdx-js/react
yarn add @next/mdx @mdx-js/loader @mdx-js/react
Configure Next.js
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
rehypePlugins: [],
},
});
module.exports = withMDX({
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
});
TypeScript Configuration
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": [
"*/*.ts",
"*/*.tsx",
"*/*.mdx"
]
}
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
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>
);
}
Define MDX Components
import { Callout } from './Callout';
import { CodeBlock } from './CodeBlock';
import { Image } from './Image';
import { Link } from './Link';
export const mdxComponents = {
Callout,
CodeBlock,
Image,
Link,
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
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
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
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
'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
'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>
);
}
Extract Frontmatter
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
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,
},
};
}
Custom Hooks in MDX
import { useMemo } from 'react';
import { getMDXComponent } from 'mdx-bundler/client';
export function useMDX(code) {
return useMemo(() => getMDXComponent(code), [code]);
}
MDX with Layouts
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
'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>
);
}
Tailwind CSS Integration
module.exports = {
content: [
'./app/*/*.{js,ts,jsx,tsx,mdx}',
'./components/*/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
],
};
Custom MDX Styles
.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;
}
Performance Optimization
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
const HeavyChart = dynamic(() => import('./HeavyChart'), {
ssr: false,
loading: () => <div>Loading chart...</div>
});
Error Handling
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
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
import { validateFrontmatter } from './schemas';
export function validateMDXContent(content, frontmatter) {
const errors = [];
try {
validateFrontmatter(frontmatter);
} catch (error) {
errors.push(`Frontmatter error: ${error.message}`);
}
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;
}