seo有效标题和描述必须由服务端在首屏html中直出,客户端js动态修改无效;局部同构刷新要求服务端与客户端对区块语义、数据边界、key及props严格一致,否则导致水合失败、seo内容丢失。

不能靠客户端 JS 动态改 <title></title> 或 <meta name="description"> 来“模拟”SEO 友好——服务端必须在首屏 HTML 中就输出正确的、与当前路由/内容完全匹配的标签值。局部同构刷新的前提,是服务端和客户端对同一区块的语义定义与数据边界 100% 一致。
服务端必须直出完整、可索引的 SEO 标签
搜索引擎爬虫不执行 JS,也不等待 hydration 完成。任何通过 document.title = 或 React 的 useEffect(() => { ... }) 设置的标题/描述,对 SEO 无效。Next.js 的 useRouter() + useEffect 组合、Remix 的 useLoaderData 后手动更新 <title></title>,都属于典型失效场景。
实操建议:
- 服务端渲染时,通过模板变量或框架提供的 head API(如 Next.js 的
generateMetadata、Remix 的headers+metaexport)静态注入<title></title>、<meta name="description">、<link rel="canonical"> - 每个页面级路由对应唯一一套标签;动态子页面(如 /post/123)需在服务端根据参数查出真实标题与描述,而非 fallback 占位符
- 避免在客户端用
useState或全局 store 驱动标签变更——除非该状态已通过window.__INITIAL_DATA__或服务端内联 script 同步注入
局部刷新区域必须有稳定 key 且 props 严格一致
React 水合失败时不会报错,只会静默丢弃服务端 HTML 并重建 DOM。一旦 mismatch,SEO 内容即丢失,且样式闪动、事件绑定失效。常见 mismatch 原因:服务端渲染时组件 props 是 null,客户端 hydration 时变成 { id: 123 };或 key 用了 Math.random()、数组索引等不可预测值。
实操建议:
- 每个可局部刷新的组件必须声明稳定 key,推荐使用服务端可确定的业务 ID,如
key={post.slug}或key={`comments-${articleId}`} - 服务端与客户端初始 props 必须字面量一致(包括
undefinedvsnull、空数组 vsundefined);建议将服务端数据 JSON 序列化后内联到 HTML 的window.__INITIAL_DATA__中供客户端读取 - 禁止在局部刷新组件内直接调用
fetch或useSWR——除非其 fallback UI 已由服务端直出(如已渲染出评论列表骨架+前 3 条真实内容)
局部刷新不能跨语义边界,尤其不能覆盖 SEO 关键区块
首屏关键内容(文章标题、摘要、主图 <img> 的 alt、结构化数据 <script type="application/ld+json"></script>)必须由服务端直出,且不能包裹在 <suspense></suspense>、React.lazy 或任何客户端延迟加载逻辑中。否则爬虫看到的是空 div 或 loading 占位符。
实操建议:
- 明确划分「服务端必出区」与「客户端接管区」:前者包含
<h1></h1>、<main></main>内核心文本、<link rel="canonical">;后者仅限交互性强、非关键信息模块(如实时评论流、相关推荐) - 局部刷新只允许发生在
<div data-hydratable="true"> 这类显式标记的容器内,且该容器的 outerHTML 在服务端与客户端必须完全相同 <li>检查线上 SSR 日志,标记每个响应中含哪些可水合区块(如 <code>/comments、/related-posts),并监控 mismatch 率;超过 0.5% 就需排查数据序列化逻辑
最易被忽略的一点:局部刷新的“局部”,不是视觉意义上的局部,而是 hydration 边界意义上的局部。一个按钮点击后更新右侧侧边栏,看似只是局部,但如果该侧边栏的 DOM 结构在服务端未生成、或 key 不稳定、或初始 props 缺失,它就不再是同构刷新,而是客户端降级渲染——SEO 价值归零。











