跳转到主要内容

CSR 项目改造为 SSG(vite-react-ssg)

⚠️ 已退役 · 禁止新用:SSG(vite-react-ssg)已不再是任何 SEO 改造选择,新旧项目一律走请求期 SSR已在 vite-react-ssg 的残留项目请按 SSG→SSR 迁移 迁出(不论内容静态/动态),不要继续停在 SSG。本指南仅留作历史参考。
你是代码迁移 Agent。你的任务是在当前项目中完成 CSR -> SSG 迁移,并保证可稳定构建与 hydration。

A. 必须遵守(MUST)

  1. 必须保留 src/App.tsx 的 JSX 路由声明(createRoutesFromElements + <Route />)。
  2. 必须导出 RouteRecord[]vite-react-ssg 使用。
  3. 必须保持 src/App.tsx 为路由源文件,不引入约定式路由方案(禁止 root.tsx / routes.ts 重构)。
  4. 必须确保构建产物包含静态 HTML 页面(SSG)。
  5. 必须使用 react-router-dom@^6.30.3(若检测为 v7,先降级)。
  6. 必须将仅客户端能力(如 Sonnernext-themes 主题读取)做“挂载后渲染”保护。
  7. 必须对首页默认执行“SEO 可见性优化”:SSG 输出可见最终态,客户端挂载后再接管动画与数据增强。
  8. 必须将首页及核心入口的内部路由改造为 SEO 友好形式:静态 HTML 中可解析目标 URL,避免仅依赖运行时事件跳转。
  9. 必须保持 src/App.tsx// @side 注释块结构稳定(注释文本、分组边界、相对顺序不变),仅允许在对应分组内做最小必要改动。
  10. 必须遵守 React Router v6 嵌套路由路径规则:顶层路由用绝对路径(带 /),子路由用相对路径(不带 /),根重定向与兜底路由使用 indexpath="*"

B. 明确禁止(DO NOT)

  1. 不要把路由体系迁移成文件路由或框架路由。
  2. 不要在模块顶层执行任何依赖浏览器环境的逻辑(window/document/localStorage/matchMedia)。
  3. 不要在 SSR 构建阶段保留 manualChunksreactreact-dom 的拆分配置。
  4. 不要修改与本迁移无关的业务行为。

C. 执行步骤(按顺序)

1) SSR 安全全量预扫描(编码前必须完成)

  • 在任何改造前,先对 src/ 全目录做一次性预扫描并汇总“待修改文件清单”,避免进入“改造 -> 部署失败 -> 补丁 -> 再部署”循环。
  • 预扫描至少覆盖以下检查点(基线要求,可按项目继续扩展):
    • 浏览器 API 直接访问:window / document / localStorage / sessionStorage 等仅浏览器环境能力。
    • Hook 初始化函数中的 SSR 同步执行逻辑:重点检查 useState / useMemo / useReducer 初始化阶段是否访问浏览器 API。
    • 非确定性表达式:如时间戳、随机数、随机 ID 等在构建期与 hydration 期可能不一致的值来源。
    • 动画初始隐藏态:尤其是 framer-motioninitial 隐藏配置,避免 SSG 首屏输出不可见内容。
  • 以上仅为最低检查基线,不是完整清单;目标是一次性收敛 SSR 风险点,而非机械命中固定语法。
  • 仅在清单收敛后再开始编码改造。

2) 依赖与脚本

  • 确保依赖:
    • react-router-dom@^6.30.3
    • vite-react-ssg@^0.9.0(devDependency)
  • 确保脚本:
    • dev: vite-react-ssg dev
    • build: vite-react-ssg build
    • preview: vite preview

