Skip to content

Plugins ​

Reading Time

Reading time for this document is 1 minute for 216 words.

Eight small packages that make a documentation site easier to build and nicer to read. Each one does a single job and they share conventions. Seven are running on the site you are looking at; Downloads is new and is not wired into this site yet.

PluginWorks withWhat it does
Auto SidebarVitePress 2Builds themeConfig.sidebar from your folders.
Auto NavbarVitePress 2Builds themeConfig.nav from your folders.
Markdown TagsVitePress, VS Code, ObsidianInline status badges and tagged-pages lists.
Reading Time TagVitePress 2[[readingTime]] becomes a reading-time tip block.
DownloadsVitePressFiles in a downloads folder become linkable downloads.
Glossary TooltipsVitePress, markdown-itHover definitions from one glossary file.
Optimize ImagesViteRe-compresses images in the build output.
Image FallbackVitePlaceholder for missing images instead of a failed build.

Install them all ​

sh
npm install --save-dev \
  @binarynoir/vitepress-auto-sidebar \
  @binarynoir/vitepress-auto-navbar \
  @binarynoir/vitepress-markdown-tags \
  @binarynoir/vitepress-reading-time-tag \
  @binarynoir/vitepress-downloads \
  @binarynoir/vite-plugin-optimize-images \
  @binarynoir/vite-plugin-image-fallback \
  markdown-it-glossary

The VitePress plugins target VitePress 2 (currently an alpha release line). Node 22 or newer is required.

One config, all seven ​

This is this site's real config.mts, included straight from the repository, so it can't drift from what is deployed. How this site works walks through it.

ts
import { defineConfig } from 'vitepress';
import path from 'node:path';
import { glossaryAbbr } from 'markdown-it-glossary';
import { readingTimeTag } from '@binarynoir/vitepress-reading-time-tag';
import { markdownTags, stripTags } from '@binarynoir/vitepress-markdown-tags';
import { taggedPagesVitePlugin } from '@binarynoir/vitepress-markdown-tags/vitepress';
import { generateNav } from '@binarynoir/vitepress-auto-navbar';
import { generateSidebar } from '@binarynoir/vitepress-auto-sidebar';
import { imageFallbackPlugin } from '@binarynoir/vite-plugin-image-fallback';
import { optimizeImagesPlugin } from '@binarynoir/vite-plugin-optimize-images';
import { downloadsMarkdown, downloadsVitePlugin } from '@binarynoir/vitepress-downloads';
import { downloadsSrcExclude } from '@binarynoir/vitepress-downloads/vitepress';

// import.meta.dirname, not __dirname: VitePress configs are ESM.
const docsRoot = path.resolve(import.meta.dirname, '..');

const SITE_URL = 'https://binarynoir.github.io';

// @binarynoir/vitepress-downloads: the same options go to all three pieces below.
const downloadsOptions = { folders: ['downloads', 'files'] };

export default defineConfig({
  title: 'BinaryNoir',
  description: 'Open-source tools and VitePress plugins by BinaryNoir, with live demos.',
  lang: 'en-US',
  cleanUrls: true,
  lastUpdated: true,

  // Files in a `downloads` or `files` folder are published as-is, never built as pages.
  srcExclude: downloadsSrcExclude(downloadsOptions),

  sitemap: { hostname: SITE_URL },

  head: [
    ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
    // Above-the-fold display font, fetched early to avoid a flash of fallback text.
    [
      'link',
      {
        rel: 'preload',
        href: '/fonts/film-noir.woff2',
        as: 'font',
        type: 'font/woff2',
        crossorigin: '',
      },
    ],
    ['meta', { name: 'theme-color', content: '#111111' }],
    ['meta', { property: 'og:type', content: 'website' }],
    ['meta', { property: 'og:title', content: 'BinaryNoir' }],
    [
      'meta',
      {
        property: 'og:description',
        content: 'Open-source tools and VitePress plugins by BinaryNoir, with live demos.',
      },
    ],
    ['meta', { property: 'og:url', content: SITE_URL }],
  ],

  themeConfig: {
    siteTitle: false,
    logo: { light: '/logo-wide-light.svg', dark: '/logo-wide-dark.svg', alt: 'BinaryNoir' },

    // @binarynoir/vitepress-auto-navbar: top nav built from the folder tree.
    // `.nav` files set order and titles; `.inherit` borrows titles from `.sidebar`.
    nav: generateNav(docsRoot, {
      maxDepth: 2,
      configFilenames: ['.nav', '.sidebar'],
    }),

    // @binarynoir/vitepress-auto-sidebar: one sidebar per top-level folder.
    sidebar: generateSidebar(docsRoot, {
      maxDepth: 3,
      maxTitleLength: 50,
    }),

    socialLinks: [{ icon: 'github', link: 'https://github.com/binarynoir' }],

    search: { provider: 'local' },

    editLink: {
      pattern: 'https://github.com/binarynoir/binarynoir.github.io/edit/main/docs/:path',
      text: 'Suggest a change to this page',
    },

    footer: {
      message:
        'Code released under the MIT License. BinaryNoir name and logos are trademarks of BinaryNoir. <a href="/legal">Legal</a> · <a href="/terms">Terms</a> · <a href="/privacy">Privacy</a>',
      copyright:
        'Copyright © 2026 John Smith III / BinaryNoir. All rights reserved except where licensed.',
    },
  },

  markdown: {
    theme: { light: 'github-light', dark: 'github-dark' },
    config(md) {
      // markdown-it-glossary: hover definitions from docs/reference/glossary.md on every page.
      // A page opts out with `glossary: false` in its frontmatter.
      md.use(glossaryAbbr, {
        file: path.join(docsRoot, 'reference/glossary.md'),
        root: docsRoot,
        // Not the default `glossary.md`: docs/plugins/glossary.md is a documentation page, not a glossary.
        scopedFile: 'section-glossary.md',
      });
      // @binarynoir/vitepress-reading-time-tag: [[readingTime]] becomes a tip block.
      md.use(readingTimeTag);
      // @binarynoir/vitepress-markdown-tags: ((tag|Done|green)) becomes a badge.
      md.use(markdownTags);
      // @binarynoir/vitepress-downloads: links into a `downloads` or `files` folder become file downloads.
      md.use(downloadsMarkdown, downloadsOptions);
    },
  },

  // Keep ((tag|...)) syntax out of <title>, search results and the page outline.
  transformPageData(pageData) {
    if (typeof pageData.title === 'string') {
      pageData.title = stripTags(pageData.title);
    }
    if (typeof pageData.frontmatter?.title === 'string') {
      pageData.frontmatter.title = stripTags(pageData.frontmatter.title);
    }
  },

  vite: {
    plugins: [
      // Missing image imports become a placeholder plus a console warning
      // instead of failing the build.
      imageFallbackPlugin(),
      // Re-compress PNG/JPEG/WebP in the build output. Source files are untouched.
      optimizeImagesPlugin({ verbose: true }),
      // Keeps `tagged: true` pages current while running `vitepress dev`.
      taggedPagesVitePlugin(),
      // Publishes the files in each `downloads` or `files` folder at their page-relative URL.
      downloadsVitePlugin(downloadsOptions),
    ],
  },
});

Source and issues ​

Each plugin lives in its own repository under github.com/binarynoir. Open an issue on the plugin's repository for bugs and requests.

Code released under the MIT License. BinaryNoir name and logos are trademarks of BinaryNoir. Legal · Terms · Privacy