Files
stack/packages/mosaic/framework/skills/vitepress/references/features-dynamic-routes.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

4.3 KiB

name, description
name description
vitepress-dynamic-routes 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:

// 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
// 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:

// packages/[pkg].paths.js
import fs from 'node:fs';

export default {
  paths() {
    return fs.readdirSync('packages').map((pkg) => ({
      params: { pkg },
    }));
  },
};

From remote API:

// 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:

<!-- packages/[pkg].md -->

# Package: {{ $params.pkg }}

Version: {{ $params.version }}

In script:

<script setup>
import { useData } from 'vitepress';
const { params } = useData();
</script>

<template>
  <h1>{{ params.pkg }}</h1>
</template>

Passing Content

For heavy content (raw markdown/HTML from CMS), use content instead of params to avoid bloating the client bundle:

// 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:

## <!-- posts/[slug].md -->

## title: {{ $params.title }}

<!-- @content -->

The <!-- @content --> placeholder is replaced with the content from the paths loader.

Watch Option

Auto-rebuild when template or data files change:

// 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

// 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,
        };
      });
  },
};
## <!-- posts/[slug].md -->

## layout: doc

# {{ $params.title }}

<time>{{ $params.date }}</time>

<!-- @content -->

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