Co-locating MDX in TanStack Router
Published Aug 29, 2026
In Next.js, configuring MDX is covered right in the official docs, and once set up, dropping Markdown files into your project is trivial.
TanStack Start doesn't have an official guide for this yet, but with a small Vite config and import.meta.glob, you can get that exact same DX: co-locating .mdx files right next to your route handlers.
Directory Layout
src/routes/blog/
├── index.tsx
├── $slug.tsx
├── hello-world.mdx
└── getting-started.mdxHere, index.tsx serves the /blog overview, $slug.tsx handles dynamic /blog/$slug posts, and the .mdx files sit right alongside them.
TanStack Router's route generator only picks up .tsx, .ts, .jsx, and .js files. It completely ignores .mdx files, so they won't interfere with your route tree.
1. Configure Vite for MDX & Frontmatter
Install @mdx-js/rollup along with remark plugins to parse frontmatter:
pnpm add -D @mdx-js/rollup remark-frontmatter remark-gfm remark-mdx-frontmatterThen add them to your vite.config.ts:
import mdx from "@mdx-js/rollup";
import remarkFrontmatter from "remark-frontmatter";
import remarkGfm from "remark-gfm";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
mdx({
remarkPlugins: [remarkGfm, remarkFrontmatter, remarkMdxFrontmatter],
}),
],
});This turns your YAML frontmatter into a plain JavaScript object exported right alongside the compiled component.
2. Load Posts with import.meta.glob
Create a helper to load all posts and their metadata:
// src/lib/blog.ts
import type { MDXProps } from "mdx/types";
import type { ComponentType } from "react";
export type BlogPostModule = {
default: ComponentType<MDXProps>;
frontmatter: {
title: string;
description: string;
date: string;
slug?: string;
};
};
// Discover all .mdx files in the blog folder at build time
const blogModules = import.meta.glob<BlogPostModule>("/src/routes/blog/*.mdx", {
eager: true,
});
const blogModulesById = new Map<string, BlogPostModule>();
export const POSTS = Object.entries(blogModules)
.map(([path, mod]) => {
const slug = mod.frontmatter.slug || path.match(/\/blog\/(.+)\.mdx$/)?.[1] || "";
blogModulesById.set(slug, mod);
return {
slug,
title: mod.frontmatter.title || slug,
description: mod.frontmatter.description || "",
date: mod.frontmatter.date,
};
})
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
export function getPost(slug: string) {
return POSTS.find((p) => p.slug === slug);
}
export function getPostModule(slug: string) {
return blogModulesById.get(slug);
}3. Render the Post in $slug.tsx
Load the metadata in your route loader and render the component:
// src/routes/blog/$slug.tsx
import { createFileRoute, notFound } from "@tanstack/react-router";
import { getPost, getPostModule } from "#/lib/blog";
export const Route = createFileRoute("/blog/$slug")({
loader: ({ params }) => {
const post = getPost(params.slug);
if (!post) throw notFound();
return post;
},
component: BlogPostPage,
});
function BlogPostPage() {
const post = Route.useLoaderData();
const blogModule = getPostModule(post.slug);
if (!blogModule) throw notFound();
const Content = blogModule.default;
return (
<article>
<h1>{post.title}</h1>
<Content />
</article>
);
}To add a new post, drop a .mdx file in src/routes/blog/. Frontmatter, slugs, and static prerendering all just work.