mdx
Creates content with MDX including Markdown with JSX, custom components, and frontmatter. Use when building content-heavy sites, documentation, blogs, or mixing Markdown with interactive components.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
MDX
Markdown for the component era - write JSX in Markdown documents.
Quick Start
Install (Next.js):
npm install @next/mdx @mdx-js/loader @mdx-js/react
Configure next.config.mjs:
import createMDX from '@next/mdx';
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
rehypePlugins: [],
},
});
export default withMDX({
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
});
Basic Usage
MDX File
---
title: Hello World
date: 2024-01-01
---
# {frontmatter.title}
This is **Markdown** with <span style={{color: 'red'}}>JSX</span>.
import { Button } from '@/components/ui/button'
<Button>Click me</Button>
## Features
- Write Markdown naturally
- Import and use React components
- Export data and components
Use in Pages
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
async function getPost(slug: string) {
try {
const post = await import(`@/content/blog/${slug}.mdx`);
return post;
} catch {
return null;
}
}
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
const { default: Content, frontmatter } = post;
return (
<article>
<h1>{frontmatter.title}</h1>
<time>{frontmatter.date}</time>
<Content />
</article>
);
}
Frontmatter
With gray-matter
npm install gray-matter
// lib/mdx.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const postsDirectory = path.join(process.cwd(), 'content/blog');
export function getPostBySlug(slug: string) {
const fullPath = path.join(postsDirectory, `${slug}.mdx`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
return {
slug,
frontmatter: data,
content,
};
}
export function getAllPosts() {
const slugs = fs.readdirSync(postsDirectory);
return slugs
.map((slug) => getPostBySlug(slug.replace(/\.mdx$/, '')))
.sort((a, b) =>
new Date(b.frontmatter.date).getTime() -
new Date(a.frontmatter.date).getTime()
);
}
Type-safe Frontmatter
// lib/mdx.ts
import { z } from 'zod';
const frontmatterSchema = z.object({
title: z.string(),
date: z.string().transform((s) => new Date(s)),
description: z.string().optional(),
tags: z.array(z.string()).default([]),
published: z.boolean().default(true),
});
export type Frontmatter = z.infer<typeof frontmatterSchema>;
export function getPostBySlug(slug: string) {
const { data, content } = matter(fileContents);
const frontmatter = frontmatterSchema.parse(data);
return { slug, frontmatter, content };
}
Custom Components
MDX Components Provider
// components/mdx-components.tsx
import Image from 'next/image';
import Link from 'next/link';
import { Callout } from '@/components/callout';
import { CodeBlock } from '@/components/code-block';
export const mdxComponents = {
// Override HTML elements
h1: (props: any) => (
<h1 className="text-4xl font-bold mt-8 mb-4" {...props} />
),
h2: (props: any) => (
<h2 className="text-3xl font-semibold mt-6 mb-3" {...props} />
),
p: (props: any) => (
<p className="leading-7 mb-4" {...props} />
),
a: (props: any) => (
<Link
className="text-blue-600 hover:underline"
{...props}
/>
),
img: (props: any) => (
<Image
className="rounded-lg my-4"
width={800}
height={400}
{...props}
/>
),
pre: (props: any) => <CodeBlock {...props} />,
code: (props: any) => (
<code className="bg-gray-100 px-1 rounded" {...props} />
),
// Custom components
Callout,
Image,
};
Using Provider
// app/layout.tsx
import { MDXProvider } from '@mdx-js/react';
import { mdxComponents } from '@/components/mdx-components';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<MDXProvider components={mdxComponents}>
{children}
</MDXProvider>
);
}
App Router (Next.js 14+)
// mdx-components.tsx (root level)
import type { MDXComponents } from 'mdx/types';
import { mdxComponents } from '@/components/mdx-components';
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
...mdxComponents,
};
}
Plugins
Remark Plugins (Markdown)
npm install remark-gfm remark-math
// next.config.mjs
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
const withMDX = createMDX({
options: {
remarkPlugins: [remarkGfm, remarkMath],
},
});
Rehype Plugins (HTML)
npm install rehype-slug rehype-autolink-headings rehype-pretty-code
// next.config.mjs
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrettyCode from 'rehype-pretty-code';
const withMDX = createMDX({
options: {
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
[rehypePrettyCode, { theme: 'github-dark' }],
],
},
});
Contentlayer (Recommended)
Setup
npm install contentlayer next-contentlayer
// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
import rehypePrettyCode from 'rehype-pretty-code';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
export const Post = defineDocumentType(() => ({
name: 'Post',
filePathPattern: 'blog/**/*.mdx',
contentType: 'mdx',
fields: {
title: { type: 'string', required: true },
date: { type: 'date', required: true },
description: { type: 'string' },
published: { type: 'boolean', default: true },
tags: { type: 'list', of: { type: 'string' }, default: [] },
},
computedFields: {
slug: {
type: 'string',
resolve: (post) => post._raw.sourceFileName.replace(/\.mdx$/, ''),
},
url: {
type: 'string',
resolve: (post) => `/blog/${post._raw.sourceFileName.replace(/\.mdx$/, '')}`,
},
},
}));
export default makeSource({
contentDirPath: 'content',
documentTypes: [Post],
mdx: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[rehypePrettyCode, { theme: 'github-dark' }],
],
},
});
Next.js Config
// next.config.mjs
import { withContentlayer } from 'next-contentlayer';
export default withContentlayer({
// Next.js config
});
Usage
// app/blog/[slug]/page.tsx
import { allPosts } from 'contentlayer/generated';
import { useMDXComponent } from 'next-contentlayer/hooks';
import { notFound } from 'next/navigation';
import { mdxComponents } from '@/components/mdx-components';
export async function generateStaticParams() {
return allPosts.map((post) => ({
slug: post.slug,
}));
}
export default function PostPage({ params }: { params: { slug: string } }) {
const post = allPosts.find((post) => post.slug === params.slug);
if (!post) {
notFound();
}
const MDXContent = useMDXComponent(post.body.code);
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<MDXContent components={mdxComponents} />
</article>
);
}
Remote MDX
MDX Remote
npm install next-mdx-remote
// app/blog/[slug]/page.tsx
import { MDXRemote } from 'next-mdx-remote/rsc';
import { mdxComponents } from '@/components/mdx-components';
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`);
return res.json();
}
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<MDXRemote
source={post.content}
components={mdxComponents}
options={{
mdxOptions: {
remarkPlugins: [],
rehypePlugins: [],
},
}}
/>
</article>
);
}
Interactive Components
Code Playground
import { Playground } from '@/components/playground'
<Playground
code={`
function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
`}
/>
Tabs
import { Tabs, Tab } from '@/components/tabs'
<Tabs>
<Tab label="npm">
```bash
npm install package-name
```
</Tab>
<Tab label="yarn">
```bash
yarn add package-name
```
</Tab>
<Tab label="pnpm">
```bash
pnpm add package-name
```
</Tab>
</Tabs>
Callouts
// components/callout.tsx
interface CalloutProps {
type?: 'info' | 'warning' | 'error';
title?: string;
children: React.ReactNode;
}
export function Callout({ type = 'info', title, children }: CalloutProps) {
const styles = {
info: 'bg-blue-50 border-blue-500',
warning: 'bg-yellow-50 border-yellow-500',
error: 'bg-red-50 border-red-500',
};
return (
<div className={`border-l-4 p-4 my-4 ${styles[type]}`}>
{title && <p className="font-bold">{title}</p>}
{children}
</div>
);
}
<Callout type="warning" title="Note">
This is an important warning message.
</Callout>
Best Practices
- Use type-safe frontmatter - Validate with Zod
- Centralize components - Single MDX components file
- Add table of contents - Parse headings
- Implement syntax highlighting - Use rehype-pretty-code
- Cache compiled MDX - Contentlayer handles this
Common Mistakes
| Mistake | Fix |
|---|---|
| Missing imports in MDX | Use components provider |
| Slow builds | Use Contentlayer |
| No syntax highlighting | Add rehype-pretty-code |
| Broken links | Use next/link component |
| Large bundle | Lazy load components |
Reference Files
- references/plugins.md - Remark/Rehype plugins
- references/components.md - Custom components
- references/contentlayer.md - Contentlayer setup
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-28
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!