From 2778a3e6109409217393d194106a42eb2153b9b4 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Tue, 3 Nov 2026 18:36:30 +0000 Subject: [PATCH] fix(docs): keep one sidebar entry per project; versions live in the switcher The sidebar had become one topic per documentation version, so a project with live and prerelease docs appeared twice and the topic list read as a flat set of versions. Each channel still contributes its own topic internally (the page list must match the version being read), but every topic now carries the project name and the picker collapses the duplicates. Versions appear only in the page switcher, which is shown only while two versions are published. --- AGENTS.md | 6 ++-- README.md | 12 ++++---- docs/decisions/0009-documentation-channels.md | 12 ++++---- .../components/starlight/TopicSelect.astro | 16 ++++++++++- src/src/lib/docs/channels.ts | 12 ++++---- src/src/lib/docs/sidebar.ts | 18 ++++++------ src/src/pages/docs/index.astro | 5 ++-- src/tests/dist/built-output.test.ts | 28 +++++++++++++------ src/tests/unit/docs-sidebar.test.ts | 17 +++++++---- 9 files changed, 79 insertions(+), 47 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 552ab6f..6248a71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,9 +87,9 @@ prints advisory observations (which never fail the build) when a project's decla pin a project's live and prerelease documentation to refs (`default`, `latest-stable`, `latest-prerelease`, or an explicit branch/tag). The default channel owns `/docs//`, the other is served at `/docs///`, and `docsPage` resolves against the default channel. - Each channel is a first-class docs set — its own sidebar topic (` `), its own - `/projects///llms*.txt` bundle, and its own entry on the docs portal — because the - same product at another version is different documentation. + Each channel is a first-class docs set — its own page list (the sidebar lists the project once + but follows the version being read), its own `/projects///llms*.txt` bundle, and + its own entry on the docs portal. The page switcher, not the sidebar, chooses the version. Only `github-path` supports two channels; `wiki`/`readme` have a single version. Promotion is automatic: the prerelease channel is withheld once a stable release (GitHub release or NuGet version) catches up with the prerelease line, and returns with the next prerelease — never diff --git a/README.md b/README.md index 0724716..33276e2 100644 --- a/README.md +++ b/README.md @@ -317,11 +317,13 @@ docs: The `live` channel is the default and keeps the canonical `/docs//` URLs; the `prerelease` channel is served at `/docs//prerelease/`. -Versions are first-class: each published channel is its own sidebar topic -(`ZodSharp 2.4.2`, `ZodSharp 3.0.0-prerelease.2`) with its own page list, its -own machine-readable bundle, and its own entry on the documentation portal — -same product, different version, therefore different docs. Every page also -offers a switcher. Only `github-path` sources support two channels. +Versions are first-class: each published version has its own page list, its own +machine-readable bundle, and its own entry on the documentation portal — same +product, different version, therefore different docs. The sidebar stays +hierarchical, with one entry per project, and its page list follows the version +you are reading. The version itself appears only in the page switcher, which is +shown while two versions are published and names both. Only `github-path` +sources support two channels. **Promotion is automatic.** The build publishes the prerelease channel only while a prerelease version outranks the newest stable release — comparing GitHub diff --git a/docs/decisions/0009-documentation-channels.md b/docs/decisions/0009-documentation-channels.md index dd52263..470a0de 100644 --- a/docs/decisions/0009-documentation-channels.md +++ b/docs/decisions/0009-documentation-channels.md @@ -149,12 +149,12 @@ history has to survive between builds, and an existing page is never overwritten while two channels are published: promotion withholds the prerelease channel, so the switcher disappears with it and nothing in the UI advertises a version that is no longer published (its retired URLs redirect instead). -- The sidebar treats each channel as a **topic of its own**: `ZodSharp 2.4.2` and - `ZodSharp 3.0.0-prerelease.2` are separate entries in the topic picker, each - with a flat page list, exactly as two projects would be. A channel is a whole - documentation set, not a section of one, so grouping both under a single - project topic would claim one tree while the pages belong to two. A - single-channel project keeps the bare project name. +- The sidebar keeps **one entry per project**. Each channel contributes its own + topic internally, so the page list always matches the version being read, but + every topic for a project carries the project's name and the topic picker + collapses the duplicates. A version is chosen with the page switcher and never + appears in the sidebar; the switcher exists only while two versions are + published. - Every published version has its own machine-readable bundle. The default channel keeps `/projects//llms.txt` and `llms-full.txt`; any other channel mirrors its documentation URL shape at diff --git a/src/src/components/starlight/TopicSelect.astro b/src/src/components/starlight/TopicSelect.astro index 6d2e463..86f9e84 100644 --- a/src/src/components/starlight/TopicSelect.astro +++ b/src/src/components/starlight/TopicSelect.astro @@ -4,13 +4,27 @@ const { topics, isPageWithTopic } = Astro.locals.starlightSidebarTopics; if (!isPageWithTopic) { return null; } + +// A project that publishes more than one documentation version contributes one +// topic per version, each with the page list for that version — but the versions +// belong in the page switcher, not in the topic list. Collapse topics that share +// a label, preferring the one the reader is currently on so its link stays +// correct. `Map` preserves insertion order, so the catalogue order is unchanged. +const byLabel = new Map(); +for (const topic of topics) { + const existing = byLabel.get(topic.label); + if (!existing || (topic.isCurrent && !existing.isCurrent)) { + byLabel.set(topic.label, topic); + } +} +const projectTopics = [...byLabel.values()]; ---