How to Manage Blog Content with MDX Files Colocated with Route Components
Colocating MDX files with route components in a Next.js App Router structure allows you to manage blog posts as self-contained folders where the file system defines the URL, eliminating complex routing configuration and enabling zero-runtime data fetching.
The woosal1337/blog repository demonstrates a content-as-code workflow using Next.js 14 App Router combined with MDX. By placing page.mdx files directly alongside their route components, this architecture creates a scalable system where each blog post lives in its own directory and automatically maps to a URL segment.
The Folder-Per-Post Architecture
This repository implements a folder-per-post strategy under app/(website)/blog/(post)/<slug>/. Each blog entry resides in its own directory named after the URL slug, containing a single page.mdx file that serves as the route component.
When a user navigates to /<slug>, Next.js automatically renders the MDX file located at app/(website)/blog/(post)/<slug>/page.mdx. This file-system-based routing eliminates the need for manual route definitions, as the directory structure mirrors the site’s URL hierarchy exactly.
For example, the file app/(website)/blog/(post)/vaulted/page.mdx automatically becomes accessible at the /vaulted URL path.
Configuring MDX Processing
The project centralizes MDX configuration in lib/mdx-options.mjs. This file registers the MDX loader and applies custom processing options, including remark and rehype plugins for enhanced Markdown functionality.
By abstracting the MDX configuration into a dedicated module, the repository maintains consistent processing rules across all blog posts. The configuration handles the transformation of Markdown syntax into React components while preserving the ability to embed JSX directly within content files.
Customizing Components with mdx-components.tsx
All MDX content receives custom component overrides through mdx-components.tsx. This file exports a component map that replaces standard HTML elements with optimized React components.
For instance, the repository overrides the default img tag to use Next.js’s optimized Image component:
// mdx-components.tsx
import Image from 'next/image';
import ExternalLinkIcon from '@/components/ui/external-link-icon';
export const components = {
img: (props) => <Image {...props} width={800} height={600} priority />,
a: (props) => (
<a {...props} target="_blank" rel="noopener noreferrer">
{props.children} <ExternalLinkIcon />
</a>
),
};
This approach guarantees consistent styling and behavior across all posts. Authors write standard Markdown syntax, but the output renders with responsive images and external-link indicators automatically applied.
Dynamic Routing and Data Fetching
While individual posts use colocated MDX files, the repository still requires a dynamic route handler to process slugs. The file app/(website)/blog/[slug]/page.tsx (or the equivalent folder-based route) imports the MDX module for the matching slug and renders it as a React component.
Because the MDX file lives in the same folder as the route logic, import paths remain straightforward and type-safe. Next.js handles the compilation of MDX to React at build time, resulting in zero-runtime data fetching overhead.
Static Generation Helpers
Helper functions in lib/blog.ts and lib/blog-utils.ts scan the filesystem at build time to generate navigation data. These utilities read the app/(website)/blog/(post) directory, extract frontmatter metadata using gray-matter, and expose post lists for the blog index and RSS feed.
The getAllPosts function in lib/blog-utils.ts demonstrates this pattern:
// lib/blog-utils.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
export function getAllPosts() {
const postsDir = path.join(process.cwd(), 'app/(website)/blog/(post)');
const slugs = fs.readdirSync(postsDir);
return slugs.map((slug) => {
const mdxPath = path.join(postsDir, slug, 'page.mdx');
const file = fs.readFileSync(mdxPath, 'utf8');
const { data } = matter(file);
return { slug, ...data };
});
}
This function returns an array of post objects containing slugs, titles, dates, and descriptions, enabling the blog index page to render a complete post list without client-side data fetching.
Practical Implementation Examples
Creating a New Post
To add a new blog entry, create a folder named after the desired URL slug:
-
Create the directory
app/(website)/blog/(post)/my-new-post/ -
Add a
page.mdxfile with frontmatter:
---
title: "My New Post"
date: "2026-08-06"
description: "An example of a new blog entry."
---
Welcome to my new post! Here’s some content written in MDX.
You can use **Markdown** syntax and JSX components.
- No additional routing code is required. The dynamic route automatically detects the new folder and serves the content at
/my-new-post.
Accessing Post Metadata
The utility functions extract metadata from MDX frontmatter to power navigation and SEO features. The blog index page imports getAllPosts from lib/blog.ts to render a chronological list of entries, while the RSS generator uses the same data to produce syndication feeds.
Benefits of Colocating MDX with Routes
- Content-as-code workflow: Authors edit a single MDX file while the file system determines the URL structure automatically.
- Zero-runtime data fetching: MDX compiles to React components at build time, eliminating client-side loading states and improving Core Web Vitals.
- Self-contained posts: Each blog entry includes its content, metadata, and implicit route configuration in one location, simplifying maintenance and archival.
- Consistent component architecture: Centralized overrides in
mdx-components.tsxensure uniform styling across all posts without requiring authors to remember implementation details. - Scalable navigation: Adding content requires only creating a new folder; the static generation helpers automatically include new posts in indexes and feeds.
Summary
Managing blog content with MDX files colocated with route components creates a maintainable, performant publishing system. The woosal1337/blog repository implements this through:
- Folder-per-post architecture under
app/(website)/blog/(post)/<slug>/withpage.mdxserving as the route component - Centralized MDX configuration in
lib/mdx-options.mjsfor consistent processing - Component overrides in
mdx-components.tsxfor optimized rendering - Static generation helpers in
lib/blog.tsandlib/blog-utils.tsfor navigation data - Zero-runtime overhead through build-time MDX compilation
This approach scales naturally as content grows, requiring no additional routing logic when adding new posts.
Frequently Asked Questions
What does colocating MDX files with route components mean?
Colocation refers to placing content files (MDX) in the same directory as their associated route logic. In this repository, each blog post lives in its own folder under app/(website)/blog/(post)/<slug>/ alongside a page.mdx file that Next.js treats as the route component for that URL segment. This keeps content and routing logic tightly coupled and easy to locate.
How does Next.js handle MDX files as route components?
Next.js 14 App Router natively supports MDX files as page components when configured with the appropriate loader. The lib/mdx-options.mjs configuration registers the MDX processor, allowing page.mdx files to export React components directly. At build time, Next.js compiles the Markdown and JSX into standard React components that render as static HTML.
What is the role of mdx-components.tsx in this setup?
The mdx-components.tsx file provides a global component map that overrides default Markdown elements. When MDX content renders, it uses these custom components instead of standard HTML tags. This enables the repository to inject optimized Next.js Image components for images, add external-link icons to anchors, and apply consistent typography without modifying individual post files.
How do I add a new blog post to this architecture?
Create a new folder under app/(website)/blog/(post)/ named after your desired URL slug, then add a page.mdx file containing frontmatter metadata and content. The dynamic route automatically serves the new content at the corresponding URL, while the helper functions in lib/blog-utils.ts automatically include the post in the blog index and RSS feed during the next build.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →