静态生成流程详解

范围:支持 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 bundlecloseBundle 阶段生成 .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-clientCLI 注入的客户端入口
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}.jsoutput/assets/index-{hash}.css 等(或 --output 指定目录)
  • 客户端入口导入 virtual:clarify-routesvirtual:clarify-config 和 OpenAPI 注册表,并对应用进行注水

步骤 2:路由清单生成

在插件配置解析和构建启动阶段:

  1. 扫描 rootDirectory 以查找 **/*.md**/*.mdx**/*.openapi.*

  2. 对于 MDX / Markdown 文件,提取 frontmatter 标题,并从标题生成页面章节

  3. 对于 OpenAPI 文件,解析规范并从接口列表生成页面章节

  4. 构建路由清单:

    type ContentRoute = {
      path: string
      basePath?: string
      locale?: string
      title: string
      filePath: string
      virtualModuleId: string
      kind: 'mdx' | 'openapi'
      sections?: ContentSection[]
      contentArtifactUrl?: string
    }
    
  5. 注册虚拟模块,以便 virtual:clarify-routes 可以提供路由清单和导航树

步骤 3:SSR bundle 构建

客户端包构建完成后,clarifyPlugin.closeBundle() 会:

  1. 创建临时 entry-server.ts
  2. 使用 Vite SSR 模式构建到 output/.ssr/entry-server.js
  3. 导入 SSR bundle;
  4. 对每条 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>
  • description meta;
  • keywords meta;
  • <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 标签或改写 shellhtml:transform
提供开发期中间件dev:configureServer

尽量保持输出确定性,确保本地构建和部署预览可复现。