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.
Creates MDX content with Markdown, JSX, and frontmatter for content-heavy sites. Use when building documentation, blogs, or pages that need interactive React components mixed with Markdown.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Markdown for the component era - write JSX in Markdown documents.
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'],
});
---
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
// 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>
);
}
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()
);
}
// 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 };
}
// 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,
};
// 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>
);
}
// 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,
};
}
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],
},
});
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' }],
],
},
});
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.config.mjs
import { withContentlayer } from 'next-contentlayer';
export default withContentlayer({
// Next.js config
});
// 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>
);
}
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>
);
}
import { Playground } from '@/components/playground'
<Playground
code={`
function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
`}
/>
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>
// 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>
| 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 |
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.