How this site works Guide
Reading Time
Reading time for this document is 1 minute for 178 words.
This site is plain VitePress 2 plus seven plugins. There is no hand-written navigation anywhere: add a Markdown file in the right folder and it shows up in the sidebar, the navbar, search and the sitemap.
Folder layout
txt
docs/
.nav top navbar order
index.md home page
plugins/ developer docs for each plugin
.sidebar order and titles for this section
apps/ user docs for the CLI apps
guide/ this section
reference/ glossary terms and the generated list of every tag
assets/ images imported by pages
public/ files served as-is (logos, favicon, icons)
.vitepress/
config.mts the one config file
theme/ registers tag badges, brand colorsWhich plugin does what
| You see | Plugin | Where it is wired |
|---|---|---|
| Top navbar | Auto Navbar | themeConfig.nav in config.mts |
| Left sidebar | Auto Sidebar | themeConfig.sidebar in config.mts |
| Status badges | Markdown Tags | markdown.config, plus the theme |
| "Reading time" box | Reading Time Tag | markdown.config |
| Dotted-underline hovers | Glossary Tooltips | markdown.config |
| Smaller images | Optimize Images | vite.plugins |
| Placeholder for a missing image | Image Fallback | vite.plugins |
The config
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),
],
},
});The theme
The only theme code is registering the badge component and loading its stylesheet:
ts
import DefaultTheme from 'vitepress/theme';
import type { Theme } from 'vitepress';
import { registerMarkdownTags } from '@binarynoir/vitepress-markdown-tags/theme';
import '@binarynoir/vitepress-markdown-tags/style.css';
import { enableGlossaryTooltips } from 'markdown-it-glossary/client';
import './custom.css';
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
// Registers the <MarkdownTag> component that ((tag|...)) compiles to.
registerMarkdownTags(app);
// Glossary definitions in a popover on hover, click and tap (title tooltips never show on touch screens).
enableGlossaryTooltips();
},
} satisfies Theme;Adding a page
- Create
docs/<section>/my-page.mdwith a# Heading. - Optionally add a line for it to that section's
.sidebarto choose its position and title. Without one it is listed alphabetically. - Run
npm run docs:dev. The page is already in the sidebar.
To add a whole new section, create a folder with an index.md. It becomes a navbar entry and its own sidebar automatically.