Your project has 5 MDX files and things are fine. But soon you'll have 50. Then 200. If you don't have a plan, you'll spend more time *finding* files than writing them.
This guide shows you how to organize MDX projects at different scales and across different frameworks.
Recommended Base Structure
This works for most MDX projects regardless of framework:
my-mdx-project/
├── content/ # All MDX content lives here
│ ├── blog/ # Blog posts
│ │ ├── 2024-01-hello.mdx
│ │ ├── 2024-02-mdx-tips.mdx
│ │ └── assets/ # Blog-specific images/media
│ │ └── hero-image.png
│ ├── docs/ # Documentation pages
│ │ ├── getting-started/
│ │ │ ├── installation.mdx
│ │ │ ├── quick-start.mdx
│ │ │ └── configuration.mdx
│ │ ├── guides/
│ │ │ ├── components.mdx
│ │ │ └── styling.mdx
│ │ └── api/
│ │ ├── overview.mdx
│ │ └── reference.mdx
│ └── pages/ # Static pages (about, contact)
│ ├── about.mdx
│ └── contact.mdx
├── components/ # Reusable React components
│ ├── mdx/ # Components designed for MDX use
│ │ ├── Callout.jsx
│ │ ├── CodeBlock.jsx
│ │ ├── Tabs.jsx
│ │ ├── Steps.jsx
│ │ └── index.js # Barrel export
│ ├── layout/ # Layout components
│ │ ├── Header.jsx
│ │ ├── Footer.jsx
│ │ ├── Sidebar.jsx
│ │ └── DocLayout.jsx
│ └── ui/ # General UI components
│ ├── Button.jsx
│ ├── Card.jsx
│ └── Badge.jsx
├── lib/ # Utility functions
│ ├── mdx.js # MDX processing utilities
│ ├── content.js # Content fetching/parsing
│ └── toc.js # Table of contents generation
├── styles/ # Stylesheets
│ ├── globals.css
│ ├── mdx.css # MDX-specific styles
│ └── syntax-theme.css # Code syntax highlighting theme
├── public/ # Static assets
│ └── images/
├── mdx-components.js # MDX component mappings
├── next.config.mjs # Framework config
├── package.json
└── README.md
The key idea: content authors work in content/, developers work in components/ and lib/. They don't step on each other.
Next.js Project Structure
Next.js with the App Router is probably the most popular setup for MDX right now.
App Router (Recommended)
my-nextjs-mdx-site/
├── app/
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── blog/
│ │ ├── page.tsx # Blog listing page
│ │ └── [slug]/
│ │ └── page.tsx # Dynamic blog post page
│ ├── docs/
│ │ ├── layout.tsx # Docs sidebar layout
│ │ ├── [[...slug]]/
│ │ │ └── page.tsx # Catch-all docs route
│ │ └── page.tsx # Docs index
│ └── globals.css
├── content/
│ ├── blog/
│ │ ├── getting-started-with-mdx.mdx
│ │ ├── advanced-mdx-patterns.mdx
│ │ └── mdx-and-nextjs.mdx
│ └── docs/
│ ├── introduction.mdx
│ ├── installation.mdx
│ └── components.mdx
├── components/
│ ├── mdx/
│ │ ├── Callout.tsx
│ │ ├── CodeBlock.tsx
│ │ ├── Image.tsx
│ │ └── index.ts
│ ├── blog/
│ │ ├── PostCard.tsx
│ │ └── PostList.tsx
│ └── docs/
│ ├── Sidebar.tsx
│ ├── TableOfContents.tsx
│ └── Pagination.tsx
├── lib/
│ ├── mdx.ts # Compile & serialize MDX
│ ├── content.ts # Read content files from disk
│ └── utils.ts
├── types/
│ └── mdx.d.ts # TypeScript types for MDX
├── mdx-components.tsx # Global MDX component map
├── next.config.mjs
├── tailwind.config.ts
├── tsconfig.json
└── package.json
Key File: mdx-components.tsx
This is where you map HTML elements to your custom components. Every MDX file in your project uses these:
import type { MDXComponents } from 'mdx/types';
import { Callout } from '@/components/mdx/Callout';
import { CodeBlock } from '@/components/mdx/CodeBlock';
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// Override default HTML elements
h1: ({ children }) => (
<h1 className="text-4xl font-bold mt-8 mb-4">{children}</h1>
),
h2: ({ children }) => (
<h2 className="text-3xl font-semibold mt-6 mb-3">{children}</h2>
),
pre: ({ children }) => <CodeBlock>{children}</CodeBlock>,
code: ({ children }) => (
<code className="bg-gray-100 px-1.5 py-0.5 rounded text-sm">
{children}
</code>
),
// Custom components available in all MDX files
Callout,
...components,
};
}
Content Utility: lib/content.ts
This reads your MDX files from disk and parses frontmatter:
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const contentDirectory = path.join(process.cwd(), 'content');
export function getContentBySlug(type: string, slug: string) {
const filePath = path.join(contentDirectory, type, `${slug}.mdx`);
const fileContents = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContents);
return {
frontmatter: data,
content,
slug,
};
}
export function getAllContent(type: string) {
const dir = path.join(contentDirectory, type);
const files = fs.readdirSync(dir).filter(f => f.endsWith('.mdx'));
return files.map(file => {
const slug = file.replace(/\.mdx$/, '');
return getContentBySlug(type, slug);
});
}
Docusaurus Project Structure
Docusaurus is built specifically for docs sites. MDX works out of the box with auto-generated sidebars:
my-docusaurus-site/
├── docs/ # Documentation (auto-generates sidebar)
│ ├── intro.mdx
│ ├── getting-started/
│ │ ├── _category_.json # Sidebar category metadata
│ │ ├── installation.mdx
│ │ ├── configuration.mdx
│ │ └── first-steps.mdx
│ ├── guides/
│ │ ├── _category_.json
│ │ ├── components.mdx
│ │ ├── styling.mdx
│ │ └── deployment.mdx
│ └── api/
│ ├── _category_.json
│ └── reference.mdx
├── blog/ # Blog posts (date-based)
│ ├── 2024-01-15-welcome.mdx
│ ├── 2024-02-01-new-features/
│ │ ├── index.mdx # Post content
│ │ └── screenshot.png # Co-located asset
│ └── authors.yml # Author metadata
├── src/
│ ├── components/ # Custom React components
│ │ ├── HomepageFeatures/
│ │ │ ├── index.tsx
│ │ │ └── styles.module.css
│ │ ├── InteractiveDemo.tsx
│ │ └── CodeExample.tsx
│ ├── css/
│ │ └── custom.css # Global style overrides
│ ├── pages/ # Custom pages (non-doc)
│ │ ├── index.tsx # Homepage
│ │ └── community.mdx
│ └── theme/ # Theme component overrides
│ ├── MDXComponents.tsx # Custom MDX component mapping
│ └── DocItem/
│ └── Layout.tsx
├── static/ # Static assets (served at root)
│ └── img/
│ ├── logo.svg
│ └── tutorial/
├── docusaurus.config.ts # Main Docusaurus configuration
├── sidebars.ts # Sidebar structure definition
├── babel.config.js
├── package.json
└── tsconfig.json
The _category_.json File
This controls how sections appear in the sidebar:
{
"label": "Getting Started",
"position": 1,
"collapsible": true,
"collapsed": false,
"link": {
"type": "generated-index",
"description": "Learn how to set up and start using MDX."
}
}
💡 Tip: Docusaurus builds your sidebar from the folder structure automatically. Use _category_.json to control labels and order. You can also number-prefix files (like 01-installation.mdx) to sort them.
Gatsby Project Structure
Gatsby pulls content through GraphQL and uses a plugin system:
my-gatsby-mdx-site/
├── content/ # MDX source content
│ ├── blog/
│ │ ├── first-post/
│ │ │ ├── index.mdx # Post content
│ │ │ └── cover.jpg # Co-located image
│ │ └── second-post/
│ │ ├── index.mdx
│ │ └── diagram.png
│ └── pages/
│ ├── about.mdx
│ └── uses.mdx
├── src/
│ ├── components/
│ │ ├── mdx/ # MDX-specific components
│ │ │ ├── Alert.jsx
│ │ │ ├── YouTubeEmbed.jsx
│ │ │ └── FileTree.jsx
│ │ ├── layout/
│ │ │ ├── Layout.jsx
│ │ │ ├── SEO.jsx
│ │ │ └── Navigation.jsx
│ │ └── blog/
│ │ ├── PostHeader.jsx
│ │ └── PostFooter.jsx
│ ├── pages/ # File-based routing
│ │ ├── index.jsx
│ │ └── 404.jsx
│ ├── templates/ # Page templates
│ │ ├── blog-post.jsx # Template for blog posts
│ │ └── doc-page.jsx # Template for doc pages
│ └── styles/
│ └── global.css
├── gatsby-config.js # Plugins & site metadata
├── gatsby-node.js # Dynamic page creation
├── gatsby-browser.js # Browser APIs & MDXProvider
├── package.json
└── tsconfig.json
Gatsby MDXProvider Setup: gatsby-browser.js
This is where you wire up your custom components globally:
import React from 'react';
import { MDXProvider } from '@mdx-js/react';
import Alert from './src/components/mdx/Alert';
import YouTubeEmbed from './src/components/mdx/YouTubeEmbed';
const components = {
Alert,
YouTubeEmbed,
// Override default elements
h1: props => <h1 className="text-4xl font-bold" {...props} />,
a: props => <a className="text-blue-600 underline" {...props} />,
};
export const wrapRootElement = ({ element }) => (
<MDXProvider components={components}>{element}</MDXProvider>
);
Component Library Organization
No matter what framework you use, organize your MDX components by what they do:
components/
├── mdx/ # Components used INSIDE MDX content
│ ├── content/ # Content display components
│ │ ├── Callout.jsx # Info/warning/error callouts
│ │ ├── Accordion.jsx # Collapsible sections
│ │ ├── Tabs.jsx # Tabbed content panels
│ │ └── Steps.jsx # Step-by-step instructions
│ ├── media/ # Media components
│ │ ├── Image.jsx # Optimized images with captions
│ │ ├── Video.jsx # Video embeds
│ │ └── CodeSandbox.jsx # Embedded code playgrounds
│ ├── code/ # Code-related components
│ │ ├── CodeBlock.jsx # Syntax-highlighted code
│ │ ├── LiveCode.jsx # Editable/runnable code
│ │ └── FileTree.jsx # File structure display
│ ├── navigation/ # Navigation helpers
│ │ ├── LinkCard.jsx # Styled internal links
│ │ └── RelatedPosts.jsx # Related content cards
│ └── index.js # Barrel export for all MDX components
├── layout/ # Page structure components
│ ├── Header.jsx
│ ├── Footer.jsx
│ ├── Sidebar.jsx
│ └── TableOfContents.jsx
└── ui/ # Generic UI components
├── Button.jsx
├── Badge.jsx
├── Card.jsx
└── Tooltip.jsx
Barrel Export: components/mdx/index.js
One import to get everything:
// Export all MDX components from a single entry point
export { default as Callout } from './content/Callout';
export { default as Accordion } from './content/Accordion';
export { default as Tabs } from './content/Tabs';
export { default as Steps } from './content/Steps';
export { default as Image } from './media/Image';
export { default as Video } from './media/Video';
export { default as CodeBlock } from './code/CodeBlock';
export { default as LiveCode } from './code/LiveCode';
export { default as FileTree } from './code/FileTree';
export { default as LinkCard } from './navigation/LinkCard';
💡 Tip: With a barrel export, your MDX provider setup becomes one line: import * as mdxComponents from '@/components/mdx'. Clean.
Configuration Files
Keep config files at the project root where people expect them:
my-mdx-project/
├── .env # Environment variables
├── .env.local # Local overrides (git-ignored)
├── .eslintrc.json # ESLint config
├── .prettierrc # Prettier formatting rules
├── .gitignore
├── contentlayer.config.ts # Contentlayer (if used)
├── mdx-components.tsx # MDX component mapping (Next.js)
├── next.config.mjs # Next.js config with MDX plugin
├── tailwind.config.ts # Tailwind CSS config
├── tsconfig.json # TypeScript config
├── rehype.config.js # Rehype plugins (optional)
├── remark.config.js # Remark plugins (optional)
└── package.json
Scaling Your Content
Here's how your structure should evolve as your site grows:
10–50 Pages: Group by Section
Keep it simple. A few folders, each with a clear purpose:
content/
├── getting-started/
├── guides/
├── api-reference/
└── blog/
50–200 Pages: Add Versioning and i18n
When you're maintaining multiple versions or languages:
content/
├── v1/
│ └── docs/
├── v2/
│ └── docs/
├── i18n/
│ ├── en/
│ ├── es/
│ └── ja/
└── blog/
200+ Pages: Organize by Product/Feature
At this scale, think in terms of products or major features:
content/
├── products/
│ ├── product-a/
│ │ ├── docs/
│ │ └── changelog/
│ └── product-b/
│ ├── docs/
│ └── changelog/
├── tutorials/
│ ├── beginner/
│ ├── intermediate/
│ └── advanced/
└── community/
├── blog/
└── showcases/
⚠️ Warning: Don't nest folders more than 4 levels deep. Once you're at content/products/product-a/docs/guides/advanced/part-3.mdx, nobody can find anything. Use frontmatter metadata for extra categorization instead of deeper folders.
Summary
- Content separate from code — MDX in
content/, components in components/ - Components organized by purpose —
mdx/ for content components, layout/ for structure, ui/ for generic stuff - Framework patterns:
- Next.js:
mdx-components.tsx + content/ directory + dynamic routes - Docusaurus:
docs/ with auto-sidebar + _category_.json for ordering - Gatsby:
content/ + templates + gatsby-browser.js for the MDXProvider - Config at root — easy to find, easy to change
- Scale gradually — start flat, add structure when you actually need it
- Barrel exports — one import gets all your MDX components
The best structure is one where a new team member can open the project and immediately know where to add a new page.