本文约 3,977 字,预计阅读 10 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:页面在浏览器里好好的,百度抓到的是空壳
前后端分离的站点投放到百度,最常见的现象是:首页收录了,标题和摘要却是空的;内页压根不收录;抓取诊断里看到的源码只有 <div id="app"></div> 加一串 script 标签。开发同学往往觉得「百度不支持 JS」,于是要么放弃 SPA,要么干脆做两套站。实际情况比这复杂一些:百度确实有带渲染能力的抓取 UA(Baiduspider-render),但渲染是异步、排队、有配额的,且渲染时机晚于首次索引。也就是说,依赖渲染的页面不是不能被收录,而是慢、不稳定、覆盖率低。对于靠长尾流量吃饭的内容站,这种不确定性是不可接受的。
原理分析:两条抓取管线,两条不同的命运
百度对页面的处理可以理解为两条管线:第一条是「源码管线」,蜘蛛抓到 HTML 后立即提取标题、正文、链接,进入快速索引,这条管线不执行 JS;第二条是「渲染管线」,把需要渲染的页面排队交给渲染服务,执行 JS 后再走一遍提取流程,这条管线有延迟和配额。绝大多数依赖客户端渲染的站点,只在第二条管线里被处理,于是就出现了「收录慢、收录少、标题错」的现象。
| 方案 | 源码是否含正文 | 抓取延迟 | 维护成本 | 适用 |
|---|---|---|---|---|
| 客户端渲染(CSR) | 否 | 数天至数周 | 低 | 后台系统、不需收录的页面 |
| 服务端渲染(SSR) | 是 | 同静态页 | 高 | 内容站、电商详情页 |
| 静态导出(SSG) | 是 | 同静态页 | 中 | 文档站、博客 |
| 动态渲染(Prerender) | 对蜘蛛是 | 低 | 中 | 已上线的 SPA 补救 |
| 混合:关键内容直出 | 是 | 低 | 中 | 渐进式改造 |
判断自己站在哪条管线上的方法很简单:在资源平台的抓取诊断里看「页面源码」,如果源码里搜不到正文关键词,那就只走了渲染管线。
保姆级实操
步骤 1:先确认问题边界。 用 curl 直接请求页面,看返回的 HTML 里有没有正文。再用百度蜘蛛 UA 请求一次,确认不是被分流到了不同版本:
UA='Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)'
curl -sS -A "$UA" 'https://mimangfei.com/article/123' | grep -c '正文关键词'
返回 0 就说明源码里没有正文,必须改造。
步骤 2:能改架构就上 SSR 或 SSG。 新建站点优先选择 Next.js、Nuxt 这类默认支持服务端渲染的框架,或者用 Astro、VitePress 做静态导出。WordPress 站点则相反——它本来就是服务端渲染,不要为了「现代化」把前台改成 SPA,那是自废武功。
步骤 3:已上线的 SPA 用动态渲染补救。 思路是:识别爬虫 UA,把请求转发给预渲染服务,返回一个已经渲染好的 HTML;真人用户继续走原来的 SPA。下面是一个极简的 Node 预渲染服务:
// prerender-server.js —— 依赖:express、puppeteer
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
const PORT = 3000;
const SITE = 'https://mimangfei.com';
const CACHE = new Map();
const TTL = 30 * 60 * 1000; // 缓存 30 分钟
let browser;
async function render(path) {
const key = path;
const hit = CACHE.get(key);
if (hit && Date.now() - hit.time < TTL) return hit.html;
if (!browser) browser = await puppeteer.launch({ args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36');
await page.goto(SITE + path, { waitUntil: 'networkidle0', timeout: 30000 });
const html = await page.content();
await page.close();
CACHE.set(key, { html, time: Date.now() });
return html;
}
app.get('*', async (req, res) => {
try {
const html = await render(req.originalUrl);
res.status(200).type('html').send(html);
} catch (err) {
res.status(502).send('prerender failed');
}
});
app.listen(PORT, () => console.log(`prerender listening on ${PORT}`));
步骤 4:在 nginx 层按 UA 分流。 只把爬虫导向预渲染服务,同时给渲染结果单独设缓存:
map $http_user_agent $is_bot {
default 0;
"~*(Baiduspider|Baiduspider-render|Sogou|Bytespider|360Spider)" 1;
}
server {
listen 443 ssl http2;
server_name mimangfei.com;
location @prerender {
proxy_pass http://127.0.0.1:3000;
proxy_set_header X-Original-URI $request_uri;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
location / {
if ($is_bot) { error_page 418 = @prerender; return 418; }
try_files $uri $uri/ /index.html;
}
}
步骤 5:补齐 HTML 层的 SEO 要素。 即便走了预渲染,也要保证 <title>、<meta name="description">、canonical、结构化数据都由服务端写入,而不是靠 JS 动态设置。可以用 react-helmet 或框架自带的 head 管理做 SSR 输出。
步骤 6:用 sitemap 与主动提交加速。 渲染管线的配额有限,能走源码管线就别等渲染。把已预渲染的 URL 通过 API 提交,缩短等待。
常见踩坑
- 用
robots.txt把 JS/CSS 全屏蔽了,渲染服务拿不到样式与脚本,渲染出来的是裸 HTML,照样判定低质。 - 案例: 某 Vue 项目上线半年只收录了首页,抓取诊断里源码一直是空壳,团队一直以为是内容质量分不够,疯狂加原创文章,直到有人用 curl 看了一眼源码,才发现压根没正文,改 SSR 后两周收录从 1 条涨到 400 多条。
- 预渲染服务没做缓存,爬虫一来就现场启 Puppeteer,服务器直接被打爆,反而产生更多 5xx。
- 分流规则只写了
Baiduspider,漏了Baiduspider-render,渲染爬虫走的是普通路径。 - 预渲染返回的 HTML 里 canonical 指向了预渲染服务自己的地址。
- SPA 路由用 hash 模式(
#/article/1),百度不把 hash 后的内容当作独立 URL,内页永远抓不到。 - 认为做了预渲染就万事大吉,忽略了首屏时间与稳定性,渲染出来的页面三秒才返回,百度依然给低分。
总结
判断前后端分离站点是否适配百度,只有一个标准:用蜘蛛 UA 请求页面,返回的 HTML 源码里能不能直接搜到正文。能做到就合规,做不到就必须改造。行动清单:curl 验证源码 → 新站直接上 SSR/SSG → 老 SPA 上动态渲染并按 UA 分流 → 服务端写入标题与 canonical → sitemap 与主动提交加速。别指望百度替你执行 JS,把关键内容直出永远是最稳的方案。AJAX 加载内容的处理原则见 百度对 AJAX 加载内容的处理,iframe 与隐藏内容的风险见 百度对 iframe 与隐藏内容的判定。
