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
new file mode 100644
index 0000000000..d0e78c1c71
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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
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
new file mode 100644
index 0000000000..1b1868a011
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.md
@@ -0,0 +1,37 @@
+# Agent Note: Documentation-site navigation and repository chrome
+
+Status: implemented
+
+English | [中文](2026-08-12-documentation-site-navigation-and-chrome.zh.md)
+
+## Problem
+
+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.
+
+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
+
+[website/docs.ts](../../../../website/docs.ts) owns section placement. `sections` declares the groups per locale, and `sectionSpec(locale, label)` returns a group's position and collapse behavior, throwing when a locale declares no placement for a label. A group absent from the declaration now fails the build instead of sorting silently to the top. Placement is per locale because the two sidebars name their groups independently: one shared list ordered both label sets by convention and accepted a label missing from either without complaint.
+
+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.
+
+`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.
+
+## Alternatives considered
+
+**A search tokenizer for Chinese queries.** Built and reverted. The premise — that MiniSearch leaves Chinese prose as untokenizable whole sentences — was tested against a term (`子代理`) that appears nowhere in the corpus; the Chinese pages write `Subagent` and `子 agent`. Measured against the unmodified index, `插件配置` returns 120 hits, `会话持久化` 85, `工作流` 28, `沙箱` 12, each ranking its own page first: `prefix: true` already reaches Chinese terms through the short tokens punctuation produces. Adjacent-character pairs grew the Chinese index from 1.23MB to 2.12MB for no gain. The attempt also surfaced a trap worth keeping: VitePress ships search-option functions to the browser through `Function.prototype.toString` and rebuilds them with `new Function`, so any such function that closes over a module-level constant throws in an empty scope and silently returns no results.
+
+**Placing the subsystem groups directly after `概念`.** Rejected: it restores the architecture page to the top but leaves generated reference, the Cordis API, and the cookbook below 43 rows.
+
+**Rewriting filename link text during projection.** The subsystem index table writes `[core.md](core.md)`, which reads as a repository file index on the site. `scripts/project-doc-site.spec.ts` asserts that exact row format, so the filenames are a deliberate convention rather than an oversight; changing what the site displays means changing the convention and its gate together, not working around them in the projector.
+
+## Consequences
+
+The reference sidebar measures 1452px with every subsystem group collapsed, against 2478px before, and the architecture page is its first entry. Section placement and collapse are declared in one manifest instead of split between the manifest and the config, and `scripts/project-doc-site.spec.ts` pins three invariants: every sidebar-owning page resolves a placement, an undeclared section is refused, and no two pages share an `order` within a section.
+
+Canonical Markdown is unchanged by the chrome stripping — the switcher and badge still serve GitHub readers. The cost is that the projector now knows two presentation conventions of the source corpus, which a page written with a different switcher wording would not match.
+
+The wordmark is a second copy of a mark that also lives in `apps/web/public/favicon.svg` and `packages/client/ui-primitives/src/FishLogo.tsx`, each carrying its own presentation. A change to the DeepSeek wordmark reaches the documentation site only by updating this copy.
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
new file mode 100644
index 0000000000..0fd685ec8c
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-12-documentation-site-navigation-and-chrome.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 文档站导航与仓库 chrome
+
+Status: implemented
+
+[English](2026-08-12-documentation-site-navigation-and-chrome.md) | 中文
+
+## 问题
+
+参考侧边栏把 43 个子系统页排在了所有其他分组之前:VitePress 配置中的 `sectionOrder` 既没有为 `子系统`/`Subsystems` 也没有为 `其他接口`/`Other interfaces` 声明位置,`indexOf` 返回 `-1`,于是它们排到了所有已排序分区的前面。点击 `参考` 导航项落在架构页,而该页自己的侧边栏条目是 62 条中的第 44 条,位于 2478px 侧边栏的 1549px 处——在视口之外。四个子系统页所用的 `order` 值已被同一分区内的其他页占用,只靠 `Array.prototype.sort` 的稳定性和 manifest 数组恰好的拼接顺序才没有错乱。
+
+另外,每个规范页面都带有写给 GitHub 读者的行——标题下的语言切换行,部分页面还有仓库徽章——站点原样投影了它们,尽管其导航栏已经提供了这两者。
+
+## 决定
+
+[website/docs.ts](../../../../website/docs.ts) 拥有分区位置。`sections` 按 locale 声明各分组,`sectionSpec(locale, label)` 返回分组的位置与折叠行为,当某 locale 未为该 label 声明位置时抛错。未出现在声明中的分组现在会让构建失败,而不是静默排到最前。位置按 locale 声明,是因为两侧侧边栏各自命名分组:单一共享列表既要按约定排列两套标签,又会对任一侧缺失的标签毫无反应。
+
+子系统页按关注点分组——总览、内核与作用域、会话与持久化、模型与上下文、执行与工具、策略与交互、平台与接入——其中六个主题组保持折叠,直到某一组包含正在阅读的页面。这些分组排在参考侧边栏的最后:展开时它们的数量超过其余所有分组之和,因此排在它们之后的任何内容都只能靠滚过整个列表才能到达。页面 `order` 由数组位置推导,不再手写数字。
+
+[scripts/project-doc-site.ts](../../../../scripts/project-doc-site.ts) 中的 `projectedPageContent` 会丢弃语言切换行和仓库徽章。切换行的匹配被限制在前八行内,因此展示该约定的教程仍能渲染出它的示例。
+
+导航栏标题是内联进 `siteTitle` 的 DeepSeek 字标,VitePress 会将其按 HTML 渲染。内联正是让字标的 `currentColor` 填充跟随当前主题的原因;`themeConfig.logo` 渲染为 ``,会把字标固定为文件声明的颜色,并且需要为每套主题各准备一份资源。侧边栏滚动条平时不可见,滚动时出现,通过 `data-` 属性而非 class 标记,因为 Vue 在 patch 该元素时会整体重写 `class`。
+
+## 考虑过的替代方案
+
+**为中文查询定制搜索分词器。** 已实现并撤回。其前提——MiniSearch 会把中文散文留作无法切分的整句——是用一个语料中根本不存在的词(`子代理`)验证的;中文页面写的是 `Subagent` 和 `子 agent`。在未改动的索引上实测,`插件配置` 返回 120 条命中、`会话持久化` 85 条、`工作流` 28 条、`沙箱` 12 条,且各自的页面均排在首位:`prefix: true` 已经能通过标点切出的短 token 命中中文词。相邻字符二元组把中文索引从 1.23MB 增至 2.12MB,却没有带来收益。该尝试还暴露出一个值得保留的陷阱:VitePress 通过 `Function.prototype.toString` 把搜索选项中的函数送到浏览器,再用 `new Function` 重建,因此任何闭包引用了模块级常量的此类函数都会在空作用域中抛错,并静默地返回零结果。
+
+**把子系统分组直接放在 `概念` 之后。** 已否决:这样能让架构页回到顶部,但生成参考、Cordis API 和开发手册仍处在 43 行之下。
+
+**在投影时重写文件名链接文字。** 子系统索引表写的是 `[core.md](core.md)`,在站点上读起来像仓库文件索引。`scripts/project-doc-site.spec.ts` 断言了该行的确切格式,因此这些文件名是刻意的约定而非疏漏;要改变站点显示的内容,就要连同该约定及其门禁一起改,而不是在投影器里绕开它们。
+
+## 影响
+
+在所有子系统分组折叠时,参考侧边栏高度为 1452px,此前为 2478px,且架构页是它的第一个条目。分区位置与折叠行为声明在同一份 manifest 中,不再分散于 manifest 与配置之间;`scripts/project-doc-site.spec.ts` 固定了三条不变式:每个拥有侧边栏的页面都能解析到位置、未声明的分区会被拒绝、同一分区内没有两个页面共用 `order`。
+
+剥离 chrome 不改动规范 Markdown——切换行与徽章仍服务于 GitHub 读者。代价是投影器现在知晓源语料的两项呈现约定,而采用不同切换行措辞的页面将不会被匹配到。
+
+字标是同一图形的第二份副本,另两份位于 `apps/web/public/favicon.svg` 和 `packages/client/ui-primitives/src/FishLogo.tsx`,各自承载自己的呈现方式。DeepSeek 字标的变更只有通过更新这份副本才能到达文档站。
diff --git a/docs/cordis-tutorial/index.i18n.yaml b/docs/cordis-tutorial/index.i18n.yaml
index 1ce61a593a..af65a4b898 100644
--- a/docs/cordis-tutorial/index.i18n.yaml
+++ b/docs/cordis-tutorial/index.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 docs/cordis-tutorial/index.md
-index.md: a10a0f93fde4f710af2ab14f74b854ee07d7c03f
-index.zh.md: fb2c4f0959eab8c7a072c44207943c31b0bed8ea
+index.md: dc9bc13c80885857d42bbc32532f678d8942a40d
+index.zh.md: 4bd3837d7df0c9bcc1d512e1505c0934b56cf0a7
diff --git a/docs/cordis-tutorial/index.md b/docs/cordis-tutorial/index.md
index a10a0f93fd..dc9bc13c80 100644
--- a/docs/cordis-tutorial/index.md
+++ b/docs/cordis-tutorial/index.md
@@ -8,6 +8,8 @@ The audience is agent developers. You do not need deep TypeScript experience; th
If you want the condensed concept reference instead of a walkthrough, read the [Cordis primer](../cordis-primer.md). The exhaustive API reference lives in the generated `cordis-surface` regions on the [subsystem pages](../subsystems/core.md) and the [Cordis core API](../cordis-api/context.md) pages.
+To write plugins for the harness itself — loaded from a `cordis.yml` and driven from the Web UI rather than the launcher below — start from [your first Harness plugin](../user/develop/basic/index.md).
+
## Setup
You need a clone of this repository with dependencies installed; the [development guide](../development.md#setup-tutorial) lists the prerequisites. No API key is needed for this tutorial; every example runs keylessly.
diff --git a/docs/cordis-tutorial/index.zh.md b/docs/cordis-tutorial/index.zh.md
index fb2c4f0959..4bd3837d7d 100644
--- a/docs/cordis-tutorial/index.zh.md
+++ b/docs/cordis-tutorial/index.zh.md
@@ -8,6 +8,8 @@ Cordis 是 DeepSeek Harness SDK 底层的插件框架:它是一个小型运行
如果你想阅读精简的概念参考,而不是逐步实践,请参阅 [Cordis 入门](../cordis-primer.md)。详尽的 API 参考见[子系统页面](../subsystems/core.md)上生成的 `cordis-surface` 区块,以及 [Cordis 核心 API](../cordis-api/context.md)页面。
+如果你要为 harness 本身编写插件——由 `cordis.yml` 加载、在 Web UI 中驱动,而不是下面这个启动器——请从[第一个 Harness 插件](../user/develop/basic/index.md)开始。
+
## 准备工作
你需要克隆本仓库并安装依赖;[开发指南](../development.md#setup-tutorial)列出了前置条件。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。
diff --git a/docs/user/develop/basic/index.i18n.yaml b/docs/user/develop/basic/index.i18n.yaml
index 1bfaea7b92..e4075e6325 100644
--- a/docs/user/develop/basic/index.i18n.yaml
+++ b/docs/user/develop/basic/index.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 docs/user/develop/basic/index.md
-index.md: 71b5bd5ef5d296999420c40d3b8c9cf46c918841
-index.zh.md: 5dafe8bf0938337fa1f38634088acf00a2fcab46
+index.md: 494b7869be6ffdf5767fac260b36b2585305b516
+index.zh.md: 92b5ad4e876b31bc10d57a19d45b1bfdf2fdb9ba
diff --git a/docs/user/develop/basic/index.md b/docs/user/develop/basic/index.md
index 71b5bd5ef5..494b7869be 100644
--- a/docs/user/develop/basic/index.md
+++ b/docs/user/develop/basic/index.md
@@ -139,3 +139,4 @@ Function form is sufficient in most cases. Use class form when the plugin provid
- [Build a tool](./tool.md) — learn the tool definition DSL
- [Plugin configuration](./config.md) — accept user configuration
+- [Cordis tutorial](../../../cordis-tutorial/index.md) — the plugin framework underneath, built from a scratch directory with no API key
diff --git a/docs/user/develop/basic/index.zh.md b/docs/user/develop/basic/index.zh.md
index 5dafe8bf09..92b5ad4e87 100644
--- a/docs/user/develop/basic/index.zh.md
+++ b/docs/user/develop/basic/index.zh.md
@@ -139,3 +139,4 @@ export default class MyService extends Service {
- [开发一个工具](./tool.md) — 详细了解工具定义 DSL
- [插件配置](./config.md) — 让插件接受用户配置
+- [Cordis 框架教程](../../../cordis-tutorial/index.md) — 底层的插件框架,在临时目录中动手构建,无需 API 密钥
diff --git a/docs/user/develop/framework/index.i18n.yaml b/docs/user/develop/framework/index.i18n.yaml
index 1c8dc3dae4..3e27e77e5a 100644
--- a/docs/user/develop/framework/index.i18n.yaml
+++ b/docs/user/develop/framework/index.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 docs/user/develop/framework/index.md
-index.md: 85701ce281d92da0c805b39291179df73eb65f51
-index.zh.md: 871aa55ef81a7dcbfe3cbde5986244220ee32f98
+index.md: 8cc673148d7fec4f7d9b994907e17293bc3a6a97
+index.zh.md: 1a1f7feb8685e124babb182544bde332b52da42c
diff --git a/docs/user/develop/framework/index.md b/docs/user/develop/framework/index.md
index 85701ce281..8cc673148d 100644
--- a/docs/user/develop/framework/index.md
+++ b/docs/user/develop/framework/index.md
@@ -134,3 +134,4 @@ effect cleaned up
- [Services and dependencies](./service.md) — expose a capability to other plugins
- [Event system](./events.md) — communicate between plugins
+- [Cordis tutorial](../../../cordis-tutorial/index.md) — the same lifecycle, services, and events built step by step against the Cordis runtime
diff --git a/docs/user/develop/framework/index.zh.md b/docs/user/develop/framework/index.zh.md
index 871aa55ef8..1a1f7feb86 100644
--- a/docs/user/develop/framework/index.zh.md
+++ b/docs/user/develop/framework/index.zh.md
@@ -134,3 +134,4 @@ effect cleaned up
- [服务与依赖](./service.md) — 让插件向其他插件提供能力
- [事件系统](./events.md) — 在插件之间通信
+- [Cordis 框架教程](../../../cordis-tutorial/index.md) — 在 Cordis 运行时上逐步搭出同一套生命周期、服务与事件
diff --git a/scripts/project-doc-site.spec.ts b/scripts/project-doc-site.spec.ts
index 107f2c1034..439664ddd0 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, type DocsPage } from '../website/docs.ts'
+import { docsPages, sectionSpec, type DocsPage } from '../website/docs.ts'
import {
addProjectionFrontmatter, projectedPageContent, publishableImage, rewriteMarkdown,
} from './project-doc-site.ts'
@@ -364,6 +364,50 @@ describe('docsPages locale routes', () => {
})
})
+describe('sidebar ordering', () => {
+ it('places every section a sidebar collection owns', () => {
+ for (const page of docsPages) {
+ if (page.sidebar === null) continue
+ expect(() => sectionSpec(page.locale, page.section), page.route).not.toThrow()
+ }
+ })
+
+ it('refuses a section with no declared placement', () => {
+ expect(() => sectionSpec('root', '数据结构'))
+ .toThrow('Sidebar section "数据结构" has no placement in the root locale.')
+ })
+
+ it('declares placements per locale rather than in one shared list', () => {
+ // Each locale ranks only its own labels, so a label one locale never uses
+ // cannot borrow a rank from the other.
+ expect(sectionSpec('root', '入门').index).toBe(0)
+ expect(sectionSpec('en', 'Guide').index).toBe(0)
+ expect(() => sectionSpec('en', '入门')).toThrow()
+ expect(() => sectionSpec('root', 'Guide')).toThrow()
+ })
+
+ 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)
+ expect(sectionSpec('root', '概念').collapsed).toBeUndefined()
+ })
+
+ it('gives each page its own position within a section', () => {
+ // Sidebar entries sort by order alone, so a shared value leaves the two
+ // pages ranked by whichever manifest block happens to be concatenated
+ // first rather than by an intent the manifest states.
+ const taken = new Map()
+ const collisions: string[] = []
+ for (const page of docsPages) {
+ const slot = `${page.locale}/${String(page.sidebar)}/${page.section}#${page.order}`
+ const holder = taken.get(slot)
+ if (holder === undefined) taken.set(slot, page.label)
+ else collisions.push(`${slot}: ${holder} / ${page.label}`)
+ }
+ expect(collisions).toEqual([])
+ })
+})
+
describe('addProjectionFrontmatter', () => {
it('adds frontmatter to an ordinary Markdown page', () => {
expect(addProjectionFrontmatter('# Guide\n', { source: 'docs/guide.md' })).toBe(
@@ -411,6 +455,25 @@ describe('projectedPageContent', () => {
expect(projectedPageContent(markdown, page('zh-guide'))).toBe(markdown)
})
+ it('drops the language switcher the navigation bar already offers', () => {
+ expect(projectedPageContent('# Guide\n\nEnglish | [中文](./en/guide)\n\nBody.\n', page('zh-guide')))
+ .toBe('# Guide\n\nBody.\n')
+ expect(projectedPageContent('# 指南\n\n[English](./en/guide) | 中文\n\n正文。\n', page('zh-guide')))
+ .toBe('# 指南\n\n正文。\n')
+ })
+
+ it('drops the repository badge every page links from its footer', () => {
+ const badge = '[](https://github.com/deepseek-ai/deepseek-harness)'
+ expect(projectedPageContent(`# Guide\n\nBody.\n\n${badge}\n`, page('zh-guide')))
+ .toBe('# Guide\n\nBody.\n')
+ })
+
+ it('keeps a switcher-shaped line that is not the page header', () => {
+ // A tutorial showing the convention must still render the example.
+ const sample = '# Guide\n\nA\n\nB\n\nC\n\nD\n\nE\n\nEnglish | [中文](./x)\n'
+ expect(projectedPageContent(sample, page('zh-guide'))).toBe(sample)
+ })
+
it('rejects a locale home source without frontmatter', () => {
expect(() => projectedPageContent('# Harness\n', page(null)))
.toThrow('locale home source "docs/index.zh.md" must start with YAML frontmatter')
diff --git a/scripts/project-doc-site.ts b/scripts/project-doc-site.ts
index e7acc73998..1d0ea9072a 100644
--- a/scripts/project-doc-site.ts
+++ b/scripts/project-doc-site.ts
@@ -292,6 +292,37 @@ export function addProjectionFrontmatter(markdown: string, page: Pick LANGUAGE_SWITCHER.test(line))
+ // Only the switcher introducing the page qualifies; further down the same
+ // text is prose or a sample rather than the page's own header.
+ if (switcher !== -1 && switcher < 8) {
+ lines.splice(switcher, lines[switcher + 1] === '' ? 2 : 1)
+ }
+ const badge = lines.findLastIndex(line => REPOSITORY_BADGE.test(line))
+ if (badge !== -1) {
+ lines.splice(lines[badge - 1] === '' ? badge - 1 : badge, lines[badge - 1] === '' ? 2 : 1)
+ }
+ return lines.join('\n')
+}
+
/**
* Select the Markdown rendered for one published page.
*
@@ -300,7 +331,7 @@ export function addProjectionFrontmatter(markdown: string, page: Pick page.sidebar === collection)
- const sections = new Map()
+function sidebar(locale: DocsLocale, collection: DocsPage['sidebar']): DefaultTheme.SidebarItem[] {
+ const pages = docsPages.filter(page => page.locale === locale && page.sidebar === collection)
+ const groups = new Map()
for (const page of pages) {
- const entries = sections.get(page.section) ?? []
+ const entries = groups.get(page.section) ?? []
entries.push(page)
- sections.set(page.section, entries)
+ groups.set(page.section, entries)
}
- return [...sections.entries()]
- .sort(([left], [right]) => sectionOrder.indexOf(left) - sectionOrder.indexOf(right))
- .map(([text, entries]) => ({
- text,
- items: entries
- .sort((left, right) => left.order - right.order)
- .map(page => ({ text: page.label, link: `/${page.route.replace(/(?:index)?\.md$/, '')}` })),
- }))
+ 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$/, '')}` })),
+ }
+ })
}
function watchCanonicalDocs(server: ViteDevServer): void {
@@ -107,10 +92,102 @@ const sharedTheme: Pick` would freeze the mark at the colors the file declares.
+ */
+const wordmark = readFileSync(resolve(import.meta.dirname, '../public/wordmark.svg'), 'utf8')
+ .trim()
+ .replace('