3) 路由导出(src/App.tsx

  • 保留现有 JSX 路由树。
  • 使用 createRoutesFromElements(...) 生成路由对象。
  • 断言并导出 RouteRecord[]
  • 若存在动态路由(如 :id),必须给对应 route 挂 getStaticPaths,返回完整 URL 列表(必须带父路径)。
  • 路径书写规范(React Router v6):
    • 顶层端路由(挂在无 pathRootLayout 下;若存在 // @side 分组则需保持其结构稳定)必须使用绝对路径,如 path="/website"path="/admin"path="/customer"
    • 子路由(嵌套在有 path 的父路由下)必须使用相对路径,如 path="workspace"path="settings"path="customers/:customerId";由路由系统自动拼接父前缀。
    • 根路由重定向与通配路由与顶层路由同级,使用 indexpath="*",不写绝对斜杠形式。

4) SSG 入口(src/main.tsx

  • 使用 ViteReactSSG 创建根入口:
    • 传入 routes
    • basename: import.meta.env.BASE_URL
  • 保持样式入口(如 index.css)正常导入。

5) Vite 配置(vite.config.ts

  • 使用 @vitejs/plugin-react
  • 若存在 manualChunks,在 isSsrBuild 为真时必须禁用 output 自定义分包,避免 SSR 构建错误。

D. 客户端能力专项约束(重点)

D1. Sonner 处理规则

  1. Sonner(toaster)应保留在全局布局,保证全站可用。
  2. 必须增加 mounted 保护,仅在客户端挂载后渲染 toaster。
  3. 任何 toast(...) 调用必须位于事件回调或 useEffect 中,禁止模块顶层触发。
判定标准:构建与首屏 hydration 期间不应出现与 toast 相关的环境错误或不一致警告。

D2. ThemeProvider(next-themes)处理规则

  1. ThemeProvider 的挂载策略在 SSR 与客户端首帧必须一致,禁止通过 mounted 条件重构同一业务树。
  2. 主题相关的分支渲染(useTheme() 驱动 DOM/class)必须在挂载后执行;未挂载前使用稳定 fallback。
  3. ThemeProvider 配置固定为 SEO 迁移安全形态:移除 enableSystem,并显式设置 defaultThemethemes
正确写法(参考): useMounted 最小实现(参考):
function useMounted() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  return mounted;
}
function RootLayout() {
  const mounted = useMounted();

  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="light"
      themes={["light", "dark"]}
      disableTransitionOnChange
    >
      <QueryClientProvider client={queryClient}>
        <TooltipProvider>
          {mounted && <Sonner />}
          <Outlet />
          <ScrollRestoration />
        </TooltipProvider>
      </QueryClientProvider>
    </ThemeProvider>
  );
}
判定标准:无明显主题闪烁和 hydration warning。

E. 首页 SEO 可见性优化(SSG 迁移默认执行)

E1. 适用范围(默认要求)

  1. 该模块在进行 SSG 迁移时对首页为默认必做项,不可跳过。
  2. 首页所有首屏/核心内容板块(标题、正文、关键指标、主要 CTA)必须在静态 HTML 中可见且可抓取。
  3. 目标不是“去掉动画”,而是“SSG 阶段先可见,客户端接管后再播放”。

E2. 核心原则

SSG 渲染时必须输出“完全可见的最终状态”;客户端挂载后再启用入场动画、视口触发和数据增强。

E3. 判定决策树(必须按顺序排查)

  1. 若组件使用 framer-motion 且存在 initial 隐藏态(如 opacity: 0 / 位移):
    • SSG 阶段使用 initial={false} 直出最终可见态;
    • 客户端挂载后再恢复入场动画配置。
  2. 若组件使用 useInView 控制显示:
    • SSG 阶段默认视为“已进入视口”(可见);
    • 客户端再按真实视口状态驱动动画。
  3. 若数据组件存在 if (data.length === 0) return null
    • 禁止直接返回空;
    • 必须保留语义化板块结构(如 section + heading)并输出占位/兜底内容,确保 HTML 结构完整。
  4. 若数据请求依赖客户端环境:
    • SSG 阶段禁用请求或使用静态兜底值;
    • 客户端挂载后再请求并覆盖展示。
  5. 若存在浏览器 API(window/document/localStorage/matchMedia):
    • 仅允许在客户端挂载后访问,避免构建失败与 hydration 不一致。

E4. 首页落地检查项(验收前必查)

  1. 查看首页静态 HTML:核心板块文本与标题均存在且默认可见(非透明、非偏移隐藏)。
  2. 无数据时首页仍保留完整板块语义结构,不出现整块缺失。
  3. 客户端接管后动画正常、无显著 hydration warning。

