Plugins
Reading Time
Reading time for this document is 1 minute for 220 words.
Nine small packages that make a documentation site easier to build and nicer to read. Each one does a single job and they share conventions. All nine are running on the site you are looking at.
| Plugin | Works with | What it does |
|---|---|---|
| Auto Sidebar | VitePress 2 | Builds themeConfig.sidebar from your folders. |
| Auto Navbar | VitePress 2 | Builds themeConfig.nav from your folders. |
| Markdown Tags | VitePress, VS Code, Obsidian | Inline status badges and tagged-pages lists. |
| Reading Time Tag | VitePress 2 | [[readingTime]] becomes a reading-time tip block. |
| Downloads | VitePress | Files in a downloads folder become linkable downloads. |
| Glossary Tooltips | VitePress, markdown-it | Hover definitions from one glossary file. |
| HTML Escape | VitePress 2, markdown-it | Stray <tags> and braces render as text, with fix-it hints. |
| Optimize Images | Vite | Re-compresses images in the build output. |
| Image Fallback | Vite | Placeholder 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/vitepress-html-escape \
@binarynoir/vite-plugin-optimize-images \
@binarynoir/vite-plugin-image-fallback \
markdown-it-glossaryThe VitePress plugins target VitePress 2 (currently an alpha release line). Node 22 or newer is required.
One config, all nine
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 { markdownHtmlEscape } from '@binarynoir/vitepress-html-escape';
// 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`, so a page with that name is never read as a section 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);
// @binarynoir/vitepress-html-escape: stray tags like Array<string>, and {{ }} in prose,
// render as text instead of breaking the Vue compile. It runs after every other rule.
// In `vitepress dev`, hints underline each fix and explain it; `vitepress build` leaves them out.
md.use(markdownHtmlEscape, { hints: process.env.NODE_ENV !== 'production' });
},
},
// 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.