vp-dynamic-nav
v0.2.0
Published
VitePress plugin that fetches and renders the navbar dynamically from a remote JSON file, with localStorage caching
Maintainers
Readme
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-navUsage
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
