Vite plugin to import Markdown files as React components with frontmatter support. It follows React
Router's route module conventions, including the default component, meta, and handle exports,
but does not depend on React Router. Use it with React Router, any other React framework that
supports Vite, or a plain React + Vite app.
pnpm add react-router-markdown// vite.config.ts
import { defineConfig } from 'vite'
import markdown from 'react-router-markdown/vite'
export default defineConfig({
plugins: [
markdown({
parseHtml: true,
wrapperModule: '@/components/markdown-wrapper',
handleData: (mdContent, frontmatter) => ({
layoutClass: 'mx-auto max-w-5xl lg:p-4',
title: frontmatter.title,
contentLength: mdContent.length,
}),
beforeTransform: (rawFileContent) =>
rawFileContent.replaceAll('%YEAR%', new Date().getFullYear().toString()),
afterTransform: (mdContent, frontmatter) => [mdContent, frontmatter],
}),
],
})parseHtml?: boolean- Parse HTML tags in markdown as HTML (default:false).wrapperModule?: string- Module specifier of a default-exported React wrapper component.beforeTransform?: (content: string) => string- Transform raw markdown before frontmatter parsing.afterTransform?: (content: string, frontmatter: Record<string, any>) => [string, Record<string, any>]- Transform parsed content/frontmatter.handleData?: (content: string, frontmatter: Record<string, any>) => Record<string, any>- Return React Routerhandleexport data.
In order for TypeScript to import .md files with type hints, add the following to your vite.d.ts
file:
/// <reference types="react-router-markdown/references" />Alternatively, you can add the following to your tsconfig.json file:
{
"compilerOptions": {
"types": ["react-router-markdown/references"]
}
}Now you can use any .md file as a React Router page.
app/routes/about.md:
---
title: 'About Us'
meta:
- title: 'About Us - ACME'
- name: description
content: 'Meta description here'
- name: robots
content: 'index, follow'
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam elementum venenatis facilisis. Nam sit
amet finibus lectus.
Nunc lobortis neque magna, id mollis arcu pulvinar ultrices. Aenean mollis dolor nulla. Aliquam
aliquet tempus ultrices. Integer ac est finibus, efficitur orci faucibus, luctus odio. Vivamus vitae
venenatis quam, imperdiet rutrum massa.app/router.ts:
import { type RouteConfig, route } from '@react-router/dev/routes'
export default [route('about', './routes/about.md')] satisfies RouteConfigapp/components/markdown-wrapper.tsx:
import { cn } from '@/lib/utils'
export default function MarkdownWrapper({
children,
frontmatter,
}: {
children: React.ReactNode
frontmatter: {
title?: string
subtitle?: string
}
}) {
const hasHeader = frontmatter.title || frontmatter.subtitle
return (
<article className={cn('prose prose-slate dark:prose-invert max-w-none')}>
{hasHeader && (
<header className="mb-8 space-y-4">
{frontmatter.title && (
<h1 className={cn('text-4xl font-bold tracking-tight lg:text-5xl')}>
{frontmatter.title}
</h1>
)}
{frontmatter.subtitle && (
<p className="text-muted-foreground text-xl leading-7">{frontmatter.subtitle}</p>
)}
</header>
)}
<div className="space-y-6">{children}</div>
</article>
)
}