mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
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:
@@ -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 目录。
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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://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'), '\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
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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/`.
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user