kbrdn-docs
August 13, 2026 · View on GitHub
One Astro Starlight site per product, one shared design system, and not a single product page authored here.
Documentation is written next to the code it documents, in the product's own repository. This monorepo is the other half: it converts that tree, themes it, and ships it to Cloudflare Pages as a static site. main is the live "latest" of every product; a frozen version is a branch, not a directory.
Managed with Bun — the only supported package manager and runtime.
npm/pnpm/yarn/npx/nodeare not used here.
What this repo is, precisely:
- A renderer, not a source.
sites/gwm/src/content/docs/<section>/is wiped and regenerated bybun run sync:gwmfromkbrdn1/gwm-cli, folderdocs/. Everything hand-written for the site (the two landingindex.mdx) is preserved across a sync. - A consistency layer.
@kbrdn/ds-sharedholds what must look identical on every product — theme tokens, the StarlightFooteroverride, reusable MDX components. Each site overrides its own accent, landing and nav on top of it. - A pipeline that runs itself. A delivery landing on gwm-cli's
mainthat touchesdocs/,changelogs/orCargo.tomlfires arepository_dispatchhere; the sync replays the conversion, commits whatever drifted, and calls the deploy. - Four green gates or nothing. oxlint, prettier,
bun teston the conversion core, thenastro check+ a full static build. The converter runs with no human in the loop, so a mapping regression would publish itself — that is what the test gate is for.
live
| Product | Address | Page source |
|---|---|---|
| gwm | https://gwm.kbrdn.dev | kbrdn1/gwm-cli, folder docs/ |
gwm-docs.pages.dev serves the same content and cannot be taken down — it is the Cloudflare project's own address. It is kept out of search indexes by sites/gwm/public/_headers, the only place in the chain that knows which host it is answering on: a robots.txt is static, therefore served identically on both domains and unable to exclude just one.
quick start
bun install
bun run dev # gwm dev server on http://localhost:4321
With Nix: nix develop gives you bun, git, curl and jq. The shell ships no node — Bun is the only supported runtime, so every script goes through bun run <script>. The repo ships an .envrc, so direnv allow enters the same shell on cd.
commands
| Command | Effect |
|---|---|
bun run dev | gwm dev server (alias of dev:gwm) |
bun run build | static build of every site |
bun run preview:gwm | serve the built gwm site |
bun run check | lint + format check + bun test + astro check |
bun run test | conversion-core tests (scripts/lib/) |
bun run lint | oxlint (lint:fix to autofix) |
bun run format | prettier --write (format:check to verify) |
bun run sync:gwm | regenerate the gwm pages from a local gwm-cli checkout |
sync:gwm reads ~/Projects/Perso/gwm-cli by default; point it elsewhere with GWM_REPO=/path/to/gwm-cli bun run sync:gwm.
layout
kbrdn-docs/
├── packages/
│ └── ds-shared/ # @kbrdn/ds-shared — theme, Starlight overrides, shared MDX components
├── sites/
│ └── gwm/ # @kbrdn/docs-gwm — Starlight site for gwm
└── scripts/
├── sync-gwm-docs.mjs # disk I/O + orchestration
└── lib/convert.mjs # pure conversion core, covered by bun test
how the gwm site stays in sync
The conversion bridges two documentation formats, one difference at a time: Nuxt Content encodes sidebar order in the filename (1.getting-started/2.first-worktree.md) where Starlight reads it from frontmatter; the opening # Title is dropped because Starlight already renders title as the <h1>; screenshots move from docs/<section>/_assets/ to src/assets/captures/ with both link shapes rewritten; internal links get a /fr prefix inside the French pages.
The generated pages stay committed here. They could be built on the fly, but ci.yml builds the site on every push and PR — without them in the tree, CI would break. So the bot commits them, and the repository stays a faithful image of what is online.
⚠️ sync-gwm.yml reads gwm-cli's main, never its dev. A page whose source has not reached main yet is therefore deleted by the next sync, and that deletion is committed as legitimate drift.
deployment
main deploys the production ("latest") deployment to Cloudflare Pages, one Pages project per product. The deploy.yml workflow is gated by the DEPLOY_ENABLED repo variable and needs the CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID secrets. All three have been in place since 2026-08-04.
Nothing needs to be triggered by hand: sync-gwm.yml calls deploy.yml explicitly once it has committed. That call is deliberate, not a side effect of the commit — a push signed by GITHUB_TOKEN triggers no workflow at all, which is GitHub's anti-recursion guard.
quality gates
Four gates, enforced by CI, and the merge contract for any change:
| Gate | Command |
|---|---|
| Lint | bun run lint |
| Format | bun run format:check |
| Type & content check | bun --filter='*' run check |
| Build (no broken refs) | bun run build |
bun run check chains the first three plus the tests; .astro files are type-checked by astro check, not by oxlint. An opt-in pre-commit hook runs them on staged files only — git config core.hooksPath .githooks.
versioning
By branch, per product: release/<product>/v<MAJOR.MINOR> freezes one version's documentation and deploys as its own Cloudflare Pages deployment. There is no in-tree versioned_docs/ mechanism, and none is coming. See CONTRIBUTING.md and PLAN.md.
related docs
PLAN.md: vision, settled decisions, architecture, roadmapCONTRIBUTING.md: branch / commit / PR conventions, quality gates, local hooksCHANGELOG.md: release indexCLAUDE.md: house rules for AI assistantspackages/ds-shared/README.md: what the shared package exports, and how a site consumes it.github/LABELS.md: issue / PR label taxonomy
license
MIT © 2026 Kylian Bardini.