Eight flavors of the same docs
These docs are more complicated than they need to be, on purpose. The same markdown is built by six static site generators and two client-rendered apps, deployed side by side under one GitHub Pages origin, linked to each other page by page, and audited by vidimus as a single site. A docs site for a tool that audits static sites is the cheapest real test bed it can have, and one generator only tests one generator's habits.
| Flavor | Path | Engine | Theme | URL style |
|---|---|---|---|---|
| VitePress | /vidimus/ | Vue, Vite | default theme, extended | clean: /vidimus/cli |
| Starlight | /vidimus/astro/ | Astro | Starlight, with component overrides | directory: /vidimus/astro/cli/ |
| Hugo | /vidimus/hugo/ | Go templates | handwritten, no JavaScript | directory: /vidimus/hugo/cli/ |
| Eleventy | /vidimus/eleventy/ | Nunjucks, markdown-it | handwritten, no JavaScript | directory: /vidimus/eleventy/cli/ |
| Zola | /vidimus/zola/ | Tera, Rust | handwritten, no JavaScript | directory: /vidimus/zola/cli/ |
| mdBook | /vidimus/mdbook/ | Handlebars, Rust | default theme, post-processed | files: /vidimus/mdbook/cli.html |
| React | /vidimus/react/ | React 19, React Router, Vite | handwritten, rendered in the browser | clean: /vidimus/react/cli |
| Angular | /vidimus/angular/ | Angular, zoneless, standalone | handwritten, rendered in the browser | clean: /vidimus/angular/cli |
VitePress is the primary flavor: every other flavor's pages declare a canonical link to the matching VitePress page, and only VitePress pages are in the sitemap.
How it is built
The markdown in docs/ is written once, in a subset every engine understands: CommonMark, GFM tables, fenced code, one # heading per page, and relative links without the .md extension (./cli, ../configuration#anchor). docs/nav.json is the sidebar for all of them, and docs/flavors.ts knows every flavor's base path and URL style.
node scripts/docs.ts build then:
- syncs the markdown into each engine's content directory, rewriting relative links to the engine's own URLs, converting frontmatter (YAML for most, TOML for Zola, none for mdBook), and precomputing each page's canonical URL, edit link and links to the same page in the other flavors;
- builds VitePress into
docs/dist/, and every other engine intodocs/dist/<flavor>/; - post-processes mdBook's output, which has no per-page hooks, to add the canonical link, the page description and the flavor bar.
The React and Angular flavors get no markdown: the sync step renders every page to HTML with marked and writes them as one JSON module that the app imports. Each app is one index.html; it looks the page up by URL, sets the title, description and canonical link, and puts the HTML in the page. Nothing is prerendered. vidimus audits that build as a host with rewrites would serve it, one index.html per app answering every route. GitHub Pages has no rewrites, so just before deploying, node scripts/docs.ts pages-copies gives every route a copy of that index.html (react/cli.html, react/audits/index.html), which Pages serves with a 200.
The showcase page is the one page with heavy content: a three.js scene bundled from npm, embeds, video and a table a script fills. Its assets are generated at build time into docs/public/showcase/.
pnpm run build # vidimus itself
pnpm run docs:build # all eight flavors into docs/dist
pnpm run docs:audit # vidimus on docs/dist, as one siteHugo, Zola and mdBook are pinned in mise.toml; mise install gets them.
What gets exercised
Eight builds produce eight sets of habits in the same output, and every audit sees all of them at once:
linksfollows the flavor bar from every page to the same page in the seven other flavors, so a URL-style mismatch between generators shows up as a broken link.seochecks eight ways of writing titles, descriptions and canonicals, and has to recognise that seven of the eight copies of each page point their canonical at the VitePress one.htmlvalidates markup from Vue's renderer, Astro, three handwritten template languages and mdBook's Handlebars theme.cspchecks the handwritten Hugo, Eleventy and Zola themes, which ship a strict meta Content-Security-Policy with no inline scripts or styles.budget,a11y,r12s,privacyandlighthousesee single-page-app hydration, islands, and plain HTML with no JavaScript at all.- The React and Angular flavors have one HTML file each.
routeslists their pages fromnav.json,server.fallbackanswers them with the app'sindex.html, andrender.includerenders them, so every audit covers the same pages in them as in the other six: the server audits open 26 routes per app, thedistaudits andlinksread the rendered DOM, andlinks.notFoundchecks that no link leads to the app's not-found view.
Anything vidimus gets wrong about one of these generators shows up here first. The flavor bar at the top of every page links to the same page in the other seven.
What it found
Setting this up found gaps in vidimus itself, each fixed with a test:
seoreported every mirrored page as a duplicate title and description and as missing from the sitemap, although each one declares a canonical link to the original. Pages canonical to another built page are now treated as duplicates by design.- The built-in server answered
/auditswithaudits/index.htmlinstead of redirecting to/audits/like GitHub Pages does, so relative links on directory index pages were reported as broken. It now redirects. r12schecked the font size of elements that are not rendered (mdBook's closed theme menu), flagged links inside sentences as small tap targets, which WCAG 2.5.8 exempts, failed a target it printed as 24px wide because it was 23.9px, and readmaximum-scale=1.5as zoom disabled.
And problems in the generators' own output, handled in the build or with scoped ignore rules in docs/vidimus.config.ts:
- Starlight's code blocks put
<div>elements inside<code>and<button>, which is invalid HTML (html, ignored under/astro/). - mdBook's default theme has two
<h1>per page, no Open Graph tags, a sidebar<iframe>without a title and an unlabelled search field (fixed by post-processing), a search form without a submit button (a11y, ignored) and ARIA attributes on a<label>, a small icon link and a deprecatedunloadlistener (Lighthouse, ignored). - Zola's
get_urlemits absolute production URLs, so a local audit loaded the stylesheet from the live site; the Zola templates use root-relative paths instead. It also always writes a404.htmlthat GitHub Pages never serves from a subdirectory, and treats double curly braces in code blocks as template syntax. - A strict meta CSP (
default-src 'none') makes Lighthouse reportrobots.txtas invalid; see troubleshooting.
Adding the client-rendered flavors and the showcase found more, again fixed in vidimus with a test:
server.fallbackwas one file for the whole site; two apps under one origin need one each, so it also takes{ match, file }rules.renderrendered every page or none, and now takesrender.include;lighthousegained per-pathoverrides.r12scounted a screen-reader-only skip link (clip: rect(0 0 0 0)) as a tap target, once the longer flavor bar put a real link next to it.shotsmasked embeds with an injected stylesheet, which a strict CSP blocks, so the map on the showcase was captured unmasked in three flavors. It also took some screenshots before a lazy<picture>had painted, before a video's controls had their duration, or before a script-filled table had its final height.- The built-in server sent
.vttcaptions asapplication/octet-stream.
And in the apps: Angular inlines a large JSON import as an object literal, which costs 400 ms of blocking time under Lighthouse's throttling, so the Angular flavor gets its pages from a JSON.parse module instead. Angular's own startup is still a ~500 ms task (React's is ~190 ms), and the Angular pages have a lower Lighthouse performance threshold for it.