How this site works Guide
Reading Time
Reading time for this document is 1 minute for 248 words.
This site is plain VitePress 2 plus nine 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
legal.md, privacy.md, terms.md
footer pages, hidden from the navbar by .nav
plugins/ developer docs for each plugin
.sidebar order and titles for this section
downloads/ files the Downloads demo publishes as-is
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 |
| File download links | Downloads | markdown.config and vite.plugins |
Stray <tags> shown as text | HTML Escape | markdown.config, plus the theme |
| 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 { 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),
],
},
});The theme
The theme registers the badge component and loads its stylesheet, and turns on the two popovers: glossary definitions, and the HTML Escape hints that explain broken markup:
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 { enableHtmlEscapeHints } from '@binarynoir/vitepress-html-escape/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();
// Wavy underlines and fix-it popovers on pages that turn on HTML Escape hints.
enableHtmlEscapeHints();
},
} satisfies Theme;Adding a page
- Create
docs/<section>/my-page.mdwith a# Heading. - Optionally, add a line for it to that section's
.sidebarfile to choose its position and title. Pages without a line are listed alphabetically where the...line is, or at the end if there is none. - Run
npm run docs:dev. The page is already in the sidebar. Anything HTML Escape had to fix, such as anArray<T>outside backticks, has a wavy underline there: hover it to see what is wrong and how to fix it. The deployed site leaves the underlines out.
To add a whole new section, create a folder with an index.md. It becomes a navbar entry and its own sidebar automatically.