Claude Code for MDX: Rich Content with React Components in Markdown — Claude Skills 360 Blog
Blog / Frontend / Claude Code for MDX: Rich Content with React Components in Markdown
Frontend

Claude Code for MDX: Rich Content with React Components in Markdown

Published: March 3, 2027
Read time: 8 min read
By: Claude Skills 360

MDX extends Markdown with JSX — you can import and render React components inline within prose content. import { Chart } from "./chart" at the top of an MDX file makes <Chart /> available anywhere in the document. Custom components override default HTML elements: pass a components map to replace h1, code, a, and blockquote with styled versions. next-mdx-remote serializes MDX server-side in Next.js App Router and renders it on the client. Astro’s @astrojs/mdx integration processes MDX with Astro’s existing Vite pipeline. Contentlayer generates type-safe document collections from MDX files with frontmatter validation. rehype-pretty-code adds syntax-highlighted code blocks using Shiki. Custom remark plugins transform the AST before rendering. Claude Code generates MDX page configurations, component maps, content type definitions, rehype/remark plugin setups, and the Next.js and Astro integration code for documentation and blog systems.

CLAUDE.md for MDX

## MDX Stack
- Versions: @mdx-js/react >= 3.0, next-mdx-remote >= 5.0 for Next.js
- Astro: @astrojs/mdx >= 4.0 — native MDX support in Astro
- Contentlayer: contentlayer2 >= 0.5 — type-safe content pipeline
- Plugins: rehype-pretty-code (syntax highlighting), remark-gfm (tables/strikethrough)
- Components: MDXProvider with components prop OR serialize({ components }) in next-mdx-remote
- Frontmatter: YAML between --- delimiters — gray-matter for parsing
- Images: next/image or Astro Image — replace <img> in components map

Next.js App Router with next-mdx-remote

// lib/mdx.ts — MDX processing pipeline
import { compileMDX } from "next-mdx-remote/rsc"
import remarkGfm from "remark-gfm"
import rehypePrettyCode from "rehype-pretty-code"
import rehypeSlug from "rehype-slug"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
import { readFile, readdir } from "fs/promises"
import path from "path"
import matter from "gray-matter"

// Custom components to inject into MDX
import { Callout } from "@/components/mdx/Callout"
import { CodeBlock } from "@/components/mdx/CodeBlock"
import { Video } from "@/components/mdx/Video"
import { Tweet } from "@/components/mdx/Tweet"

const MDX_COMPONENTS = {
  Callout,
  Video,
  Tweet,
  // Override HTML elements
  pre: CodeBlock,
  a: ({ href, children, ...props }: React.ComponentPropsWithRef<"a">) => (
    <a
      href={href}
      target={href?.startsWith("http") ? "_blank" : undefined}
      rel={href?.startsWith("http") ? "noopener noreferrer" : undefined}
      className="text-blue-600 hover:underline"
      {...props}
    >
      {children}
    </a>
  ),
  img: ({ src, alt, ...props }: React.ComponentPropsWithRef<"img">) => (
    // eslint-disable-next-line @next/next/no-img-element
    <img src={src} alt={alt ?? ""} className="rounded-lg my-6 w-full" {...props} />
  ),
}

const CONTENT_DIR = path.join(process.cwd(), "content")

export interface PostFrontmatter {
  title: string
  description: string
  publishedAt: string
  author: string
  tags: string[]
  featured?: boolean
}

export async function getPost(slug: string) {
  const filePath = path.join(CONTENT_DIR, "posts", `${slug}.mdx`)
  const source = await readFile(filePath, "utf-8")

  const { content, data } = matter(source)
  const frontmatter = data as PostFrontmatter

  const { content: rendered } = await compileMDX({
    source: content,
    components: MDX_COMPONENTS,
    options: {
      mdxOptions: {
        remarkPlugins: [remarkGfm],
        rehypePlugins: [
          rehypeSlug,
          [
            rehypeAutolinkHeadings,
            { behavior: "wrap", properties: { className: ["anchor"] } },
          ],
          [
            rehypePrettyCode,
            {
              theme: { dark: "github-dark", light: "github-light" },
              keepBackground: false,
              // Add line numbers
              onVisitLine(node: any) {
                if (node.children.length === 0) {
                  node.children = [{ type: "text", value: " " }]
                }
              },
            },
          ],
        ],
      },
    },
  })

  return { frontmatter, slug, content: rendered }
}

export async function getAllPosts() {
  const dir = path.join(CONTENT_DIR, "posts")
  const files = await readdir(dir)

  const posts = await Promise.all(
    files
      .filter(f => f.endsWith(".mdx"))
      .map(async f => {
        const slug = f.replace(/\.mdx$/, "")
        const source = await readFile(path.join(dir, f), "utf-8")
        const { data } = matter(source)
        return { slug, frontmatter: data as PostFrontmatter }
      })
  )

  return posts.sort(
    (a, b) =>
      new Date(b.frontmatter.publishedAt).getTime() -
      new Date(a.frontmatter.publishedAt).getTime()
  )
}
// app/blog/[slug]/page.tsx — Next.js MDX page
import { getPost, getAllPosts } from "@/lib/mdx"
import { notFound } from "next/navigation"

