docs(website): serve every page as raw Markdown with an llms.txt index

Append .md to any published route for the page as plain Markdown: the
build emits a raw-Markdown twin of every route (frontmatter dropped,
home bodies kept, images beside pages) plus a manifest-generated
llms.txt, the dev server serves both per request, and the post-build
gate fails when either is missing. Fixes #2846.
This commit is contained in:
Yichen Jiang
2026-08-20 22:10:15 +08:00
parent a67b9a4d31
commit f3ce8218cc
10 changed files with 493 additions and 37 deletions
+57 -6
View File
@@ -1,12 +1,12 @@
/** VitePress configuration for the locally projected documentation site. */
import { readFileSync } from 'node:fs'
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import type { DefaultTheme, PageData } from 'vitepress'
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 { docsSourceFiles, projectDocs } from '../../scripts/project-doc-site.ts'
import { docsSourceFiles, emitRawMarkdownPages, llmsTxt, projectDocs, rawMarkdownRoute } from '../../scripts/project-doc-site.ts'
projectDocs()
@@ -111,6 +111,43 @@ function watchCanonicalDocs(server: ViteDevServer): void {
})
}
/**
* Serve the raw-Markdown twin of each route and llms.txt during development,
* matching what `buildEnd` emits into the static build. Pages project from
* their canonical sources per request, so an edit shows without a rebuild.
*/
function serveRawMarkdown(server: ViteDevServer): void {
server.middlewares.use((req, res, next) => {
if (req.url === undefined || (req.method !== 'GET' && req.method !== 'HEAD')) {
next()
return
}
// 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.
const fetchDest = req.headers['sec-fetch-dest']
if (fetchDest !== undefined && fetchDest !== 'document') {
next()
return
}
const pathname = req.url.split(/[?#]/, 1)[0] ?? ''
const sitePath = pathname.startsWith(base) ? pathname.slice(base.length) : pathname.replace(/^\//, '')
if (sitePath === 'llms.txt') {
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end(llmsTxt({ base, ...siteIdentity }))
return
}
const content = sitePath.endsWith('.md') ? rawMarkdownRoute(sitePath) : undefined
if (content === undefined) {
next()
return
}
res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
res.end(content)
})
}
function escapeVueInterpolation(html: string): string {
return html.replaceAll('{{', '{{').replaceAll('}}', '}}')
}
@@ -163,6 +200,12 @@ const sharedTheme: Pick<DefaultTheme.Config, 'search' | 'socialLinks' | 'editLin
/** Site base path, carrying the leading and trailing slashes VitePress requires. */
const base = process.env.DOCS_BASE ?? '/'
/** Site identity shared by the VitePress configuration and the llms.txt index. */
const siteIdentity = {
title: 'DeepSeek Harness',
description: '用于构建 Agent Harness 的插件化 SDK',
}
/**
* The DeepSeek wordmark, inlined so its `currentColor` fills follow the active
* theme. An `<img>` would freeze the mark at the colors the file declares.
@@ -247,9 +290,14 @@ function siteTitle(previewTag: string): string {
}
export default withMermaid({
title: 'DeepSeek Harness',
description: '用于构建 Agent Harness 的插件化 SDK',
title: siteIdentity.title,
description: siteIdentity.description,
base,
/** Emit the raw-Markdown twin of every route plus llms.txt beside the rendered site. */
buildEnd(siteConfig: SiteConfig) {
emitRawMarkdownPages(siteConfig.outDir)
writeFileSync(resolve(siteConfig.outDir, 'llms.txt'), llmsTxt({ base, ...siteIdentity }))
},
head: [
// VitePress leaves head hrefs untouched, so the base belongs here explicitly.
['link', { rel: 'icon', type: 'image/svg+xml', href: `${base}favicon.svg` }],
@@ -322,7 +370,10 @@ export default withMermaid({
plugins: [
{
name: 'deepseek-harness-doc-projector',
configureServer: watchCanonicalDocs,
configureServer(server) {
watchCanonicalDocs(server)
serveRawMarkdown(server)
},
},
],
},
+2
View File
@@ -10,4 +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.
Run `pnpm docs:check` after changing this subtree; the gate rejects additional non-ignored Markdown under `website/`.