Ver código fonte

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.
Yichen Jiang 2 meses atrás
pai
commit
d6af042cf7

+ 2 - 2
.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

+ 4 - 0
.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 `<img>`, 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.

+ 4 - 0
.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` 渲染为 `<img>`,会把字标固定为文件声明的颜色,并且需要为每套主题各准备一份资源。侧边栏滚动条平时不可见,滚动时出现,通过 `data-` 属性而非 class 标记,因为 Vue 在 patch 该元素时会整体重写 `class`。

+ 14 - 1
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)

+ 21 - 24
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<DocsPage['sidebar']>): 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<string, DocsPage[]>()
-  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'),

+ 45 - 1
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)
+}