Skip to content

0006. A VitePress site on GitHub Pages, with the web app under /app/ ​

  • Status: accepted
  • Date: 2026-11-01

Context ​

Before the repository goes public, the owner asked for state-of-the-art documentation, published with GitHub Pages together with the web app itself: a user guide, a format reference whose fidelity tables come from the pinned baselines, a developer guide, and decision records, with screenshots generated by a script so that they stay current.

The documentation lived in README.md and in working notes (spike reports, design notes). The site must keep CanViewer's privacy promise: no request outside its own origin, no analytics, no web font from a CDN, and no search service.

Decision ​

  • VitePress builds the site from docs/. It is a Vite tool, like the app, and its default theme is complete: navigation, sidebar, outline, dark mode, and a local search index that is built into the site.
  • No external request. The theme is VitePress's default theme without its web font, using the app's own bundled fonts (app/src/fonts.css) and colours. Search is VitePress's local search (MiniSearch, an index file on the site itself). There are no analytics. The site's icon is the app's icon, taken from desktop/app-icon.svg at build time.
  • One source for GitHub and the site. Pages are plain Markdown that read well on GitHub too. Relative links work in both: a markdown-it rule rewrites a link that leaves the site (to a source file, or to the working notes) into a link to the file on GitHub, and fails the build when the target does not exist. Screenshots carry GitHub's #gh-light-mode-only and #gh-dark-mode-only fragments, which the same rule turns into classes that follow the site's theme switch.
  • What is published: guide/, reference/, develop/, decisions/ and an archive/ page. The working notes in docs/spikes/ and docs/design/ stay in the repository, unpublished, and the archive page links to them on GitHub.
  • Generated, not written: the fidelity tables come from the baselines through scripts/docs/fidelity-tables.mjs, and a test fails when a page and the baselines disagree. The screenshots come from scripts/screenshots.mjs (Playwright, both themes, both languages, WebP).
  • GitHub Pages carries both: the site at the root and the web app under /app/, built with Vite's relative base (--base ./), so that its files, its Pyodide directory and its service worker's scope all sit below /app/.
  • One workflow, pages.yml, builds the site and the app on pull requests that touch them (build only) and on pushes to main. It checks every link in the sources and in the built site, runs the network check against the app under /app/, and crawls every page of the site in Chromium, failing on any request outside the origin. It deploys from main only when the repository is public, because Pages for a private repository needs a paid plan.

Consequences ​

  • The README is short and points at the site.
  • A change that alters a reader's output also changes the generated tables, in the same pull request.
  • Screenshots are regenerated with one command after a UI change, and reviewed as images in the diff.
  • What GitHub, as the host, sees is documented on the privacy page: requests for the site's and the app's files, never the user's files.
  • Pages for new features go into guide/; new decision records into decisions/, where the sidebar picks them up by file name.
  • The site adds VitePress (MIT) as a development dependency. It is not part of the app's bundle.

Alternatives considered ​

  • Docusaurus, MkDocs: mature, but a second stack (React with its own bundler, or Python) next to Vite, and MkDocs' Material theme loads Google Fonts unless configured otherwise.
  • The README and Markdown files on GitHub only: no published guide, no search, and nothing at the URL where the app is.
  • The app at the root and the docs under a path: the documentation is the entry point for new users; the app is one click away under /app/.
  • Algolia DocSearch: a search service, which sends every query to a third party.