静态生成流程详解
范围:支持 MDX / Markdown 与 OpenAPI 内容,每个路由输出独立 HTML 文件,并在浏览器端 Hydration 为 React Router 应用。Vite 是 CLI 内部构建工具,不作为用户配置入口暴露。
目标
| # | 需求 | 实现方式 |
|---|---|---|
| 1 | 静态部署:每个页面有独立的 HTML 文件 | 构建时 SSR,为每条路由写入 output/<route>/index.html |
| 2 | 首屏后提供流畅的客户端体验 | 注入 <script> 将静态标记注水(hydrate)为 React Router SPA |
| 3 | 支持 MDX / Markdown 和 OpenAPI 渲染 | CLI 内部引擎扫描 rootDirectory 下的 *.md、*.mdx 和 *.openapi.*,通过 @mdx-js/rollup 编译 |
| 4 | 使用 Vite 作为内部构建管线 | 仅使用公开 API,不要求普通用户维护 Vite 配置 |
| 5 | 构建期 SSR | 生成临时 SSR 入口并输出到构建目录的 .ssr/ 子目录,再导入该 bundle 渲染路由 |
| 6 | 内置渲染器样式管线 | CLI 内部装配渲染器样式和 Tailwind CSS 支持 |
架构概览
┌─────────────────────────────────────────────────────────────┐
│ 构建阶段(vite build) │
│ │
│ 1. Vite 打包客户端入口(virtual:clarify-entry-client) │
│ → output/assets/ │
│ │
│ 2. clarifyPlugin closeBundle(): │
│ a. 复用已解析的 MDX/OpenAPI 路由和虚拟模块 │
│ b. 创建临时 SSR 入口文件 │
│ c. Vite 构建 SSR bundle → output/.ssr/entry-server.js │
│ d. 导入 SSR bundle 中的 render(url) │
│ e. 为每条路由:renderToHTML(url) → 写入 HTML │
│ f. 写出 raw content 和 llms.txt │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 运行时(浏览器) │
│ │
│ 1. 浏览器接收到静态 HTML(SEO 友好) │
│ 2. `<script>` 加载客户端打包产物 │
│ 3. hydrateRoot 在已有标记上挂载 React Router │
│ 4. 后续跳转 → 客户端 SPA 无刷新导航 │
│ │
└─────────────────────────────────────────────────────────────┘
关键设计决策
两阶段构建(客户端 + SSR)
Clarify 不需要服务端运行时。生产构建只在构建阶段执行 SSR,并把结果写成静态 HTML。
为了在构建时将 React 组件渲染为 HTML 字符串,CLI 会生成一个临时 SSR 入口,然后使用 Vite 构建 SSR bundle:
- 客户端包:正常 Vite
build()输出 JS/CSS 资源到output/(或--output指定目录) - SSR bundle:
closeBundle阶段生成.ssr/entry-server.js,并在 Node.js 中导入执行 - HTML 写入:读取客户端构建产出的
index.html模板,把renderToHTML(url)的结果注入#root
这带来了:
- ✅ 产物仍然是可直接部署的静态文件
- ✅ 无服务端运行时要求
- ✅ SSR 与客户端使用同一套路由和虚拟模块数据
虚拟模块
Clarify 采用虚拟模块模式实现更清晰的分离:
| 虚拟模块 | 用途 |
|---|---|
virtual:clarify-config | 合并项目配置与构建选项,作为运行时配置暴露 |
virtual:clarify-routes | 路由清单、导航树和页面章节数据 |
virtual:clarify-openapi-registry | 解析后的 OpenAPI 规范注册表 |
virtual:clarify-entry-client | CLI 注入的客户端入口 |
virtual:clarify-page/* | 每个内容页面对应的虚拟模块 |
这些模块由 clarifyPlugin.resolveId() 解析,由 clarifyPlugin.load() 生成。
路由发现
Clarify 使用基于文件系统的路由,并支持可选的 frontmatter 覆盖:
rootDirectory/
├── index.mdx → /
├── getting-started.mdx → /getting-started
├── guides/
│ ├── index.mdx → /guides
│ └── theming.mdx → /guides/theming
- 路由从相对于
rootDirectory的文件路径派生 - 支持
.md、.mdx和.openapi.json/.yaml/.yml文件 - 导航结构可通过配置文件的
tabs字段自定义;每个 tab 内的pages定义侧边栏
HTML 模板
每个生成的 HTML 文件基于客户端构建产出的 index.html 模板,再注入 SSR 内容:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{siteTitle}</title>
<!-- Vite 注入构建后的 JS/CSS 资源 -->
</head>
<body>
<div id="root">
<!-- SSR: renderToString 输出 -->
</div>
</body>
</html>
Clarify 会根据当前路由语言修正 <html lang> / dir,并在缺少 description meta 时注入站点描述。脚本和样式资源由 Vite 的 HTML 构建流程管理。
详细构建流程
步骤 1:客户端包构建
Vite 运行正常构建:
- 入口:
virtual:clarify-entry-client - 输出:
output/assets/index-{hash}.js、output/assets/index-{hash}.css等(或--output指定目录) - 客户端入口导入
virtual:clarify-routes、virtual:clarify-config和 OpenAPI 注册表,并对应用进行注水
步骤 2:路由清单生成
在插件配置解析和构建启动阶段:
-
扫描
rootDirectory以查找**/*.md、**/*.mdx和**/*.openapi.* -
对于 MDX / Markdown 文件,提取 frontmatter 标题,并从标题生成页面章节
-
对于 OpenAPI 文件,解析规范并从接口列表生成页面章节
-
构建路由清单:
type ContentRoute = { path: string basePath?: string locale?: string title: string filePath: string virtualModuleId: string kind: 'mdx' | 'openapi' sections?: ContentSection[] contentArtifactUrl?: string } -
注册虚拟模块,以便
virtual:clarify-routes可以提供路由清单和导航树
步骤 3:SSR bundle 构建
客户端包构建完成后,clarifyPlugin.closeBundle() 会:
- 创建临时
entry-server.ts; - 使用 Vite SSR 模式构建到
output/.ssr/entry-server.js; - 导入 SSR bundle;
- 对每条
ContentRoute调用render(url)。
SSR 使用 virtual:clarify-routes/server,页面组件为 eager import,保证构建阶段可以同步渲染。
步骤 4:HTML 写入
每条路由会基于客户端构建产物中的 index.html 模板生成独立 HTML:
/ → output/index.html
/getting-started → output/getting-started/index.html
/en-US/guides → output/en-US/guides/index.html
写入时会同步更新:
<title>;descriptionmeta;keywordsmeta;<html lang>;- 可选的
<html dir>。
步骤 5:内容制品与 llms.txt
内置 content artifacts 插件在 routes:resolved 阶段为路由附加 contentArtifactUrl,在开发服务器中提供对应内容,并在 build:done 阶段写入文件。
这些制品用于:
- 复制原始 Markdown / OpenAPI 内容;
- 复制 raw content 链接;
- 为 AI/LLM 工具提供可读取的文档入口;
- 保留 OpenAPI 原始规范;
- 生成
llms.txt。
错误行为
generateOptions.ssg.failOnError 默认是 true。任一路由 SSR 失败时,生产构建会失败,避免发布不完整文档。
如果未来需要 best-effort 输出,应通过显式配置开启,并在文档中说明代价;默认行为应继续偏向构建期发现问题。
扩展建议
| 需求 | 推荐 Hook |
|---|---|
| 生成搜索索引 | build:done |
| 给路由附加额外元数据 | routes:resolved |
| 向 Renderer 提供虚拟数据 | modules:before |
| 注入 HTML 标签或改写 shell | html:transform |
| 提供开发期中间件 | dev:configureServer |
尽量保持输出确定性,确保本地构建和部署预览可复现。