--- name: vitepress-dynamic-routes description: Generate multiple pages from a single markdown template using paths loader files --- # Dynamic Routes Generate many pages from a single markdown file and dynamic data. Useful for blogs, package docs, or any data-driven pages. ## Basic Setup Create a template file with parameter in brackets and a paths loader: ``` . └─ packages/ ├─ [pkg].md # Route template └─ [pkg].paths.js # Paths loader ``` The paths loader exports a `paths` method returning route parameters: ```js // packages/[pkg].paths.js export default { paths() { return [ { params: { pkg: 'foo' }}, { params: { pkg: 'bar' }}, { params: { pkg: 'baz' }} ] } } ``` Generated pages: - `/packages/foo.html` - `/packages/bar.html` - `/packages/baz.html` ## Multiple Parameters ``` . └─ packages/ ├─ [pkg]-[version].md └─ [pkg]-[version].paths.js ``` ```js // packages/[pkg]-[version].paths.js export default { paths() { return [ { params: { pkg: 'foo', version: '1.0.0' }}, { params: { pkg: 'foo', version: '2.0.0' }}, { params: { pkg: 'bar', version: '1.0.0' }} ] } } ``` ## Dynamic Path Generation From local files: ```js // packages/[pkg].paths.js import fs from 'node:fs' export default { paths() { return fs.readdirSync('packages').map(pkg => ({ params: { pkg } })) } } ``` From remote API: ```js // packages/[pkg].paths.js export default { async paths() { const packages = await fetch('https://api.example.com/packages').then(r => r.json()) return packages.map(pkg => ({ params: { pkg: pkg.name, version: pkg.version } })) } } ``` ## Accessing Params in Page Template globals: ```md # Package: {{ $params.pkg }} Version: {{ $params.version }} ``` In script: ```vue ``` ## Passing Content For heavy content (raw markdown/HTML from CMS), use `content` instead of params to avoid bloating the client bundle: ```js // posts/[slug].paths.js export default { async paths() { const posts = await fetch('https://cms.example.com/posts').then(r => r.json()) return posts.map(post => ({ params: { slug: post.slug }, content: post.content // Raw markdown or HTML })) } } ``` Render content in template: ```md --- title: {{ $params.title }} --- ``` The `` placeholder is replaced with the content from the paths loader. ## Watch Option Auto-rebuild when template or data files change: ```js // posts/[slug].paths.js export default { watch: [ './templates/**/*.njk', '../data/**/*.json' ], paths(watchedFiles) { const dataFiles = watchedFiles.filter(f => f.endsWith('.json')) return dataFiles.map(file => { const data = JSON.parse(fs.readFileSync(file, 'utf-8')) return { params: { slug: data.slug }, content: renderTemplate(data) } }) } } ``` ## Complete Example: Blog ```js // posts/[slug].paths.js import fs from 'node:fs' import matter from 'gray-matter' export default { watch: ['./posts/*.md'], paths(files) { return files .filter(f => !f.includes('[slug]')) .map(file => { const content = fs.readFileSync(file, 'utf-8') const { data, content: body } = matter(content) const slug = file.match(/([^/]+)\.md$/)[1] return { params: { slug, title: data.title, date: data.date }, content: body } }) } } ``` ```md --- layout: doc --- # {{ $params.title }} ``` ## Key Points - Template file uses `[param]` syntax in filename - Paths loader file must be named `[param].paths.js` or `.ts` - `paths()` returns array of `{ params: {...}, content?: string }` - Use `$params` in templates or `useData().params` in scripts - Use `content` for heavy data to avoid client bundle bloat - `watch` enables HMR for template/data file changes