JS 渲染页面的搜索引擎收录排查实战:从空壳 HTML 到爬虫可读的完整修复思路

搜索引擎抓取网站的时候,第一步不是「理解你的文章写了什么」,而是「能不能把页面完整地取回来」。对爬虫来说,一个渲染失败、响应超时、或者返回一堆空壳 HTML 的页面,等同于不存在——它有内容也没用,因为在爬虫眼里那个位置是空的。

这件事在今天的建站环境里变得比十年前容易出问题得多。以前大家写的是服务端渲染的静态 HTML,爬虫看到的和用户看到的基本一致。现在用 Cloudflare、用 JS 框架、用各种「一键加速」服务,页面很大一部分内容要等浏览器执行 JavaScript 才出现。爬到空壳的情况很常见,而且站长在浏览器里看不到任何异常——你自己打开网站一切正常,因为你的浏览器跑了 JS。

这篇文章讲的是单页应用(SPA)与 JavaScript 渲染页面的收录问题:为什么搜索爬虫拿不到内容、怎么判断自己中招了、以及不换框架的前提下有哪些可行的修复路线。全文以实际排查手段为主,命令可以直接用。

一、先搞清楚爬虫到底看见了什么

排查的第一原则是:不要用浏览器去判断爬虫看到的内容。用 curl,它不会执行 JavaScript,拿到的就是原始 HTML——这最接近早期爬虫的视角。

典型症状是这样的。假设一个商品页的标题由前端框架渲染:

# 统计原始 HTML 的长度、可见汉字数、脚本标签数
curl -sL -A 'Mozilla/5.0' https://你的域名/detail/123 -o /tmp/raw.html
python3 - <<'PY'
import re
h = open('/tmp/raw.html', encoding='utf-8', errors='ignore').read()
visible = re.sub(r'<[^>]+>', '', h)
cjk = sum(1 for c in visible if '\u4e00' <= c <= '\u9fff')
print('HTML 长度:', len(h))
print('可见汉字数:', cjk)
print('脚本标签数:', len(re.findall(r'<script', h)))
PY

如果输出是「HTML 长度 3000、正文可见汉字 12、脚本数 8」,那基本可以确认:爬虫拿到的就是一个空壳。你可以在浏览器里右键查看源代码(不是「检查元素」,那个显示的是渲染后的 DOM),对比一下就能直观地看到差异——源代码里全是 <div id="app"></div> 这种占位。

顺便说一个容易忽略的点:Google 和 Bing 是能执行 JavaScript 的,百度在这方面的能力要弱得多。所以很多人会遇到「谷歌收录正常、百度一个字都收不到」的现象,这类站点的目标受众如果是中文搜索用户,JS 渲染问题几乎是致命的。不要因为 Bing 站长工具里看着正常就放心。

二、判断爬虫真实行为的四个手段

光看 HTTP 响应还不够,要看爬虫实际干了什么。下面四个手段按成本从低到高排列。

1. 日志比对:最简单的证据

在 Nginx 访问日志里找搜索爬虫,看它们的请求路径和响应码:

# 看最近 7 天各搜索引擎爬虫的抓取量与状态码分布
zgrep -hE 'Baiduspider|Googlebot|bingbot|YandexBot|Sogou' /var/log/nginx/access.log* \
  | awk '{print $9, $NF}' | sort | uniq -c | sort -rn | head -20

如果发现爬虫请求大量返回 200 但页面是空壳(这从日志看不出来,需要结合前面的 curl 测试),或者大量 403/503(WAF 或限流拦住了),说明问题出在服务端而不是内容本身。特别注意 403 和 429:很多站点装了安全插件或者开了 CDN 的爬虫防护,结果把正规搜索引擎的爬虫也一并拦掉了,这会造成非常隐蔽的「不收录」,因为你在后台完全看不到异常。

2. 用 ?_escaped_fragment_= 之外的官方工具验证

Google 提供了「网址检查」的实时抓取测试,Bing 有 URL 检查工具,百度的搜索资源平台有「抓取诊断」。用这些工具的「抓取结果」看它返回的 HTML,比看日志准确得多。百度的抓取诊断会直接展示它抓到的页面代码,如果那里是一堆脚本没有正文,问题就坐实了。