E5. 内部路由跳转 SEO 友好改造(首页重点)

  1. 首页导航、卡片、Banner、CTA 等核心入口,必须输出可抓取的链接地址(<a href> 或 React Router Link to 最终渲染为 href)。
  2. 禁止将“跳转能力”仅放在 onClick + navigate(...)window.location、自定义事件中而无可解析 href
  3. 对于“整卡可点击”场景,优先使用语义化链接包裹或在卡片内部提供明确链接节点,避免仅绑定点击事件。
  4. 链接目标必须对应可访问页面;动态路由目标需与 getStaticPaths 覆盖范围一致,避免落到无静态结果页面。
  5. 仅用于登录态分流或权限校验的客户端重定向可保留,但不得替代首页核心内容的常规内部链接。

F. 问题处理细则(编码过程中必须实时执行)

  1. 重定向风险点:统一识别重定向页面中的 Navigate 用法,改为 return null + useEffect 客户端重定向。
  2. 主题系统风险点(next-themes):ThemeProvider 保持可扩展配置,启用 mounted 守卫,移除 enableSystem;单主题场景固定 defaultThemethemes
  3. 浏览器 API 风险点:排查 window / document / localStorage / sessionStorage 在模块顶层、渲染路径、Hook 初始化函数(useState / useMemo / useReducer)中的访问;SSR 阶段同步执行路径必须迁移到 useEffect 或加 typeof window !== "undefined" 守卫。
  4. 非确定性值风险点:排查时间戳、随机数、随机 ID 等值来源;若出现在 useState 默认值、组件顶层变量或 JSX 渲染逻辑中,改为稳定值或移至 useEffect,仅在事件回调中保留非确定性计算。
  5. 客户端组件挂载风险点:Sonner / Toast 必须在 mounted 后渲染。
  6. 重定向页面结构风险点:禁止使用 Head 组件。
  7. 构建配置风险点:SSG 构建阶段必须禁用 manualChunks(通过 isSsrBuild 条件判断)。
  8. 已知可接受限制 A:根路由重定向页面的 hydration warning 属于架构性限制,可接受;禁止围绕该点反复补丁。
  9. 已知可接受限制 B:framer-motionmotion.* 可能产生少量 style 级 hydration 差异,可接受;优先保证“SSG 先可见、客户端再接管动画”,禁止围绕该限制反复补丁。
  10. SEO 链接可抓取风险点:首页与核心板块中的内部跳转,禁止仅 onClick + navigate(...),必须输出可解析 href
  11. 链接可达性风险点:校验内部链接目标可访问,动态路由需被 getStaticPaths 覆盖。

G. 交付验收(必须全部通过)

  1. pnpm build 成功。
  2. dist 产出静态 HTML 页面(含主要路由及动态路由静态结果)。
  3. 运行后无 window/document is not defined 报错。
  4. 运行后无显著 hydration warning(重点检查主题和 Sonner;根路由重定向页的已知 warning 除外)。
  5. App.tsx 仍为 JSX 路由源文件。
  6. 首页静态 HTML 中核心板块默认可见且结构完整(无“透明但存在”或“整块缺失”)。
  7. 首页静态 HTML 中可提取到核心内部链接(含主要导航/CTA),且目标路由与静态产物覆盖一致。
  8. 目标为单次构建通过:若因遗漏浏览器 API 或非确定性表达式导致二次补丁,必须回溯并补齐“步骤 1 预扫描”清单。

H. 失败处理优先级(按顺序排查)

  1. 先检查 react-router-dom 版本是否误为 v7。
  2. 再检查动态路由 getStaticPaths 是否挂在正确 route 且返回完整路径。
  3. 再检查 manualChunks 是否在 SSR 构建中被禁用。
  4. 最后检查是否遗漏客户端挂载保护(Sonner / useTheme 分支组件)。
  5. 若首页可抓取链接不足,优先排查是否仍存在仅事件驱动的 CSR 跳转入口。