CSR 项目改造为 SSG(vite-react-ssg)
⚠️ 已退役 · 禁止新用:SSG(vite-react-ssg)已不再是任何 SEO 改造选择,新旧项目一律走请求期 SSR。已在 vite-react-ssg 的残留项目请按 SSG→SSR 迁移 迁出(不论内容静态/动态),不要继续停在 SSG。本指南仅留作历史参考。你是代码迁移 Agent。你的任务是在当前项目中完成 CSR -> SSG 迁移,并保证可稳定构建与 hydration。
A. 必须遵守(MUST)
- 必须保留
src/App.tsx的 JSX 路由声明(createRoutesFromElements + <Route />)。 - 必须导出
RouteRecord[]给vite-react-ssg使用。 - 必须保持
src/App.tsx为路由源文件,不引入约定式路由方案(禁止root.tsx/routes.ts重构)。 - 必须确保构建产物包含静态 HTML 页面(SSG)。
- 必须使用
react-router-dom@^6.30.3(若检测为 v7,先降级)。 - 必须将仅客户端能力(如
Sonner、next-themes主题读取)做“挂载后渲染”保护。 - 必须对首页默认执行“SEO 可见性优化”:SSG 输出可见最终态,客户端挂载后再接管动画与数据增强。
- 必须将首页及核心入口的内部路由改造为 SEO 友好形式:静态 HTML 中可解析目标 URL,避免仅依赖运行时事件跳转。
- 必须保持
src/App.tsx中// @side注释块结构稳定(注释文本、分组边界、相对顺序不变),仅允许在对应分组内做最小必要改动。 - 必须遵守 React Router v6 嵌套路由路径规则:顶层路由用绝对路径(带
/),子路由用相对路径(不带/),根重定向与兜底路由使用index或path="*"。
B. 明确禁止(DO NOT)
- 不要把路由体系迁移成文件路由或框架路由。
- 不要在模块顶层执行任何依赖浏览器环境的逻辑(
window/document/localStorage/matchMedia)。 - 不要在 SSR 构建阶段保留
manualChunks对react、react-dom的拆分配置。 - 不要修改与本迁移无关的业务行为。
C. 执行步骤(按顺序)
1) SSR 安全全量预扫描(编码前必须完成)
- 在任何改造前,先对
src/全目录做一次性预扫描并汇总“待修改文件清单”,避免进入“改造 -> 部署失败 -> 补丁 -> 再部署”循环。 - 预扫描至少覆盖以下检查点(基线要求,可按项目继续扩展):
- 浏览器 API 直接访问:
window/document/localStorage/sessionStorage等仅浏览器环境能力。 - Hook 初始化函数中的 SSR 同步执行逻辑:重点检查
useState/useMemo/useReducer初始化阶段是否访问浏览器 API。 - 非确定性表达式:如时间戳、随机数、随机 ID 等在构建期与 hydration 期可能不一致的值来源。
- 动画初始隐藏态:尤其是
framer-motion的initial隐藏配置,避免 SSG 首屏输出不可见内容。
- 浏览器 API 直接访问:
- 以上仅为最低检查基线,不是完整清单;目标是一次性收敛 SSR 风险点,而非机械命中固定语法。
- 仅在清单收敛后再开始编码改造。
2) 依赖与脚本
- 确保依赖:
react-router-dom@^6.30.3vite-react-ssg@^0.9.0(devDependency)
- 确保脚本:
dev: vite-react-ssg devbuild: vite-react-ssg buildpreview: vite preview
3) 路由导出(src/App.tsx)
- 保留现有 JSX 路由树。
- 使用
createRoutesFromElements(...)生成路由对象。 - 断言并导出
RouteRecord[]。 - 若存在动态路由(如
:id),必须给对应 route 挂getStaticPaths,返回完整 URL 列表(必须带父路径)。 - 路径书写规范(React Router v6):
- 顶层端路由(挂在无
path的RootLayout下;若存在// @side分组则需保持其结构稳定)必须使用绝对路径,如path="/website"、path="/admin"、path="/customer"。 - 子路由(嵌套在有
path的父路由下)必须使用相对路径,如path="workspace"、path="settings"、path="customers/:customerId";由路由系统自动拼接父前缀。 - 根路由重定向与通配路由与顶层路由同级,使用
index或path="*",不写绝对斜杠形式。
- 顶层端路由(挂在无
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 处理规则
Sonner(toaster)应保留在全局布局,保证全站可用。- 必须增加
mounted保护,仅在客户端挂载后渲染 toaster。 - 任何
toast(...)调用必须位于事件回调或useEffect中,禁止模块顶层触发。
D2. ThemeProvider(next-themes)处理规则
ThemeProvider的挂载策略在 SSR 与客户端首帧必须一致,禁止通过mounted条件重构同一业务树。- 主题相关的分支渲染(
useTheme()驱动 DOM/class)必须在挂载后执行;未挂载前使用稳定 fallback。 ThemeProvider配置固定为 SEO 迁移安全形态:移除enableSystem,并显式设置defaultTheme与themes。
useMounted 最小实现(参考):
E. 首页 SEO 可见性优化(SSG 迁移默认执行)
E1. 适用范围(默认要求)
- 该模块在进行 SSG 迁移时对首页为默认必做项,不可跳过。
- 首页所有首屏/核心内容板块(标题、正文、关键指标、主要 CTA)必须在静态 HTML 中可见且可抓取。
- 目标不是“去掉动画”,而是“SSG 阶段先可见,客户端接管后再播放”。
E2. 核心原则
SSG 渲染时必须输出“完全可见的最终状态”;客户端挂载后再启用入场动画、视口触发和数据增强。E3. 判定决策树(必须按顺序排查)
- 若组件使用
framer-motion且存在initial隐藏态(如opacity: 0/ 位移):- SSG 阶段使用
initial={false}直出最终可见态; - 客户端挂载后再恢复入场动画配置。
- SSG 阶段使用
- 若组件使用
useInView控制显示:- SSG 阶段默认视为“已进入视口”(可见);
- 客户端再按真实视口状态驱动动画。
- 若数据组件存在
if (data.length === 0) return null:- 禁止直接返回空;
- 必须保留语义化板块结构(如 section + heading)并输出占位/兜底内容,确保 HTML 结构完整。
- 若数据请求依赖客户端环境:
- SSG 阶段禁用请求或使用静态兜底值;
- 客户端挂载后再请求并覆盖展示。
- 若存在浏览器 API(
window/document/localStorage/matchMedia):- 仅允许在客户端挂载后访问,避免构建失败与 hydration 不一致。
E4. 首页落地检查项(验收前必查)
- 查看首页静态 HTML:核心板块文本与标题均存在且默认可见(非透明、非偏移隐藏)。
- 无数据时首页仍保留完整板块语义结构,不出现整块缺失。
- 客户端接管后动画正常、无显著 hydration warning。
E5. 内部路由跳转 SEO 友好改造(首页重点)
- 首页导航、卡片、Banner、CTA 等核心入口,必须输出可抓取的链接地址(
<a href>或 React RouterLink to最终渲染为href)。 - 禁止将“跳转能力”仅放在
onClick + navigate(...)、window.location、自定义事件中而无可解析href。 - 对于“整卡可点击”场景,优先使用语义化链接包裹或在卡片内部提供明确链接节点,避免仅绑定点击事件。
- 链接目标必须对应可访问页面;动态路由目标需与
getStaticPaths覆盖范围一致,避免落到无静态结果页面。 - 仅用于登录态分流或权限校验的客户端重定向可保留,但不得替代首页核心内容的常规内部链接。
F. 问题处理细则(编码过程中必须实时执行)
- 重定向风险点:统一识别重定向页面中的
Navigate用法,改为return null+useEffect客户端重定向。 - 主题系统风险点(
next-themes):ThemeProvider保持可扩展配置,启用mounted守卫,移除enableSystem;单主题场景固定defaultTheme与themes。 - 浏览器 API 风险点:排查
window/document/localStorage/sessionStorage在模块顶层、渲染路径、Hook 初始化函数(useState/useMemo/useReducer)中的访问;SSR 阶段同步执行路径必须迁移到useEffect或加typeof window !== "undefined"守卫。 - 非确定性值风险点:排查时间戳、随机数、随机 ID 等值来源;若出现在
useState默认值、组件顶层变量或 JSX 渲染逻辑中,改为稳定值或移至useEffect,仅在事件回调中保留非确定性计算。 - 客户端组件挂载风险点:
Sonner/Toast必须在mounted后渲染。 - 重定向页面结构风险点:禁止使用
Head组件。 - 构建配置风险点:SSG 构建阶段必须禁用
manualChunks(通过isSsrBuild条件判断)。 - 已知可接受限制 A:根路由重定向页面的 hydration warning 属于架构性限制,可接受;禁止围绕该点反复补丁。
- 已知可接受限制 B:
framer-motion的motion.*可能产生少量 style 级 hydration 差异,可接受;优先保证“SSG 先可见、客户端再接管动画”,禁止围绕该限制反复补丁。 - SEO 链接可抓取风险点:首页与核心板块中的内部跳转,禁止仅
onClick + navigate(...),必须输出可解析href。 - 链接可达性风险点:校验内部链接目标可访问,动态路由需被
getStaticPaths覆盖。
G. 交付验收(必须全部通过)
pnpm build成功。dist产出静态 HTML 页面(含主要路由及动态路由静态结果)。- 运行后无
window/document is not defined报错。 - 运行后无显著 hydration warning(重点检查主题和 Sonner;根路由重定向页的已知 warning 除外)。
App.tsx仍为 JSX 路由源文件。- 首页静态 HTML 中核心板块默认可见且结构完整(无“透明但存在”或“整块缺失”)。
- 首页静态 HTML 中可提取到核心内部链接(含主要导航/CTA),且目标路由与静态产物覆盖一致。
- 目标为单次构建通过:若因遗漏浏览器 API 或非确定性表达式导致二次补丁,必须回溯并补齐“步骤 1 预扫描”清单。
H. 失败处理优先级(按顺序排查)
- 先检查
react-router-dom版本是否误为 v7。 - 再检查动态路由
getStaticPaths是否挂在正确 route 且返回完整路径。 - 再检查
manualChunks是否在 SSR 构建中被禁用。 - 最后检查是否遗漏客户端挂载保护(
Sonner/useTheme分支组件)。 - 若首页可抓取链接不足,优先排查是否仍存在仅事件驱动的 CSR 跳转入口。

