Create FasterCreate Faster
Content

MDX

Pre-configured MDX support with frontmatter parsing, custom components, and dynamic routing.

Supported frameworks

Next.js installs @mdx-js/loader, @mdx-js/react, @next/mdx and next-mdx-remote. TanStack Start installs @mdx-js/rollup, remark-frontmatter and remark-mdx-frontmatter.

→ MDX Documentation

TanStack Start

Documents in contents/ are compiled at build time by @mdx-js/rollup and loaded lazily with import.meta.glob, one chunk per document. YAML frontmatter is exported as frontmatter. The example route /mdx/{-$slug} renders /mdx as home.mdx and /mdx/cool as cool.mdx.

Files added:

src/
├── lib/
│   └── mdx.ts                  # Lazy document loader
├── mdx-components.tsx          # Custom MDX component mappings
├── styles/
│   └── mdx.css                 # MDX-specific styling
└── routes/mdx/
    └── {-$slug}.tsx            # MDX route example

contents/
├── home.mdx                    # Example content
└── cool.mdx                    # Example content

Modified files:

  • vite.config.ts - Adds the @mdx-js/rollup plugin with remark-frontmatter and remark-mdx-frontmatter
  • src/styles.css - Imports styles/mdx.css
  • src/routes/index.tsx - Links to the example page

Next.js

Documents in contents/ are read with fs and rendered by next-mdx-remote/rsc. The example route /mdx/[[...slug]] prerenders one page per document at build time, /mdx for home.mdx and /mdx/cool for cool.mdx, and returns 404 for any other slug. It also builds with cloudflare-static, where each document is exported as an HTML page.

Files added:

src/
├── lib/
│   └── mdx.ts                  # parseFrontmatter() utility
├── mdx-components.tsx          # Custom MDX component mappings
├── styles/
│   └── mdx.css                 # MDX-specific styling
└── app/mdx/[[...slug]]/
    └── page.tsx                # MDX route example

contents/
├── home.mdx                    # Example content
└── cool.mdx                    # Example content

Modified files:

  • next.config.ts - Adds MDX support with createMDX and .mdx/.md extensions

mdx.ts

Frontmatter parsing utility:

src/lib/mdx.ts
export type MdxFrontmatter = {
  title: string;
  summary: string;
  publishedAt: string;
  imageUrl?: string;
};

export type ParsedFrontmatterReturn = {
  metadata: Partial<MdxFrontmatter>;
  content: string;
};

export function parseFrontmatter(fileContent: string): ParsedFrontmatterReturn {
  const frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
  const match = frontmatterRegex.exec(fileContent);
  const frontMatterBlock = match![1];
  const content = fileContent.replace(frontmatterRegex, '').trim();
  const frontMatterLines = frontMatterBlock.trim().split('\n');
  const metadata: Partial<MdxFrontmatter> = {};

  frontMatterLines.forEach((line) => {
    const [key, ...valueArr] = line.split(': ');
    let value = valueArr.join(': ').trim();
    value = value.replace(/^['"](.*)['"]$/, '$1');
    metadata[key.trim() as keyof MdxFrontmatter] = value;
  });

  return { metadata, content };
}

page.tsx

MDX route prerendered for every document, with metadata generation:

src/app/mdx/[[...slug]]/page.tsx
import fs from 'node:fs';
import path from 'node:path';
import { notFound } from 'next/navigation';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { parseFrontmatter } from '@/lib/mdx';
import { mdxComponents } from '@/mdx-components';

const CONTENTS_DIR = path.join(process.cwd(), 'contents');
const HOME_DOCUMENT = 'home';

interface PageProps {
  params: Promise<{ slug?: string[] }>;
}

export function generateStaticParams(): { slug: string[] }[] {
  return fs
    .readdirSync(CONTENTS_DIR)
    .filter((file) => file.endsWith('.mdx'))
    .map((file) => path.basename(file, '.mdx'))
    .map((name) => ({ slug: name === HOME_DOCUMENT ? [] : [name] }));
}

export const generateMetadata = async ({ params }: PageProps) => {
  const { slug } = await params;
  const { metadata } = parseFrontmatter(readDocument(slug));
  return { title: metadata.title, description: metadata.summary };
};

export default async function Page({ params }: PageProps) {
  const { slug } = await params;
  const { metadata, content: mdxContent } = parseFrontmatter(readDocument(slug));

  return (
    <main className='max-w-162.5 mx-auto px-4 py-4 md:py-8'>
      <MDXRemote source={mdxContent} components={mdxComponents} />
      <pre>
        <code>{JSON.stringify({ metadata }, null, 2)}</code>
      </pre>
    </main>
  );
}

function readDocument(slug: string[] = []): string {
  const isListed = generateStaticParams().some(
    (params) => params.slug.length === slug.length && params.slug.every((segment, index) => segment === slug[index]),
  );
  if (!isListed) notFound();

  const name = slug[0] ?? HOME_DOCUMENT;
  return fs.readFileSync(path.join(CONTENTS_DIR, `${name}.mdx`), 'utf-8');
}

On this page