As of
Est. 

The Second Sidebar

📑 I added a sidebar to the right. It can be opened and closed with the
📑 icon at the top right corner of the page.

VitePress automatically inserts a table of contents for the current page below its link in the sidebar navigation: When you select a file in the sidebar, the level two and three headings make new entries in the sidebar as subentries of the page. At least for me this felt a bit confusing and distracting. The maximum depth of entries shown can be configured. Setting sidebarDepth: 2 in the frontmatter of the markdown files turns off the subheadings. But then there is no table of contents.

Need for another Sidebar

It is possible to insert a table of contents using the built-in [[toc]] markdown. This creates the table at that point and when you scroll down the page, it is no longer visible. I wanted the table to be always visible on large screens and to be able to show and hide it on smaller devices. Just like the sidebar on the left side. So I added another sidebar to the right.

My sidebar is an element located on the right side of the navigation bar of the default layout. Similar to the Topic Map, it has an icon (📑) that toggles its visibility. When toggled, it slides in and out. Like the left sidebar, it opens automatically when the screen is wide enough.

Teleport to the Sidebar

Even though this sidebar is one of the outer parts of the layout, the content of this sidebar should be different for every page. I implemented this using Vue's teleport component. The main area of the page can define some part of its content and send it to the sidebar.

Different pages can send different content to the sidebar. On the glossary pages, I use the LocalSearchResults component, which displays the search results. The search term for the search is static. It is the name of the glossary page.

vue
<template>
  <TopAside>
    <LocalSearchResults :search="name" exclude-glossary />
  </TopAside>
</template>
<template>
  <TopAside>
    <LocalSearchResults :search="name" exclude-glossary />
  </TopAside>
</template>

The main pages send a table of contents to the sidebar:

vue
<template>
  <TopAside><TableOfContents /></TopAside>
</template>
<template>
  <TopAside><TableOfContents /></TopAside>
</template>
Of course, this also can be done without teleports

It is perhaps worth noting that using the teleport component is only one way to achieve this result. It can also be done without teleports. VitePress provides all the necessary data via composables. To implement the content of the sidebar as part of the layout, we first need to determine wether or not we are on a glossary page. Use useRoute().path to find out what the current page is. Then insert a LocalSearchResults component for glossary pages and a table of contents for the main pages. The headers for the table of contents can also be fetched in a generic way using the page's meta data, see below.

Using teleports reduces the complexity of that approach. It does not require a central logic, that knows all usages of the sidebar. Each page simply decides for itself what should be presented there.

Table of Contents

Now that we have a place to show the table of contents, how is it built?

The data for the table of contents is provided by VitePress as part of a page's metadata. When VitePress converts the Markdown to Vue render functions, it also captures any level two and level three headings found in the markdown. This structure is available via useData().page.value.headers. The expression useData().page.value.title yields the page title.

With that information, it's straight forward to crete a tree of unordered list elements. My implementation uses a bit more of VitePress than just the meta data. Here is an example of the generated HTML:

html
<div class="table-of-contents">
  <ul class="side-bar-links">
    <li class="side-bar-link">
      <a class="sidebar-link-item" href="#the-second-sidebar">The Second Sidebar</a>
    </li>
    <ul class="side-bar-links">
      <li class="side-bar-link">
        <a class="sidebar-link-item" href="#need-for-another-sidebar">Need for another Sidebar</a>
      </li>
      <li class="side-bar-link">
        <a class="sidebar-link-item active" href="#teleport-to-the-sidebar">Teleport to the Sidebar</a>
      </li>
      <li class="side-bar-link">
        <a class="sidebar-link-item active" href="#table-of-contents">Table of Contents</a>
      </li>
    </ul>
  </ul>
</div>
<div class="table-of-contents">
  <ul class="side-bar-links">
    <li class="side-bar-link">
      <a class="sidebar-link-item" href="#the-second-sidebar">The Second Sidebar</a>
    </li>
    <ul class="side-bar-links">
      <li class="side-bar-link">
        <a class="sidebar-link-item" href="#need-for-another-sidebar">Need for another Sidebar</a>
      </li>
      <li class="side-bar-link">
        <a class="sidebar-link-item active" href="#teleport-to-the-sidebar">Teleport to the Sidebar</a>
      </li>
      <li class="side-bar-link">
        <a class="sidebar-link-item active" href="#table-of-contents">Table of Contents</a>
      </li>
    </ul>
  </ul>
</div>

The default VitePress layout has an event listener for scroll events. It automatically detects links to the section that is actually displayed in the content area of the page, provided that they have the correct CSS class. So I reused the CSS classes of the original sidebar of VitePress. Now when you scroll the page, an active class is automatically added to the current entry in the table of contents. I use this to highlight the entry.