vp-dynamic-nav
| Install | |
|---|---|
npm install vp-dynamic-nav |
|
| Latest Version: | 0.2.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Aug 26, 2026 |
| Links: | npm |
vp-dynamic-nav
A VitePress plugin that fetches and renders the entire navbar dynamically from a remote JSON file. Older deployed versions of your docs always show the current nav without a rebuild.
How it works
- On page load, nav is populated from
dynamicNavInitial(config) or an empty state - Cached nav from a previous visit is applied immediately from
localStorage - A fresh fetch from
dynamicNavUrlruns in the background; on success the nav and cache are updated - On fetch failure the cached (or initial) data is kept — no empty nav
Install
npm install vp-dynamic-nav
Usage
docs/.vitepress/theme/index.js
import DefaultTheme from "vitepress/theme";
import { createDynamicNav } from "vp-dynamic-nav";
export default createDynamicNav(DefaultTheme);
docs/.vitepress/config.mjs — add to themeConfig:
themeConfig: {
// URL to fetch navbar.json from (GitHub raw content recommended)
dynamicNavUrl:
"https://raw.githubusercontent.com/<org>/<repo>/main/docs/.vitepress/navbar.json",
// Optional: shown immediately before cache/fetch resolves (prevents empty flash on first visit)
dynamicNavInitial: [
{
text: "Version",
items: [
{ text: "main", link: "https://docs.example.com/main/en/" },
{ text: "v1.0 (stable)", link: "https://docs.example.com/v1.0/en/" },
],
},
],
// Optional: override the localStorage cache key (defaults to dynamicNavUrl)
// dynamicNavCacheKey: "my-docs-navbar",
}
docs/.vitepress/navbar.json — the remote source of truth (committed to main):
{
"nav": [
{
"text": "Version",
"items": [
{ "text": "main", "link": "https://docs.example.com/main/en/" },
{ "text": "v1.0 (stable)", "link": "https://docs.example.com/v1.0/en/" }
]
}
]
}
Preserving the current page
By default, clicking a nav item always sends the reader to that item's own link — e.g. a "Version" entry always lands on that version's front page, even if the reader was deep in some other page. Add "preservePath": true to a group (or an individual item) to instead keep the reader on the equivalent page:
{
"nav": [
{
"text": "Version",
"preservePath": true,
"items": [
{ "text": "main", "link": "https://docs.example.com/main/en/" },
{ "text": "v1.0 (stable)", "link": "https://docs.example.com/v1.0/en/" }
]
}
]
}
A reader on https://docs.example.com/main/en/config/pid-tuning.html who clicks "v1.0 (stable)" is sent to https://docs.example.com/v1.0/en/config/pid-tuning.html instead of the v1.0 front page. Set preservePath: false on an individual item to opt it back out of a group-level default.
Notes:
- Only same-origin links are rewritten (e.g. inert on a dev server or preview deploy served from a different origin — those items keep their plain
link). - The rewrite only applies when the current path has at least as many segments as the link's own path (so it never produces a shorter, truncated URL).
- Pages that don't exist at the equivalent path in the target (e.g. a page unique to
mainthat hasn't been backported tov1.0) will 404 — this plugin doesn't verify the target page exists. - The rewrite only takes effect after the component mounts client-side (it can't run during SSR, since it depends on the reader's current URL). On the very first paint, a rewritten link briefly shows its plain, non-rewritten
href/targetuntil mount — this avoids a hydration mismatch, since Vue does not patch plain attributes that differ between server-rendered and client-rendered output.
Advanced: manual wiring
If your theme already has a custom Layout, import the component directly instead of using createDynamicNav:
import { h } from "vue";
import DefaultTheme from "vitepress/theme";
import { DynamicNav } from "vp-dynamic-nav";
export default {
extends: DefaultTheme,
Layout: () =>
h(DefaultTheme.Layout, null, {
"nav-bar-content-before": () => h(DynamicNav),
"nav-screen-content-after": () => h(DynamicNav, { screen: true }),
}),
};
CSS
createDynamicNav injects a small <style> block into document.head at app startup. It sets CSS order values on VitePress's .content-body flex children to position the nav after the search bar and before the language/appearance/social controls, and adds a divider line between the nav and the language selector.
You can override any of these rules in your own stylesheet with a selector of equal or greater specificity, e.g.:
/* docs/.vitepress/theme/style.css */
.VPNavBar .content-body .VPDynamicNav.bar { order: 10; }
Compatibility
Requires VitePress ^1.0.0. The plugin uses VitePress internal components (VPNavBarMenuGroup, etc.) via the vitepress/dist/* export wildcard — stable across VitePress 1.x.
License
MIT
Related Packages
VitePress plugin - markdown custom container to display content in tabs
VitePress plugin that renders Vue components with tabs showing preview and sourc...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.2.0 | ^3.0.0 | MIT | |
| 0.1.0 | ^3.0.0 | MIT | |
| 0.0.3 | ^3.0.0 | MIT | |
| 0.0.2 | ^3.0.0 | MIT | |
| 0.0.1 | ^3.0.0 | MIT |