From d6af042cf7ceeadcb9c9b1d860ddb1687717da03 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 12 Aug 2026 14:02:29 +0800 Subject: [PATCH] docs(website): derive navigation targets from the publication manifest The navigation bar named `/guide/` while the manifest published the guide's first page at `guide/quickstart.md`, so the item served a 404 in both locales. `landingLink` resolves each item against `orderedPages`, the ordering the sidebar already renders, and a test asserts every navigation target is a route the manifest publishes. --- ...ation-site-navigation-and-chrome.i18n.yaml | 4 +- ...ocumentation-site-navigation-and-chrome.md | 4 ++ ...mentation-site-navigation-and-chrome.zh.md | 4 ++ scripts/project-doc-site.spec.ts | 15 +++++- website/.vitepress/config.ts | 45 +++++++++--------- website/docs.ts | 46 ++++++++++++++++++- 6 files changed, 90 insertions(+), 28 deletions(-) diff --git a/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.i18n.yaml b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.i18n.yaml index d0e78c1c71..c5b67aa400 100644 --- a/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.md -2026-08-12-documentation-site-navigation-and-chrome.md: 1b1868a011744decf1c0a25a825fd022ea3609ab -2026-08-12-documentation-site-navigation-and-chrome.zh.md: 0fd685ec8c6b7f3f2fd93013ebaf9266a70b2c81 +2026-08-12-documentation-site-navigation-and-chrome.md: 07f88d303a96676806cce0801bce5d478fb5406e +2026-08-12-documentation-site-navigation-and-chrome.zh.md: ca605e2a890bac4bcff9f22a6c6ab8928d1a8aa8 diff --git a/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.md b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.md index 1b1868a011..07f88d303a 100644 --- a/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.md +++ b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.md @@ -8,6 +8,8 @@ English | [中文](2026-08-12-documentation-site-navigation-and-chrome.zh.md) The reference sidebar rendered its 43 subsystem pages first, ahead of every other group: `sectionOrder` in the VitePress config listed no position for `子系统`/`Subsystems` — nor for `其他接口`/`Other interfaces` — so `indexOf` returned `-1` and sorted them ahead of the ordered sections. Clicking the `参考` navigation item landed on the architecture page whose own sidebar entry was link 44 of 62, 1549px down a 2478px sidebar — outside the viewport. Four subsystem pages carried `order` values already taken by other pages in the same section, resolved only by `Array.prototype.sort` stability and the order the manifest's arrays happened to be concatenated. +The navigation bar named `/guide/` while the manifest published the guide's first page at `guide/quickstart.md`, so that item served a 404: written-down navigation targets drift from the routes the manifest publishes. + Separately, every canonical page carries lines written for its GitHub reader — a language switcher under the heading, and for some, a repository badge — which the site projected verbatim even though its navigation bar already offers both. ## Decision @@ -16,6 +18,8 @@ Separately, every canonical page carries lines written for its GitHub reader — Subsystem pages are grouped by concern — overview, core and scopes, sessions and persistence, model and context, execution and tools, policy and interaction, platform and access — and the six topical groups render collapsed until one holds the page being read. The groups sort last within the reference sidebar: expanded, they outnumber every other group combined, so anything placed after them is reachable only by scrolling past the whole list. Page `order` derives from array position rather than a hand-written number. +`landingLink(locale, collection)` derives each navigation item's target from `orderedPages`, the same ordering the sidebar renders, so an item always opens its collection's first published page. + `projectedPageContent` in [scripts/project-doc-site.ts](../../../../scripts/project-doc-site.ts) drops the language-switcher line and the repository badge. The switcher match is confined to the first eight lines so a tutorial that shows the convention still renders its example. The navigation-bar title is the DeepSeek wordmark inlined into `siteTitle`, which VitePress renders as HTML. Inlining is what lets the mark's `currentColor` fills follow the active theme; `themeConfig.logo` renders an ``, which freezes the mark at the colors its file declares and would need one asset per theme. The sidebar scrollbar rests invisible and appears while scrolling, marked by a `data-` attribute rather than a class because Vue rewrites `class` wholesale when it patches the element. diff --git a/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.zh.md b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.zh.md index 0fd685ec8c..ca605e2a89 100644 --- a/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.zh.md +++ b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.zh.md @@ -8,6 +8,8 @@ Status: implemented 参考侧边栏把 43 个子系统页排在了所有其他分组之前:VitePress 配置中的 `sectionOrder` 既没有为 `子系统`/`Subsystems` 也没有为 `其他接口`/`Other interfaces` 声明位置,`indexOf` 返回 `-1`,于是它们排到了所有已排序分区的前面。点击 `参考` 导航项落在架构页,而该页自己的侧边栏条目是 62 条中的第 44 条,位于 2478px 侧边栏的 1549px 处——在视口之外。四个子系统页所用的 `order` 值已被同一分区内的其他页占用,只靠 `Array.prototype.sort` 的稳定性和 manifest 数组恰好的拼接顺序才没有错乱。 +顶栏把 `入门` 指向 `/guide/`,而 manifest 已把入门首页发布在 `guide/quickstart.md`,该导航项因此返回 404:写死的导航目标会与 manifest 实际发布的路由脱节。 + 另外,每个规范页面都带有写给 GitHub 读者的行——标题下的语言切换行,部分页面还有仓库徽章——站点原样投影了它们,尽管其导航栏已经提供了这两者。 ## 决定 @@ -16,6 +18,8 @@ Status: implemented 子系统页按关注点分组——总览、内核与作用域、会话与持久化、模型与上下文、执行与工具、策略与交互、平台与接入——其中六个主题组保持折叠,直到某一组包含正在阅读的页面。这些分组排在参考侧边栏的最后:展开时它们的数量超过其余所有分组之和,因此排在它们之后的任何内容都只能靠滚过整个列表才能到达。页面 `order` 由数组位置推导,不再手写数字。 +`landingLink(locale, collection)` 依据 `orderedPages`——即侧边栏所用的同一套排序——推导每个导航项的目标,因此导航项始终打开该分区已发布的首个页面。 + [scripts/project-doc-site.ts](../../../../scripts/project-doc-site.ts) 中的 `projectedPageContent` 会丢弃语言切换行和仓库徽章。切换行的匹配被限制在前八行内,因此展示该约定的教程仍能渲染出它的示例。 导航栏标题是内联进 `siteTitle` 的 DeepSeek 字标,VitePress 会将其按 HTML 渲染。内联正是让字标的 `currentColor` 填充跟随当前主题的原因;`themeConfig.logo` 渲染为 ``,会把字标固定为文件声明的颜色,并且需要为每套主题各准备一份资源。侧边栏滚动条平时不可见,滚动时出现,通过 `data-` 属性而非 class 标记,因为 Vue 在 patch 该元素时会整体重写 `class`。 diff --git a/scripts/project-doc-site.spec.ts b/scripts/project-doc-site.spec.ts index 439664ddd0..e52017cbd8 100644 --- a/scripts/project-doc-site.spec.ts +++ b/scripts/project-doc-site.spec.ts @@ -5,7 +5,7 @@ import { existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, realpathSyn import { tmpdir } from 'node:os' import { basename, join, resolve } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { docsPages, sectionSpec, type DocsPage } from '../website/docs.ts' +import { docsPages, landingLink, routeLink, sectionSpec, type DocsPage } from '../website/docs.ts' import { addProjectionFrontmatter, projectedPageContent, publishableImage, rewriteMarkdown, } from './project-doc-site.ts' @@ -386,6 +386,19 @@ describe('sidebar ordering', () => { expect(() => sectionSpec('root', 'Guide')).toThrow() }) + it('lands every navigation item on a page the manifest publishes', () => { + // The navigation bar named `/guide/` while the manifest published the guide's + // first page at `guide/quickstart.md`, so the item served a 404. + const collections = [ + ['root', 'zh-guide'], ['root', 'zh-develop'], ['root', 'zh-reference'], + ['en', 'en-guide'], ['en', 'en-develop'], ['en', 'en-reference'], + ] as const + const published = new Set(docsPages.map(page => routeLink(page.route))) + for (const [locale, collection] of collections) { + expect(published, `${locale}/${collection}`).toContain(landingLink(locale, collection)) + } + }) + it('collapses the subsystem groups and leaves the smaller ones open', () => { expect(sectionSpec('root', '执行与工具').collapsed).toBe(true) expect(sectionSpec('en', 'Execution and tools').collapsed).toBe(true) diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index f611804927..e451ecb68c 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -5,33 +5,30 @@ import { resolve } from 'node:path' import type { DefaultTheme, PageData } from 'vitepress' import type { ViteDevServer } from 'vite' import { withMermaid } from 'vitepress-plugin-mermaid' -import { docsPages, sectionSpec, type DocsLocale, type DocsPage } from '../docs.ts' +import { landingLink, orderedPages, routeLink, sectionSpec, type DocsLocale, type DocsPage } from '../docs.ts' import { docsSourceFiles, projectDocs } from '../../scripts/project-doc-site.ts' projectDocs() -function sidebar(locale: DocsLocale, collection: DocsPage['sidebar']): DefaultTheme.SidebarItem[] { - const pages = docsPages.filter(page => page.locale === locale && page.sidebar === collection) +function sidebar(locale: DocsLocale, collection: NonNullable): DefaultTheme.SidebarItem[] { + // `orderedPages` already sorts by section placement, so insertion order + // carries the group order and each group keeps its pages in sequence. const groups = new Map() - for (const page of pages) { + for (const page of orderedPages(locale, collection)) { const entries = groups.get(page.section) ?? [] entries.push(page) groups.set(page.section, entries) } - return [...groups.entries()] - .sort(([left], [right]) => sectionSpec(locale, left).index - sectionSpec(locale, right).index) - .map(([text, entries]) => { - const { collapsed } = sectionSpec(locale, text) - return { - text, - // A present `collapsed` is what makes the default theme render the - // group as collapsible at all, so an open group must omit the key. - ...(collapsed === undefined ? {} : { collapsed }), - items: entries - .sort((left, right) => left.order - right.order) - .map(page => ({ text: page.label, link: `/${page.route.replace(/(?:index)?\.md$/, '')}` })), - } - }) + return [...groups.entries()].map(([text, entries]) => { + const { collapsed } = sectionSpec(locale, text) + return { + text, + // A present `collapsed` is what makes the default theme render the + // group as collapsible at all, so an open group must omit the key. + ...(collapsed === undefined ? {} : { collapsed }), + items: entries.map(page => ({ text: page.label, link: routeLink(page.route) })), + } + }) } function watchCanonicalDocs(server: ViteDevServer): void { @@ -199,9 +196,9 @@ export default withMermaid({ themeConfig: { siteTitle: siteTitle('技术预览'), nav: [ - { text: '入门', link: '/guide/', activeMatch: '^/guide/' }, - { text: '开发', link: '/develop/basic/', activeMatch: '^/develop/' }, - { text: '参考', link: '/reference/', activeMatch: '^/reference/' }, + { text: '入门', link: landingLink('root', 'zh-guide'), activeMatch: '^/guide/' }, + { text: '开发', link: landingLink('root', 'zh-develop'), activeMatch: '^/develop/' }, + { text: '参考', link: landingLink('root', 'zh-reference'), activeMatch: '^/reference/' }, ], sidebar: { '/guide/': sidebar('root', 'zh-guide'), @@ -226,9 +223,9 @@ export default withMermaid({ themeConfig: { siteTitle: siteTitle('Preview'), nav: [ - { text: 'Guide', link: '/en/guide/', activeMatch: '^/en/guide/' }, - { text: 'Develop', link: '/en/develop/basic/', activeMatch: '^/en/develop/' }, - { text: 'Reference', link: '/en/reference/', activeMatch: '^/en/reference/' }, + { text: 'Guide', link: landingLink('en', 'en-guide'), activeMatch: '^/en/guide/' }, + { text: 'Develop', link: landingLink('en', 'en-develop'), activeMatch: '^/en/develop/' }, + { text: 'Reference', link: landingLink('en', 'en-reference'), activeMatch: '^/en/reference/' }, ], sidebar: { '/en/guide/': sidebar('en', 'en-guide'), diff --git a/website/docs.ts b/website/docs.ts index 7615952d0e..12280585cc 100644 --- a/website/docs.ts +++ b/website/docs.ts @@ -11,7 +11,7 @@ export type DocsLocale = 'root' | 'en' /** Sidebar collection rendered for one locale and top-level module. */ -type DocsSidebar = +export type DocsSidebar = | 'zh-guide' | 'zh-develop' | 'zh-reference' @@ -478,3 +478,47 @@ export const docsPages: DocsPage[] = [ ...subsystemsReference, ...reference, ] + +/** + * Pages of one sidebar collection, in the order the sidebar lists them. + * + * @param locale - Route tree whose sidebar is being built. + * @param collection - Sidebar collection to read. + * @returns The collection's pages, ordered by section placement then by `order`. + */ +export function orderedPages(locale: DocsLocale, collection: DocsSidebar): DocsPage[] { + return docsPages + .filter(page => page.locale === locale && page.sidebar === collection) + .sort((left, right) => ( + sectionSpec(locale, left.section).index - sectionSpec(locale, right.section).index + || left.order - right.order + )) +} + +/** + * Site-relative link for a published route. + * + * @param route - Manifest route, including its `.md` suffix. + * @returns The link VitePress serves the route at. + */ +export function routeLink(route: string): string { + return `/${route.replace(/(?:index)?\.md$/, '')}` +} + +/** + * Where a top-level navigation item lands. + * + * The target is derived rather than written down: a collection whose first page + * is renamed or reordered would otherwise leave the navigation bar pointing at + * a route the manifest no longer publishes. + * + * @param locale - Route tree the navigation item belongs to. + * @param collection - Sidebar collection the item opens. + * @returns Site-relative link of the collection's first page. + * @throws When the collection publishes no page. + */ +export function landingLink(locale: DocsLocale, collection: DocsSidebar): string { + const first = orderedPages(locale, collection)[0] + if (first === undefined) throw new Error(`Sidebar collection "${collection}" publishes no page.`) + return routeLink(first.route) +}