本文约 4,762 字,预计阅读 12 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:前台很丝滑,谷歌却只看到一堆 script
做外贸站和品牌站的朋友这两年大量转向 Headless 架构:WordPress 只当后台,前台用 Next.js、Nuxt 或 Astro 渲染。上线后页面体验确实快,但一个月后打开 Google Search Console 的”网页”报告,发现大量 URL 显示”已抓取,但目前未编入索引”,或者索引了但搜索结果里摘录的是空白。用 URL 检查工具的”测试实际网页”一看,渲染后的 HTML 里 <main> 区域是空的,正文全靠客户端 fetch 拉回来。
问题不在 WordPress,而在渲染时机。传统 WordPress 主题是服务端把完整 HTML 吐给浏览器,Googlebot 拿到就能解析;Headless 站点如果走纯客户端渲染(CSR),Googlebot 第一次拿到的是骨架 HTML,必须依赖 Web Rendering Service(WRS)排队执行 JavaScript 才能看到内容,这个队列可能延迟数小时到数天,赶上抓取预算紧张就永远排不上。所以 Headless 站的 SEO 核心命题只有一个:让爬虫第一次请求就拿到完整 HTML。
原理分析:五种渲染方式的收录差异
Google 的索引流程分两阶段:先抓取原始 HTML 并抽取链接、结构化数据,再把页面放进渲染队列执行 JS 补全 DOM。第一阶段拿不到的主内容,就只能赌第二阶段的队列资源。
| 方式 | 首次 HTML | 收录稳定性 | 适用场景 |
|---|---|---|---|
| CSR 纯客户端 | 骨架 | 差 | 后台、登录后页面 |
| SSR 服务端渲染 | 完整 | 好 | 内容频繁变化 |
| SSG 静态生成 | 完整 | 最好 | 文章、产品页 |
| ISR 增量静态再生成 | 完整 | 最好 | 大体量内容站 |
| 动态渲染(按 UA 分流) | 爬虫给完整 | 一般 | 老系统临时过渡 |
Google 官方文档明确把动态渲染定位为”workaround(临时替代方案)”,不是长期推荐方案,原因有二:一是给爬虫和用户看到不同内容,容易被判定为伪装(cloaking)风险;二是维护两套渲染链路成本高。能预渲染就预渲染,动态渲染只留给无法改造的老项目。
预渲染也不是免费午餐:ISR 需要设计好”谁去触发重新生成”,否则编辑在后台改完文章,前台还是旧内容,反而制造了另一类 SEO 问题——页面内容与结构化数据不一致。
保姆级实操
步骤 1:确认内容出口。 WordPress 后台装 WPGraphQL 插件,把内容以 GraphQL 暴露;若不想装插件,也可直接用 REST API 的 /wp-json/wp/v2/posts?_fields=id,slug,title,excerpt,content&per_page=100。REST 更通用,GraphQL 取字段更省流量。
步骤 2:文章页用 ISR 生成静态 HTML。 用 generateStaticParams 预生成热门 slug,未预生成的走按需生成并缓存。
// app/blog/[slug]/page.jsx —— Next.js App Router + ISR
const ENDPOINT = 'https://wp.example.com/graphql';
async function fetchPost(slug) {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `query($slug: ID!) {
post(id: $slug, idType: SLUG) {
title
content
modified
seo { title desc canonicalJson schema } # 由 WPGraphQL for SEO 扩展提供
}
}`,
variables: { slug },
}),
next: { revalidate: 3600 }, // 每小时后台再生成一次
});
const { data } = await res.json();
return data?.post ?? null;
}
export async function generateMetadata({ params }) {
const post = await fetchPost(params.slug);
return { title: post?.seo?.title, description: post?.seo?.desc,
alternates: { canonical: post?.seo?.canonicalJson?.canonical } };
}
export default async function Page({ params }) {
const post = await fetchPost(params.slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(post.seo.schema) }} />
</article>
);
}
步骤 3:让后台编辑触发重新生成。 用 transition_post_status 钩子在文章发布或更新时回调前台的 revalidate 接口,避免编辑等一小时。
<?php
// 主题 functions.php 或自定义插件
add_action('transition_post_status', function ($new, $old, $post) {
if ($post->post_type !== 'post') return;
if ($new !== 'publish' && $old !== 'publish') return;
$slug = $post->post_name;
wp_remote_post('https://front.example.com/api/revalidate', [
'blocking' => false, // 不阻塞后台保存动作
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode([
'path' => '/blog/' . $slug,
'token' => getenv('REVALIDATE_TOKEN'), // 从环境变量读取,禁止硬编码
]),
]);
}, 10, 3);
前台接口需校验 REVALIDATE_TOKEN,再调用 revalidatePath(path),切勿开放给匿名请求。
步骤 4:用 Googlebot UA 自检原始 HTML。 不要只看浏览器渲染结果,要看服务端吐出的源码。
# 对比普通 UA 与 Googlebot UA 拿到的原始 HTML 字节数
curl -sL -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
https://front.example.com/blog/hello | grep -c "<h1"
# 直接查看服务端返回的正文长度,判断是否为骨架页
curl -s https://front.example.com/blog/hello | wc -c
步骤 5:提交并验证。 用 next-sitemap 生成 sitemap.xml,提交到 Search Console;再用”网址检查 → 测试实际网址 → 查看已抓取的网页”确认渲染后 HTML 含正文,并用富媒体搜索结果测试工具校验 JSON-LD。这一步通过,才算真的解决。
关于整体收录策略的补充,可配合 Google Search Console 使用教程 中的索引报告一起看。
常见踩坑
- 案例: 某外贸站把
getStaticProps写成useEffect里fetch,上线两个月 3000 篇文章只收录 47 篇。改成 SSG 后,两周内收录率回到 92%。判断方法很简单:禁用浏览器 JS 后刷新页面,看不到正文就是 CSR。 - 只给 Googlebot 返回预渲染 HTML、给真人返回 CSR 页面,若两边内容差异过大(比如爬虫版塞了额外关键词),会被判定为伪装站点,风险远高于收益。
- ISR 的
revalidate时间设成 7 天,导致价格与库存长期不更新,结构化数据里的offers.price与实际不符,购物类富媒体结果被人工处置移除。 - Headless 后仍在 WordPress 端装 Yoast 生成 sitemap,而前台域名完全不同,提交的 sitemap 里全是后台域名,等于没提交。sitemap 必须生成在前台域名下。
- 忘记处理
robots.txt与 canonical:前台、后台两个域名同时可访问,形成重复内容。后台域名应加noindex响应头或全站 301 到前台。 - 分页与分类页没有对应的静态路由,
/blog/page/2是纯客户端路由,Google 抓到 200 但正文为空,形成大量软 404。 - 过度依赖渲染队列:Google 的渲染资源有限,页面 JS 包超过 1MB 时经常渲染超时,主内容再次丢失。
总结
判断标准只有一条:关闭 JavaScript 后能否看到完整正文。能看到,架构就是安全的;看不到,就必须改造成 SSR 或 SSG/ISR。实施顺序建议为——先把文章与产品页做成 ISR,再补 sitemap 与 canonical,最后用 GSC”测试实际网址”逐条验证。动态渲染只作为老系统的半年过渡方案,且必须保证爬虫版与用户版内容实质一致。 Headless 不是 SEO 的敌人,把内容提前渲染出来才是它的正确打开方式,相关前端性能取舍可参考 Core Web Vitals 优化指南。