3. 用命令行模拟渲染前后对比

想看「渲染后」的样子,可以用无头浏览器抓一次页面。Node 环境下装 puppeteer,或者直接用 chromium 的命令行模式:

# 用 Chromium 无头模式输出渲染后的 DOM
chromium --headless --disable-gpu --dump-dom \
  --virtual-time-budget=5000 https://你的域名/detail/123 > rendered.html

# 再做一次 curl,对比两者正文长度
curl -sL -A 'Mozilla/5.0' https://你的域名/detail/123 > raw.html

--virtual-time-budget=5000 表示给页面 5 秒钟执行 JS。两个文件的正文长度差出一个量级,就是典型的 JS 渲染依赖。这个方法也适合验证修复后的效果。

4. 检查 robots.txt 有没有把资源挡在外面

这是 SPA 站点最常见、也最容易被忽略的一个自我毁灭式配置:robots.txt 里写了 Disallow: /api/ 或者 Disallow: /*.js$,本意是「不想让爬虫浪费时间」,结果爬虫拿不到 JS 和接口数据,页面就永远渲染不出内容。

curl -sL https://你的域名/robots.txt

原则是:如果页面内容依赖 JavaScript 或接口才能渲染,那么这些 JS 和接口就必须对搜索引擎爬虫开放。屏蔽 JS 却指望内容被收录,逻辑上是矛盾的。

三、四条可行的修复路线

确认问题后,怎么修?从投入产出角度排序,推荐路线按下面的顺序考虑。

路线一:服务端渲染(SSR)或静态生成(SSG)

这是最彻底也最推荐的方案。Next.js、Nuxt、SvelteKit 这些框架都内置了 SSR/SSG 能力:页面在服务器端就生成好完整 HTML,浏览器拿到的第一屏就是有内容的,JavaScript 只是用来做交互增强(渐进增强)。爬虫不需要执行任何 JS 就能拿到全部正文。

对个人站长来说,如果站点内容更新频率不高(博客、文档、产品介绍页),静态生成往往是最优解——打包时就把所有页面生成成 HTML,直接挂 Nginx 或 CDN,速度快、成本低、收录友好,而且几乎没有运行时依赖。

代价是改造工作量。如果站点已经用 SPA 上线并且在跑,重构意味着一次不小的迁移。这时可以先用下面的过渡方案顶住收录问题。

路线二:预渲染(Prerender)

预渲染的思路是「不改现有前端,加一层转换」:服务器判断来访者是不是搜索引擎爬虫,如果是,就用无头浏览器渲染好页面再把 HTML 返回去;如果是普通用户,返回原来的 SPA。这样用户侧体验不变,爬虫侧拿到的是完整 HTML。

自己实现的话,核心是一段 Nginx 的 UA 判断加代理:

# 简化示例:把爬虫请求代理到预渲染服务
map $http_user_agent $is_bot {
    default 0;
    ~*(Baiduspider|Googlebot|bingbot|YandexBot|Sogou|360Spider|Bytespider) 1;
}

server {
    listen 443 ssl http2;
    server_name 你的域名;

    location / {
        if ($is_bot) {
            proxy_pass http://127.0.0.1:3000;
        }
        root /www/wwwroot/spa/dist;
        try_files $uri /index.html;
    }
}

这个方案有几个必须知道的坑。第一,UA 判断是有风险的:爬虫的 UA 会变(新出现的爬虫、小众搜索引擎),靠列表匹配总有漏,被漏掉就等于没收录。第二,恶意爬虫会伪装成搜索引擎 UA,如果不做「反向 DNS 验证」,预渲染服务会被刷爆,反而加重服务器负担。正规做法是在预渲染服务里对请求 IP 做反向解析,确认它真的属于对应搜索引擎(Google 和 Bing 都提供了官方 IP 段列表)。第三,预渲染服务本身要有缓存,否则每次爬虫请求都启动一次无头浏览器,CPU 会很吃紧。

如果不想自己维护这套东西,市面上有托管服务可以直接用,但它们通常按请求量收费,量大了成本会上升,需要自己权衡。

路线三:动态渲染作为过渡

如果短期内没法上 SSR 也没法上预渲染,最低成本的补救是:在空壳 HTML 里预置关键内容。做法是在服务端渲染时,把标题、摘要、正文前几段、结构化数据直接写进 HTML(即使前端随后会把它替换掉)。爬虫拿到这些内容就能建立索引,用户看到的是正常的 SPA 界面。

<div id="app"></div>
<!-- 给爬虫看的内容骨架,前端挂载后会被覆盖 -->
<h1>文章标题</h1>
<article>
  <p>正文前 200 字……</p>
</article>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Article",
 "headline":"文章标题","datePublished":"2026-09-17"}
</script>

这种做法严格说并不是最佳实践(内容存在两份,有维护成本,且如果前端替换失败会出现重复内容),但它的实现成本最低,能立刻改善收录,适合作为过渡期方案。要用的话,务必保持骨架内容和前端渲染内容一致,避免出现「爬虫看到 A、用户看到 B」的作弊嫌疑。

路线四:给爬虫一条独立的内容通道

还有一种做法是给爬虫提供独立的内容入口——比如生成一份纯静态的站点地图页,把所有文章标题和摘要以服务端渲染的方式输出,再通过内链让爬虫能顺着爬到每一篇文章。这不解决单页渲染问题,但能保证「内容被索引到」,适合内容型站点做兜底。

四、配套要做的几件事

修好渲染问题之后,还有几个配套动作,缺一个效果就打折扣。

  • canonical 标签要正确。SPA 常有带参数、带 hash 的多种 URL 指向同一页面,如果每个 URL 都被当成独立页面,收录会分散甚至被判重复内容。确保每个页面有唯一的 canonical 指向规范 URL
  • sitemap.xml 要真实有效,并且只包含 200 状态的规范 URL。如果 sitemap 里塞了大量 404/301 地址,会拖低爬虫对整个站点的信任度
  • 内链要能爬。SPA 里用 onclickhistory.pushState 跳转的链接,爬虫是跟不下去的。要用真正的 <a href> 编写导航和文章互链,让爬虫能通过链接发现页面
  • 首屏性能影响抓取预算。爬虫不可能在低效页面上一等就是半分钟,页面首字节和渲染太慢,抓取频率会自动下降。JS 包体积极其重要性——一个 3MB 的 bundle 会让渲染型爬虫对整站失去耐心
  • 在搜索资源平台主动推送。百度、必应都支持主动推送(IndexNow 协议就是通用的一种),页面更新后主动告知,比等爬虫自己来快很多
  • 定期复查日志。修复后一到两周内,观察爬虫请求量是否上升、是否开始抓取原本不被抓取的内页路径、索引量是否增长。百度搜索资源平台有「索引量」曲线,谷歌 Search Console 有「网页」报告,这是判断修复是否奏效的最终依据

五、一个快速自查清单

如果你怀疑自己的站点有 JS 渲染收录问题,按下面顺序自查一遍,十分钟内能得出结论:

  • curl 取一次页面,看正文可见字符数是否接近浏览器里看到的内容量
  • 对比「查看源代码」和「检查元素」的内容差异,差异越大问题越严重
  • 检查 robots.txt 有没有屏蔽 JS 文件、CSS 文件或数据接口
  • 检查是否有 WAF / CDN 规则误伤搜索引擎爬虫,看日志里的 403/429
  • 用搜索资源平台的抓取诊断工具,直接看爬虫拿到的那份 HTML
  • 确认页面有正确的 canonical、有可爬的 <a> 内链、sitemap 里的 URL 全部可用

六、小结

JS 渲染导致的不收录,本质上是「你给爬虫看的东西」和「你给用户看的东西」不是同一个。修复的思路也就一句话:让爬虫在第一份 HTML 里就能读到核心内容。SSR/SSG 是正道,预渲染是可用的过渡,动态渲染是低成本的兜底。

技术路线的选择最终要回到站点本身:内容更新频率、团队维护能力、愿意为收录投入多少改造时间。但无论选哪条路,都请先做那一次 curl 对比——因为在你亲眼看到「爬虫拿到的是一页空壳」之前,这个问题很难被真正重视起来。而收录这件事最残酷的地方在于:内容再好,只要爬虫看不见,就等于没写。

Last modification:September 17th, 2026 at 12:27 pm

Leave a Comment