一次接口文档站工程化实践:VitePress、Swagger 与 Cloudflare 部署踩坑记录

Jiayu Xu Lv2

这次做文档站,不是为了把 Markdown 换个皮肤。

真正的问题是:接口文档已经散落在很多地方了。

有些在 README 里,有些在接口定义里,有些在 Swagger JSON 里,有些在聊天记录和联调说明里。短期看都能用,长期看就会变成一个很典型的问题:

1
2
3
4
5
前端问接口怎么调;
后端说看 Swagger;
测试说字段含义不清楚;
设备端说流程文档找不到;
后来的人不知道哪份才是最新的。

所以这次的目标不是“搭一个好看的文档站”,而是把接口事实源、人工说明、构建产物和发布流程串成一条稳定链路。

最后落下来的方案是:

1
2
3
4
5
Markdown 负责解释语义和联调流程
Swagger 负责呈现接口结构和参数
VitePress 负责文档站体验
Cloudflare 负责静态托管和全球访问
发布脚本负责把这一切变成可重复动作

这篇文章记录这次实践里的设计取舍、Cloudflare 工具链准备、AI Agent 技能安装,以及几个实际踩坑。

先划边界

文档站最容易失控的地方,是一开始就想把所有东西都放进去。

接口文档、架构设计、研发计划、部署记录、事故复盘、Prompt 记录、测试用例、内部 TODO,全都放进去当然很完整,但它很快会变成另一个没人敢改的资料仓库。

这次我给文档站定了一个很窄的边界:

1
只放对外联调需要看的内容。

比如:

  • App 端接口
  • 管理后台接口
  • 设备端接口
  • 媒体上传接口
  • 跨端绑定流程
  • WebSocket / SSE 事件说明
  • Swagger API 入口

而这些内容不放进公开文档站:

  • 内部部署密钥
  • 长期研发规划
  • 事故细节
  • 临时 TODO
  • 大模型测试用例
  • 只对后端开发有意义的实现笔记

这个边界很重要。

文档站不是知识库的全部,它只是联调入口。只要这个判断稳定,后面目录怎么分、构建怎么做、发布怎么自动化,都比较好定。

Markdown 和 Swagger 不要互相替代

很多团队会在这两件事之间摇摆:

1
2
有 Swagger 了,还要写文档吗?
写了 Markdown,还要 Swagger 吗?

我的结论是:都要,但职责不同。

Swagger 擅长回答这些问题:

1
2
3
4
5
路径是什么?
方法是什么?
请求参数有哪些?
响应字段有哪些?
状态码是什么?

Markdown 擅长回答这些问题:

1
2
3
4
5
6
这个接口在什么流程里调用?
前端应该先调哪个,再调哪个?
哪些字段只是展示用,哪些字段会影响状态?
设备端断线重连时怎么处理?
历史兼容逻辑是什么?
联调时最容易错在哪里?

Swagger 是结构化事实,Markdown 是上下文说明。两者放在同一个文档站里,联调体验会比单独丢一个 Swagger 地址好很多。

为什么用 VitePress

这类工程文档站,我不太想引入太重的系统。

VitePress 的好处是足够简单:

  • Markdown 原生友好
  • 本地预览很快
  • 侧边栏、导航、搜索、代码高亮都够用
  • 构建产物是纯静态文件
  • 和现有代码仓库放在一起维护成本低

它不适合做复杂权限系统,也不适合替代内部知识库。但如果目标是“把一组工程 Markdown 发布成可浏览的接口文档站”,它很合适。

这次目录大致按使用方分组,而不是完全按后端服务分组:

1
2
3
4
5
6
7
8
9
10
11
12
docs/
index.md
integration/
README.md
app/
admin/
device/
media/
shared-topic-a.md
shared-topic-b.md
.vitepress/
config.mts

这个分法更贴近读者。

前端同学通常不会关心某个后端服务叫什么,他关心的是“我在 App 端该看哪里”。测试同学也更关心流程,而不是服务边界。

侧边栏不要手写到崩

文档站刚开始的时候,手写侧边栏没问题。

但接口文档会越写越多。如果每新增一篇文档都要手动改一次 config.mts,迟早会出现几类问题:

1
2
3
4
文档存在,但侧边栏没入口;
标题改了,侧边栏没同步;
文件顺序乱了;
README 和 index 的链接处理不一致。

更稳的方式是让配置自动扫描 Markdown 文件,并从一级标题提取展示名。

思路大概是:

1
2
3
4
5
6
function titleFor(relativePath: string): string {
const content = readFileSync(join(docsRoot, relativePath), 'utf8')
const heading = content.match(/^#\s+(.+)$/m)
if (heading) return heading[1].trim()
return basename(relativePath, '.md')
}

再按目录生成分组:

1
2
3
4
5
6
7
8
9
10
function group(text: string, dir: string) {
return {
text,
collapsed: true,
items: [
item(`${dir}/README.md`),
...listMarkdown(dir).map(item),
],
}
}

这样文档标题只维护一次:就在 Markdown 里。

Swagger 要进构建产物

一开始最容易走的路,是继续保留独立 Swagger 地址,然后在文档站里放一个外链。

这样能用,但体验不好:

1
2
3
4
文档站是一套入口;
Swagger 又是一套入口;
每个服务的 JSON 又是不同入口;
版本是否一致也不直观。

更好的方式是构建文档站时顺手做三件事:

1
2
3
1. 从接口定义生成各服务 Swagger JSON
2. 把 Swagger UI 静态资源复制进构建产物
3. 在 /api/ 下提供统一 Swagger 页面

最终页面只需要一个入口:

1
/api/

Swagger 页面内部再用下拉框切换服务:

1
2
3
4
5
6
App API
Admin API
Device API
Media API
Auth API
Gateway API

这件事看起来很小,但对联调很关键。

所有人只记一个地址,文档说明和接口结构也来自同一次构建。

Cloudflare 选 Pages 还是 Workers

这里有一个很常见的混淆:静态文档站到底应该部署到 Cloudflare Pages,还是 Cloudflare Workers?

我的判断是:

1
2
如果只是 VitePress / Hexo / Docusaurus 这类静态站,优先考虑 Cloudflare Pages。
如果项目已经使用 Workers Static Assets,或者需要 Worker 逻辑拦截请求,再用 Workers。

Cloudflare Pages 的模型更贴合静态站:

  • 连接 Git 仓库自动构建
  • 或者直接上传构建产物
  • _redirects_headers 支持静态站常见规则
  • 预览部署也比较自然

如果走 Direct Upload,大概是这样:

1
2
npm run docs:build
npx wrangler pages deploy docs/.vitepress/dist

如果是 Workers Static Assets,则通常会有一个 wrangler.jsonc

1
2
3
4
5
6
7
{
"name": "docs-site",
"compatibility_date": "2026-06-14",
"assets": {
"directory": "./dist"
}
}

然后发布:

1
npx wrangler deploy

这两条路都能部署静态资源,但心智模型不一样。

Pages 更像“静态站托管平台”,Workers 更像“边缘运行时 + 静态资源”。如果只是文档站,不要一上来就把自己带进 Worker 代码里。

Wrangler 最好放进项目

Cloudflare 的 CLI 是 Wrangler。

现在不要靠机器上某个全局版本来部署,最好把 Wrangler 固定在项目依赖里:

1
2
3
npm i -D wrangler@latest
npx wrangler --version
npx wrangler login

这样至少有两个好处:

1
2
1. CI 和本地使用同一套 CLI 版本;
2. 新同事 clone 项目后不需要猜该装哪个 wrangler。

如果只是临时执行,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
2
/plugin marketplace add cloudflare/skills
/plugin install cloudflare@cloudflare

Codex 里可以直接安装 Cloudflare 插件。Cloudflare 官方的 Codex setup 文档里也明确说明,安装插件会同时带上 Cloudflare Skills 和 MCP server。

Skills 的价值不是替你部署,而是让 Agent 在动手前先知道这些边界:

1
2
3
4
5
6
这是 Pages 还是 Workers?
是否需要 wrangler.jsonc?
是否需要 _redirects?
静态资源目录在哪里?
是否涉及 D1、R2、KV、Durable Objects?
当前命令是不是已经过时?

尤其是文档站这种场景,AI 很容易写出“能跑但不合适”的方案。先给它 Cloudflare 上下文,能少走很多弯路。

坑一:clean URL 刷新 404

VitePress 可以打开 cleanUrls,把:

1
/integration/device.html

变成:

1
/integration/device

浏览时很好看,但部署到静态托管后,用户直接刷新 /integration/device,平台未必知道要返回 /integration/device.html

解决方式是在构建产物里生成 _redirects

1
2
3
/api /api/index.html 200
/api/ /api/index.html 200
/integration/device /integration/device.html 200

Cloudflare Pages 会读取构建目录里的 _redirects 文件,并按规则处理静态资源响应。

这一步最好自动生成,不要手写。因为文档一多,手写 rewrite 很容易漏。

坑二:Swagger UI 页面打开但空白

Swagger UI 本身不是一个 HTML 文件就完了。

它还需要:

1
2
3
swagger-ui.css
swagger-ui-bundle.js
各服务 swagger.json

如果只复制 index.html,本地可能因为路径凑巧能打开,部署后就变成空白页或者控制台 404。

构建脚本里应该明确把资源复制进 /api/swagger-ui/,再把 JSON 放到 /api/*.json

坑三:/api/ 被前端路由接管

VitePress 是静态站,但它也有自己的客户端路由。

如果导航链接写得不够明确,点击 /api/ 时可能被文档站路由错误处理,或者部署平台把它当成普通文档路径。

我的处理方式是:

1
2
3
文档 nav 里 API Swagger 指向 /api/
Swagger 页面是独立 HTML
构建时给 /api 和 /api/ 都写 rewrite

这样 /api/ 就是一个明确的静态入口,不参与文档 Markdown 路由。

坑四:首页不是普通文档页

文档站首页应该回答:

1
2
3
4
5
我是谁?
我应该从哪里开始看?
各端入口在哪里?
Swagger 在哪里?
当前约定是什么?

它不应该一进来就是几十个侧边栏链接。

首页更适合作为导航页,普通文档页再进入细分侧边栏。否则第一次打开的人很容易被目录淹没。

坑五:内部资料误发布

接口文档站经常和项目仓库放在一起。

这很好维护,但也带来风险:仓库里的内部文档不一定都适合公开发布。

比如:

1
2
3
4
5
deployment.md
incident.md
roadmap.md
llm-cases.md
private-notes.md

VitePress 可以通过 srcExclude 排除不该进入站点的文件。

同时,文档站如果只是给联调人员使用,建议加:

1
<meta name="robots" content="noindex,nofollow" />

再配合 Cloudflare 的 _headers

1
2
3
4
/*
X-Robots-Tag: noindex, nofollow
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin

这不能替代权限控制,但至少避免搜索引擎主动收录。

坑六:发布产物和源文档混在一起

源文件和构建产物最好分开。

源文件是:

1
2
3
docs/**/*.md
docs/.vitepress/config.mts
scripts/build_docs_site.sh

构建产物是:

1
docs/.vitepress/dist/

如果直接把 dist 提交回主仓库,代码 review 会很难看,搜索索引和 hash 文件也会制造大量噪音。

更清楚的做法是:

1
2
3
主仓库维护源文档和构建脚本;
发布仓库只保存构建后的静态产物;
主仓库用 submodule 或发布记录追踪线上版本。

这样 review 的时候看源文档,线上部署的时候看发布仓库,各自职责清楚。

坑七:文档不跟发版走

最危险的文档不是没有文档,而是文档过期。

接口字段变了,Swagger 更新了,Markdown 没改;或者 Markdown 改了,Swagger 还是旧的。这种状态会比没有文档更误导人。

我更倾向把文档站发布放进发版链路:

1
2
3
4
5
6
生成接口定义
构建文档站
检查 Swagger 页面
发布静态产物
更新发布指针
再做正式部署

至少在工程机制上,让“接口变更”和“文档更新”靠近一点。

一条可复用的构建链路

最后沉淀下来的脚本流程可以概括成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
set -euo pipefail

# 1. 生成 Swagger JSON
make swagger

# 2. 构建 VitePress
npx vitepress build docs

# 3. 准备 /api/ Swagger UI
mkdir -p docs/.vitepress/dist/api
cp scripts/docs/swagger-ui.html docs/.vitepress/dist/api/index.html
cp node_modules/swagger-ui-dist/swagger-ui.css docs/.vitepress/dist/api/swagger-ui/
cp node_modules/swagger-ui-dist/swagger-ui-bundle.js docs/.vitepress/dist/api/swagger-ui/

# 4. 复制各服务 JSON
cp swagger/*.json docs/.vitepress/dist/api/

# 5. 生成 Cloudflare _redirects
node scripts/generate-redirects.js

发布可以有两种方式。

如果用 Cloudflare Pages Git 集成:

1
2
push 发布仓库
Cloudflare Pages 自动构建或直接发布静态文件

如果用 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
2
3
4
5
6
接口事实源
对接说明
Swagger 展示
静态构建
Cloudflare 发布
版本追踪

当这条链路稳定后,文档就不再是最后补的一份说明,而是交付的一部分。

这也是这次实践里最大的收获。

参考链接

  • 标题: 一次接口文档站工程化实践: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 进行许可。