Source of truth for the SourceBans++ documentation site published at
https://sbpp.github.io/. Built with Astro +
Starlight and deployed via the
sibling sbpp/sbpp.github.io repo (which is now a thin deploy
shell — all authoring happens here).
| Path | What it is |
|---|---|
astro.config.mjs |
Site config, sidebar tree, social links, custom CSS wiring. |
src/content.config.ts |
Astro content-collection schema. Re-exports Starlight's docsLoader + docsSchema so the src/content/docs/ collection is wired with the right loader and front-matter validation. Edit when adding a new collection (none currently). |
src/content/docs/ |
All page content, organised by sidebar group (getting-started/, setup/, troubleshooting/, …). .md for plain pages, .mdx for pages that use Starlight components (Tabs, LinkCard, Card, etc.). |
src/content/docs/sponsor.mdx |
Canonical sponsor landing page reached via the topbar heart icon, the per-page footer link, and .github/FUNDING.yml's custom: URL (issue #1416). Uses template: splash (no sidebar) and is intentionally absent from the sidebar tree in astro.config.mjs. Renders the funding-platforms list + tier-grouped sponsor roll via src/components/Sponsors.astro. |
src/data/sponsors.json |
Canonical source of truth for the project's funding platforms, sponsor tiers, and sponsor roll. Consumed by src/components/Sponsors.astro and (eventually) by the main repo README.md's <!-- sponsors:start --> ... <!-- sponsors:end --> injection markers + the panel footer in companion issue #1417. Adding a new platform (Open Collective, Patreon, …) or a new sponsor is a one-line edit here — no component change required. |
src/styles/sbpp.css |
Panel-parity overrides — brand orange, zinc neutrals, semantic asides, geometry, focus ring. Mirrors web/themes/default/css/theme.css token-for-token. When the panel's :root / html.dark blocks change, mirror the change here in the same PR (AGENTS.md "Keep the docs in sync"). |
src/components/ThemeProvider.astro |
Override of Starlight's stock dark-leaning theme provider. Defaults the unset preference to 'auto' (resolves via prefers-color-scheme) to match the panel's 'system' first-paint default, AND ships a <noscript><style> block that re-applies the LIGHT-mode tokens onto :root[data-theme='dark'] so JS-disabled visitors see light (Starlight 0.30 hardcodes data-theme="dark" SSR'd; the panel paints light without JS, so this guard restores parity). The user toggle still wins on subsequent visits via localStorage['starlight-theme']. |
src/components/Footer.astro |
Override of Starlight's stock per-page footer. Re-renders the stock EditLink + LastUpdated + Pagination + optional Built-with-Starlight kudos (pulled straight from Starlight's virtual: namespace so future Starlight upgrades pick up new footer chrome automatically) and appends a small "Support SourceBans++" affordance below them. The topbar already carries a matching heart-icon social link; the footer is the second surface so anyone who reads to the end of a docs page can find the sponsor link without scrolling back to the top. Both surfaces route to the canonical /sponsor/ landing page (issue #1416), not a single platform URL — adding Open Collective / Patreon later is a data-only change on src/data/sponsors.json. |
src/components/Sponsors.astro |
Reusable component that renders the funding-platforms list + tier-grouped sponsor roll from src/data/sponsors.json. Used on sponsor.mdx today; index.mdx (and eventually the panel footer from companion issue #1417) can embed it with showPlatforms={false} to reuse just the tier-grouped list. Two props: showPlatforms (default true) gates the platforms section, showEmptyHint (default true) gates the "be the first" line when the sponsor roll is empty. |
src/assets/logo.svg + public/favicon.svg |
The panel's brand mark, copied verbatim from web/themes/default/images/favicon.svg. |
src/assets/auto/{install,panel}/ |
Auto-captured screenshots from docs/scripts/capture.mjs. These ARE committed so the screenshot diff lands with the UI change. |
scripts/capture.mjs |
Playwright-driven capture script (see Capturing screenshots below). |
tsconfig.json |
Extends astro/tsconfigs/strict. |
Standard Astro dev loop. Node 20 LTS or newer.
cd docs
npm install
npm run devThe dev server prints a localhost URL (default http://localhost:4321).
Edits to anything under src/ hot-reload without a restart.
To produce a production build:
cd docs
npm run build
npm run preview # serve the built site locallyThe production build runs Pagefind under the hood and writes the
search index into dist/pagefind/. The deploy shell in
sbpp.github.io picks this up and serves it as-is.
Auto-captured screenshots live under src/assets/auto/. The capture
script needs the dev stack running:
# from the repo root
./sbpp.sh up
# wait for the panel to come up at http://localhost:8080
# (admin/admin login is seeded automatically)
cd docs
npm install # first time only
npx playwright install chromium # first time only
npm run captureThe script writes PNGs into src/assets/auto/install/ and
src/assets/auto/panel/. Inspect git diff src/assets/auto/ to see
what changed; commit the deltas alongside the UI change that produced
them.
Capture geometry:
- Viewport: 1920×1080 (Full HD), full-page screenshots. The rendered PNG carries the full scrollable surface so high-DPI / zoomed-in inspection works without re-running the capture. The docs site renders the PNGs at responsive widths.
- Panel routes are captured TWICE — once light, once dark.
Each route emits a
<route>-light.pngand a<route>-dark.png(e.g.panel-02-dashboard-light.png/panel-02-dashboard-dark.png). The dark pass seedslocalStorage['sbpp-theme']before navigation so the inline FOUC bootloader incore/header.tpllands thedarkclass on<html>on the first paint (no white flash). See AGENTS.md "Anti-FOUC theme bootloader" for the contract. - Install routes are captured ONCE in light. The wizard's
_chrome.tpldoesn't loadtheme.jsor the FOUC bootloader (the wizard has no theme toggle by design — see AGENTS.md "Install wizard"); a "dark install" capture would just be the same light render with a different filename. - The output subdirectory is wiped at the start of each run.
CAPTURE_ROUTES=panelclearssrc/assets/auto/panel/before capturing,=installclearssrc/assets/auto/install/, and the default=allclears both. Stale PNGs from earlier runs (route list changes, pre-dual-theme legacy filenames) don't linger in the committed diff.
The hardcoded STEAM_API_KEY is 00000000000000000000000000000000
(an all-zero dummy) — the dev seed never round-trips back to Steam,
so the zero key is safe and avoids leaking real keys into screenshots.
To override the panel URL (e.g. running a parallel stack on a different port — see AGENTS.md "Parallel stacks"):
PANEL_URL=http://localhost:8189 npm run captureCAPTURE_ROUTES picks which route group to capture; defaults to
all (both groups). Useful when iterating on a specific surface:
CAPTURE_ROUTES=panel npm run capture # post-install panel only
CAPTURE_ROUTES=install npm run capture # wizard surfaces onlyThe install captures have a sharp edge worth knowing about. The
dev stack's entrypoint (docker/php/web-entrypoint.sh) creates
web/config.php on first boot, and the install wizard's #1335 C2
guard refuses to start whenever config.php exists — it renders a
static 409 "already installed" page instead of step 1. Running
CAPTURE_ROUTES=install against a default-boot dev stack therefore
silently screenshots that 409 page for every install route, which
is almost never what you want.
To capture real wizard surfaces locally, move config.php aside
for the duration of the install pass and restore it afterwards:
# from the repo root, with the dev stack already running
mv web/config.php .sbpp-config-stash.php
( cd docs && CAPTURE_ROUTES=install npm run capture )
mv .sbpp-config-stash.php web/config.phpThe stash path is a sibling of web/ on purpose. The dev container's
entrypoint runs as root, so web/config.php lands on the bind mount
owned by root:root; stashing through /tmp is a sticky-bit trap —
the same-filesystem rename preserves ownership, and the unprivileged
restore can't unlink a root-owned file out of a sticky directory
(per rename(2)'s EPERM semantics — see the workflow's stash step
for the full rationale). .sbpp-config-stash.php is gitignored so an
interrupted run never leaks the credentials into a commit.
CI does the same stash/restore dance automatically in
docs-screenshots-capture.yml; this is only relevant when iterating
locally.
Four workflows under .github/workflows/ cover the docs site:
| Workflow | Trigger | What it does |
|---|---|---|
docs-build.yml |
PRs + main pushes touching docs/** |
Runs npm run build. Uploads the built dist/ as an artifact. |
docs-deploy-trigger.yml |
main pushes touching docs/** |
Fires a repository_dispatch (event_type=docs-changed) into sbpp/sbpp.github.io, which kicks the actual GitHub Pages deploy. Requires the DOCS_DEPLOY_PAT repo secret (fine-grained PAT, Actions: Read and write on sbpp.github.io only). Until the secret is set, the dispatch step is skipped on every run (the run is green-with-skipped, not red-failing); the deploy shell in sbpp.github.io still has a workflow_dispatch button as a manual fallback. |
docs-screenshots-build.yml |
PRs touching docs/scripts/capture.mjs or docs/package*.json |
Sandboxed verification: npm ci + node --check scripts/capture.mjs. No secrets, no write permissions; runs the standard pull_request token. Catches "did the capture script still parse" on every PR. |
docs-screenshots-capture.yml |
PRs labelled safe-to-screenshot (same-repo only) + workflow_dispatch |
Boots the dev stack, seeds the DB, runs npm run capture from a TRUSTED-FROM-MAIN checkout, commits PNG deltas back to the PR branch. |
docs-screenshots-capture.yml runs pull_request_target with
contents: write so it can write the regenerated PNGs back to the PR
branch. To keep that token out of contributor reach, the workflow:
- Splits the checkout. The trusted code surface (the capture
script + its package-lock.json) is checked out from the PR's base
branch (effectively
main). The PR head is checked out into a separate directory that's used only fordocker compose upand as the screenshot output destination — no JS/PHP code from the PR head runs on the runner. - Gates on the
safe-to-screenshotlabel. A maintainer applies the label after reviewing the PR's docker / install / panel changes; the workflow'sif:guard short-circuits without it. - Auto-strips the label on every push. A
unlabel-on-synchronizejob removessafe-to-screenshotwhenever a new commit lands so the maintainer must re-apply after reviewing the new code. A label applied to a benign-looking opening commit doesn't grant blanket consent for a follow-up commit. - Refuses fork PRs. The
head.repo.full_name == github.repositorycheck rejects fork-originated PRs at the workflow level — pushing the branch into the upstream repo first is the supported path for contributions that need screenshot regeneration.
Maintainers: when you apply safe-to-screenshot, eyeball the PR's
diff under docker/, web/install/, the web/themes/default/
templates, and docs/scripts/capture.mjs first. Anything that could
exfil environment data or escape the docker sandbox is the threat
model; everything else is fine.
The label needs to exist in the repo first — until that happens (one-
time repo setup), the workflow runs but its if: gate silently
returns false. Create the label via the repo's Issues → Labels page
(or gh label create safe-to-screenshot --description "Maintainer ack to run docs-screenshots-capture.yml" --color 'D73A4A').
- Plain Markdown by default. Use
.mdxonly when the page needs Starlight components (Tabs,LinkCard,CardGrid,Asideas a component,Steps, etc.). - For prose asides, prefer the Markdown-native
:::note/:::tip/:::caution/:::dangersyntax over<Aside>. Both render the same; the prose form keeps Markdown files readable in plain editors. - Cross-link to the most relevant troubleshooting / setup page on
any step that has a known failure mode (DB step → Database errors
- Could not find driver, write-perms step → Browser freeze / Cloudflare, etc.). Cross-link asides are not optional polish — they're the difference between "you're stuck" and "click here".
- Internal links use absolute paths with a trailing slash:
[Quickstart](/getting-started/quickstart/). Starlight's link resolver rewrites these onto the configuredbase. - External links open in the same tab; Starlight applies
rel="noopener"+ an external-link affordance automatically. - Code-block languages use Shiki names (
sh,php,sql,ini,yaml,json,text). For SourceMod KeyValues files, useini— it's structurally close enough to highlight cleanly. - Headings: each page has exactly one
# H1(set via the front-mattertitle); body content starts at## H2. Skipping levels (e.g.## H2→#### H4) breaks Starlight's auto-ToC.
These docs live in sbpp/sourcebans-pp under docs/.
The site at https://sbpp.github.io/ is published from there by CI
on every merge to main. Open PRs against this directory; the deploy
shell repo doesn't accept content PRs anymore.