本文约 3,907 字,预计阅读 10 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:上线 Headless 一个月,收录掉了六成
改造往往这样发生:技术团队验证了新的 Headless 架构,页面 LCP 漂亮得能拿出去讲,但一个月后运营发现:老页面的排名还在慢慢掉,新页面几乎不收录,站长平台的抓取次数从每天几百掉到几十。原因不在前端框架本身,而在于 Transfer 过程中被”顺手”丢掉的东西:原本 SEO 插件承担的 meta robots、canonical、sitemap、结构化数据,现在没人管了;原本在 HTML 里的正文,现在要靠执行 JS 才能出现;原本只有一个域名,现在出现了 API 域名的重复内容风险。这些都不是框架层面的缺陷,是工程细节没补上。
原理分析:抓取方看到的是”响应”,不是”你的代码”
搜索引擎处理页面的顺序大致是:取 HTML → 解析 → (可能)渲染 JS → 建立索引。Google 能执行 JavaScript,但渲染是排队的、滞后的;百度对 JS 渲染的支持相对保守,且渲染资源有限。因此 Headless 站点的第一原则是:正文必须存在于服务端返回的 HTML 里(SSR 或 SSG/ISR),而不是 useEffect 里 fetch 后再填。
第二原则是「单一 URL 表达」。Headless 站点天然有两个域名(后端 admin/api 域名 + 前端),很容易出现 /wp-json/wp/v2/posts/123 这类接口页、/graphql 端点、以及后端主题仍可访问的 /?p=123 页面同时存在。这些 URL 必须与前端 canonical 页面区分清楚:接口用 X-Robots-Tag 或 robots 屏蔽,后端用 noindex 头覆盖全站,前端每个页面自带自我引用 canonical。
| 环节 | 常见错误 | 正确做法 |
|---|---|---|
| 渲染 | 纯 CSR | SSG/ISR/SSR 输出正文 |
| 元信息 | 沿用后端 meta tag | 前端按页面生成 |
| Canonical | 缺失或指向后端域名 | 自我引用前端 URL |
| Sitemap | 用后端生成的 | 前端生成或重写 URL |
| Robots | 屏蔽了整站 | 屏蔽接口路径即可 |
保姆级实操
步骤 1:确认服务端渲染。 在 Next.js 里用 App Router 的 Server Component,或在 Pages Router 里用 getStaticProps / getServerSideProps;渲染后请用 curl 直接看 HTML 里是否包含正文:
curl -s https://www.example.com/blog/hello-world | grep -o "文章的某个关键词" | head -3
如果 grep 不到任何正文关键词,说明还在 CSR 模式,需要调整渲染方式。
步骤 2:前端生成完整 meta。 App Router 用 generateMetadata,并保证 canonical 自我引用:
export async function generateMetadata({ params }) {
const post = await fetchPost(params.slug);
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `https://www.example.com/blog/${params.slug}/` },
robots: post.noindex ? { index: false, follow: false } : { index: true, follow: true },
openGraph: { title: post.title, images: [post.cover] },
};
}
步骤 3:给后端域名整体加 noindex 响应头。 后端只是数据源,不需要收录:
server {
server_name admin.example.com;
add_header X-Robots-Tag "noindex, nofollow" always;
location /graphql {
# GraphQL 端点同时对搜索引擎和恶意扫描收口
if ($request_method != POST) { return 405; }
proxy_pass http://127.0.0.1:8080;
}
}
步骤 4:修正后端 sitemap 的 URL。 如果继续使用 Yoast/Rank Math 生成的 sitemap 通常仍指向后端域名,可以用 Yoast 的 wpseo_sitemap_url 过滤器重写:
add_filter( 'wpseo_sitemap_url', function ( $url ) {
$url['loc'] = str_replace( 'https://admin.example.com', 'https://www.example.com', $url['loc'] );
return $url;
} );
更稳的做法是前端自己生成 sitemap.xml(Node 侧遍历 GraphQL 全量 slug 输出),彻底摆脱后端 URL 依赖。
步骤 5:结构化数据随前端输出。 Title Description 之外别忘了 Article/Breadcrumb JSON-LD。Breadcrumb 尤其重要,它的 URL 必须是前端地址:
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: '首页', item: 'https://www.example.com/' },
{ '@type': 'ListItem', position: 2, name: post.title, item: `https://www.example.com/blog/${post.slug}/` }
]
};
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />;
步骤 6:用日志验证爬虫实际到了哪里。 Baidu UA 形如 Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html),Google 是 Googlebot。看看它们是否抓到了页面本身而不是只抓首页:
grep "Baiduspider" /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
常见踩坑
- 用
location / { return 301 https://www.example.com; }把后端整体跳转,结果把 Graphql/xmlrpc 一起跳转,前端请求全 301。 - 案例: 某 Headless 站把
robots.txt写成Disallow: /,理由是”接口不想被抓”,结果前端页面也被连带屏蔽,半年后发现新内容零收录。 generateMetadata里抛异常导致整页 500,反而比没 meta 更糟,需要用 try/catch 兜底。- 前后端都输出了
<link rel="canonical">,出现两套互相打架的地址。 - 分页用过
?page=2但没处理 canonical 与- prev/next,Google 把分页内容变为 duplication group。 - 图片 lazy 用了
IntersectionObserver,虽然 Google 能渲染,但百度往往只看到占位图。 - 前端域名更换后旧的 301 链太长(超过 5 跳),爬虫放弃跟随。Headless WordPress 搭建方案
总结
Headless 的 SEO 判断标准是三条:服务端返回的 HTML 里有没有正文、 canonical 是否自我引用且唯一、sitemap 指向是否与 canonical 一致。行动清单:curl 验渲染 → 前端生成 meta 与 canonical → 后端整体 X-Robots-Tag → sitemap URL 对齐前端 → 用日志看爬虫实际命中。如果这五步做不齐,宁可先在 WordPress 里用页面缓存,也别急着上 Headless。Sitemap 与 robots 的细粒度规则详见 WordPress robots 与 sitemap 配置。
