Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/`, the
other is served at `/docs/<id>/<channel>/`, and `docsPage` resolves against the default channel.
Each channel is a first-class docs set — its own sidebar topic (`<Project> <version>`), its own
`/projects/<id>/<channel>/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/<id>/<channel>/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
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,13 @@ docs:

The `live` channel is the default and keeps the canonical `/docs/<project>/`
URLs; the `prerelease` channel is served at `/docs/<project>/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
Expand Down
12 changes: 6 additions & 6 deletions docs/decisions/0009-documentation-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/llms.txt` and `llms-full.txt`; any other channel
mirrors its documentation URL shape at
Expand Down
16 changes: 15 additions & 1 deletion src/src/components/starlight/TopicSelect.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (typeof topics)[number]>();
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()];
---

<label class="topic-select">
<span class="sr-only">Select documentation</span>
<select onchange="window.location.assign(this.value)">
{
topics.map((topic) => (
projectTopics.map((topic) => (
<option value={topic.link} selected={topic.isCurrent}>
{topic.label}
</option>
Expand Down
12 changes: 6 additions & 6 deletions src/src/lib/docs/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,13 +433,13 @@ export function channelDisplayLabel(channel: { id: DocChannelId; label: string }
}

/**
* Sidebar topic label for one documentation version.
* Version-qualified label for one documentation version.
*
* Each channel is a first-class documentation set (ADR 0009), so a project that
* publishes two channels appears as two topics — `ZodSharp 2.4.2` and
* `ZodSharp 3.0.0-prerelease.2` — rather than as one topic that groups both
* versions. The product name stays in front so the pair still reads as the same
* product, and a single-channel project keeps its historical bare name.
* Used where two versions are listed as siblings — the documentation portal and
* the root `llms.txt` link list — so `ZodSharp 2.4.2` and
* `ZodSharp 3.0.0-prerelease.2` are distinguishable. The sidebar does **not**
* use it: there a project appears once, and the page switcher chooses the
* version. A single-channel project keeps the bare project name.
*/
export function channelTopicLabel(
projectName: string,
Expand Down
18 changes: 8 additions & 10 deletions src/src/lib/docs/sidebar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ResolvedProject } from '../manifest/load';
import type { DocsManifest, DocsManifestChannel } from './aggregate';

import { channelBasePath, channelTopicLabel } from './channels';
import { channelBasePath } from './channels';

export interface SidebarSlugEntry {
label?: string;
Expand Down Expand Up @@ -53,14 +53,12 @@ export function buildChannelItems(
/**
* Build the sidebar topics for a single project's documentation.
*
* A channel is a whole documentation set, not a section of one: the same
* product at a different version has different pages, different links and its
* own `llms.txt`. Grouping the channels inside one topic would make the sidebar
* claim a single tree while the pages belong to two, and it would bury the
* second version behind a collapsed group. Each channel therefore becomes its
* own topic — `ZodSharp 2.4.2` and `ZodSharp 3.0.0-prerelease.2` — with a flat
* item list, so switching versions works exactly like switching projects.
* A single-channel project keeps its historical topic (the bare project name).
* A channel is a whole documentation set, so each published version contributes
* its own topic and its own page list — the sidebar must describe the version
* the reader is on. The versions are **not** shown in the sidebar, though: every
* topic for a project carries the project's name, and the topic picker collapses
* the duplicates (see `TopicSelect.astro`). Choosing a version is the job of the
* page switcher, which only appears while two versions are published.
*/
export function buildProjectTopics(
project: ResolvedProject,
Expand All @@ -78,7 +76,7 @@ export function buildProjectTopics(
return ordered
.map((channel) => ({
id: channels.length > 1 ? `${project.id}-${channel.id}` : project.id,
label: channelTopicLabel(project.name, channel, channels.length),
label: project.name,
link: channelBasePath(project.id, channel.id, channel.default),
items: buildChannelItems(project.id, channel),
}))
Expand Down
5 changes: 2 additions & 3 deletions src/src/pages/docs/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,8 @@ const pageCount = docsManifest?.projects.reduce(
<h2 class="text-xl font-semibold">Documentation versions</h2>
<p class="mt-3 max-w-3xl text-sm text-muted">
Some projects publish more than one version of their documentation. Each version is a
documentation set in its own right — its own pages, its own sidebar entry and its own
machine-readable bundle — because installing a different version means reading different
docs.
documentation set in its own right — its own pages and its own machine-readable bundle —
because installing a different version means reading different docs.
</p>
<ul class="mt-5 space-y-5">
{versionedProjects.map(({ project, channels }) => (
Expand Down
28 changes: 20 additions & 8 deletions src/tests/dist/built-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,15 +447,27 @@ describe('built documentation channels', () => {
}
});

test.skipIf(!prerelease)('each documentation version is its own sidebar topic', () => {
// Versions are distinct sidebar entries, not groups inside one project
// topic: the same product, two documentation sets. The topic picker lists
// both, each pointing at its own landing page.
test.skipIf(!prerelease)('the sidebar lists the project once and hides the versions', () => {
// The reader is on the prerelease version: its page list is the sidebar
// body, but the topic picker still lists `ZodSharp` exactly once and no
// version label appears in any option.
const content = requireBuilt(prereleaseIndex);
expect(content).toContain(`ZodSharp ${live!.label}`);
expect(content).toContain(`ZodSharp ${prerelease!.label}`);
expect(content).toContain('value="/docs/zodsharp/"');
expect(content).toContain('value="/docs/zodsharp/prerelease/"');
const optionLabels = [...content.matchAll(/<option[^>]*>([^<]*)<\/option>/g)].map((match) =>
(match[1] ?? '').trim(),
);
expect(optionLabels).toContain('ZodSharp');
expect(optionLabels.some((label) => label.includes(live!.label))).toBe(false);
expect(optionLabels.some((label) => label.includes(prerelease!.label))).toBe(false);
expect((content.match(/value="\/docs\/zodsharp/g) ?? []).length).toBe(1);

// The page switcher is the only place versions appear, and it shows both.
expect(content).toContain(channelDisplayLabel(live!));
expect(content).toContain(channelDisplayLabel(prerelease!));
});

test('the version switcher only appears while a second version is published', () => {
// A single-channel project has no version to choose.
expect(requireBuilt('docs/results/index.html')).not.toContain('doc-channel-menu');
});

test.skipIf(!prerelease)('each documentation version publishes its own llms bundles', () => {
Expand Down
17 changes: 12 additions & 5 deletions src/tests/unit/docs-sidebar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,26 @@ describe('documentation sidebar topics', () => {
]);
});

test('each published version is its own topic, not a group inside one', () => {
test('a versioned project still reads as one project in the sidebar', () => {
const topics = buildProjectTopics(project!, bothChannels());
expect(topics.map((topic) => topic.label)).toEqual([
'ZodSharp 2.4.2',
'ZodSharp 3.0.0-prerelease.2',
]);
// One topic per version internally (each with that version's pages), but
// every topic carries the project name: versions never leak into the
// sidebar — the page switcher is where a reader chooses one.
expect(topics.map((topic) => topic.label)).toEqual(['ZodSharp', 'ZodSharp']);
expect(topics.map((topic) => topic.link)).toEqual([
'/docs/zodsharp/',
'/docs/zodsharp/prerelease/',
]);
expect(topics.map((topic) => topic.id)).toEqual(['zodsharp-live', 'zodsharp-prerelease']);
});

test('the sidebar never names a version', () => {
for (const topic of buildProjectTopics(project!, bothChannels())) {
expect(topic.label).not.toContain('2.4.2');
expect(topic.label).not.toContain('3.0.0');
}
});

test('the two versions never share a page or a group', () => {
const [live, prerelease] = buildProjectTopics(project!, bothChannels());
const liveSlugs = new Set(live!.items.map((item) => item.slug));
Expand Down
Loading