Skip to content

getSidebar's matching logic is buggy: Does not support non-strictly hierarchical sidebar config objects (with subtree lifting via duplication) #4841

@chlsczx

Description

@chlsczx

Describe the bug

If I have such a sidebar config object:

{
  "/a": [
    {
      "text": "a copy",
      "link": "/a/a-copy",
      "collapsed": true,
      "items": [
        {
          "text": "Runtime API Examples",
          "link": "/a/a-copy/api-examples.md",
          ".sort_base": "api-examples.md"
        },
        {
          "text": "Markdown Extension Examples",
          "link": "/a/a-copy/markdown-examples.md",
          ".sort_base": "markdown-examples.md"
        }
      ],
      ".sort_base": "a-copy"
    },
    {
      "text": "📂 b-copy",
      "collapsed": true,
      "items": [
        {
          "text": "Markdown Extension Examples",
          "link": "/a/b-copy/markdown-examples.md",
          ".sort_base": "markdown-examples.md"
        },
        {
          "text": "Runtime API Examples",
          "link": "/a/b-copy/z_api-examples.md",
          ".sort_base": "z_api-examples.md"
        }
      ],
      ".sort_base": "b-copy"
    }
  ],
  "/": [
    {
      "text": "Runtime API Examples",
      "link": "/api-examples.md",
      ".sort_base": "api-examples.md"
    },
    {
      "text": "Markdown Extension Examples",
      "link": "/markdown-examples.md",
      ".sort_base": "markdown-examples.md"
    },
    {
      "text": "a",
      "link": "/a",
      "collapsed": true,
      "items": [
        {
          "text": "a copy",
          "link": "/a/a-copy",
          "collapsed": true,
          "items": [
            {
              "text": "Runtime API Examples",
              "link": "/a/a-copy/api-examples.md",
              ".sort_base": "api-examples.md"
            },
            {
              "text": "Markdown Extension Examples",
              "link": "/a/a-copy/markdown-examples.md",
              ".sort_base": "markdown-examples.md"
            }
          ],
          ".sort_base": "a-copy"
        },
        {
          "text": "📂 b-copy",
          "collapsed": true,
          "items": [
            {
              "text": "Markdown Extension Examples",
              "link": "/a/b-copy/markdown-examples.md",
              ".sort_base": "markdown-examples.md"
            },
            {
              "text": "Runtime API Examples",
              "link": "/a/b-copy/z_api-examples.md",
              ".sort_base": "z_api-examples.md"
            }
          ],
          ".sort_base": "b-copy"
        }
      ],
      ".sort_base": "a"
    }
  ]
}

when I access path "/api-examples.md", it will decide the sidebar to "/a", just because "/api-examples.md" starts with "/a".

Reproduction

Clone my repo, run

pnpm i

and

pnpm run dev

go to "http:///markdown-examples.html"

you will see the sidebar is good right now.

and click the Runtime API Examples, boomb💥 the sidebar which belongs to "/" disappeared.

Expected behavior

I expect that when i access path /api-examples.html, the sidebar remains same as when I access /markdown-examples.html.

System Info

$ npx envinfo --system --npmPackages vitepress --binaries --browsers
Need to install the following packages:
[email protected]
Ok to proceed? (y) y


  System:
    OS: Windows 11 10.0.26100
    CPU: (16) x64 AMD Ryzen 7 8845H w/ Radeon 780M Graphics
    Memory: 6.63 GB / 31.29 GB
  Binaries:
    Node: 23.7.0 - C:\nvm4w\nodejs\node.EXE
    Yarn: 1.22.22 - C:\nvm4w\nodejs\yarn.CMD
    npm: 10.9.2 - C:\nvm4w\nodejs\npm.CMD
    pnpm: 10.9.0 - C:\nvm4w\nodejs\pnpm.CMD
  Browsers:
    Edge: Chromium (138.0.3351.55)
  npmPackages:
    vitepress: ^1.6.3 => 1.6.3

Additional context

Tried to figure out the cause in vitepress sourcecode, and I think the function getSidebar in src\client\theme-default\support\sidebar.ts is buggy, it shouldn't just only check the starting chararcters of the path, should also to check the remaining part after the same starting string...

the code in code repo now:

/**
 * Get the `Sidebar` from sidebar option. This method will ensure to get correct
 * sidebar config from `MultiSideBarConfig` with various path combinations such
 * as matching `guide/` and `/guide/`. If no matching config was found, it will
 * return empty array.
 */
export function getSidebar(
  _sidebar: DefaultTheme.Sidebar | undefined,
  path: string
): SidebarItem[] {
  if (Array.isArray(_sidebar)) return addBase(_sidebar)
  if (_sidebar == null) return []

  path = ensureStartingSlash(path)

  const dir = Object.keys(_sidebar)
    .sort((a, b) => {
      return b.split('/').length - a.split('/').length
    })
    .find((dir) => {
      // make sure the multi sidebar key starts with slash too
      return path.startsWith(ensureStartingSlash(dir))
    })

  const sidebar = dir ? _sidebar[dir] : []
  return Array.isArray(sidebar)
    ? addBase(sidebar)
    : addBase(sidebar.items, sidebar.base)
}

Thinking it will work this way:

/**
 * Get the `Sidebar` from sidebar option. This method will ensure to get correct
 * sidebar config from `MultiSideBarConfig` with various path combinations such
 * as matching `guide/` and `/guide/`. If no matching config was found, it will
 * return empty array.
 */
export function getSidebar(
  _sidebar: DefaultTheme.Sidebar | undefined,
  path: string
): SidebarItem[] {
  if (Array.isArray(_sidebar)) return addBase(_sidebar)
  if (_sidebar == null) return []

  path = ensureStartingSlash(path)

  const dir = Object.keys(_sidebar)
    .sort((a, b) => {
      // longer dir link has higher priority
      return b.split('/').length - a.split('/').length
    })
    .find((dir) => {
      const dirWithStartingSlash = ensureStartingSlash(dir)
      // make sure the multi sidebar key starts with slash too
      if (path.startsWith(dirWithStartingSlash)) {
        // "/" match everything and it has lowest priority
        if (dirWithStartingSlash === '/') return true
        const remains = path.replace(dirWithStartingSlash, '')
        if (remains.startsWith('/') || remains === '') return true
      }
    })

  const sidebar = dir ? _sidebar[dir] : []
  return Array.isArray(sidebar)
    ? addBase(sidebar)
    : addBase(sidebar.items, sidebar.base)
}

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions