一次接口文档站工程化实践:VitePress、Swagger 与 Cloudflare 部署踩坑记录
这次做文档站,不是为了把 Markdown 换个皮肤。
真正的问题是:接口文档已经散落在很多地方了。
有些在 README 里,有些在接口定义里,有些在 Swagger JSON 里,有些在聊天记录和联调说明里。短期看都能用,长期看就会变成一个很典型的问题:
1 | 前端问接口怎么调; |
所以这次的目标不是“搭一个好看的文档站”,而是把接口事实源、人工说明、构建产物和发布流程串成一条稳定链路。
最后落下来的方案是:
1 | Markdown 负责解释语义和联调流程 |
这篇文章记录这次实践里的设计取舍、Cloudflare 工具链准备、AI Agent 技能安装,以及几个实际踩坑。
先划边界
文档站最容易失控的地方,是一开始就想把所有东西都放进去。
接口文档、架构设计、研发计划、部署记录、事故复盘、Prompt 记录、测试用例、内部 TODO,全都放进去当然很完整,但它很快会变成另一个没人敢改的资料仓库。
这次我给文档站定了一个很窄的边界:
1 | 只放对外联调需要看的内容。 |
比如:
- App 端接口
- 管理后台接口
- 设备端接口
- 媒体上传接口
- 跨端绑定流程
- WebSocket / SSE 事件说明
- Swagger API 入口
而这些内容不放进公开文档站:
- 内部部署密钥
- 长期研发规划
- 事故细节
- 临时 TODO
- 大模型测试用例
- 只对后端开发有意义的实现笔记
这个边界很重要。
文档站不是知识库的全部,它只是联调入口。只要这个判断稳定,后面目录怎么分、构建怎么做、发布怎么自动化,都比较好定。
Markdown 和 Swagger 不要互相替代
很多团队会在这两件事之间摇摆:
1 | 有 Swagger 了,还要写文档吗? |
我的结论是:都要,但职责不同。
Swagger 擅长回答这些问题:
1 | 路径是什么? |
Markdown 擅长回答这些问题:
1 | 这个接口在什么流程里调用? |
Swagger 是结构化事实,Markdown 是上下文说明。两者放在同一个文档站里,联调体验会比单独丢一个 Swagger 地址好很多。
为什么用 VitePress
这类工程文档站,我不太想引入太重的系统。
VitePress 的好处是足够简单:
- Markdown 原生友好
- 本地预览很快
- 侧边栏、导航、搜索、代码高亮都够用
- 构建产物是纯静态文件
- 和现有代码仓库放在一起维护成本低
它不适合做复杂权限系统,也不适合替代内部知识库。但如果目标是“把一组工程 Markdown 发布成可浏览的接口文档站”,它很合适。
这次目录大致按使用方分组,而不是完全按后端服务分组:
1 | docs/ |
这个分法更贴近读者。
前端同学通常不会关心某个后端服务叫什么,他关心的是“我在 App 端该看哪里”。测试同学也更关心流程,而不是服务边界。
侧边栏不要手写到崩
文档站刚开始的时候,手写侧边栏没问题。
但接口文档会越写越多。如果每新增一篇文档都要手动改一次 config.mts,迟早会出现几类问题:
1 | 文档存在,但侧边栏没入口; |
更稳的方式是让配置自动扫描 Markdown 文件,并从一级标题提取展示名。
思路大概是:
1 | function titleFor(relativePath: string): string { |
再按目录生成分组:
1 | function group(text: string, dir: string) { |
这样文档标题只维护一次:就在 Markdown 里。
Swagger 要进构建产物
一开始最容易走的路,是继续保留独立 Swagger 地址,然后在文档站里放一个外链。
这样能用,但体验不好:
1 | 文档站是一套入口; |
更好的方式是构建文档站时顺手做三件事:
1 | 1. 从接口定义生成各服务 Swagger JSON |
最终页面只需要一个入口:
1 | /api/ |
Swagger 页面内部再用下拉框切换服务:
1 | App API |
这件事看起来很小,但对联调很关键。
所有人只记一个地址,文档说明和接口结构也来自同一次构建。
Cloudflare 选 Pages 还是 Workers
这里有一个很常见的混淆:静态文档站到底应该部署到 Cloudflare Pages,还是 Cloudflare Workers?
我的判断是:
1 | 如果只是 VitePress / Hexo / Docusaurus 这类静态站,优先考虑 Cloudflare Pages。 |
Cloudflare Pages 的模型更贴合静态站:
- 连接 Git 仓库自动构建
- 或者直接上传构建产物
_redirects和_headers支持静态站常见规则- 预览部署也比较自然
如果走 Direct Upload,大概是这样:
1 | npm run docs:build |
如果是 Workers Static Assets,则通常会有一个 wrangler.jsonc:
1 | { |
然后发布:
1 | npx wrangler deploy |
这两条路都能部署静态资源,但心智模型不一样。
Pages 更像“静态站托管平台”,Workers 更像“边缘运行时 + 静态资源”。如果只是文档站,不要一上来就把自己带进 Worker 代码里。
Wrangler 最好放进项目
Cloudflare 的 CLI 是 Wrangler。
现在不要靠机器上某个全局版本来部署,最好把 Wrangler 固定在项目依赖里:
1 | npm i -D wrangler@latest |
这样至少有两个好处:
1 | 1. CI 和本地使用同一套 CLI 版本; |
如果只是临时执行,npx wrangler ... 也能工作,但工程项目最好还是把部署工具纳入依赖。
AI Agent 也要先装 Cloudflare Skills
现在很多 Cloudflare 配置会交给 AI 编程助手一起处理。
但这里有个问题:Cloudflare 平台变化很快,AI Agent 很容易把 Pages、Workers、Workers Sites、Static Assets、Wrangler v2/v3/v4 的旧知识混在一起。
所以在让 AI 改 Cloudflare 配置前,最好先安装 Cloudflare Skills。
通用方式:
1 | npx skills add https://github.com/cloudflare/skills |
Claude Code 可以用插件方式:
1 | /plugin marketplace add cloudflare/skills |
Codex 里可以直接安装 Cloudflare 插件。Cloudflare 官方的 Codex setup 文档里也明确说明,安装插件会同时带上 Cloudflare Skills 和 MCP server。
Skills 的价值不是替你部署,而是让 Agent 在动手前先知道这些边界:
1 | 这是 Pages 还是 Workers? |
尤其是文档站这种场景,AI 很容易写出“能跑但不合适”的方案。先给它 Cloudflare 上下文,能少走很多弯路。
坑一:clean URL 刷新 404
VitePress 可以打开 cleanUrls,把:
1 | /integration/device.html |
变成:
1 | /integration/device |
浏览时很好看,但部署到静态托管后,用户直接刷新 /integration/device,平台未必知道要返回 /integration/device.html。
解决方式是在构建产物里生成 _redirects:
1 | /api /api/index.html 200 |
Cloudflare Pages 会读取构建目录里的 _redirects 文件,并按规则处理静态资源响应。
这一步最好自动生成,不要手写。因为文档一多,手写 rewrite 很容易漏。
坑二:Swagger UI 页面打开但空白
Swagger UI 本身不是一个 HTML 文件就完了。
它还需要:
1 | swagger-ui.css |
如果只复制 index.html,本地可能因为路径凑巧能打开,部署后就变成空白页或者控制台 404。
构建脚本里应该明确把资源复制进 /api/swagger-ui/,再把 JSON 放到 /api/*.json。
坑三:/api/ 被前端路由接管
VitePress 是静态站,但它也有自己的客户端路由。
如果导航链接写得不够明确,点击 /api/ 时可能被文档站路由错误处理,或者部署平台把它当成普通文档路径。
我的处理方式是:
1 | 文档 nav 里 API Swagger 指向 /api/ |
这样 /api/ 就是一个明确的静态入口,不参与文档 Markdown 路由。
坑四:首页不是普通文档页
文档站首页应该回答:
1 | 我是谁? |
它不应该一进来就是几十个侧边栏链接。
首页更适合作为导航页,普通文档页再进入细分侧边栏。否则第一次打开的人很容易被目录淹没。
坑五:内部资料误发布
接口文档站经常和项目仓库放在一起。
这很好维护,但也带来风险:仓库里的内部文档不一定都适合公开发布。
比如:
1 | deployment.md |
VitePress 可以通过 srcExclude 排除不该进入站点的文件。
同时,文档站如果只是给联调人员使用,建议加:
1 | <meta name="robots" content="noindex,nofollow" /> |
再配合 Cloudflare 的 _headers:
1 | /* |
这不能替代权限控制,但至少避免搜索引擎主动收录。
坑六:发布产物和源文档混在一起
源文件和构建产物最好分开。
源文件是:
1 | docs/**/*.md |
构建产物是:
1 | docs/.vitepress/dist/ |
如果直接把 dist 提交回主仓库,代码 review 会很难看,搜索索引和 hash 文件也会制造大量噪音。
更清楚的做法是:
1 | 主仓库维护源文档和构建脚本; |
这样 review 的时候看源文档,线上部署的时候看发布仓库,各自职责清楚。
坑七:文档不跟发版走
最危险的文档不是没有文档,而是文档过期。
接口字段变了,Swagger 更新了,Markdown 没改;或者 Markdown 改了,Swagger 还是旧的。这种状态会比没有文档更误导人。
我更倾向把文档站发布放进发版链路:
1 | 生成接口定义 |
至少在工程机制上,让“接口变更”和“文档更新”靠近一点。
一条可复用的构建链路
最后沉淀下来的脚本流程可以概括成这样:
1 | set -euo pipefail |
发布可以有两种方式。
如果用 Cloudflare Pages Git 集成:
1 | push 发布仓库 |
如果用 Wrangler Direct Upload:
1 | npx wrangler pages deploy docs/.vitepress/dist |
如果用 Workers Static Assets:
1 | npx wrangler deploy |
选哪一种不重要,重要的是它要可重复。
最后给一份 checklist
以后再做类似文档站,我会先按这张表检查。
| 检查项 | 问题 |
|---|---|
| 文档边界 | 这个站点是公开联调文档,还是内部知识库? |
| 事实源 | 接口以 .api、OpenAPI 还是代码路由为准? |
| Markdown | 是否解释了调用流程、状态含义和端侧注意事项? |
| Swagger | 是否随构建自动生成? |
| 导航 | 是否按读者视角分组,而不是按内部服务名分组? |
| clean URL | 刷新深层路径是否正常? |
/api/ |
Swagger 页面是否能独立访问? |
| 静态资源 | Swagger UI 的 CSS、JS、JSON 是否都进入产物? |
| robots | 是否需要禁止搜索引擎收录? |
| 发布 | 源文件和产物是否分离? |
| Agent | AI 助手是否具备 Cloudflare 当前上下文? |
结语
文档站的价值不在于“有一个站”。
它真正有价值的地方,是把这些东西放到同一条工程链路里:
1 | 接口事实源 |
当这条链路稳定后,文档就不再是最后补的一份说明,而是交付的一部分。
这也是这次实践里最大的收获。
参考链接
- 标题: 一次接口文档站工程化实践:VitePress、Swagger 与 Cloudflare 部署踩坑记录
- 作者: Jiayu Xu
- 创建于 : 2026-06-14 14:09:47
- 更新于 : 2026-06-14 14:11:22
- 链接: https://www.rasior.com/2026/06/14/20260614_vitepress-swagger-cloudflare-docs/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。