圣灯在线
2026-11-07· 8 分钟SEOReactNext.js服务端渲染

为什么你的首页在 Google 眼里是空的

用 React 或 Next.js 写的站,首屏 HTML 常常只有一个空 div。Google 能执行 JS,但渲染预算有限。这不是玄学,是可以用一条 curl 命令验证的事。

我在给自己一个内容站做诊断时,跑了一条命令,输出让整件事变得非常清楚:

curl -s https://your-site.com/ | wc -c

首页 HTML:33 KB。

看着不小。但再跑一条:

curl -s https://your-site.com/ | grep -o "<main[^>]*>" -A2 | head -5

得到:

<main class="flex-1">
  <div id="root"></div>

33 KB 里,正文是 0 字节。剩下的全是 <script>、字体预加载、CSS。

这就是"内容明明写了但搜不到"的技术解释。


为什么 Google 能执行 JS,这仍然是问题

严格说,Googlebot 确实能渲染 JavaScript。所以很多人得出结论:"不用管,Google 会处理"。

这个结论错在三处:

1. 渲染是排队的,不是实时的

抓取(crawl)和渲染(render)是两个阶段。Google 先抓 HTML,存起来,之后由渲染队列来处理。这个延迟可能是几分钟,也可能是几周——取决于站点权重、服务器响应速度、页面数量。

新站、小站排在后面。你的内容在索引里"迟到",不是"不到",但对新站来说两者差别不大。

2. 渲染有预算

每个站点被渲染的页面数量是有限的。一个 126 页的站,如果全部依赖客户端渲染,Google 可能只渲染其中一部分,剩下的就按"抓到的空 HTML"处理。

这就是为什么很多站会出现"有些页面被索引了,有些没有"——不是内容差异,是渲染预算。

3. 渲染失败就当作空页面

JS 报错、接口超时、第三方脚本阻塞,任何一个环节出问题,Google 拿到的都是空白。

而你的用户(用着正常浏览器、网络通畅)看不到这个问题。

这是最危险的地方:故障是静默的。 用户体验正常,你完全不知道 Google 看到了什么。


怎么验证(三分钟)

方法一:curl 抓原始 HTML

curl -s https://your-site.com/ \
  | python3 -c "
import sys, re, html
raw = sys.stdin.read()
m = re.search(r'<main[^>]*>(.*?)</main>', raw, flags=re.S|re.I)
seg = m.group(1) if m else raw
t = re.sub(r'<(script|style|noscript)[^>]*>.*?</\1>', ' ', seg, flags=re.S|re.I)
t = re.sub(r'<[^>]+>', ' ', t)
t = re.sub(r'\s+', ' ', html.unescape(t)).strip()
print('正文长度:', len(t))
print('前 300 字:', t[:300])
"

判断标准:

正文长度 结论
< 300 字符 严重:几乎是空页面
300 – 1000 不足:只有标题和零星文案
1000 – 3000 勉强,但内容偏薄
> 3000 正常

方法二:Chrome 关掉 JavaScript

DevTools → Ctrl+Shift+P → 输入 Disable JavaScript → 回车 → 刷新。

你看到的就是 Googlebot 第一次抓取时的画面。

方法三:查看 view-source:

浏览器地址栏输入 view-source:https://your-site.com/,Ctrl+F 搜你首页的一句话。

搜不到,问题就确认了。


哪些内容必须在服务端

不是所有东西都要迁。判断标准很简单:这段话是给"人读"的,还是给"人操作"的?

必须服务端渲染 可以留客户端
正文、产品描述、价格 表单交互、实时校验
H1 / H2 标题层级 动画、过渡、悬停效果
FAQ 内容 用户登录后的个人数据
内链(导航、相关推荐) 弹窗、抽屉
面包屑 实时搜索建议
结构化数据 JSON-LD 埋点、A/B 测试

一句话:搜索引擎需要"读"的,必须服务端有;用户需要"点"的,客户端做没问题。


