diff --git a/.agents/skills/docs-aggregation-rules/SKILL.md b/.agents/skills/docs-aggregation-rules/SKILL.md index ac78d1c..c3ea877 100644 --- a/.agents/skills/docs-aggregation-rules/SKILL.md +++ b/.agents/skills/docs-aggregation-rules/SKILL.md @@ -109,8 +109,11 @@ The wikis of several product repositories are mirrored into `docs/wiki`, so the - Page order comes from, in priority: `docs.order` (explicit slug list) → a `_Sidebar.md` link order (both for `wiki` and for a mirrored wiki under `github-path`) → `1000`. - The landing page is always `order: -1`, so it heads the sidebar as "Overview". -- Page slugs are slugified file names (`Getting-Started.md` → `getting-started`) and the URLs are - `/docs///`. A `docsPage` in a use case must use that slug form. +- A page's slug is its path from the docs root (ADR 0011): a flat corpus keeps the single-segment + slug (`Getting-Started.md` → `getting-started`, served at `/docs//getting-started/`), and a + topic folder stays a path segment (`schemas/Custom-Rules.md` → + `/docs//schemas/custom-rules/`). A `docsPage` in a use case must use a slug that still resolves + in the **default** channel, which is why it stays a single segment. ## Injected front matter (per page) @@ -128,6 +131,13 @@ H1s are demoted to keep one title per document. - Each documented project becomes a `starlight-sidebar-topics` topic whose items come from `src/.cache/docs/index.json` (so the mirror and the manifest must come from the same sync). +- The sidebar mirrors the published tree (ADR 0011): every folder in a slug becomes a nested + `starlight` group labelled with the folder's name spelled out (`schemas` → `Schemas`), placed where + its first page appears and holding all of that folder's pages in channel order. Nesting is + recursive, a folder's `index.md` links the folder's own address, and a flat corpus renders exactly + the flat list it always did (`buildChannelItems` in `src/lib/docs/sidebar.ts`). +- Each aggregated page also names the folders above it, above the title, in the same spelling as the + sidebar (`PageTitle.astro` + `docSectionTrail`). - Archived projects are excluded from the docs portal, the sidebar topics, and per-project LLM bundles, even if they declare `docs`. - Every documented project gets `/projects//llms.txt` and `/projects//llms-full.txt`, linked @@ -142,3 +152,7 @@ just data-sync # expect "docs : pages" per project node -e "console.log(require('./src/.cache/docs/index.json').projects)" just build && just check-generated # asserts the mirror matches the manifest ``` + +A reorganised tree is verified in the built output: the old flat slug must be a meta-refresh page +pointing at the new address, on every channel that publishes the tree (`/docs///` and +`/docs//prerelease//`), and the sidebar of a nested channel must contain a `group-label`. diff --git a/AGENTS.md b/AGENTS.md index 7278b1d..5503e2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,8 +97,13 @@ prints advisory observations (which never fail the build) when a project's decla - A page's slug is its path from the docs root (ADR 0011): a topic folder stays a path segment (`schemas/Custom-Rules.md` is published at `/docs//schemas/custom-rules/`), a flat corpus keeps the single-segment slug it always had, and every relative link resolves against the page - that contains it. A project whose docs are reorganised records the old→new slug in - `docs.redirects`, which the build turns into redirects; a redirect never shadows a real page. + that contains it. The display mirrors that tree: the docs sidebar nests each folder as a group + (labelled with the folder's name spelled out) and each page names its section above the title, + so a flat corpus renders exactly as before. A project whose docs are reorganised records the + old→new slug in `docs.redirects`, which the build turns into redirects on **every** channel that + publishes the tree — the default channel at `/docs//` and the prerelease one at + `/docs//prerelease/`, each pair written only once its destination exists; a redirect never + shadows a real page. - Agent Skills a project publishes are declared in `skills:` (ADR 0010) at `skills//SKILL.md`; the sync mirrors the whole directory to `/skills///` and each project page renders the same install block diff --git a/docs/decisions/0011-documentation-folders.md b/docs/decisions/0011-documentation-folders.md index 7ff3ad6..9cc8d2d 100644 --- a/docs/decisions/0011-documentation-folders.md +++ b/docs/decisions/0011-documentation-folders.md @@ -75,7 +75,35 @@ the newer tree, the new address is unpublished too, so forwarding the reader wou be forwarding into a 404 — the post-build link crawl fails the build on exactly that, which is how this was found. Both halves activate per page, at promotion. -### 4. `docsPage` stays a single segment +A pair is emitted for every base the project publishes: the default channel at +`/docs//` and, when `live` and `prerelease` are both declared, the prerelease +channel at `/docs//prerelease/`. The reorganised tree is usually published on +the prerelease channel *first* — that is what a default-branch docs change is — +and its readers follow URLs that move at the same moment the default channel's +do later. The same two guards decide which pairs are real, so a withheld channel +still writes nothing here; its URLs belong to the retired-channel redirects. + +### 4. The sidebar and the page show the tree the URL describes + +Publishing a hierarchy in the URL and then flattening it in the sidebar would +leave the reader with the one thing folders were meant to fix: grouping that the +address disagrees with. So the display follows the slug: + +- `buildChannelItems` nests a channel's pages into Starlight sidebar groups, one + per folder, labelled with the folder's segment spelled out (`schemas` → + `Schemas`, `json-schema` → `JSON Schema`). A group appears where its first page + appears and absorbs its later pages, so a folder is listed once and the sync's + reading order (`docs.order` / `_Sidebar.md`) survives. +- Nesting is recursive, because a slug is a path: `guides/getting-started/install` + is a group inside a group. A folder's index file (`schemas/index.md`) is + published at the folder's own address, so its sidebar entry links the folder. +- A flat corpus has no folder in any slug, so it produces no group and no code + path changes for the projects that never nested. +- A page names its section above the title (`PageTitle.astro`), using the same + spelling as the sidebar, so a reader arriving from search knows which part of + the documentation they are in. + +### 5. `docsPage` stays a single segment The catalogue's deep-link slug keeps its `^[a-z0-9-]+$` shape and resolves against the default channel. A deep link must therefore keep working across two trees that @@ -94,7 +122,14 @@ move the failure to the moment a channel's tree diverges. - Documentation written for a flat corpus is unaffected in both slug and link terms, so this landed without touching another project's catalogue. - ZodSharp's own move is the first user of `docs.redirects`: 30 entries that stay - inert until the release that carries the folders is promoted to `live`. + inert on the default channel until the release that carries the folders is + promoted to `live`, and apply immediately to the prerelease channel, which reads + the reorganised tree today. +- `buildChannelItems` returns a tree instead of a list. That is the only shape + change the display took: `SidebarDocItem[]` replaced `SidebarSlugEntry[]`, and + every other use of the docs cache (`llms` bundles, discovery resources, the docs + portal) still reads the flat page list, which remains the source of truth for + what a channel publishes. ## Alternatives considered diff --git a/src/src/components/starlight/PageTitle.astro b/src/src/components/starlight/PageTitle.astro index befeb26..5517714 100644 --- a/src/src/components/starlight/PageTitle.astro +++ b/src/src/components/starlight/PageTitle.astro @@ -4,6 +4,7 @@ import StarOnGitHub from '~/components/StarOnGitHub.astro'; import StatusBadge from '~/components/StatusBadge.astro'; import { readDocsManifest } from '~/lib/docs/aggregate'; import { channelBasePath, channelDisplayLabel } from '~/lib/docs/channels'; +import { docSectionTrail } from '~/lib/docs/sidebar'; import { isStale, STALE_AFTER_DAYS } from '~/lib/docs/staleness'; import { projectChannelLlmsFullPath, projectChannelLlmsPath } from '~/lib/discovery/urls'; import { LLMS_LINK_ATTRS } from '~/lib/llms'; @@ -35,6 +36,14 @@ const showChannelSwitcher = channels.length > 1 && activeChannel !== undefined; const channelHref = (channelId: 'live' | 'prerelease', isDefault: boolean): string => withBase(channelBasePath(sourceProjectId ?? '', channelId, isDefault)); +// A page's folder is part of its URL (ADR 0011), so a reader who arrives from +// search on `schemas/custom-rules` is told which section of the documentation +// they are in, not only which project. The trail is the same spelling the +// sidebar uses for its folder groups, and it is empty for a flat corpus — a +// page at the docs root has no section above it. +const activePage = activeChannel?.pages.find((page) => page.sourcePath === data?.sourcePath); +const sectionTrail = activePage ? docSectionTrail(activePage.slug) : []; + // A tag-pinned channel does not move, so its review date is the release date: // flagging it as stale would be noise rather than drift. const pinnedChannel = activeChannel?.refKind === 'tag'; @@ -86,6 +95,15 @@ const starCount = : (getReleaseIndex().data.repos[sourceRepo]?.stargazersCount ?? null); --- +{ + sectionTrail.length > 0 && ( +

+ Documentation section: + {sectionTrail.join(' / ')} +

+ ) +} +

{data?.title}

{ @@ -224,6 +242,19 @@ const starCount = }