为什么你的首页在 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 个"已抓取未索引"的页面在几周内全部进了索引——但自然流量并没有因此暴涨,因为内容本身对用户没有足够价值。
先确保能被读到,再谈值不值得读。
顺序反了,两边都白做。