docs(website): serve index routes at their clean-URL .md addresses

Each index route also emits a parent-level alias twin, projected over
the alias route so its relative links stay correct; llms.txt and the
docs now state the drop-trailing-slash convention exactly. Frontmatter
failures name their page, the twin pass refuses to overwrite existing
build files, the dev middleware documents the deliberate in-page
fetch() divergence, and the per-locale collection order moves to one
shared export. Refs #2846.
This commit is contained in:
Yichen Jiang
2026-08-20 22:10:35 +08:00
parent f3ce8218cc
commit 17c85209a0
10 changed files with 174 additions and 50 deletions
@@ -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-20-doc-site-raw-markdown-twins.md
2026-08-20-doc-site-raw-markdown-twins.md: 8174d67b43776d7264710d88d11540288bf7b63f
2026-08-20-doc-site-raw-markdown-twins.zh.md: 1babf957a6bdb1bd0a6ed91d6bbcb44b84aa713c
2026-08-20-doc-site-raw-markdown-twins.md: 5b45657d13d02bc7211e47cad2143afd6890d4e2
2026-08-20-doc-site-raw-markdown-twins.zh.md: d62088fe837e3f2eefe9e44e19dcfd3823e79560
@@ -14,9 +14,11 @@ The documentation site serves rendered HTML only, so an agent reading the docs h
One projection serves both trees because its site-internal links are relative. `./sibling.md` renders as a clean URL on the HTML site and resolves file-to-file in the raw tree, so the twins need no second link-rewriting mode. Every route is emitted, including the frontmatter-only locale homes, because published pages link to them and the raw tree must stay link-closed; a spec walks every emitted relative link to pin that closure.
An index route renders as a directory URL, so "append `.md`" lands on `<dir>.md` once the trailing slash is dropped; each index route therefore also emits a parent-level alias twin at that path. The alias is not a copy — a copied `index.md` would carry its relative links one directory too high — but its own projection over the alias route, resolved against the canonical manifest so links keep targeting canonical twins. The root home has no parent to alias into; `/` is documented as `/index.md`. A twin or image may never overwrite a file the build already carries, such as a `public/` copy; a name collision fails the emission.
`llms.txt` is generated from the publication manifest at the site root: both locale trees in sidebar order, one `- [label](<base><route>): <section>` row per page, links site-absolute under the deploy-time `DOCS_BASE`. Locale homes stay out — the file itself is the agent entry point.
The dev server serves the same surface. A middleware in the doc-projector plugin projects `.md` requests from their canonical sources per hit and generates `llms.txt` on demand, so `docs:dev` matches production without a rebuild.
The dev server serves the same surface for navigations and header-less clients. A middleware in the doc-projector plugin projects `.md` requests from their canonical sources per hit and generates `llms.txt` on demand, so `docs:dev` matches production without a rebuild; an in-page `fetch()` (`Sec-Fetch-Dest: empty`) deliberately still reaches Vite in dev, while production static hosting answers it with the raw file.
`verify-doc-site-fragments`, the post-build gate, fails the build when any route's twin or `llms.txt` is missing from the output, so deleting the `buildEnd` wiring cannot pass CI.
@@ -28,8 +30,8 @@ The dev server serves the same surface. A middleware in the doc-projector plugin
**Absolute links inside the twins.** platform.claude.com links absolutely because its host is fixed. This site's base varies between local `/` and the Pages subpath, and the projector's relative links resolve in both trees as they are, so absolute rewriting would add a second link grammar without improving resolution.
**Skip locale homes in the raw tree.** Published pages link to `docs/user/index.md`, so omitting the home routes breaks link closure. Their full bodies (H1 plus guide links) cost nothing, and the redirect frontmatter means nothing outside VitePress.
**Skip locale homes in the raw tree.** Published pages link to `docs/user/index.md`, so omitting the home routes breaks link closure. The body that survives chrome stripping (the H1) costs nothing, and the redirect frontmatter means nothing outside VitePress.
## Consequences
Agents fetch any page as plain Markdown at `<page URL>.md` and discover the whole set at `/llms.txt`; the rendered site is unchanged. The build output carries one extra Markdown file per route plus image copies beside them — kilobytes against the bundled assets. Twin content keeps GitHub-style heading text while the rendered site slugs punctuation-heavy headings differently; agents resolve headings themselves, so no gate covers raw-tree fragments. Deferred as unneeded for the twins' audience: `llms-full.txt`, and a per-page "view as Markdown" control, which would require a theme directory the stock-theme site deliberately lacks.
Agents fetch any page as plain Markdown by dropping the URL's trailing slash and appending `.md`, and discover the whole set at `/llms.txt`; the rendered site is unchanged. The build output carries one extra Markdown file per route, an alias per index route, and image copies beside them — kilobytes against the bundled assets. Twin content keeps GitHub-style heading text while the rendered site slugs punctuation-heavy headings differently; agents resolve headings themselves, so no gate covers raw-tree fragments. Deferred as unneeded for the twins' audience: `llms-full.txt`, and a per-page "view as Markdown" control, which would require a theme directory the stock-theme site deliberately lacks.
@@ -14,9 +14,11 @@ Status: implemented
一份投影同时服务两棵树,因为站内链接是相对路径。`./sibling.md` 在 HTML 站渲染为 clean URL,在原始树中按文件对文件解析,孪生页不需要第二套链接改写模式。所有路由都被发射,包括仅有 frontmatter 的 locale 首页:已发布页面链接到它们,原始树必须保持链接封闭;一个 spec 遍历发射树中的每条相对链接来钉住这条闭合性。
index 路由在渲染站上呈现为目录 URL,"加 `.md`"在去掉末尾斜杠后落在 `<dir>.md` 上;因此每个 index 路由还发射一个父级别名孪生页。别名不是拷贝——拷贝的 `index.md` 会让相对链接整体上移一层——而是以别名 route 为基准的独立投影,链接解析仍针对 canonical manifest,始终指向 canonical 孪生页。根首页没有可放别名的父级;`/` 在文档中写明用 `/index.md`。孪生页与图片一律不得覆盖构建目录中已存在的文件(例如 `public/` 副本);同名冲突使发射失败。
`llms.txt` 由发布 manifest 生成于站根:两棵语言树按侧边栏顺序排列,每页一行 `- [label](<base><route>): <section>`,链接为携带部署期 `DOCS_BASE` 的站内绝对路径。locale 首页不列入——这个文件本身就是 agent 的入口。
开发服务器提供同一表面。doc-projector 插件中的 middleware 逐请求从 canonical 源投影 `.md` 请求并按需生成 `llms.txt``docs:dev` 无需重建即与线上一致。
开发服务器对导航与无头客户端提供同一表面。doc-projector 插件中的 middleware 逐请求从 canonical 源投影 `.md` 请求并按需生成 `llms.txt``docs:dev` 无需重建即与线上一致;页面内 `fetch()``Sec-Fetch-Dest: empty`)在 dev 下刻意仍交给 Vite,而生产静态托管对它返回原始文件
构建后门禁 `verify-doc-site-fragments` 在任一路由的孪生页或 `llms.txt` 缺失时判定构建失败,删掉 `buildEnd` 接线无法通过 CI。
@@ -28,8 +30,8 @@ Status: implemented
**孪生页内用绝对链接。** platform.claude.com 用绝对链接是因为其主机固定。本站 base 在本地 `/` 与 Pages 子路径之间变化,而投影器的相对链接在两棵树中原样可解析,绝对化改写只会引入第二套链接语法而不改善解析。
**原始树跳过 locale 首页。** 已发布页面链接到 `docs/user/index.md`,省略首页路由会破坏链接封闭。首页正文(H1 加指南链接)成本为零,重定向 frontmatter 在 VitePress 之外没有意义。
**原始树跳过 locale 首页。** 已发布页面链接到 `docs/user/index.md`,省略首页路由会破坏链接封闭。剥离版式后剩下的正文(即 H1)成本为零,重定向 frontmatter 在 VitePress 之外没有意义。
## Consequences
Agent `<page URL>.md` 获取任意页面的纯 Markdown,并在 `/llms.txt` 发现全集;渲染站不变。构建输出为每个路由多带一个 Markdown 文件旁置图片副本——相对打包资产只是千字节级。孪生页保留 GitHub 风格的标题文本,而渲染站对含标点的标题使用不同 slug;agent 自行解析标题,因此没有门禁覆盖原始树的 fragment。因孪生页受众不需要而暂缓:`llms-full.txt`,以及每页的"查看 Markdown"控件——后者需要 stock-theme 站点刻意不设的 theme 目录。
Agent 把页面 URL 去掉末尾斜杠再加 `.md` 获取该页纯 Markdown,并在 `/llms.txt` 发现全集;渲染站不变。构建输出为每个路由多带一个 Markdown 文件、每个 index 路由多带一个别名,外加旁置图片副本——相对打包资产只是千字节级。孪生页保留 GitHub 风格的标题文本,而渲染站对含标点的标题使用不同 slug;agent 自行解析标题,因此没有门禁覆盖原始树的 fragment。因孪生页受众不需要而暂缓:`llms-full.txt`,以及每页的"查看 Markdown"控件——后者需要 stock-theme 站点刻意不设的 theme 目录。
+1 -1
View File
@@ -5,7 +5,7 @@ description: Use when publishing, updating, moving, or removing DeepSeek Harness
# Synchronizing the DeepSeek Harness Documentation Site
Keep repository Markdown as the only editable content source. Treat the website as a tested projection: [website/docs.ts](../../../website/docs.ts) selects public pages, [scripts/project-doc-site.ts](../../../scripts/project-doc-site.ts) rewrites them into the disposable `website/.generated/` tree, and VitePress builds that tree. The build additionally emits a raw-Markdown twin of every route (`<page URL>.md`) and a root `llms.txt` index; both derive from the same manifest and projector, so publishing, moving, or removing a page updates them automatically and `docs:build` fails when one is missing.
Keep repository Markdown as the only editable content source. Treat the website as a tested projection: [website/docs.ts](../../../website/docs.ts) selects public pages, [scripts/project-doc-site.ts](../../../scripts/project-doc-site.ts) rewrites them into the disposable `website/.generated/` tree, and VitePress builds that tree. The build additionally emits a raw-Markdown twin of every route (page URL minus any trailing slash, plus `.md`; index routes also get a parent-level alias) and a root `llms.txt` index; both derive from the same manifest and projector, so publishing, moving, or removing a page updates them automatically and `docs:build` fails when one is missing.
Repository translations follow the sibling pairing contract: English `foo.md`, Chinese `foo.zh.md`, and `foo.i18n.yaml` live together. Never create `zh-CN/` or other locale directories for website content. The site route trees are independent of that source layout: `foo.zh.md` projects to the root route and `foo.md` projects to the matching `/en/` route.
+56 -7
View File
@@ -12,7 +12,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
import { docsPages, landingLink, routeLink, sectionSpec, type DocsPage } from '../website/docs.ts'
import {
addProjectionFrontmatter, emitRawMarkdownPages, llmsTxt, projectedPageContent, publishableImage,
rawMarkdownPageContent, rawMarkdownRoute, resolveRepositoryRef, rewriteMarkdown,
rawMarkdownFiles, rawMarkdownPageContent, rawMarkdownRoute, resolveRepositoryRef, rewriteMarkdown,
} from './project-doc-site.ts'
const roots: string[] = []
@@ -567,17 +567,21 @@ describe('rawMarkdownPageContent', () => {
it('keeps the home body the rendered site omits and drops the VitePress frontmatter', () => {
expect(rawMarkdownPageContent(
'---\nlayout: false\nhead:\n - - meta\n - http-equiv: refresh\n content: 0; url=./guide/quickstart\n---\n\n# Harness\n\nEnglish | [中文](./index.md)\n\nBody.\n',
'docs/user/index.zh.md',
)).toBe('# Harness\n\nBody.\n')
})
it('drops the language switcher and repository badge like the rendered site', () => {
const badge = '[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)'
expect(rawMarkdownPageContent(`# Guide\n\nEnglish | [中文](./x)\n\nBody.\n\n${badge}\n`))
expect(rawMarkdownPageContent(`# Guide\n\nEnglish | [中文](./x)\n\nBody.\n\n${badge}\n`, 'docs/guide.md'))
.toBe('# Guide\n\nBody.\n')
})
it('rejects unclosed frontmatter', () => {
expect(() => rawMarkdownPageContent('---\nlayout: false\n')).toThrow('unclosed YAML frontmatter')
it('rejects unclosed frontmatter and names the page', () => {
// The twin pass is the first place an ordinary page's frontmatter is
// parsed, so an anonymous error would leave 168 routes to search.
expect(() => rawMarkdownPageContent('---\nlayout: false\n', 'docs/broken.md'))
.toThrow('project-doc-site: "docs/broken.md" has unclosed YAML frontmatter')
})
})
@@ -616,6 +620,51 @@ describe('emitRawMarkdownPages', () => {
expect(readFileSync(join(out, 'index.md'), 'utf8')).toBe('# Home\n\n[A](./a.md)\n')
})
it('emits a parent-level alias for an index route with links recomputed', () => {
// A copied alias would carry the index page's relative links one directory
// too high, so the alias is its own projection over the alias route.
const { root, pages } = fixture()
writeFileSync(join(root, 'docs/c.md'), '# C\n\n[A](a.md)\n')
pages.push({
locale: 'root', contentLocale: 'en-US', source: 'docs/c.md', route: 'guide/index.md',
label: 'C', sidebar: 'zh-guide', section: 'Test', order: 3,
})
const out = mirrorDir()
emitRawMarkdownPages(out, { pages, repoRoot: root, repositoryRef: 'abc123' })
expect(readFileSync(join(out, 'guide/index.md'), 'utf8')).toBe('# C\n\n[A](../a.md)\n')
expect(readFileSync(join(out, 'guide.md'), 'utf8')).toBe('# C\n\n[A](./a.md)\n')
})
it('refuses to overwrite a file the build already carries', () => {
// The twin pass writes into a populated build directory, and VitePress has
// already copied `website/public/` there; a page image sharing one of
// those names must fail loud instead of silently replacing the site file.
const { root, pages } = fixture()
writeFileSync(join(root, 'docs/a.md'), '![logo](../packages/logo.svg)\n')
const out = mirrorDir()
writeFileSync(join(out, 'logo.svg'), 'public copy\n')
expect(() => {
emitRawMarkdownPages(out, { pages, repoRoot: realpathSync(root), repositoryRef: 'abc123' })
}).toThrow('would overwrite')
expect(readFileSync(join(out, 'logo.svg'), 'utf8')).toBe('public copy\n')
})
})
describe('rawMarkdownFiles', () => {
it('lists every route plus a parent alias per index route', () => {
const files = rawMarkdownFiles()
for (const page of docsPages) expect(files).toContain(page.route)
expect(files).toContain('reference.md')
expect(files).toContain('en/reference.md')
expect(files).toContain('en.md')
// The root home has no parent to alias into; `/` is documented as `/index.md`.
expect(files).not.toContain('.md')
expect(new Set(files).size).toBe(files.length)
})
})
describe('raw Markdown projection of the published manifest', () => {
@@ -630,9 +679,9 @@ describe('raw Markdown projection of the published manifest', () => {
rmSync(mirror, { recursive: true, force: true })
})
it('emits every published route', () => {
for (const page of docsPages) {
expect(existsSync(join(mirror, page.route)), page.route).toBe(true)
it('emits every published route and every index alias', () => {
for (const file of rawMarkdownFiles()) {
expect(existsSync(join(mirror, file)), file).toBe(true)
}
})
+79 -22
View File
@@ -4,7 +4,8 @@
* The generated tree is disposable: sources stay in their owning `docs/`
* tier, while this adapter rewrites cross-source links for the public site.
* The same projection also emits a raw-Markdown twin of every route into the
* build output, so `<page URL>.md` serves the page as plain Markdown.
* build output, so a page's URL, minus any trailing slash, plus `.md` serves
* it as plain Markdown.
*/
import {
@@ -15,7 +16,7 @@ import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmFromMarkdown } from 'mdast-util-gfm'
import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
import { docsPages, orderedPages, type DocsLocale, type DocsPage, type DocsSidebar } from '../website/docs.ts'
import { docsPages, localeCollections, orderedPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
import {
isExternalOrAbsoluteMarkdownUrl,
markdownDestination,
@@ -333,11 +334,18 @@ function defaultProjectionContext(): ProjectionContext {
return { pages: docsPages, repoRoot: root, repositoryRef: resolveRepositoryRef(process.env) }
}
/** Project every page and its images into one target tree. */
/**
* Project every page and its images into one target tree.
*
* `entries` are what gets emitted; link resolution always reads the canonical
* `context.pages`, so an alias entry sharing a source with its index route
* emits at its own path while links keep targeting canonical routes.
*/
function projectPagesInto(
targetRoot: string,
context: ProjectionContext,
pageContent: (markdown: string, page: DocsPage) => string,
entries: DocsPage[] = context.pages,
): void {
const routes = new Set<string>()
/** Projected path to the repository file that claimed it, pages and images alike. */
@@ -352,10 +360,19 @@ function projectPagesInto(
+ ` both project to ${relative(targetRoot, target).split(sep).join('/')}.`,
)
}
// A file the projection did not claim is another producer's output — in
// the twin pass, the build VitePress just wrote, including `public/`
// copies. Overwriting one would silently corrupt the site.
if (holder === undefined && existsSync(target)) {
throw new Error(
`project-doc-site: ${repoPath(sourceAbs, context.repoRoot)} would overwrite existing build file`
+ ` ${relative(targetRoot, target).split(sep).join('/')}.`,
)
}
claimed.set(target, sourceAbs)
}
for (const page of context.pages) {
for (const page of entries) {
if (routes.has(page.route)) throw new Error(`project-doc-site: duplicate route ${JSON.stringify(page.route)}.`)
routes.add(page.route)
const sourceAbs = resolve(context.repoRoot, page.source)
@@ -410,41 +427,81 @@ export function projectDocs(): void {
* Strip the leading YAML frontmatter of a projected page.
*
* @param markdown Rewritten canonical Markdown content.
* @param source Repository-relative page source, named by the failure.
* @returns The content after the frontmatter block, or the input when none opens it.
*/
function withoutFrontmatter(markdown: string): string {
function withoutFrontmatter(markdown: string, source: string): string {
if (!markdown.startsWith('---\n')) return markdown
const closingDelimiter = '\n---\n'
const closing = markdown.indexOf(closingDelimiter, 4)
if (closing === -1) throw new Error('project-doc-site: page has unclosed YAML frontmatter.')
if (closing === -1) {
throw new Error(`project-doc-site: ${JSON.stringify(source)} has unclosed YAML frontmatter.`)
}
return markdown.slice(closing + closingDelimiter.length).replace(/^\n+/, '')
}
/**
* The raw-Markdown twin of one published page.
*
* Frontmatter is VitePress rendering configuration, so it is dropped rather
* than truncated to: a locale home page keeps its body here even though the
* rendered site replaces it with a redirect.
* Frontmatter is VitePress rendering configuration and is dropped. A locale
* home page therefore keeps its body here, while the rendered site truncates
* it to the frontmatter redirect.
*
* @param markdown Rewritten canonical Markdown content.
* @param source Repository-relative page source, named by frontmatter failures.
* @returns Plain Markdown without frontmatter or repository chrome.
*/
export function rawMarkdownPageContent(markdown: string): string {
return withoutRepositoryChrome(withoutFrontmatter(markdown))
export function rawMarkdownPageContent(markdown: string, source: string): string {
return withoutRepositoryChrome(withoutFrontmatter(markdown, source))
}
/**
* Parent-level alias route of an index route, or `undefined` for other routes.
*
* The rendered site shows an index route as a directory URL, so "append
* `.md`" naturally lands on `<dir>.md` once the trailing slash is dropped.
* The root `index.md` has no parent to alias into.
*/
function indexAliasRoute(route: string): string | undefined {
const match = /^(.+)\/index\.md$/.exec(route)
return match?.[1] === undefined ? undefined : `${match[1]}.md`
}
/**
* Site-relative Markdown files the raw-Markdown projection emits: every
* route, plus one parent-level alias per index route.
*
* @param pages Pages to project, defaulting to the publication manifest.
* @returns The emitted paths, routes first.
*/
export function rawMarkdownFiles(pages: DocsPage[] = docsPages): string[] {
const aliases = pages.map(page => indexAliasRoute(page.route)).filter(alias => alias !== undefined)
return [...pages.map(page => page.route), ...aliases]
}
/**
* Emit the raw-Markdown twin of every published route into a built site, so
* static hosting serves `<page URL>.md` beside each rendered page. Referenced
* images are copied beside the pages, keeping the same relative URLs valid in
* both trees. Existing build files stay in place.
* static hosting serves the page's URL, minus any trailing slash, plus `.md`
* as plain Markdown. Each index route also emits a parent-level alias twin,
* projected over the alias route so its relative links stay correct.
* Referenced images are copied beside the pages, keeping the same relative
* URLs valid in both trees. Existing build files stay in place, and a name
* collision with one fails the emission.
*
* @param outDir Build output directory to emit into.
* @param context Manifest and repository inputs, defaulting to this repository.
*/
export function emitRawMarkdownPages(outDir: string, context: ProjectionContext = defaultProjectionContext()): void {
projectPagesInto(outDir, context, markdown => rawMarkdownPageContent(markdown))
const aliases = context.pages.flatMap((page) => {
const alias = indexAliasRoute(page.route)
return alias === undefined ? [] : [{ ...page, route: alias }]
})
projectPagesInto(
outDir,
context,
(markdown, page) => rawMarkdownPageContent(markdown, page.source),
[...context.pages, ...aliases],
)
}
/**
@@ -469,7 +526,7 @@ export function rawMarkdownRoute(route: string, context: ProjectionContext = def
repoRoot: context.repoRoot,
repositoryRef: context.repositoryRef,
placeImage: absPath => `./${encodeURI(basename(absPath))}`,
}))
}), page.source)
}
/** Site identity written into llms.txt. */
@@ -483,9 +540,9 @@ export interface LlmsTxtSite {
}
/** Locale groups llms.txt lists, in the order the site's navigation presents them. */
const llmsTxtLocales: readonly { heading: string; locale: DocsLocale; collections: readonly DocsSidebar[] }[] = [
{ heading: '简体中文', locale: 'root', collections: ['zh-guide', 'zh-develop', 'zh-reference'] },
{ heading: 'English', locale: 'en', collections: ['en-guide', 'en-develop', 'en-reference'] },
const llmsTxtLocales: readonly { heading: string; locale: DocsLocale }[] = [
{ heading: '简体中文', locale: 'root' },
{ heading: 'English', locale: 'en' },
]
/**
@@ -504,11 +561,11 @@ export function llmsTxt(site: LlmsTxtSite): string {
'',
`> ${site.description}`,
'',
'每个页面 URL 加 `.md` 后缀即为该页原始 Markdown。Append `.md` to any page URL for its raw Markdown source.',
'页面 URL 去掉末尾斜杠再加 `.md` 即为该页原始 Markdown(根路径用 `/index.md`);下方列表是各页精确地址。Drop any trailing slash and append `.md` to a page URL for its raw Markdown (the site root is `/index.md`); the list below carries the exact addresses.',
]
for (const { heading, locale, collections } of llmsTxtLocales) {
for (const { heading, locale } of llmsTxtLocales) {
lines.push('', `## ${heading}`, '')
for (const collection of collections) {
for (const collection of localeCollections[locale]) {
for (const page of orderedPages(locale, collection)) {
lines.push(`- [${page.label}](${site.base}${page.route}): ${page.section}`)
}
+4 -3
View File
@@ -11,7 +11,7 @@
import { existsSync, globSync, readFileSync } from 'node:fs'
import { resolve, sep } from 'node:path'
import { JSDOM } from 'jsdom'
import { docsPages } from '../website/docs.ts'
import { rawMarkdownFiles } from './project-doc-site.ts'
const root = resolve(import.meta.dirname, '..')
@@ -154,11 +154,12 @@ export function missingSiteFiles(distRoot: string, expected: readonly string[]):
function main(): number {
const distRoot = resolve(root, 'website/.dist')
const report = inspectSiteFragments(distRoot)
const missing = missingSiteFiles(distRoot, [...docsPages.map(page => page.route), 'llms.txt'])
const expected = rawMarkdownFiles()
const missing = missingSiteFiles(distRoot, [...expected, 'llms.txt'])
if (report.broken.length === 0 && missing.length === 0) {
console.log(
`verify-doc-site-fragments: ${report.checked} internal fragment reference(s) resolve;`
+ ` ${docsPages.length} raw-Markdown route(s) and llms.txt emitted.`,
+ ` ${expected.length} raw-Markdown file(s) and llms.txt emitted.`,
)
return 0
}
+11 -8
View File
@@ -5,7 +5,7 @@ import { resolve } from 'node:path'
import type { DefaultTheme, PageData, SiteConfig } from 'vitepress'
import type { ViteDevServer } from 'vite'
import { withMermaid } from 'vitepress-plugin-mermaid'
import { landingLink, orderedPages, routeLink, sectionSpec, type DocsLocale, type DocsPage, type DocsSidebar } from '../docs.ts'
import { landingLink, localeCollections, orderedPages, routeLink, sectionSpec, type DocsLocale, type DocsPage, type DocsSidebar } from '../docs.ts'
import { docsSourceFiles, emitRawMarkdownPages, llmsTxt, projectDocs, rawMarkdownRoute } from '../../scripts/project-doc-site.ts'
projectDocs()
@@ -58,14 +58,14 @@ interface GuideModules {
*/
const guideModules = {
root: {
guide: 'zh-guide',
develop: { label: '开发', collection: 'zh-develop' },
reference: { label: '参考', collection: 'zh-reference' },
guide: localeCollections.root[0],
develop: { label: '开发', collection: localeCollections.root[1] },
reference: { label: '参考', collection: localeCollections.root[2] },
},
en: {
guide: 'en-guide',
develop: { label: 'Development', collection: 'en-develop' },
reference: { label: 'Reference', collection: 'en-reference' },
guide: localeCollections.en[0],
develop: { label: 'Development', collection: localeCollections.en[1] },
reference: { label: 'Reference', collection: localeCollections.en[2] },
},
} satisfies Record<DocsLocale, GuideModules>
@@ -125,7 +125,10 @@ function serveRawMarkdown(server: ViteDevServer): void {
// The dev client imports page modules at these same `.md` URLs, and a
// module script must reach Vite's transform. Browsers declare the purpose:
// `script` for module imports, `document` for address-bar navigation.
// Header-less clients (curl, agents) read the raw twin.
// Header-less clients (curl, agents) read the raw twin. In-page fetch()
// (`empty`) also passes to Vite — a deliberate dev-only divergence that
// keeps Vite's own requests unbroken, while production static hosting
// answers such a fetch with the raw file.
const fetchDest = req.headers['sec-fetch-dest']
if (fetchDest !== undefined && fetchDest !== 'document') {
next()
+1 -1
View File
@@ -10,6 +10,6 @@ Keep canonical prose and generated catalogs in their owning `docs/` tier, then e
The projector writes disposable Markdown to the ignored `website/.generated/` directory. Never edit or commit `.generated/`, `.cache/`, or `.dist/`.
The build also emits each route's raw-Markdown twin and a root `llms.txt` index into `.dist/`, so `<page URL>.md` serves the page as plain Markdown. Both derive from the publication manifest at build time; neither is ever a file in this tree.
The build also emits each route's raw-Markdown twin (with a parent-level alias per index route) and a root `llms.txt` index into `.dist/`, so a page's URL, minus any trailing slash, plus `.md` serves it as plain Markdown. Both derive from the publication manifest at build time; neither is ever a file in this tree.
Run `pnpm docs:check` after changing this subtree; the gate rejects additional non-ignored Markdown under `website/`.
+10
View File
@@ -412,6 +412,16 @@ const reference = [
}]),
]
/**
* Sidebar collections of each locale, in the order the site's navigation
* presents them. The navigation bar and the llms.txt index both read this
* sequence, so a new collection lands in both surfaces together.
*/
export const localeCollections = {
root: ['zh-guide', 'zh-develop', 'zh-reference'],
en: ['en-guide', 'en-develop', 'en-reference'],
} as const satisfies Record<DocsLocale, readonly DocsSidebar[]>
/** A sidebar group, matched to pages by `label`. */
export interface DocsSection {
/** Group heading, equal to the `section` field of every page it holds. */