export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(p => ({ slug: p.slug }))
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug).catch(() => null)
  if (!post) notFound()

  return (
    <article className="prose lg:prose-xl mx-auto py-12 px-4">
      <h1>{post.frontmatter.title}</h1>
      <p className="text-muted-foreground">{post.frontmatter.description}</p>
      <div className="mt-8">{post.content}</div>
    </article>
  )
}

MDX Components

// components/mdx/Callout.tsx — custom MDX component
import { cn } from "@/lib/utils"
import { Info, AlertTriangle, CheckCircle, XCircle } from "lucide-react"

type CalloutType = "info" | "warning" | "success" | "error"

const calloutConfig: Record<CalloutType, {
  icon: React.ElementType
  className: string
  iconClassName: string
}> = {
  info: {
    icon: Info,
    className: "bg-blue-50 border-blue-200 text-blue-900",
    iconClassName: "text-blue-500",
  },
  warning: {
    icon: AlertTriangle,
    className: "bg-amber-50 border-amber-200 text-amber-900",
    iconClassName: "text-amber-500",
  },
  success: {
    icon: CheckCircle,
    className: "bg-green-50 border-green-200 text-green-900",
    iconClassName: "text-green-500",
  },
  error: {
    icon: XCircle,
    className: "bg-red-50 border-red-200 text-red-900",
    iconClassName: "text-red-500",
  },
}

export function Callout({
  type = "info",
  children,
}: {
  type?: CalloutType
  children: React.ReactNode
}) {
  const { icon: Icon, className, iconClassName } = calloutConfig[type]

  return (
    <div className={cn("flex gap-3 rounded-lg border p-4 my-6", className)}>
      <Icon className={cn("h-5 w-5 shrink-0 mt-0.5", iconClassName)} />
      <div className="text-sm leading-relaxed [&>p]:m-0">{children}</div>
    </div>
  )
}
// components/mdx/CodeBlock.tsx — syntax highlighted code block wrapper
"use client"
import { useState } from "react"
import { Check, Copy } from "lucide-react"
import { cn } from "@/lib/utils"

export function CodeBlock({
  children,
  className,
  ...props
}: React.ComponentPropsWithoutRef<"pre">) {
  const [copied, setCopied] = useState(false)

  function copyCode() {
    const code = (children as React.ReactElement)?.props?.children as string
    navigator.clipboard.writeText(String(code).trim())
    setCopied(true)
    setTimeout(() => setCopied(false), 2000)
  }

  return (
    <div className="relative group my-6">
      <button
        onClick={copyCode}
        className={cn(
          "absolute right-3 top-3 opacity-0 group-hover:opacity-100 transition-opacity",
          "rounded-md bg-white/10 p-1.5 text-gray-400 hover:text-white"
        )}
        aria-label="Copy code"
      >
        {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
      </button>
      <pre className={cn("overflow-x-auto rounded-lg p-4", className)} {...props}>
        {children}
      </pre>
    </div>
  )
}

Astro MDX Integration

// astro.config.ts — Astro with MDX
import { defineConfig } from "astro/config"
import mdx from "@astrojs/mdx"
import react from "@astrojs/react"
import rehypePrettyCode from "rehype-pretty-code"
import remarkGfm from "remark-gfm"

export default defineConfig({
  integrations: [
    mdx({
      remarkPlugins: [remarkGfm],
      rehypePlugins: [
        [rehypePrettyCode, { theme: "github-dark" }],
      ],
      // Globally inject components into MDX without importing
      components: {
        Callout: "./src/components/mdx/Callout.astro",
      },
    }),
    react(),
  ],
})
---
// content/posts/my-post.mdx — sample MDX with frontmatter
title: "Getting Started"
description: "A complete guide to getting started"
publishedAt: "2024-01-15"
author: "Jane Smith"
tags: ["tutorial", "beginner"]
---

import { Chart } from "@/components/Chart"
import { DataTable } from "@/components/DataTable"

# Getting Started

This is a paragraph with **bold** and _italic_ text.

<Callout type="info">
  This component is available globally in Astro MDX — no import needed.
</Callout>

## Interactive Data

<Chart data={[{ month: "Jan", revenue: 4200 }, { month: "Feb", revenue: 5100 }]} />

## Code Example

```typescript
const greeting = "Hello, MDX!"
console.log(greeting)

## Custom Remark Plugin

```typescript
// lib/remark-reading-time.ts — custom remark plugin for reading time
import { visit } from "unist-util-visit"
import type { Root, Text } from "mdast"

export function remarkReadingTime() {
  return (tree: Root, file: any) => {
    let wordCount = 0

    visit(tree, "text", (node: Text) => {
      wordCount += node.value.split(/\s+/).filter(Boolean).length
    })

    const readingTimeMinutes = Math.max(1, Math.round(wordCount / 200))
    file.data.astro = file.data.astro ?? {}
    file.data.astro.frontmatter = {
      ...file.data.astro.frontmatter,
      readingTime: `${readingTimeMinutes} min read`,
      wordCount,
    }
  }
}

For the Contentlayer type-safe content pipeline that validates MDX frontmatter against TypeScript types and generates typed document collections without manual gray-matter parsing, the Next.js App Router guide covers Contentlayer integration. For the Keystatic CMS alternative that stores MDX content as files in git with a built-in Studio UI for content editors — better when non-developers need to edit content without touching raw MDX files, the file-based approach keeps all content versioned in git. The Claude Skills 360 bundle includes MDX skill sets covering component maps, rehype plugins, and Next.js integration. Start with the free tier to try MDX configuration generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free