typescriptintermediate

MDX Blog with Next.js

Set up MDX support for a blog with custom components, frontmatter, and syntax highlighting.

typescript
// next.config.mjs
import createMDX from '@next/mdx';

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
  },
});

export default withMDX({
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
});

// mdx-components.tsx (root level)
import type { MDXComponents } from 'mdx/types';

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h1: ({ children }) => (
      <h1 className="text-3xl font-bold mt-8 mb-4">{children}</h1>
    ),
    h2: ({ children }) => (
      <h2 className="text-2xl font-semibold mt-6 mb-3">{children}</h2>
    ),
    p: ({ children }) => (
      <p className="text-gray-300 leading-relaxed mb-4">{children}</p>
    ),
    code: ({ children }) => (
      <code className="bg-gray-800 px-1.5 py-0.5 rounded text-sm">
        {children}
      </code>
    ),
    pre: ({ children }) => (
      <pre className="bg-gray-900 p-4 rounded-lg overflow-x-auto mb-4">
        {children}
      </pre>
    ),
    a: ({ href, children }) => (
      <a href={href} className="text-blue-400 hover:underline" target="_blank" rel="noopener">
        {children}
      </a>
    ),
    ...components,
  };
}

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { readdir } from 'fs/promises';
import path from 'path';

export async function generateStaticParams() {
  const files = await readdir(path.join(process.cwd(), 'content/blog'));
  return files
    .filter((f) => f.endsWith('.mdx'))
    .map((f) => ({ slug: f.replace('.mdx', '') }));
}

export default async function BlogPost({
  params,
}: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  try {
    const { default: Content, metadata } = await import(`@/content/blog/${slug}.mdx`);
    return (
      <article className="prose dark:prose-invert max-w-3xl mx-auto">
        <h1>{metadata.title}</h1>
        <Content />
      </article>
    );
  } catch {
    notFound();
  }
}

Use Cases

  • technical blogs
  • documentation
  • content-heavy sites

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.