三种修复模式

模式 A:Server Component + Client Island(React 19 / Next.js App Router)

最标准,也是我最推荐的。

// app/product/[slug]/page.tsx —— 服务端组件
import { getProduct } from "@/lib/products";
import { BuyButton } from "./buy-button";   // 客户端组件

export default async function Page({ params }: { params: { slug: string } }) {
  const product = await getProduct(params.slug);

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>        {/* 服务端:搜索引擎读得到 */}
      <ul>
        {product.features.map(f => <li key={f}>{f}</li>)}
      </ul>

      <BuyButton productId={product.id} />  {/* 客户端:用户点得到 */}
    </main>
  );
}
// buy-button.tsx —— 只有这一小块是客户端
"use client";
export function BuyButton({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false);
  // ...
}

关键:"use client" 的边界要划在最小的交互单元上,不要划在页面上。

模式 B:交互页保留客户端,layout 补服务端内容

如果你的页面本身就是强交互的(抽奖、表单、仪表盘),整页迁移成本太高。

这时不需要改页面,在 layout.tsx(服务端)里加内容区块:

// app/dashboard/layout.tsx —— 服务端
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      {children}                       {/* 客户端交互,原样不动 */}
      <SeoContentBlock                   {/* 服务端:进入首轮 HTML */}
        title="这个工具怎么用"
        blocks={[...]}
        faqs={[...]}
      />
    </>
  );
}

这个方法我用了三次(三个交互页),首屏正文从 46–58 字符提到 3,677–7,786 字符,业务代码零改动。

注意:layout 里的内容不受子页面 Suspense 影响,所以一定能进第一版 HTML。这是它比 page 更可靠的原因。

模式 C:静态生成(SSG / ISR)

内容型页面最好的选择,连服务端渲染都省了。

// Next.js App Router
export const revalidate = 3600;   // 每小时重新生成

export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map(p => ({ slug: p.slug }));
}

构建时就把 HTML 生成好,首屏内容完整,速度快,爬虫友好。


一个问题:加了服务端内容,会不会重复?

会——如果你的 HTML 里既有服务端渲染的正文,又有客户端 hydration 后再渲染一遍同样内容。

React 的 hydration 会复用服务端标记,正常情况下不会重复。真正容易出问题的是:

// ❌ 服务端渲染了一次
<h1>产品名</h1>
// ❌ 客户端又渲染一次
<ClientHeader />   // 里面也有 <h1>产品名</h1>

结果:HTML 里两个 H1。

修法:每个页面只保留一个 H1,且它来自服务端。

排查命令:

curl -s https://your-site.com/ | grep -c "<h1"

大于 1 就要看是谁重复了。


检查清单

□ curl 抓原始 HTML,统计 <main> 纯文本长度(> 1000 才算合格)
□ grep -c "<h1" 确认只有一个
□ 关掉 JS 刷新页面,内容是否还在
□ view-source: 搜索首页第一句话,能否找到
□ 检查 "use client" 是否划在整页上(应该划在组件上)
□ 确认 layout 里的 SEO 内容不受 Suspense 影响
□ 交互页是否至少有 2000 字的服务端说明内容
□ 结构化数据是否也在服务端输出(不能由 JS 注入)

最后一条特别容易漏:useEffect 里动态插入 <script type="application/ld+json"> 的写法,Googlebot 未必能拿到。


最后

服务端渲染解决的是「能不能被读到」的问题,不是「会不会被喜欢」的问题。

我自己的站把首屏正文从 46 字符修到 3,677 字符之后,那 21 个"已抓取未索引"的页面在几周内全部进了索引——但自然流量并没有因此暴涨,因为内容本身对用户没有足够价值。

先确保能被读到,再谈值不值得读。

顺序反了,两边都白做。

你的站有这些问题吗?

用免费体检工具输入域名,30 秒看结果。也可以直接写信说清情况,我告诉你值不值得修。