ARTICLE · 981287
【工程篇】单向文档投影:如何让 VitePress 在线文档站与 GitHub 源码 100% 保持同步
“文档站不是第二份代码库,而是原生 Markdown 源码的一面镜子。把静态站当成编译产物的单向投影,才能从根源上终结两套文档脱节的宿命。”

一、现实痛点:文档站与代码库的“双头脱节”
在维护现代开源项目或企业级框架时,工程团队往往面临着一个经典的体验冲突:
- 在 GitHub 侧
:希望开发者在仓库中浏览 docs/*.md时,拥有最原生、最纯粹的 Markdown 阅读体验,所有相对链接(如../api/session.md)与图片均能无缝点击跳转; - 在公网在线站侧
:又希望搭建一套基于 VitePress / Docusaurus 的华丽静态文档站,具备搜索、多语言切换、侧边栏导航与优雅的排版。
为了兼顾两者,许多团队最常见的做法是在仓库里建一个 website/ 目录,然后把 docs/ 里的 Markdown 文件人肉复制一份过去。
这种“双头维护”模式必然会带来三大灾难:
- 事实源分裂与文档漂移(Doc Drift)
开发者修了一个 Bug,顺手更新了 docs/core.md,但忘了同步更新website/src/core.md。一个月后,公网文档站与 GitHub 源码文档彻底分道扬镳,用户在群里不断吐槽“照着官网文档写代码跑不通”。 - 相对路径打包后大面积 404
在 GitHub 仓库中有效的相对文件链接(例如跳到 packages/core/src/index.ts),在经过 VitePress 静态打包后直接变成断链或 404 错误页面;跨页面的 Heading 锚点也因生成器 Slug 算法不同而频繁失效。 - 临时构建缓存污染代码库
为了让 VitePress 识别文档,开发者在 website/目录下生成了大量中间文件,稍有不慎就通过git add提交进了版本库,导致 Git 历史充斥着机器生成的脏文件。
DeepSeek Harness(DSH)提出的 dsh-doc-site-sync 技能,以一种极其优雅的架构范式终结了这种混乱:“源码是唯一真相,静态站只是单向编译投影(Single Source of Truth & Unidirectional Projection)”。
二、单向文档投影架构设计
DSH 严格规定:全仓所有的技术文档只能在 docs/ 原生目录下编写一次。
在线静态文档站绝不拥有独立的 Markdown 源文件,而是在构建时通过自动化脚本将 docs/ 投影为瞬时的临时目录树:
┌─────────────────────────────────────────────────────────────┐
│ 唯一事实源 (Source of Truth) │
│ • 原生 Markdown: docs/**/*.md (支持 GitHub 原生相对跳转) │
│ • 声明式清单: website/docs.ts (严格控制对外公开发布边界) │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ AST 单向投影引擎 (project-doc-site.ts) │
│ • 路径映射与双语派生: foo.zh.md -> / , foo.md -> /en/ │
│ • 链接智能重写: 站内转 Route,源码转 GitHub SHA 带行号 │
│ • 静态资源搬运: 图片自动提取至临时静态目录 │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 临时生成树 (website/.generated/ - 瞬时用后即弃) │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ VitePress 编译器 ──> 最终公网静态站点 HTML │
└─────────────────────────────────────────────────────────────┘

