Dynamic Routes
VitePress supports dynamic route generation, allowing you to create multiple pages from a single template with different parameters.
Basic Concept
Dynamic routes use path parameters in square brackets (e.g., [id].md) to generate multiple pages from one template.
Create a dynamic route file
Create posts/[id].md: ---
title : Post
---
# Post: {{ $params.id }}
Content for {{ $params.id }}
Create a paths loader
Create posts/[id].paths.js or .ts: export default {
paths () {
return [
{ params: { id: 'hello' } },
{ params: { id: 'world' } }
]
}
}
Generated pages
VitePress generates:
posts/hello.html
posts/world.html
Path Loader Files
Naming Convention
For a dynamic route [param].md, create a corresponding paths file:
[param].paths.js
[param].paths.ts
[param].paths.mjs
[param].paths.mts
defineRoutes Helper
Use defineRoutes for type inference:
import { defineRoutes } from 'vitepress'
export default defineRoutes ({
paths () {
return [
{ params: { id: '1' } },
{ params: { id: '2' } }
]
}
})
Reference: /home/daytona/workspace/source/src/node/plugins/dynamicRoutesPlugin.ts:70
export function defineRoutes ( loader : RouteModule ) : RouteModule {
return loader
}
Path Configuration
UserRouteConfig Interface
Reference: /home/daytona/workspace/source/src/node/plugins/dynamicRoutesPlugin.ts:19
interface UserRouteConfig {
params : Record < string , string >
content ?: string
}
Basic Paths
export default {
paths () {
return [
{ params: { id: 'hello' , lang: 'en' } },
{ params: { id: 'world' , lang: 'zh' } }
]
}
}
Async Paths
Fetch data from APIs:
export default {
async paths () {
const response = await fetch ( 'https://api.example.com/posts' )
const posts = await response . json ()
return posts . map ( post => ({
params: {
id: post . id ,
slug: post . slug
}
}))
}
}
Watching Files
Use the watch option to regenerate routes when files change:
import fs from 'fs'
import { parse } from 'yaml'
export default {
watch: [ './posts/*.yaml' ] ,
paths ( watchedFiles ) {
return watchedFiles . map ( file => {
const content = fs . readFileSync ( file , 'utf-8' )
const data = parse ( content )
return {
params: {
id: data . id ,
title: data . title
}
}
})
}
}
Reference: /home/daytona/workspace/source/src/node/plugins/dynamicRoutesPlugin.ts:44
export interface RouteModule {
watch ?: string [] | string
paths :
| UserRouteConfig []
| (( watchedFiles : string []) => Awaitable < UserRouteConfig []>)
transformPageData ?: UserConfig [ 'transformPageData' ]
options ?: { globOptions ?: GlobOptions }
}
Watch Patterns
Supports glob patterns:
{
watch : [
'./data/*.json' ,
'./posts/**/*.md' ,
'config.yaml'
],
paths ( watchedFiles ) {
// watchedFiles contains absolute paths of matched files
}
}
Accessing Parameters
In Markdown
Access params via $params:
# {{ $params.title }}
Author: {{ $params.author }}
In Vue Components
Use useData() composable:
< script setup >
import { useData } from 'vitepress'
const { params } = useData ()
</ script >
< template >
< h1 > {{ params . title }} </ h1 >
< p > ID: {{ params . id }} </ p >
</ template >
Reference: /home/daytona/workspace/source/src/client/app/data.ts:102
export function useData < T = any >() : VitePressData < T > {
const data = inject ( dataSymbol )
if ( ! data ) {
throw new Error ( 'vitepress data not properly injected in app' )
}
return data
}
Content Injection
Dynamic Content
Inject content dynamically using the content field:
import fs from 'fs'
export default {
watch: [ './posts/*.md' ] ,
paths ( watchedFiles ) {
return watchedFiles . map ( file => {
const content = fs . readFileSync ( file , 'utf-8' )
const id = file . match ( / \/ ( \w + ) \. md $ / )[ 1 ]
return {
params: { id },
content: content // Inject raw content
}
})
}
}
Content Marker
Use <!-- @content --> in your template:
---
title : Blog Post
---
# {{ $params.title }}
<!-- @content -->
Reference: /home/daytona/workspace/source/src/node/plugins/dynamicRoutesPlugin.ts:162
if ( content ) {
baseContent = baseContent . replace (
/<!-- \s * @content \s * -->/ ,
content . replace ( / \$ / g , '$$$' )
)
}
Content injection is designed for CMS integration, rendering content as static local content instead of runtime data.
Page Data Transformation
Transform page data for specific routes:
import { defineRoutes } from 'vitepress'
import type { PageData } from 'vitepress'
export default defineRoutes ({
paths () {
return [
{ params: { id: 'hello' } },
{ params: { id: 'world' } }
]
} ,
transformPageData ( pageData : PageData ) {
// Transform page data
pageData . title = `Post: ${ pageData . params . id } `
pageData . description = `Description for ${ pageData . params . id } `
}
})
Multiple Parameters
Use multiple parameters in file paths:
Example: [lang]/[category]/[id].md
docs/[lang]/[category]/[id].md
# {{ $params.lang }} - {{ $params.category }}
{{ $params.id }}
Paths Loader
docs/[lang]/[category]/[id].paths.js
export default {
paths () {
const routes = []
const langs = [ 'en' , 'zh' ]
const categories = [ 'guide' , 'api' ]
const ids = [ 'intro' , 'advanced' ]
langs . forEach ( lang => {
categories . forEach ( category => {
ids . forEach ( id => {
routes . push ({
params: { lang , category , id }
})
})
})
})
return routes
}
}
Generates:
docs/en/guide/intro.html
docs/en/guide/advanced.html
docs/en/api/intro.html
etc.
Advanced Example: Blog with Data
blog/[slug].paths.ts
blog/[slug].md
import { defineRoutes } from 'vitepress'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
interface Post {
slug : string
title : string
date : string
author : string
}
export default defineRoutes ({
watch: [ './posts/*.md' ] ,
paths ( watchedFiles ) {
return watchedFiles . map ( file => {
const content = fs . readFileSync ( file , 'utf-8' )
const { data , content : markdown } = matter ( content )
const slug = path . basename ( file , '.md' )
return {
params: {
slug ,
title: data . title ,
date: data . date ,
author: data . author
},
content: markdown
}
})
} ,
transformPageData ( pageData ) {
pageData . frontmatter . date = pageData . params . date
pageData . frontmatter . author = pageData . params . author
}
})
Hot Module Replacement
Dynamic routes support HMR:
Changes to .paths.js trigger route regeneration
Changes to watched files update affected routes
Changes to the template update all generated pages
Reference: /home/daytona/workspace/source/src/node/plugins/dynamicRoutesPlugin.ts:186
if (
route . watch ?. length &&
pm ( route . watch , route . options . globOptions )( normalizedFile )
) {
route . routes = undefined
watchedFileChanged = true
modules . push ( ... getModules ( file , this . environment . moduleGraph , false ))
}
Routes are cached after first load. Cache is invalidated when:
The paths loader file changes
Watched files change
Dependencies change
Multiple dynamic routes are resolved concurrently for faster builds.
export default {
buildConcurrency: 8 // Adjust based on CPU cores
}
Troubleshooting
If you see “Missing paths file for dynamic route” warnings, ensure:
The .paths.js file exists alongside the .md file
The file names match exactly (e.g., [id].md → [id].paths.js)
The file exports a default object with a paths property