Skip to main content
.md and .mdx files in src/routes are pages. They use the same layouts, middleware, loaders, actions, and SPA navigation as .tsx pages. The only difference is the page body is written in markdown.
A TypeScript page wins if both about.tsx and about.mdx exist.

Frontmatter and toc

YAML frontmatter and a heading table of contents are passed as props, together with any loader data.
src/routes/guide.mdx
  • Flattened frontmatter fields (title, description) sit next to loader props. Loader fields win on conflict.
  • frontmatter is the full object.
  • toc is a nested list of headings. Heading ids match the slugs rendered on the page (hello-world for ## Hello world). Use static heading text so the slug stays readable — # {props.title} becomes propstitle because the TOC is built from source.
Read them from a layout or island with the same hooks used for loader data:
src/routes/_layout.tsx
useLoaderData() also includes frontmatter and toc on markdown routes.

Layouts, middleware, loaders

Nothing else changes.
src/routes/guide.server.tsx
src/routes/_layout.tsx, _layout.server.tsx, _middleware.ts, and src/middleware wrap markdown pages the same way they wrap JSX pages. Island files work too: notes.island.mdx.

MDX

.mdx can use JSX. Components from src/components/markdown are already in scope — do not import them. .md is markdown only (GFM tables, task lists, and strikethrough are on by default).
src/routes/docs/counter.mdx
Counter is the default export of src/components/markdown/Counter.tsx.

Markdown components

Default exports in src/components/markdown are registered automatically.
src/components/markdown/Callout.tsx
Change the folder with markdown.componentsDir:
vite.config.ts

Plugins

Pass remark / rehype / recma plugins. remark-gfm and rehype-slug are already included.
vite.config.ts

Custom renderer (comark)

The default compiler is @mdx-js/mdx. Replace it when you want another engine. parse is the HTML path: return markup and we wrap it as a React page. Frontmatter and toc still become props.
vite.config.ts
file is { id, filename, source, content, frontmatter, toc, format }. content is the body with YAML already stripped. compile replaces the whole module. Return JavaScript whose default export is a React component. Missing frontmatter / toc exports are filled in for you.
compile wins if both are set.

TypeScript

*.md / *.mdx default exports are typed as React pages with frontmatter and toc named exports. Add better-router/client to tsconfig compilerOptions.types or include client.d.ts as you already do for the other virtual modules.