How the documentation site works: architecture, local development, CI, deployment, and troubleshooting.
For general contribution guidelines (PR labels, changelog format), see the root CONTRIBUTING.md.
- Overview
- Prerequisites
- Local Development
- Versioning
- Branch Strategy and Deployment
- CI Workflows
- Script Reference
- Release-Notes Overrides
- Troubleshooting
The docs site lives in docs/website/. It is a fully static site (Next.js output: 'export') served via CDN by the hosting provider. GitHub stores only the source code -- the hosting provider watches repo branches, runs the build (SSG), and deploys automatically. There are no GitHub Actions deploy workflows; GitHub Actions handles validation and gating only.
| Component | Details |
|---|---|
| Framework | Next.js 15 (App Router) + React 19 |
| Docs framework | Fumadocs (fumadocs-core, fumadocs-mdx, fumadocs-ui) |
| Styling | Tailwind CSS |
| Content | MDX files in docs/website/content/docs/ |
| API docs | Auto-generated via TypeDoc (docs/website/scripts/generate-api-docs.ts) |
| Build output | docs/website/dist/ (static HTML/CSS/JS) |
| Hosting | Static site CDN (hosting provider runs the build and serves the output) |
Content falls into two categories:
| Category | Path | Committed? |
|---|---|---|
| Manual content (guides, tutorials, addons) | content/docs/sdk/, content/docs/addons/, content/docs/about-qvac/, etc. |
Yes |
| SDK API summary (generated) | content/docs/reference/api/index.mdx, content/docs/reference/api/v<X.Y>.x.mdx |
Yes (committed once per minor release) |
| SDK release notes (generated) | content/docs/reference/release-notes/index.mdx, content/docs/reference/release-notes/v<X.Y>.x.mdx |
Yes (committed on every minor and patch release) |
The SDK API summary and release notes are generated from TypeScript source / package CHANGELOGs via TypeDoc and Nunjucks. They live as a single MDX file per minor series — the latest minor at index.mdx, older minors as sibling v<X.Y>.x.mdx files (literal x marker; one permanent page per minor line, accumulating patch sections inside). Generation is triggered by the release pipeline; locally a maintainer can regenerate to preview.
The generation pipeline has two phases — TypeDoc extraction and Nunjucks rendering. Output is deterministic: identical SDK input always produces identical MDX. (AI-assisted authoring happens locally via Cursor skills, never inside this pipeline.)
SDK source (packages/sdk)
│
▼
Phase 1: TypeDoc extraction ──► api-data.json
│
▼
Phase 2: Nunjucks rendering ──► content/docs/reference/api/index.mdx (latest minor)
──► content/docs/reference/api/v<X.Y>.x.mdx (frozen older minor series)
──► src/lib/versions.ts (version switcher)
Release notes are per minor series too — each minor line owns one
permanent MDX page that accumulates patch sections as ## vX.Y.Z
directly under the ## vX.Y.0 minor block. The body of each section is
inlined verbatim from each SDK pod package's
packages/<pkg>/changelog/<version>/CHANGELOG_LLM.md under a per-package
### @qvac/<pkg> subsection (heading levels demoted so they nest under
the page hierarchy).
- Bun (scripts use
bunfor.envloading and TypeScript execution) - Node.js (for
npm run dev/npm run build) - Access to the SDK package source (
packages/sdkin the monorepo, or a standalone clone)
cd docs/website
npm install
cp .env.example .env # then set SDK_PATH (see below)
npm run dev # http://localhost:3000Without generating API docs, the site loads but SDK API links will 404.
The generation scripts need SDK_PATH to point at the SDK package root (the directory containing index.ts and tsconfig.json).
Copy .env.example to .env and set the path:
# Windows
SDK_PATH=D:\QVAC\qvac\packages\sdk
# Linux / macOS
SDK_PATH=/path/to/qvac/packages/sdkBun loads .env automatically when running scripts.
Two entry points depending on what you want to do:
1. Render the API summary for a single version (no version-bumping):
bun run scripts/generate-api-docs.ts <version> [flags]Examples:
# Re-render the latest summary into content/docs/reference/api/index.mdx
bun run scripts/generate-api-docs.ts 0.11.0 --latest
# Render an older minor series into content/docs/reference/api/v0.10.x.mdx (no --latest)
bun run scripts/generate-api-docs.ts 0.10.0
# Bump only the frontmatter title (called by the minor freeze flow):
# no TypeDoc, no render
bun run scripts/generate-api-docs.ts 0.10.0 --target=v0.10.x.mdx --title-onlyThis will:
- Run TypeDoc against the SDK entry point (
SDK_PATH/index.ts) and writeapi-data.json - Render a single MDX via the Nunjucks
single-page.njktemplate:--latest→content/docs/reference/api/index.mdx--target=<file>→content/docs/reference/api/<file>(explicit override)- otherwise →
content/docs/reference/api/v<X.Y>.x.mdx(series-named)
- Run a smoke test that checks for
## Functionsand## Errorsheadings
--title-only short-circuits this: it skips TypeDoc + render and only
rewrites the title: line of the existing target MDX, then runs the
same smoke test.
Flags:
| Flag | Description |
|---|---|
--latest |
Write to index.mdx instead of v<X.Y>.x.mdx. |
--target=<file> |
Override the output filename inside api/ (mutually exclusive with --latest). |
--title-only |
Rewrite the frontmatter title in-place (skips TypeDoc + render). Used by the minor-release freeze step to relabel the outgoing snapshot. |
--force-extract |
Bypass the mtime cache and re-run TypeDoc extraction. |
2. Release a new version end-to-end (freeze outgoing, generate incoming, refresh dropdown):
# Auto-detects minor (X.Y.0) vs patch (X.Y.Z, Z >= 1)
bun run scripts/release-version.ts <new-version> [--force-extract]This is the orchestrator the CI pipeline calls. It dispatches to the
focused release-version-minor.ts / release-version-patch.ts modules
based on the patch number, and never commits or opens PRs itself — the
wrapping workflow does that. See
Release-version orchestrators below.
After generating docs, refresh src/lib/versions.ts from disk:
bun run scripts/update-versions-list.ts [--latest=X.Y.Z]This walks content/docs/reference/api/ and content/docs/reference/release-notes/ for vX.Y.x.mdx siblings (series-named) and rebuilds the section manifests (API_SECTION, RELEASE_NOTES_SECTION). The optional --latest=X.Y.Z flag overrides which precise patch is recorded as section.latest (used for the page title's latest-patch range); the selector itself only shows series labels (v0.11.x (latest), v0.10.x, ...). Defaults to the SDK's package.json version when --latest is omitted.
When running inside the monorepo, use the orchestrator script that reads the SDK version from packages/sdk/package.json automatically:
bun run docs:generateThis runs generate-api-docs.ts --latest followed by update-versions-list.ts in sequence — useful for previewing a regen against the current SDK without bumping the latest pointer.
Only the API summary and release notes are versioned. Every other content surface (about-qvac, getting-started, examples, tutorials, addons, cli, http-server, home) lives at a single bare path that always reflects the current SDK.
Each versioned section is one folder under content/docs/reference/ containing one MDX per minor series (literal x marker in the filename):
content/docs/
├── about-qvac/ -> not versioned
├── addons/ -> not versioned
├── cli.mdx -> not versioned
├── http-server.mdx -> not versioned
├── index.mdx -> not versioned (home)
├── sdk/ -> not versioned
│ ├── examples/ -> not versioned
│ ├── getting-started/ -> not versioned
│ └── tutorials/ -> not versioned
└── reference/
├── api/
│ ├── index.mdx -> latest minor series (current SDK)
│ ├── v0.10.x.mdx -> archived minor series
│ ├── v0.9.x.mdx
│ ├── v0.8.x.mdx
│ └── v0.7.x.mdx
└── release-notes/
├── index.mdx -> latest minor series
├── v0.10.x.mdx -> accumulates ## vX.Y.Z patch sections under the minor
├── v0.9.x.mdx
├── v0.8.x.mdx
└── v0.7.x.mdx
- Format:
vX.Y.x(literalxfor the patch component). One permanent page per minor line. index.mdx: The current latest minor series, served from the bare basePath (e.g./reference/api,/reference/release-notes).vX.Y.x.mdx: Archived minor series, served from<basePath>/v<X.Y>.x(e.g./reference/api/v0.10.x). Created byscripts/create-version-bundle.ts(called fromrelease-version-minor.ts) when a newer minor replaces the outgoing one — it just copiesindex.mdxto a series-named sibling.- Version list: Two
VersionedSectionrecords (API_SECTION,RELEASE_NOTES_SECTION) insrc/lib/versions.ts, refreshed byscripts/update-versions-list.tsfrom disk. Each carries bothlatest(precise patch, e.g.v0.11.3) andlatestSeries(e.g.v0.11.x). The selector labels and URLs use the series form; the precise patch only surfaces in titles / description ranges. - Sidebar tree: Single
customTreeinsrc/lib/custom-tree.ts. TheAPIandRelease notesentries are flat single-page links; the version selector beside the page title (only on/reference/api*and/reference/release-notes*) handles series switching via full-page reload.
SDK release docs are generated locally as part of the release prep, not by a CI workflow. The qv-sdk-changelog skill (Step 8) runs the release-version.ts dispatcher in the same working tree as the changelog, so both land in a single release PR. The dispatcher reads the version, picks minor (freeze outgoing → regenerate) for X.Y.0 and patch (insert ## vX.Y.Z section under the minor block — API summary untouched) for X.Y.Z with Z >= 1, and forwards to the focused orchestrator.
| Trigger | API summary | Release notes | Versions list |
|---|---|---|---|
release-sdk-X.Y.0 (minor) |
Re-run TypeDoc → new index.mdx. Outgoing minor frozen as v<outgoingMajor>.<outgoingMinor>.x.mdx. |
Full render of the new minor's ## vX.Y.0 block (per-package verbatim CHANGELOG_LLM.md under ### @qvac/<pkg>) into index.mdx. Outgoing minor frozen as v<outgoingMajor>.<outgoingMinor>.x.mdx. |
latest = X.Y.0, latestSeries = vX.Y.x. |
release-sdk-X.Y.Z matching current latest minor (patch-latest) |
Not touched. Patches by definition don't change public API. | Insert ## v<X.Y.Z> section directly after the existing ## v<X.Y>.0 block in index.mdx. Re-runs are idempotent (the section is replaced in place). Description range bumps to include the new patch. |
latest = X.Y.Z (selector label unchanged — still vX.Y.x (latest)). |
release-sdk-X.Y.Z for an archived minor (patch-archived) |
Not touched. | Insert the same section into the existing v<X.Y>.x.mdx page. No rename. |
latest unchanged (script omits --latest). |
Re-running a patch is idempotent — the existing ## vX.Y.Z block is detected and replaced in place rather than appended again. The newest patch always sits directly below the minor block; older patches stay further down.
Each ## vX.Y.Z section's body is read verbatim from each SDK pod
package's per-version folder (packages/<pkg>/changelog/<X.Y.Z>/CHANGELOG_LLM.md,
falling back to raw CHANGELOG.md). The H1 release-notes banner is
stripped and every surviving heading is demoted by two levels so it
nests cleanly under the page's ### @qvac/<pkg> subsection. Packages
without a folder for that version are skipped (the SDK typically lists
all five pod packages; in practice only @qvac/sdk shares the version
namespace with the SDK pod's release cadence).
A thin dispatcher (release-version.ts) auto-detects minor vs patch from the version's patch number and forwards to one of two focused modules:
release-version-minor.ts — for X.Y.0 releases.
- Reads the current
latestfromsrc/lib/versions.ts(the outgoing version). - Calls
scripts/create-version-bundle.ts <outgoing>— copiesreference/api/index.mdxto the series siblingv<outgoingMajor>.<outgoingMinor>.x.mdxand the same for release notes. - Title-only relabel: rewrites the frozen snapshots' titles to drop the
(latest)marker. - Calls
scripts/generate-api-docs.ts <new> --latest— overwritesreference/api/index.mdxwith the new minor's content. - Calls
scripts/generate-release-notes.ts <new> --latest— same for release notes, reading per-package CHANGELOG_LLM.md verbatim. - Calls
scripts/update-versions-list.ts --latest=<new>— refreshesversions.tsso the dropdown picks up the new latest series plus the frozen older sibling.
release-version-patch.ts — for X.Y.Z releases with Z >= 1. Inspects src/lib/versions.ts to choose between patch-latest (write to index.mdx) and patch-archived (write to the existing vX.Y.x.mdx). The script never invokes the API summary generator.
All three modules are pure file mutations — they never git commit or gh pr create. The wrapping GitHub workflow opens the PR.
main = staging docs-production = production
────────────── ────────────────────────────
New commit on main Merge PR: main -> docs-production
│ │
▼ ▼
Hosting provider builds Hosting provider builds
& deploys to staging & deploys to production
mainis the staging environment. The hosting provider watches this branch; any new commit triggers a build and deploy to the staging site.docs-productionis the production environment. The hosting provider watches this branch; any new commit (via merged PR frommain) triggers a build and deploy to the production site.
With main + docs-production, every production deploy has a reviewable PR showing exactly what changed.
SDK release prep: qv-sdk-changelog skill (Step 8) generates docs locally
│
▼
Generated docs committed in the SDK release PR (alongside the changelog)
│
▼
Release PR (and its backmerge) merges to main
│
▼
Hosting provider detects new commit on main and rebuilds staging
Generated docs ship inside the reviewable SDK release PR rather than via a
separate auto-opened docs PR — so the API reference and release notes are
reviewed together with the changelog that produced them. Any other push to
main (docs content changes, merged PRs from contributors) still triggers the
hosting provider's build the same way.
Staging is verified and ready
│
▼
Open PR: main -> docs-production
│
▼
Review the diff, approve, merge
│
▼
Hosting provider detects new commit on docs-production
│
▼
Hosting provider builds the static site and deploys to production
The reviewer is responsible for confirming staging is healthy and that
the docs PR Checks have passed on main before merging into
docs-production. There is no automated CI gate on the production PR
— promotion is fully manual on purpose.
One GitHub Actions workflow validates docs PRs; SDK release docs are generated locally by a Cursor skill (no release workflow).
File: .github/workflows/docs-website-pr-checks.yml
Triggers: Pull requests to main that change docs/website/**, or manual dispatch.
What it does:
- Installs dependencies with Bun
- Runs
bun run buildto validate the site compiles - Runs Vitest tests (sidebar consistency, link integrity, single-page rendering, changelog parser) excluding TSDoc completeness tests that require SDK source
- Optionally installs the SDK and runs the TSDoc completeness audit in warning mode
Purpose: Catches build errors and broken links in docs PRs before merge.
The API summary index.mdx lives at content/docs/reference/api/ and is committed to the repo (refreshed locally by the qv-sdk-changelog skill Step 8 during SDK release prep), so PR checkouts always have it on disk — no placeholder step is needed.
Where: the qv-sdk-changelog Cursor skill, Step 8 (.cursor/skills/qv-sdk-changelog/SKILL.md). There is no GitHub Actions docs-release workflow — generation runs locally during release prep and ships in the SDK release PR alongside the changelog.
When: while preparing an @qvac/sdk release (after the changelog / CHANGELOG_LLM.md is generated). Skipped for non-sdk packages.
What it does:
- Runs
release-version.ts <version> --force-extractfromdocs/website, which dispatches:- Minor (
X.Y.0) — full flow: freezes the outgoingindex.mdxinto a series siblingv<outgoingMajor>.<outgoingMinor>.x.mdx, generates the new API summary intoindex.mdx(TypeDoc + render — output is deterministic by construction), generates the new release notes intoindex.mdx(per-package verbatimCHANGELOG_LLM.mdunder a single## v<X.Y.0>block), refreshessrc/lib/versions.ts. - Patch (
X.Y.Z,Z >= 1) —release-version-patch.tsinspectssrc/lib/versions.tsand pickspatch-latest(incomingX.Y== latestX.Y: insert## v<X.Y.Z>directly after the existing## v<X.Y>.0block ofindex.mdx) orpatch-archived(older minor: insert the same section into the existingv<X.Y>.x.mdx, no rename). The API summary page is never touched by patches.
- Minor (
- Runs
npm run buildfromdocs/websiteto verify the site still compiles (fail-stop on error). - Only the generated surfaces are committed —
content/docs/reference/api/**,content/docs/reference/release-notes/**, andsrc/lib/versions.ts. The skill only generates files (it never runsgit add); reviewgit statusand commit these, while all build/generation byproducts (api-data.json,.next/,.source/,out/,dist/) are gitignored so they never show up.
The dual-checkout race window the old CI workflow guarded against does not apply locally: the skill runs in the single release working tree after the changelog is generated, so the SDK source and CHANGELOGs are already the released state.
Once the SDK release PR (and its backmerge) lands on main, the hosting provider's main build picks it up and deploys to staging.
Patches never re-run TypeDoc — they touch only the frontmatter title of the API summary and append a section to the release notes — so api-data.json only changes on minor releases.
All scripts live in docs/website/scripts/ and are designed to run with Bun.
| Script | npm alias | Description |
|---|---|---|
release-version.ts |
docs:release-version |
Unified release dispatcher: parses the version and forwards to the minor or patch orchestrator. Called by the qv-sdk-changelog skill (Step 8) during release prep. |
release-version-minor.ts |
-- | Minor (X.Y.0) orchestrator: freeze outgoing series → generate new latest from per-package CHANGELOG_LLM.md → refresh versions.ts. Importable from release-version.ts. |
release-version-patch.ts |
-- | Patch (X.Y.Z, Z>=1) orchestrator: insert ## v<X.Y.Z> after the existing minor block on the appropriate series page. Never touches the API summary. Importable from release-version.ts. |
generate-api-docs.ts |
docs:generate-api |
Renders one minor series' API summary MDX. --title-only rewrites only the frontmatter title (called from the minor freeze flow); --target=<file> overrides the output filename. |
api-docs/extract.ts |
-- | Phase 1: TypeDoc analysis, writes api-data.json |
api-docs/render.ts |
-- | Phase 2: Nunjucks rendering of single-page.njk from api-data.json |
api-docs/audit-tsdoc.ts |
docs:audit-tsdoc |
TSDoc completeness audit (standalone or via extraction) |
generate-release-notes.ts |
docs:generate-release-notes |
Generates / augments the release-notes series MDX. Default mode renders the page from scratch with a ## v<X.Y.0> block; --append-patch inserts a ## v<X.Y.Z> block directly after the minor; --title-only relabels the frontmatter title only. |
update-versions-list.ts |
docs:update-versions |
Rebuilds src/lib/versions.ts from reference/api/v*.x.mdx and reference/release-notes/v*.x.mdx siblings on disk. --latest=X.Y.Z records the precise patch in latest (the selector still labels series-only). |
run-docs-generate.ts |
docs:generate |
Convenience: regenerates the latest summary + refreshes versions.ts using the monorepo SDK's package.json version (no version bump) |
create-version-bundle.ts |
docs:create-version |
Copies the current index.mdx of each versioned section to v<X.Y>.x.mdx (called from release-version-minor.ts) |
lib/release-shared.ts |
-- | Shared helpers for the release orchestrators (version parsing, versions.ts reader, series-sibling resolver, series-name helpers) |
lib/changelog-parser.ts |
-- | Changelog parsing — readChangelogLLMVerbatim for the verbatim per-package render plus legacy parseChangelog / parseChangelogFolder / mergeChangelogs exports kept for unit-test fixtures and ad-hoc tooling |
lib/link-validator.ts |
-- | Internal link extraction + resolution (used by the link-integrity test) |
AI-assisted authoring (drafting descriptions or examples) happens locally via Cursor skills — never inside this pipeline. Output of every script in this table is deterministic.
For fully reproducible
api-data.jsonsetSOURCE_DATE_EPOCHto a fixed Unix timestamp (reproducible-builds convention). Without it,ApiData.generatedAtis the literal string"unspecified"so byte-identity checks still pass.
To customize the generated release notes page for a specific version, create a markdown file at:
docs/website/release-notes-overrides/<version>.md
For example, release-notes-overrides/0.11.0.md. The file should contain ## Heading sections that are injected at the top of the page (after frontmatter, before the per-version ## vX.Y.Z blocks). Useful for highlights, migration guides, or breaking-change callouts that don't fit inside any single package's CHANGELOG_LLM.md. Overrides only apply to full minor renders (default mode), not to the patch append flow.
SDK entry point not found: /path/to/sdk/index.ts
Cause: SDK_PATH is not set or points to the wrong directory.
Fix:
- Verify
.envexists indocs/website/(copy from.env.example) - Ensure
SDK_PATHpoints to the SDK package root containingindex.tsandtsconfig.json - On Windows, use backslashes or forward slashes — both work with Bun
No API functions extracted. Check that:
1. Functions are exported in index.ts
2. Functions have JSDoc comments
3. TypeScript compiles without errors
Cause: TypeDoc couldn't find any exported, documented functions.
Fix:
- Confirm the SDK
index.tsexports public functions - Ensure exported functions have JSDoc comments (TypeDoc skips undocumented items with
excludePrivate) - Check that the SDK's
tsconfig.jsonis valid
Cause: TypeDoc encountered a fatal error parsing the SDK source.
Fix:
- Run
tsc --noEmitin the SDK package to check for TypeScript errors - The generation script uses
skipErrorChecking: true, so minor TS errors are tolerated — this usually indicates a structural issue
Version vX.Y.Z was not found
Cause: update-versions-list.ts ran but the version's MDX file doesn't exist on disk.
Fix: Run docs:generate-api -- <version> --latest (writes index.mdx) or docs:generate-api -- <version> (writes vX.Y.Z.mdx) first, then docs:update-versions. For a full release flow use docs:release-version -- <version> (auto-detects minor vs patch) instead.
The committed content/docs/reference/api/index.mdx is what next build reads. If the build still fails:
- Check that
source.config.tsandnext.config.mjsare valid - Run
bun run buildlocally to reproduce - Look for broken MDX frontmatter or invalid imports in
content/
If a release ran but produced a broken reference/api/index.mdx or reference/release-notes/index.mdx, restore it by re-running the orchestrator against the previous version:
# Auto-detects minor (full freeze + regen) vs patch (title-only + append).
bun run scripts/release-version.ts <previous-X.Y.Z> --force-extractThen revert the bad commit / branch state via git. There is no automatic backup directory — versioning is the safety net (every previous version exists as a sibling vX.Y.Z.mdx).
Cause: A function's JSDoc is missing or malformed.
Fix:
- The generator replaces literal
undefinedstrings with—as a safety net - Validation will throw if descriptions contain
undefinedor[object Object] - Add proper JSDoc to the offending function in the SDK source and regenerate