Skip to content

docs: migrate to Starlight + merge authoring into sourcebans-pp (#1333)#1339

Merged
rumblefrog merged 17 commits into
mainfrom
docs/migrate-to-starlight-1333
May 11, 2026
Merged

docs: migrate to Starlight + merge authoring into sourcebans-pp (#1333)#1339
rumblefrog merged 17 commits into
mainfrom
docs/migrate-to-starlight-1333

Conversation

@rumblefrog

Copy link
Copy Markdown
Member

Closes #1333.

Migrates the SourceBans++ docs from Hugo (separate sbpp/sbpp.github.io repo) to Astro + Starlight, with the source of truth landing under docs/ in this repo. The deploy shell at sbpp.github.io is updated in the sibling PR (cutover step 1 below).

What ships in this PR (9 workstreams)

  1. Scaffolddocs/ Astro project: Astro 5 + Starlight 0.30 + Pagefind (built into Starlight). tsconfig.json extends astro/tsconfigs/strict. Brand mark reused verbatim from web/themes/default/images/favicon.svg (orange shield + cross). Dependabot has an npm ecosystem entry for docs/ on the same monthly cadence as the existing composer entry.

  2. Themedocs/src/styles/sbpp.css mirrors theme.css's :root and html.dark blocks token-for-token (brand-50…950, zinc-50…950, semantic asides info / success / warning / danger with their *-bg soft fills, geometry, focus ring). Aside repaints wire :::note → info, :::tip → success, :::caution → warning, :::danger → danger. color-scheme: light/dark declared per scope. A custom ThemeProvider.astro defaults the unset preference to 'auto' (matching the panel's localStorage.getItem('sbpp-theme') || 'system'); a <noscript><style> block re-applies the LIGHT-mode tokens to :root[data-theme='dark'] so JS-disabled visitors see the same first-paint as the panel.

  3. Content port — All 13 source pages from sbpp.github.io/content/docs/ plus the FAQ index (14 total) ported to YAML front matter + idiomatic Markdown. Hugo-isms stripped (bref / weight / toc / draft / translationKey). <mark> / <samp> → backticks. > Note: → Starlight asides. /docs/foo → new IA routes (kebab-case slugs). PHP version bumped to 8.5 in the new docs to match the current web/composer.json floor (post-Bump minimum PHP version to 8.5 #1289).

  4. UX folds — Tabs on quickstart (Linux / Windows / Shared hosting / Docker; chmod block only in the Linux tab, with per-path comments above each chmod line explaining why each path needs to be writable). Tabs on setup/mariadb (Ubuntu repo / official repo / MySQL, plus MariaDB-or-MySQL-8 vs MySQL-5.x). Tabs on prerequisites (MariaDB / MySQL). "What's next" <CardGrid> footer on quickstart linking to adding-server / plugin-setup / discord-forward-setup. Cross-link asides on every step that has a known failure mode.

  5. Legacy soft-archivelegacy/index.md carries the issue's verbatim caution banner; the pre-1.5.4.7 plugin upgrade content moved into legacy/plugin-pre-1.5.4.7.md (out of updating/index.md). Section is excluded from the main sidebar per spec; reachable via a footer link emitted by the new Footer.astro component override.

  6. CI workflows — Three workflows under .github/workflows/:

    • docs-build.yml — push-to-main + PR, paths-gated to docs/**, Node 20, npm ci, builds, uploads dist/ as artifact.
    • docs-deploy-trigger.yml — push-to-main only. Mints installation token via actions/create-github-app-token@v1 and fires repository_dispatch (event_type=docs-changed) into sbpp.github.io with client_payload[source_sha].
    • docs-screenshots-build.yml (sandboxed pull_request) + docs-screenshots-capture.yml (label-gated pull_request_target that refuses fork PRs and runs the capture script from the PR base branch — see security model below).
  7. Auto-capture scriptdocs/scripts/capture.mjs is a runnable Playwright skeleton with the install routes that the URL-only approach actually reaches cold (1, 2, 5) plus 5 panel routes. Stable filename slugs. Hardcoded all-zero STEAM_API_KEY. npm run capture script wired. Routes 3/4/6 of the install wizard need a POST-handoff drive (the wizard re-validates the prefix at the top of every later step and bounces direct deep-links back to step 2); follow-up issue.

  8. AGENTS.md — All 4 rows from Docs: migrate to Starlight and merge authoring into sourcebans-pp #1333 §8 added verbatim to the "Keep the docs in sync" table. The quick-rule bullet appended verbatim to "Quick rules".

  9. Bookkeepingdocs/README.md documents local dev, build, capture, the workflows in a table, the safe-to-screenshot label note + gh label create command, and authoring conventions.

Review-fix changes (commits 9653c27319b88e)

A second pass addressed every blocker and major from the adversarial review:

  • Security: screenshot workflow split. The original docs-screenshots.yml ran pull_request_target with contents: write, then npm ci'd PR-head code. Replaced with two workflows: docs-screenshots-build.yml (sandboxed pull_request, no secrets, no writes — verifies the script parses on every PR) and docs-screenshots-capture.yml (gated on a maintainer-applied safe-to-screenshot label that auto-strips on every push, refuses fork PRs, runs the capture script from the PR base branch / writes outputs into the PR head). See docs/README.md "Screenshot capture security model" for the full threat model.
  • Legacy section restructured per spec. Pre-1.5.4.7 plugin upgrade content moved into legacy/plugin-pre-1.5.4.7.md; section excluded from the main sidebar; new Footer.astro override surfaces a "Legacy docs" affordance on every page.
  • Theme parity tightened. ThemeProvider.astro defaults to 'auto' (matching the panel's 'system' default) AND ships a <noscript><style> block so JS-disabled visitors see light first paint, matching the panel's no-JS behavior.
  • astro-mermaid dropped. No docs page actually uses a fenced ```mermaid block; integration + dep removed (115 packages dropped from the lockfile). Re-add in the same PR as the first diagram surface.
  • Worker-authored pages flagged. setup/plugin-setup.md and getting-started/prerequisites.mdx carry top-of-page :::note banners; sbpp_admcfg.smx added to plugin list (was missing); mbstring added to prerequisites extension list (the install wizard checks for it).
  • CSS RTL safety. border-leftborder-inline-start on aside repaints (matters for the future LLM-translation pipeline).

Cutover prerequisites (NOT in this PR)

These are repo-level config / org-level App setup that must land before the workflows are useful. Workflow top-of-file comments + docs/README.md all surface these.

  1. Sibling repo reset. sbpp/sbpp.github.io@main reset to a thin deploy shell + deploy.yml. See sibling PR ➜ sbpp/sbpp.github.io#NN (link below once filed). GitHub Pages source flipped from "Deploy from a branch" → "GitHub Actions" in the Pages settings.

  2. sbpp-docs-deploy GitHub App. Org-owned, scope Actions: write on sbpp.github.io only, installed on sbpp/sbpp.github.io. Repo VARIABLE DOCS_DEPLOY_APP_ID and repo SECRET DOCS_DEPLOY_APP_KEY set on sbpp/sourcebans-pp.

  3. Create the safe-to-screenshot label on this repo so docs-screenshots-capture.yml's if: gate has a label to trigger on:

    gh label create safe-to-screenshot \
      --description \"Maintainer ack to run docs-screenshots-capture.yml\" \
      --color 'D73A4A'

Validation done

  • cd docs && npm install — succeeds; lockfile committed.
  • cd docs && npm run build — exits 0, builds 20 pages, Pagefind indexes 19, sitemap-index.xml emitted.
  • cd docs && npx astro check — 0 errors, 0 warnings, 0 hints.

Known follow-ups (separate issues)

rumblefrog added 17 commits May 10, 2026 20:53
Workstream 1 of the Hugo→Starlight migration. New docs/ directory
holds the Astro + Starlight project that publishes to sbpp.github.io
once the cutover lands. The sibling repo becomes a thin deploy
shell; source of truth lives here so user-facing changes ship with
their docs in the same PR.

Pieces in this commit:

* docs/package.json — astro 5 + @astrojs/starlight + astro-mermaid
  + @playwright/test (drives docs/scripts/capture.mjs in a later
  commit) + sharp (Astro asset pipeline). Pagefind ships inside
  Starlight; no separate dep.
* docs/astro.config.mjs — site=https://sbpp.github.io/, base=/,
  full sidebar tree (Getting Started, Setup, Updating,
  Troubleshooting, Integrations, Customization, FAQ, Legacy),
  GitHub + Discord social links, edit-link back to this repo.
  ThemeProvider override (next bullet) is wired here so first
  paint matches the panel.
* docs/src/components/ThemeProvider.astro — overrides Starlight's
  stock dark-leaning fallback with a default of 'light'. Mirrors
  the panel's first-paint behavior; user toggle still wins on
  subsequent visits via localStorage['starlight-theme'].
* docs/src/assets/logo.svg + docs/public/favicon.svg — same
  brand mark as the panel (web/themes/default/images/favicon.svg).
* docs/src/assets/auto/{install,panel}/.gitkeep — destination for
  the auto-capture pipeline (Workstream 7); committed empty so
  layout is in place before the first capture run.
* docs/tsconfig.json — extends astro/tsconfigs/strict.
* .gitignore — append docs/{node_modules,dist,.astro,pnpm-lock}.
  Auto-captured screenshots under docs/src/assets/auto/ stay
  TRACKED so the screenshot diff lands with the UI change.
* .github/dependabot.yml — add npm ecosystem rooted at /docs/.

Theme tokens land in the next commit (Workstream 2).
Workstream 2. New docs/src/styles/sbpp.css carries the panel-parity
overrides wired in via the customCss array in astro.config.mjs.

The mapping is token-for-token from web/themes/default/css/theme.css
(panel :root + html.dark blocks → Starlight's --sl-color-* /
--sl-color-bg-* / --sl-color-asides-* hooks):

* Brand orange (--brand-50 … --brand-950) → --sl-color-accent triplet
  + accent-text. Light mode uses --brand-600 as primary / --brand-700
  as hover/strong (the same AA-contrast trade-off the panel made).
  Dark mode uses --brand-600 as primary / --brand-400 as text-accent
  for legibility on the zinc-950 surface.
* Zinc neutrals → --sl-color-gray-1..7 + page/surface/sidebar bgs.
* Semantic asides (:::note / :::tip / :::caution / :::danger) repaint
  to the panel's --info / --success / --warning / --danger pairs +
  matching soft fills. Light mode uses the explicit hex pairs from
  theme.css (#2563eb/#eff6ff for info, etc.); dark mode mirrors the
  rgb(<hex> / 0.4) recipe theme.css lines 97-101 use.
* Geometry — radii (--sbpp-radius-{sm,md,lg,xl}) + shadow scales +
  the focus-ring outline (`2px solid var(--sl-color-accent);
  outline-offset: 2px`) ported across so cards / code blocks /
  buttons / asides have the same shape language as the panel.
* color-scheme: light / color-scheme: dark mirrored from theme.css
  line 44 / 89 (the #1309 declaration that lets browsers paint
  native UA surfaces — open <select> panels, scrollbars, autofill,
  date pickers — in the matching scheme).
* ::selection paints orange in both modes so the brand is visible
  even on incidental highlight.

The override deliberately skips font wiring (per #1333 §2 — color
identity carries the "one product" feel; matching fonts adds asset-
sharing complexity for diminishing return). The panel ships
vendored Inter + JetBrains Mono; Starlight stays on its defaults.

The header comment + AGENTS.md row added in a later commit are how
future maintainers know to mirror panel-side changes here.
Workstreams 3 + 4 of the migration. The two are entangled — the
tabs/asides/cross-link landings ARE the content port for the pages
that use them — so they ship together.

13 source pages converted from Hugo TOML front matter to Starlight
YAML, plus a landing page (index.mdx, splash template) and the
legacy soft-archive landing (workstream 5). Final layout:

* getting-started/quickstart.mdx     ← quickstart.md (TABS)
* getting-started/prerequisites.mdx  ← extracted §Prerequisites + new (TABS)
* setup/adding-server.md             ← adding_server.md
* setup/plugin-setup.md              ← new (was a "see Quickstart" stub)
* setup/ports.md                     ← ports.md
* setup/mariadb.mdx                  ← mariadb.md (TABS)
* updating/index.md                  ← updating.md
* troubleshooting/browser-freeze.md            ← browser_freeze.md
* troubleshooting/could-not-find-driver.md     ← could_not_find_driver.md
* troubleshooting/database-errors.md           ← database_errors.md
* troubleshooting/debugging-connection.md      ← debugging_connection.md
* integrations/discord-forward-setup.md        ← discord_forward_setup.md
* customization/removing-default-message.md    ← removing_default_message.md
* customization/translating.md                 ← translating.md
* faq/index.md                       ← faq/_index.md
* faq/inquiries.md                   ← inquiries.md (placed under FAQ
                                        per the issue's "your judgment"
                                        — both are Q&A-shaped and FAQ
                                        is the more discoverable home)
* legacy/index.md                    ← workstream 5 (caution-banner stub)
* index.mdx                          ← new landing (splash template)

Per-page port rules applied (workstream 3):

* TOML +++…+++ → YAML ---…---. Dropped: bref (became `description`
  when not already set), weight (→ sidebar.order), toc, draft,
  translationKey.
* HTML-in-Markdown swept: <mark>X</mark> → `X` (inline code) for
  placeholder values (Server Hostname, RCON Password, etc.);
  <samp>X</samp> → `X` (inline code) for example URLs and
  command names; <a target="_blank_"> stripped (Starlight handles
  external link affordances).
* `> Note:` blockquotes promoted to Starlight asides:
  - 1.6.x warning in quickstart      → :::caution
  - Cloudflare/Rocket Loader note    → :::caution
  - Smarty 5 {php} → {load_template} → :::caution
  - "you can…" suggestions           → :::tip
  - Discord forwarder web/in-game    → :::note
* Internal links rewritten from /docs/foo to the new IA:
  - /docs/quickstart        → /getting-started/quickstart/
  - /docs/mariadb           → /setup/mariadb/ (+ #granting-permission)
  - /docs/adding_server     → /setup/adding-server/
  - /docs/updating          → /updating/
  - /docs/inquiries         → /faq/inquiries/
  - {{< ref "mariadb.md#granting-permission" >}} ditto
  - {{< ref "docs/quickstart.md" >}} ditto

UX folds (workstream 4):

* Tabs in quickstart.mdx (Linux/Windows/Shared/Docker) — chmod block
  ONLY in the Linux tab per the issue.
* Tabs in mariadb.mdx (Ubuntu repo / official repo / MySQL) plus a
  paired tabs block for the GRANT syntax (MariaDB-MySQL8+ vs MySQL-5.x).
* Tabs in prerequisites.mdx for the MariaDB vs MySQL DB-choice section.
* Cross-link asides on every step with a known failure mode:
  - DB step → /troubleshooting/database-errors/ +
    /troubleshooting/could-not-find-driver/
  - SteamID format → :::tip with a tooling pointer
  - Cloudflare/Rocket Loader → caution + link to
    /troubleshooting/browser-freeze/
  - 1.6.x preflight on quickstart → caution + link to /updating/
* "What's next" footer on quickstart.mdx — LinkCard grid pointing to
  adding-server, plugin-setup, discord-forward-setup.
* Landing page (index.mdx) ships a splash hero + 4-card grid
  (Quickstart / Adding Server / Troubleshooting / Updating).
* Code copy buttons inherited from Starlight default — no opt-out.

Other notes:

* PHP version reference is the current floor (>= 8.5, per
  web/composer.json — AGENTS.md says PHP 8.5 floor). The
  legacy 1.7.0 step in updating/index.md keeps the historical
  ">= 8.2" wording for context but the prerequisites page cites the
  current floor.
* Inquiries kept under /faq/ rather than under Customization;
  every entry is a Q&A and reads more naturally next to FAQ proper.
* `kv` (KeyValues) codeblock language tags swapped to `ini` for
  Shiki compatibility — KeyValues is structurally close enough that
  the highlighter doesn't get confused.
* docs/src/content.config.ts pinned to Starlight's stock docsLoader
  + docsSchema; no custom front-matter validators.

The legacy soft-archive lands as a single index.md (workstream 5);
specific upgrade-from-X content moves into legacy/ in future PRs as
old paths are documented.
Workstream 6. Three new workflows under .github/workflows/, each
gated on docs/** path filters so they stay out of the way of PRs
that don't touch the site.

* docs-build.yml — runs `npm run build` on PRs + main pushes that
  touch docs/. Uploads the built dist/ as an artifact (7-day
  retention) so reviewers can poke at chrome locally without
  rebuilding. Falls back to `npm install` until the lockfile
  ships in a follow-up.

* docs-deploy-trigger.yml — runs on main pushes only, fires a
  repository_dispatch (event_type=docs-changed) into
  sbpp.github.io via a GitHub App-minted token. Uses
  actions/create-github-app-token@v1 with vars.DOCS_DEPLOY_APP_ID
  + secrets.DOCS_DEPLOY_APP_KEY (one-time setup before the workflow
  can succeed; documented inline + in the PR description).
  Concurrency group=docs-deploy-trigger so coalesced pushes don't
  pile up dispatches.

* docs-screenshots.yml — runs on `affects-ui`-labelled PRs +
  workflow_dispatch. Boots the dev stack via `docker compose up
  -d --wait`, runs `npm run capture` (lands in workstream 7),
  tears down via `docker compose down -v`, then auto-commits any
  PNG deltas back to the PR branch via stefanzweifel/git-auto-commit-action@v5.
  Uses pull_request_target so it can write to forks; pinned to
  the PR head SHA so a malicious PR can't swap the ref under us.
  STEAM_API_KEY is the hardcoded all-zero dummy from #1333 §7
  (the dev seed never round-trips back to Steam).

All three carry top-of-file comments explaining purpose, cadence,
and any required repo-side configuration. The capture script and
the AGENTS.md row that mentions the `affects-ui` label land in
later commits.
Workstream 7. New docs/scripts/capture.mjs drives Playwright +
Chromium against the local dev stack to grab installer + panel
screenshots. Wired via `npm run capture` in docs/package.json
(landed in the scaffold commit).

Output paths:
  docs/src/assets/auto/install/install-NN-<step>.png
  docs/src/assets/auto/panel/panel-NN-<page>.png

Stable filenames so docs pages can reference these paths once and
not re-edit when a screenshot regenerates.

The script defaults:
  PANEL_URL          → http://localhost:8080
  STEAM_API_KEY      → 00000000000000000000000000000000  (#1333 §7)
  ADMIN_USER / PASS  → admin / admin
  VIEWPORT           → 1280×800

The route list ships as a SKELETON for the first PR — install steps
1-6 + five panel routes (login, dashboard, banlist, servers,
admin-dashboard). Several routes carry inline TODOs because they
need actual click-through driving (the wizard handoff between
steps 2-4 + the login form for admin-only routes), and the
shipped login routine here is a best-effort that falls through if
the form doesn't paint as expected.

This is intentional: the screenshot ergonomics aren't the load-
bearing part of the migration, and `docs-screenshots.yml` runs
against this script wholesale, so a follow-up PR can flesh out
each route without touching the CI plumbing.

Documentation for the prerequisites + run sequence lands in
docs/README.md in the bookkeeping commit.
Workstream 8. Three additions to the existing AGENTS.md surface so
agents working on the panel know to mirror their changes into docs/
in the same PR:

* Five new rows in the "Keep the docs in sync" table covering the
  user-facing change axes the docs site is the source of truth for:
  install / upgrade / troubleshooting flows, config knobs (paired
  with UPGRADING.md when breaking), new self-hoster-visible
  features (with a sidebar entry in astro.config.mjs), the
  `affects-ui` label for screenshot-relevant PRs, and the
  panel-theme-token-mirror rule for web/themes/default/css/theme.css
  ↔ docs/src/styles/sbpp.css.

* One new "Quick rule" bullet that elevates the same expectation
  to a top-level rule: docs ship in the same PR as the user-facing
  change, screenshots auto-capture, theme tokens mirror.

* Two new "Where to find what" rows: one for editing / adding a
  docs page (covers the .md vs .mdx split, sidebar wiring, dev
  loop, CI gates, source-of-truth contract), one for the
  screenshot capture pipeline (Playwright, output paths, CI
  trigger, the all-zero STEAM_API_KEY).

All wording verbatim from the issue body where the issue gave a
template, otherwise hewing to the AGENTS.md voice (prescriptive,
one-liner-by-default with the "where to look" pointer).
Workstream 9 of the migration. Onboarding doc for the new docs/
project — covers:

* Where things live (per-directory map of the Astro + Starlight
  source tree, with the panel-parity contract called out for
  src/styles/sbpp.css).
* Local dev loop (`cd docs && npm install && npm run dev`).
* Capture story (`./sbpp.sh up && cd docs && npm run capture`),
  including the all-zero STEAM_API_KEY note from #1333 §7 and the
  PANEL_URL override for parallel-stack worktrees.
* CI workflow table (docs-build, docs-deploy-trigger,
  docs-screenshots) with the one-time setup notes for the
  GitHub App + the `affects-ui` label.
* Authoring conventions — .md vs .mdx split, prefer Markdown-
  native :::note over <Aside>, cross-link asides, absolute-path
  internal links, Shiki language names (KV → ini), heading-level
  contract for the auto-ToC.
* Source-of-truth statement so contributors arriving from
  sbpp.github.io land in the right place.

This is the bookkeeping commit. The PR description (drafted in
the orchestrator's PR-open step) carries the equivalent of the
"requires DOCS_DEPLOY_APP_ID/KEY + affects-ui label setup" notes
for reviewers.
…ckfile (#1333)

`npm run build` was failing in two places that only surface once
the dependency tree is actually installed:

1. **`social` config shape.** Starlight 0.30 takes `social` as a
   `Record<KnownPlatform, url>` (see
   `node_modules/@astrojs/starlight/schemas/social.ts`); the
   `[{icon, label, href}]` array shape was added in a later minor.
   Swap the array literal for the record shape and pin the gotcha
   in a comment so a future `^0.30.0` → `^0.32.0` bump knows to
   re-flip it.

2. **MDX autolink.** `index.mdx` had `<https://sbpp.github.io/>` —
   plain Markdown's autolink syntax, but MDX parses `<...>` as a
   JSX tag and bails on the `/`. Swap to a labelled
   `[sbpp.github.io](https://sbpp.github.io/)` link.

3. **Commit the lockfile.** `npm install` generated a
   `package-lock.json` that the workflows (`docs-build.yml`,
   `docs-screenshots.yml`) explicitly check for to drop into the
   `npm ci` deterministic-install path. Without the lockfile the
   workflows fall back to `npm install` and emit a `::warning::`
   on every run; committing it now both makes CI quiet and pins
   the dep tree across environments.

Verified locally: `npm run build` exits 0, builds 19 pages,
Pagefind indexes 18 of them (the 404 doesn't index by design),
sitemap-index.xml is generated, `astro check` reports
0 errors / 0 warnings / 0 hints.
…ted-capture (#1333; B1, B2, m2, m3 partial, m4)

The original docs-screenshots.yml ran `pull_request_target` with
`contents: write`, then checked out the PR head and `npm ci`'d it. Any
contributor PR could exfil the upstream repo's GITHUB_TOKEN through a
postinstall hook or a tweaked capture.mjs (B1). The auto-commit step
also wouldn't have worked on fork PRs — wrong checkout shape (B2).

Split into two workflows:

- `docs-screenshots-build.yml` runs on every `pull_request` that
  touches the capture script or its manifests. No secrets, no write
  permissions, standard `pull_request` token. `npm ci` + `node --check`
  on the script: catches "did this change still parse" cheaply.

- `docs-screenshots-capture.yml` does the actual stack boot + capture +
  auto-commit. Defenses:
  * Trusted code (capture.mjs + lockfile + node_modules) checked out
    from the PR's BASE branch, NOT the PR head. `npm ci` postinstall
    scripts always come from main.
  * PR head checked out into a separate `pr-head/` directory used only
    for `docker compose up` (sandboxed by docker) and as the
    screenshot output destination. No JS/PHP code from the PR head
    runs on the runner.
  * Gated on the `safe-to-screenshot` label (maintainer ack).
  * `unlabel-on-synchronize` job auto-strips the label on every push
    so a maintainer must re-apply after each commit set — a benign
    opening commit can't grant blanket consent for follow-ups.
  * Refuses fork PRs at the workflow `if:` level (canonical
    "compromise the upstream maintainer" shape). Push the branch
    upstream first if the contribution needs screenshot regen.
  * `git-auto-commit-action@v5` runs from `pr-head/` with the
    canonical fork-PR checkout shape (`repository:` + `ref: head_ref`
    + `persist-credentials: true`). The fork-rejection gate above
    means in practice we always push to the upstream repo, but the
    canonical shape means a future opt-in fork support change
    doesn't silently break the push (B2 fix).

`capture.mjs` grows a `CAPTURE_OUT_OVERRIDE` env var so the trusted
script (running in `trusted/docs/`) can write into the PR head's
`docs/src/assets/auto/` directory. Default behavior unchanged for
local runs.

Also folds in `m2` (call `./sbpp.sh db-seed` between stack boot and
capture so post-install panel routes screenshot the populated state),
`m3 partial` (prune install routes to the three reachable cold —
licence, db-details, admin-create — defer steps 3/4/6 to a follow-up
that drives the POST handoff chain), and `m4` (drop the stale "needs
login" TODO from `panel-05-admin-dashboard` — `loginAsAdmin` already
runs before the panel routes).

Updates `docs/README.md` (CI table + new "Screenshot capture security
model" section) and `AGENTS.md` (rule + quick-rules bullet) to point
at the new label name and the local-run-first guidance.
Three things wrong with the worker's first cut:

- B3: the pre-1.5.4.7 plugin upgrade content was left in
  `updating/index.md` (lines 86-106 of the worker's diff) instead of
  being moved into `legacy/`. Issue #1333 §5 explicitly says: "The
  pre-1.7 / pre-1.8 upgrade paths from `updating.md` lines 51-60 move
  here." Moved the section to a new `legacy/plugin-pre-1.5.4.7.md`
  page; left a one-line "see Legacy → Plugin upgrade from <= 1.5.4.7"
  pointer in `updating/index.md` so users still find it.

- B4: the worker's `astro.config.mjs` added the Legacy group to the
  main sidebar. Same §5 line: "Legacy section is excluded from the
  main sidebar but reachable from a footer link and from the live
  `Updating` page." Removed the sidebar group; left a comment
  pointing at the override + the inline updating link as the two
  surviving discovery paths.

- B5: there was no chrome-level discovery hook for /legacy/ — the
  spec says "footer link". Added `src/components/Footer.astro` that
  re-renders Starlight's stock per-page footer (EditLink +
  LastUpdated + Pagination + the optional Built-with-Starlight kudos)
  and appends a small "Legacy docs" affordance below it. We re-export
  Starlight's stock sub-components from the `virtual:` namespace
  rather than re-implementing them so a Starlight upgrade that adds
  new footer affordances picks them up automatically. Wired into
  `astro.config.mjs` as `components: { Footer: ... }`.

- m5: the legacy banner copy was a worker rewrite ("These pages cover
  legacy SourceBans++ versions that are no longer actively
  maintained..."). Replaced with the issue's verbatim wording:
  "Documentation in this section refers to versions that are no
  longer maintained. The information may be inaccurate or out of
  date." (#1333 §5). Removed the "will be pruned in a future cleanup
  pass" sentence to match.

Updated the legacy index's "Pages" section to actually list the new
`/legacy/plugin-pre-1.5.4.7/` page instead of saying "empty for now".
…#1333; M1, M7, m9)

Two paint mismatches between docs and panel chrome, both load-bearing
for the "feels like one product" goal:

- M1: ThemeProvider's `else` branch (no localStorage entry yet) was
  setting theme to 'light'. The panel reads
  `localStorage.getItem('sbpp-theme') || 'system'` and resolves
  'system' via `prefers-color-scheme` (web/themes/default/js/theme.js
  lines 19-30). Dark-OS user landed on docs LIGHT then panel DARK —
  cross-product mismatch. Flipped the default to 'auto' so both
  surfaces honor system preference on first paint.

- m9: that fix also makes the previously-unreachable `'auto'` arm of
  the resolved-theme expression reachable, which is the whole point.

- M7: JS-disabled visitors landed on dark unconditionally because
  Starlight 0.30 hardcodes `<html data-theme="dark">` SSR'd into
  every page (node_modules/@astrojs/starlight/components/Page.astro
  line 39), and the worker's ThemeProvider script is JS-only. The
  panel paints LIGHT for no-JS visitors (no `prefers-color-scheme`
  media query in `theme.css`), so docs and panel diverged here too.
  Added a `<noscript><style>` block to ThemeProvider.astro that
  re-applies the LIGHT-mode color tokens to `:root[data-theme='dark']`
  so the cascade lands on light when JS isn't available. The block
  duplicates the LIGHT token list from sbpp.css; the file's leading
  comment carries the "keep these in sync" rule.

Updated `docs/README.md`'s "Where things live" table to describe the
new dual behavior + flag the noscript-block-must-mirror-sbpp.css
maintenance contract.
The integration was scaffolded into the initial migration commit but
zero docs pages actually use a fenced \`\`\`mermaid block:

    rg '\`\`\`mermaid' docs/src/   # no matches

That's a 36KB integration + ~600KB Mermaid runtime as a Vite chunk
family + Dependabot churn for nothing the chrome currently exercises.
The reviewer caught this as M3.

When a docs page genuinely needs a diagram (likeliest place is the
upgrader / install flow per #1333 §1's "we already use diagrams in
ARCHITECTURE.md, consistency wins" framing), re-add the integration
in the same PR as the first \`\`\`mermaid codeblock so the bundle
weight has a paying customer. Don't add a stub mermaid block just to
justify it — that's a smell.

\`npm uninstall astro-mermaid\` removed 115 packages from the lockfile
(astro-mermaid + its mermaid + d3 / dagre / cytoscape / @braintree
and friends transitive tree). Removed the import + the integration
config block from astro.config.mjs and replaced it with a comment
that documents the rationale + the re-add story.
Two pages were authored from scratch by the migration worker without
being marked as such; reviewers couldn't tell which content was a
faithful port from the legacy Hugo site vs. invented text needing
extra scrutiny.

- M4: setup/plugin-setup.md is entirely new (the legacy
  sbpp.github.io site had no dedicated plugin-setup page). The
  worker also OMITTED sbpp_admcfg.smx from the companion plugin
  list, even though game/addons/sourcemod/scripting/sbpp_admcfg.sp
  ships in-tree. Added it (admin-config loader: "Reads Admin Files"
  per the .sp myinfo block, replaces SourceMod's stock
  admin-flatfile.smx). Added a top-of-page :::note flagging the
  page as new content + pointing reviewers at the source tree as
  the verification surface.

- M5: getting-started/prerequisites.mdx is mostly an extract of
  Quickstart's prerequisites section, but it was authored from
  scratch with extra framing the legacy source didn't have
  (utf8mb4 paragraph, recovery-surface mention, an expanded
  Composer paragraph). Added a top-of-page :::note flagging it as
  new content + pointing at web/install/pages/page.3.php and
  web/composer.json as the verification surface.

Spot-check pass over the prerequisites claims:
  * PHP >= 8.5 — verified (composer.json line 30).
  * memory_limit >= 64M — recommendation only; install wizard
    doesn't actually check this. Reworded to call that out
    explicitly.
  * Extensions list — install wizard checks
    `openssl, xml, gmp, pdo_mysql, mbstring` (page.3.php line 84).
    Worker had `gmp / pdo_mysql / openssl / xml` and was missing
    `mbstring`. Added it; the `php -m | grep` snippet now greps
    for all five.
  * `recovery.php` — verified (web/install/recovery.php exists
    post-#1332). Reworded the paragraph to mention the file by
    name so the description is grounded in something concrete.
The aside repaint at sbpp.css line 193 used `border-left` (physical
property) where stock Starlight uses `border-inline-start` (logical,
writing-direction-aware). Issue #1333 mentions LLM-driven translation
in "Future automation goals", so RTL locales (Arabic, Hebrew) will
land here eventually — and they would silently get the accent stripe
on the visually-trailing edge if we shipped the physical property.
One-line swap + a comment explaining why so a future override doesn't
silently regress it.
The legacy Hugo source's chmod section had inline bullets describing
why each path needs to be writable (`config.php file is writable`,
`/demos folder is writable`, etc.). The worker collapsed all five
chmods into a comment-less block, so a non-technical operator
following the steps can't tell why they're chmod'ing each path —
just that they should.

Interleaved a one-line comment above each chmod line in the Linux
tab block of `getting-started/quickstart.mdx` describing what the
path is used for (config.php = wizard credentials, demos/ = player
ban-evidence demos, themes_c/ = Smarty compiled-template cache,
images/games/ = per-game admin-uploadable icons, images/maps/ =
per-map thumbnails on the Servers list). Surface knowledge tax goes
down without changing the actual commands the operator runs.
Cheap-wins basket from the reviewer's minor findings list:

- M2: AGENTS.md row 5 paraphrased the spec — `see #1333 §2.` instead
  of the issue's verbatim `see #2.`. Reverted to the verbatim form
  per the reviewer's ask.

- m1: docs-build.yml carried a "fall back to npm install if no
  lockfile" branch that's dead now that docs/package-lock.json is
  committed. Removed; `npm ci` is the deterministic path and will
  fail loudly if anyone bumps package.json without regenerating the
  lockfile (which is exactly the gate we want).

- m6: .gitignore line 83 ignored docs/pnpm-lock.yaml without
  explanation. Dropped the line — we picked npm and an accidental
  pnpm lockfile slip should be a visible diff a reviewer can
  challenge, not silently swallowed.

- m8: docs/README.md "Where things live" omitted
  `src/content.config.ts`. Added a row describing it as the 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).

Two minor findings deliberately deferred:
  * m3 (partial — install routes 3/4/6 stay deferred): tracked in
    the inline comment above INSTALL_ROUTES in
    `docs/scripts/capture.mjs`. Driving the install wizard's POST
    handoff chain is a follow-up that lands when a real screenshot
    surface needs it; until then, the URL-only routes (1, 2, 5)
    are the three that actually paint cold.
  * m7 (inquiries FAQ "browse the docs" pointer): the worker's port
    already cross-links to specific troubleshooting / setup pages on
    every Q&A, so the generic "browse the docs" trailer would be a
    weaker version of the targeted links already present. Skipped.
…ild/check fixes)

Two follow-ups from running the verification gates:

- The new `docs/src/content/docs/legacy/plugin-pre-1.5.4.7.md` page
  builds to `/legacy/plugin-pre-1547/` by default — Astro 5 strips
  dots from filename-derived slugs, which loses the version intent
  in the URL. Cross-links from updating/index.md and legacy/index.md
  use the dotted form. Pinned the slug explicitly via frontmatter
  (`slug: legacy/plugin-pre-1.5.4.7`) so the URL matches the
  cross-links.

- `astro check` errored on the new `Footer.astro` override because
  it imports `virtual:starlight/components/{EditLink,LastUpdated,
  Pagination}` and Starlight ships those type declarations in an
  unscoped `virtual-internal.d.ts` that isn't a public package
  export entry. Added `docs/src/env.d.ts` with a triple-slash
  reference path into the package so consumer overrides type-check
  cleanly. Rerunning `npx astro check` now reports 0 errors / 0
  warnings across all 7 files.
@rumblefrog rumblefrog added this pull request to the merge queue May 11, 2026
Merged via the queue into main with commit 40e6f54 May 11, 2026
2 checks passed
@rumblefrog rumblefrog deleted the docs/migrate-to-starlight-1333 branch May 11, 2026 02:18
Rushaway pushed a commit to srcdslab/sourcebans-pp that referenced this pull request May 15, 2026
… (with skip-when-unconfigured guard) (sbpp#1347)

* ci(docs): skip dispatch when GitHub App not configured

Adds a job-level `if: vars.DOCS_DEPLOY_APP_ID != ''` guard to
`docs-deploy-trigger.yml` so the workflow no-ops cleanly until
the cutover steps in sbpp#1333 step 2 land (App creation +
`DOCS_DEPLOY_APP_ID` var + `DOCS_DEPLOY_APP_KEY` secret).

Pre-fix, every push to `docs/**` after sbpp#1339 merged ran the
dispatch job, hit `actions/create-github-app-token@v1` with an
empty `app-id` input, and exited red with `Input required and
not supplied: app-id` (run 25646867498). Operators landing
unrelated docs PRs would now see a stream of red runs that
look like real CI failures but are actually the expected
"cutover not done yet" state — exactly the friction the
"Required repo configuration" comment block was meant to
warn about.

The job-level `if:` is the canonical "feature-flag a job"
shape: unset repo variables resolve to empty string in
expressions, so the guard is transparent once the var lands
and the job runs every push as designed. Skipped runs show
up as a grey "Skipped" badge in the Actions tab — the right
visual signal for "intentionally inert until configured".

Comment block above the workflow expanded to call this out
explicitly so future readers don't try to "fix" the guard
by removing it.

* ci(docs): switch dispatch trigger from GitHub App to fine-grained PAT

Replaces the `actions/create-github-app-token@v1` round-trip and
the App-side `vars.DOCS_DEPLOY_APP_ID` + `secrets.DOCS_DEPLOY_APP_KEY`
pair with a direct `secrets.DOCS_DEPLOY_PAT` (a fine-grained PAT
scoped to `Actions: Read and write` on `sbpp.github.io` only).

The previous fix in this PR's first commit (`0bf26262`) gated the
job on `vars.DOCS_DEPLOY_APP_ID`. Renamed the gate variable to
`vars.DOCS_DEPLOY_ENABLED` since "App ID" is no longer the right
mental model — the variable is now a pure feature-flag the operator
sets to opt in once the PAT is configured. (We can't reference
`secrets.*` directly in a job-level `if:` per GitHub Actions'
context-availability rules, so the paired VARIABLE is the canonical
"feature-flag a job" shape.)

Why PAT over App: a fine-grained PAT scoped to one repo + one
permission carries the same blast radius as the org-owned App but
takes ~2 minutes to provision instead of ~10. Tradeoff is the
PAT is tied to a personal account and has a max one-year expiry
that needs rotation; for a project of this size that's a fair
deal. Operator instructions live in the workflow's top-of-file
comment block + `docs/README.md`'s workflow table.

The job-level `if:` guard pattern is unchanged in shape — every
push to `docs/**` shows up as "Skipped" (grey badge) until the
operator sets BOTH `secrets.DOCS_DEPLOY_PAT` and
`vars.DOCS_DEPLOY_ENABLED`. The deploy shell in `sbpp.github.io`
also has a `workflow_dispatch` trigger as a manual fallback while
the PAT is pending.

* ci(docs): drop redundant DOCS_DEPLOY_ENABLED variable

The previous commit on this branch added a paired
`vars.DOCS_DEPLOY_ENABLED` feature flag because I'd convinced
myself `secrets.*` was unavailable in any `if:` context. That's
half-true: it's unavailable in JOB-level `if:` (which is what I
was trying to use), but it IS available in STEP-level `if:`.

Since the trigger job has only one step (the dispatch), gating
that single step on `if: secrets.DOCS_DEPLOY_PAT != ''` is
behaviorally equivalent to a job-level guard — the runner spins
up briefly to evaluate the step, sees no credential, marks the
step Skipped, and the run finishes green-with-skipped instead
of red-failing. The "wasted runner spin" cost I cited as
justification for the variable is real but tiny (a few seconds
of CI minutes vs ~30s of job-startup overhead either way), and
the green-with-skipped status is arguably MORE informative than
a fully-skipped grey job — it tells you "I ran, had no work to
do" rather than "I didn't run".

The variable was the operator's "I've done the cutover" signal,
but the secret being set IS that signal. One source of truth,
one less thing to forget. Cutover is now: create PAT, paste into
`secrets.DOCS_DEPLOY_PAT`, done.

Comment block above the workflow + `docs/README.md` workflow
table updated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docs: migrate to Starlight and merge authoring into sourcebans-pp

1 participant