---
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
{{ params.pkg }}
```
## 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