Design: Docs browser SPA (/docs/)
Date: 2026-05-04 Status: Approved for implementation planning (brainstorm consolidated)
Context
The repository’s canonical prose lives under docs/ (guides, ADRs, problem statements, normative specs, superpowers/, etc.). The public site today serves a static root page (web/public/index.html) and the admin installation UI as a Vite + Svelte 5 SPA under /admin/ (see docs/site-deployment.md). There is no dedicated browser experience for browsing docs/ as a tree with rendered Markdown.
This document specifies a second static SPA served under /docs/, built with the same stack as admin (not SvelteKit), sharing one Vite configuration, one dev server, and one vite build.
Goals
- Browse all of
docs/as rendered HTML with URLs that mirror the repository layout (full tree for now). - Build-time Markdown: compile
docs/**/*.mdduring the Vite build (and regenerate in dev as needed); support GitHub-flavored Markdown and Mermaid fenced blocks (Mermaid rendered client-side after navigation). - Layout: main pane shows the current document; a collapsible navigation pane on the right shows a hierarchical tree matching
docs/directories and files. - Single Vite instance:
rootatweb/, two HTML entry points (web/admin/index.html,web/docs/index.html), one dev server, one production build. - Deployment: artifacts land under
_bundle/public/docs/alongside admin and the root static page; Worker behavior stays public for docs (no OAuth). - Extensibility: the doc discovery pipeline exposes a single place to add filtering (include/deny rules) later without redesigning the app.
Non-goals (initial phase)
- Search, mindmaps, or other alternate navigation (may follow).
- Authentication for
/docs/(public read). - Porting the admin SPA to SvelteKit (explicitly deferred).
- SSR or per-page static HTML for SEO (SPA + client routing is sufficient for v1).
Architectural approach
Chosen: build-time HTML (or structured body) + manifest + thin Svelte shell
- A build step (Vite plugin or equivalent) enumerates Markdown files under
docs/, optionally passes paths through a filter function (identity / no-op in v1), parses with remark + GFM, serializes to HTML (e.g. rehype-stringify), and emits:- Per-page payload (by stable id or path key): HTML string, title, source path.
manifest.json(or TypeScript module): nested tree for the sidebar (folders,.mdleaves, labels from file names or first heading).
- The docs SPA loads the manifest at startup (or lazy-loads chunks), maps the route to a page entry, injects HTML into the main pane (with sanitization if we choose defense in depth for trusted-repo content), runs Mermaid on fenced blocks after each navigation.
- Internal links: rewrite relative links between
.mdfiles to in-app routes under/docs/....
Deferred alternatives: MDAST-to-Svelte components (heavier); separate Mermaid pre-render to SVG at build time (adds CLI weight; revisit if needed).
Section 1 — Vite: one project, two SPAs
Layout
web/admin/— existing admin app (minor path fixes only; see Section 4).web/docs/— new app:index.html,src/main.ts,App.svelte, router, shell layout, sidebar tree.
Configuration
vite.config.ts(repo root):root=path.join(repoRoot, "web").base: "/"so emitted script/link URLs are origin-absolute (e.g./assets/<hash>.js). This avoids Vite’s single-baselimitation while still hosting each app under/admin/and/docs/(each folder serves itsindex.html; assets live under shared/assets/).build.rollupOptions.input: two entries —admin/index.html,docs/index.html.- Dev SPA fallback:
configureServermiddleware (or equivalent): requests under/admin/and/docs/that do not match a static file should serve the correspondingindex.htmlso client routers and deep links work.
Proxy and Worker
- Keep
/api→127.0.0.1:8787for admin OAuth duringnpm run dev(unchanged intent).
Shared /assets/
- Production and preview deploys must copy
dist/assets/to_bundle/public/assets/in addition todist/admin/→_bundle/public/admin/anddist/docs/→_bundle/public/docs/. - Convention: treat
/assets/at the site origin as owned by the Vite build (hashed filenames reduce collision risk). The static root page underweb/public/should not introduce conflicting/assets/paths.
Section 2 — Routing and URLs
- Base path:
/docs/(trailing slash policy aligned with admin). - Document URL: path under
docs/without thedocs/prefix and without.md, e.g.docs/guides/admin/installation.md→/docs/guides/admin/installation. - Edge cases (spell out in implementation):
README.mdnaming, futureindex.md, characters that need encoding — pick one rule and apply consistently in the manifest and link rewriter. - Router: same family as admin (
svelte-spa-routeror equivalent) with a/docs-aware prefix.
Section 3 — Markdown pipeline
- Input roots: repository
docs/(paths resolved from repo root in the build plugin). - GFM:
remark-gfm(or current standard equivalent). - Mermaid: identify
```mermaidfences in HTML or AST; mark with stable attributes/classes/ids so the client can callmermaid.run()(or batch API) after the DOM updates. - Filtering (future): implement
listDocFiles(): string[](or similar) that applies an optional predicate; v1 uses the identity filter. Document where operators would configure deny globs when needed.
Section 4 — Minimal admin SPA changes
To coexist with base: "/" and lifted Vite root:
web/admin/index.html: use./src/main.tsinstead of/src/main.tsso the module resolves whenrootisweb/.adminAppBasePath()inweb/admin/src/lib/auth/oauth.ts: do not rely onimport.meta.env.BASEfor OAuthredirect_uri(it would become/). Use the fixed app prefix/admin/(the existingDEFAULT_ADMIN_BASEis sufficient as the canonical value).- Vitest / tooling: update
includeglobs and anysrc-relative paths in rootvite.config.tstoadmin/src/**(and adddocs/src/**when tests exist). Adjustweb/admin/src/vite-env.d.tscomments so they match the newbasebehavior.
No behavioral change intended for OAuth beyond correct redirect_uri origin path.
Section 5 — CI and deploy bundle
Extend Prepare deploy bundle in .github/workflows/site-build.yml (and any mirrored local instructions in docs/site-deployment.md):
- Run
npm run buildonce (builds admin + docs). - Copy
web/dist/assets/→_bundle/public/assets/(create if missing). - Copy
web/dist/admin/contents →_bundle/public/admin/(preserve current layout intent). - Copy
web/dist/docs/contents →_bundle/public/docs/.
Deploy Site remains default-branch trusted wrangler.toml; artifact copy rules stay only public/ and worker/ from the zip.
Section 6 — Worker and static asset routing
- No new
/apiroutes required for docs. - Rely on existing static assets + SPA fallback behavior for navigations; verify that
/docs/*deep links resolve to the docsindex.htmlthe same way/admin/*does for admin. If Cloudflare static asset routing treats multi-SPA andrun_worker_firstthe same as today, document any caveat discovered during implementation.
Section 7 — UI shell
- Main pane: scrollable article region; typography and code-block styling consistent with a readable doc site (reuse or lightly fork admin’s neutral styling patterns where practical).
- Right sidebar: collapsible folder tree; clicking a leaf navigates via the client router; optional current-doc highlight.
- Responsive: for narrow viewports, collapse the tree behind a control (exact breakpoint left to implementation).
Section 8 — Testing
- Unit tests: manifest shape, path ↔ URL mapping, link rewriting, optional filter hook (when added).
- Lightweight regression: one fixture or snapshot covering GFM table/task list and a small Mermaid diagram if feasible without flakiness.
Open questions (implementation)
- Exact HTML sanitization strategy (trusted repo vs harden with DOMPurify or rehype-sanitize).
- Whether to code-split large manifest vs single JSON (trade load vs simplicity).
References
docs/site-deployment.md— Build Site / Deploy Site flow.vite.config.ts— current admin-only Vite root (to be generalized per Section 1).docs/ADRs/0019-web-source-and-cloudflare-site-layout.md—web/vscloudflare_site/split.