三、四大核心机制深度拆解
1. 源码即唯一真相:严禁手改生成目录
在 DSH 的开发守则中,确立了不可逾越的铁律:
- 绝对禁止
在 website/.generated/、website/.cache/或website/zh-CN/等目录下直接新建或修改 Markdown 文件; 所有的修改、翻译和排版,必须且只能发生在 docs/原生目录下的源文件中;website/.generated/被列入 .gitignore,它在本地构建与 CI 中完全是瞬时、可随时清空、用后即弃的派生物。
2. 声明式映射清单(website/docs.ts)
哪些文档该对外发布?公网侧边栏如何组织?DSH 通过 website/docs.ts 进行全声明式配置,使用 pairedPages() 契约自动管理:
import { pairedPages, typeDocsPage } from'./docs';
exportconstpages: DocsPage[] = [
pairedPages({
source: 'docs/architecture.md', // 原生英文 Markdown 路径
route: 'architecture.md', // VitePress 路由路径
sidebar: 'zh-guide', // 所属侧边栏集合
section: '核心架构解析', // 分组标题
order: 1, // 排序权重
}),
];
🌟 声明式清单的两大精妙设计:
- 自动派生中英双语路由
:只需声明英文源文件路径, pairedPages()会自动识别同级的docs/architecture.zh.md,并将中文自动投射到根路由(/architecture),英文自动投射到/en/architecture; - 构建公开发布白名单
:开发团队内部沉淀的临时 RFC、事故复盘(Postmortem)、内部测试用例文档等虽然存放在 docs/下,但只要未在docs.ts中注册,投影引擎就会自动将其隔离,绝不会泄露到公网文档站。
3. 智能链接与资源 AST 重写(Link Projector)
当原生 Markdown 被投影到 VitePress 目录树时,投影引擎(project-doc-site.ts)会遍历 Markdown AST,执行四级智能重写规则:
【AST 链接投影四级重写规则】
┌───────────────────────┬─────────────────────────────────────────────┐
│ 目标类型 │ 投影转换结果 │
├───────────────────────┼─────────────────────────────────────────────┤
│ 目标在 docs.ts 清单内 │ 自动重写为静态站相对路由 (如 /architecture) │
├───────────────────────┼─────────────────────────────────────────────┤
│ 目标为仓内源码/单测 │ 自动转为 GitHub 官方源码链接 (带精确行号) │
├───────────────────────┼─────────────────────────────────────────────┤
│ 相对图片路径 │ 自动复制图片至静态目录并重写引用 URL │
├───────────────────────┼─────────────────────────────────────────────┤
│ 目标文件不存在 │ 🚨 投影构建直接阻断报错,杜绝死链上线 │
└───────────────────────┴─────────────────────────────────────────────┘
💻 举个生动的转换例子:
在 docs/guide.md 中写下这样一行原生 Markdown:
参考 [会话管理实现](../packages/core/src/session.ts#L15-L30) 与 [架构文档](./architecture.md)。
经过单向投影引擎编译后,在 VitePress 静态站中会自动被智能转译为:
参考 <ahref="https://github.com/my-org/my-repo/blob/master/packages/core/src/session.ts#L15-L30">会话管理实现</a> 与 <ahref="/architecture">架构文档</a>。
无论读者是在 GitHub 网页浏览,还是在独立文档站查阅,所有链接 100% 顺畅直达!

4. 跨语言锚点一致性检查(verify-doc-site-fragments)
VitePress 在根据标题生成 URL Slug 锚点时,对中文字符的处理算法与 GitHub 默认的 Slug 生成器存在细微差异。
为了防止多语言跳转时出现“明明页面存在,但跳不到指定小节”的尴尬断层,DSH 制定了双重保障机制:
- 显式插入 HTML 锚点
:在需要跨语言、跨页面引用的关键标题上方,显式标注固定 ID: <aid="disposer-rule"></a>
### 资源注销闭环(Disposer Rule) - CI 自动化锚点门禁
:在提交前运行 pnpm docs:check,静态扫描全站所有页面内部跳转锚点,一旦发现悬空 Fragment 立即阻断。
🛠️ DSH 文档站维护核心命令速查表
pnpm docs:dev | .generated/ 内容,下次重启直接被覆盖丢失 | ||
pnpm docs:check | |||
pnpm run doc-sync |
四、工程启示与落地指南
- 坚决杜绝文档的多份物理拷贝
任何需要人工同步两份内容的架构设计都是不可持续的。将文档站定位为“编译后的只读视图”,是保持文档与源码强一致的唯一正道。 - 把链接重写交给编译器,把精力留给内容创作
作者编写文档时应当且只应当关注“当前仓库的相对目录关系”。利用 AST 编译器自动适配 GitHub 与静态站两种形态,能极大解放团队心智。 - 建立文档站的自动化冒烟门禁
像对待业务代码一样对待文档站:将死链检查(Link Check)和锚点验证(Fragment Check)作为 CI 的必跑项,让文档 Bug 在合并前被扼杀在摇篮里。
💡 下期预告(全专栏收官大结局):
源文档只改了一句话,AI 却把整篇几千字全重新翻译了一遍,导致社区人工润色的成果被无情覆盖;更要命的是代码块里的注释在中英文档里经常不同步!
最后一篇我们将拆解dsh-translate-docs,看 DeepSeek 如何用「差异简报驱动(Briefing-Driven)」与「代码块字节全等门禁」打造极速、精准的双语国际化流水线!欢迎「关注」专栏并设为「星标」🌟,第一时间获取硬核工程深度解析!如果本文对你有启发,请点个「在看」与「赞」支持一下!