vp-dynamic-nav

VitePress plugin that fetches and renders the navbar dynamically from a remote JSON file, with localStorage caching
4,229
Install
npm install vp-dynamic-nav
Latest Version:0.2.0
Vue:3
License:MIT
Last Updated:Aug 26, 2026
Links: npm
Maintainer: hamishwillee

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 dynamicNavUrl runs 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 main that hasn't been backported to v1.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/target until 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

@red-asuka/vitepress-plugin-tabs

VitePress plugin - markdown custom container to display content in tabs

49,189 10
vitepress-theme-website

vitepress plugin for website

1,857 20
vitepress-plugin-component

VitePress plugin that renders Vue components with tabs showing preview and sourc...

1,223 2
vitepress-codeblock-collapse

Auto-collapse long code blocks in VitePress with expand/collapse toggle

22,634 16

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