VitePress automatic sidebar: reading markdown files from a folder in VitePress

Reading markdown files from a folder in VitePress

I cannot believe how long I struggled with getting this to work… but patience is key ;)

I needed to generate a list of all files in a specific folder, so that I wouldn’t continuously have to update the VitePress configuration file.

This is quite an ugly solution that would need an iteration or two, but whatever - it works for now.

The following dependencies were needed:

pnpm add -D glob fs

I think gray-matter should already be bundled with VitePress.

Here’s my sidebar.ts file in docs/.vitepress:

import { glob } from 'glob';
import fs from 'fs';
import matter from 'gray-matter';

export interface INavItem {
  title: string;
  id: string;
  link: string;
  done: boolean;
}

export default function getFiles(path: string): Promise<INavItem[]> {
  return new Promise((resolve, reject) => {
    let navigation: INavItem[] = [];
    const filePath = path;
    const files: string[] = glob.sync(filePath);
    files.sort();
    for (const file of files) {
      const fileData = fs.readFileSync(file).toString();
      const fm = matter(fileData);

      let title = fm.data.title;
      if (fm.data.title === undefined) {
        title = file;
      }

      navigation.push({
        title,
        id: fm.data.id, // Custom data
        link: file.replace('docs', ''), // Passing in the entire docs folder path, so removing this manually
        done: Boolean(fm.data.done), // Also custom data
      });
    }
    resolve(navigation);
  });
}

And in docs/.vitepress/config.ts:

import getFiles, { INavItem } from './sidebar';

const module1 = await getFiles('docs/modules/v1/**/*.md'); // Path to folder
const module2 = await getFiles('docs/modules/v2/**/*.md'); // Path to folder

// Unclear why the resolved promise result is not a valid iterable, so
// re-creating the links, seems stupid but…
function generateSidebarItems(module: any) {
  const processedLinks = [];
  for(let i = 0; i < module.length; i++) {
    let title = '';
    const moduleData: INavItem = module[i];
    if (!moduleData.done) {
      title += '<span style="font-size: 0.5rem; position: relative; top: -1px;">❌</span> ';
    }
    title += `<span class="custom-class">${moduleData.title}</span>`;
    processedLinks.push({
      text: title,
      link: moduleData.link,
    })
  }
  return processedLinks;
}

export default defineConfig({
  // other properties
  sidebar: [
    {
      text: 'Module 1',
      collapsible: true,
      collapsed,
      items: generateSidebarItems(module1),
    },
    {
      text: 'Module 2',
      collapsible: true,
      collapsed,
      items: generateSidebarItems(module2),
    },
  ],
})

And the markdown frontmatter looks like this:

---
title: Example module
id: exampleid
done: false
---
# {{ $frontmatter.title }}
Lorem ipsum…

Of course, there are also nicer plugins/solutions, like this repo. I also tried this code snippet from 2022 but the sidebar kept being generated/done, before it had time to read the files from disk and populate the list.

I also tried some hacky solution with using the eminent import.meta.glob that Vite has, but it didn’t work in VitePress at the time of writing.

And I tried ”Build-Time Data Loading” which seemed like something that would have been suitable in this case, and rejoiced when I got it to work, until I realized it only worked once VitePress had an instance running, and not out-of-the-box. So fail on that attempt too.