你那个用 AJAX 做得贼溜的页面,上线后在 Google 里搜不到自己?点开 Search Console,发现爬虫抓回来的页面内容全是空的?别急着怀疑人生——不是你代码写错了,是 AJAX 和搜索引擎之间,压根没对上频道。
这事儿我踩过太多坑。今天不讲虚的,就聊怎么让爬虫真正“看见”你那些动态加载的内容。
为什么搜索引擎抓不到你的AJAX内容?
搜索引擎爬虫不点鼠标,也不等加载动画。它打开一个 URL,拿到 HTML 就走。如果你的页面主体靠 fetch() 或 axios 异步拉数据,而初始 HTML 里只有 <div id="app"></div> 这种空壳子,那爬虫看到的就是一片白。
2018 年我接手一个电商后台改版项目,商品列表全靠 AJAX 渲染。上线两个月,Google 只收录了首页。一查抓取预览,所有商品区域都是空的。后来才发现:爬虫根本没执行 JS,更不会等接口返回再截图。
关键问题还不只是“内容没出来”,而是“路径没对应”。比如用户点进 /product/123,页面靠 JS 改变 URL 并加载数据——但服务器根本不认识这个路径。爬虫连门都找不到,自然没法进门。
3个方法让爬虫“看见”你的动态内容
方法1:用服务端渲染(SSR)替代纯前端渲染
把数据请求从浏览器搬到服务器上。用户访问 /product/123 时,服务端先调 API 拿到商品信息,拼好完整 HTML 再发给浏览器。爬虫来的时候,看到的就是带标题、描述、价格的真实页面。
我帮一家做数据分析的 SaaS 公司做过 SSR 改造。他们原来用 Vue 做报表页,所有图表数据都是 AJAX 加载。改完之后,不仅 Google 开始收录各个报表页,用户打开速度也快了一截——不用再等 JS 下载、解析、执行完才出内容。
怎么做?React 推荐 Next.js,Vue 推荐 Nuxt.js。核心动作就一条:把原来写在 mounted() 或 useEffect 里的请求逻辑,挪到框架提供的服务端钩子里(比如 Next 的 getServerSideProps,Nuxt 的 asyncData)。
注意避坑:服务端代码里不能直接用 window、document 或 localStorage。需要判断运行环境,或者用 onMounted 这类客户端专属钩子兜底。
方法2:利用 Google 的“动态渲染”策略
如果重构成 SSR 太费劲,可以试试“动态渲染”:对爬虫返回预渲染好的静态 HTML,对真实用户还是跑原来的 AJAX 流程。
实现方式有两种:
- 自建方案:用 Puppeteer 或 Playwright 在服务器起个无头浏览器,实时渲染页面并返回 HTML;
- 托管方案:用 Prerender.io 这类服务,它会根据 User-Agent 自动识别爬虫,并返回缓存的静态版本。
之前帮一个新闻聚合站接入 Prerender.io,配置完第二天,Search Console 里就能看到新页面开始被收录。小站点基本不用额外开发,几十块一个月;大一点的站成本会上去,但比推翻重做一套后端轻得多。
提醒一句:动态渲染适合内容更新频率中等的场景。如果是秒级刷新的直播页或实时行情,预渲染出来的 HTML 很可能已经过期。这时候就得权衡——要不要为 SEO 牺牲一点实时性?
方法3:改用 History API 实现“可爬取”的URL
别再用 #!/product/123 这种 hashbang 路由了。Google 明确说过不推荐,而且这种地址分享出去难看、复制粘贴容易出错、爬虫也不爱理。
换成标准路径:/product/123。这需要两步配合:
- 前端用
history.pushState()更新地址栏,并监听popstate做路由跳转; - 后端必须能响应
/product/123这个请求,至少返回一个包含基础 SEO 信息(比如<title>、<meta description>)的 HTML。
我之前重构过一个技术论坛,把老的 hashbang 路由全换成 History API。几个月后,帖子页收录量涨了几倍。用户发链接不再带一串 #,搜索引擎也能顺着 /thread/456 这样的路径,把每个帖子当独立页面抓取。
使用pushState时,如何避免404?
你前端改了 URL,但服务器压根不知道 /product/123 是啥,用户直接访问就 404——这是最常卡住人的一步。
解法很直接:让服务器对所有未知路径,都返回你的单页应用入口文件(比如 index.html)。这样前端路由才有机会接管。
- Nginx 用户,在配置里加一行:
try_files $uri /index.html; - Apache 用户,用
.htaccess配置:FallbackResource /index.html - 如果用 Vercel 或 Netlify,它们默认就支持,不用额外操作。
重点来了:pushState 不会触发新请求,所以你必须确保——当用户在浏览器地址栏输入 /product/123 并回车时,服务器返回的是一个完整的 HTML 页面(哪怕只是骨架),而不是 404。否则,连 JS 都没机会执行。
用<link rel="canonical">解决重复内容问题
AJAX 页面特别容易“自我复制”:
/product/123/product/123?ref=sidebar/product/123#tab=reviews- 甚至
/product/123?sort=price&limit=20
这些 URL 内容高度重合,但搜索引擎会当成不同页面处理,分散权重,还可能被判定为低质重复。
解决方案:在每个页面 <head> 里加一行:
<link rel="canonical" href="https://yoursite.com/product/123">
告诉搜索引擎:“这才是正主,其他都是分身。” 它会把所有相似页面的权重,集中到 canonical 指向的那个地址上。
之前处理过一个电商站,因为 AJAX 分页和筛选参数,生成了上千个几乎一样的商品页。加上 canonical 后,无效收录明显减少,主商品页的自然流量反而上去了。
注意:canonical 必须指向一个真实可访问的页面,别写死成 /product/123 却忘了服务端没配这条路。
用<meta name="fragment">让爬虫主动请求静态版本
这是个老办法,现在 Google 官方已不推荐,但 Bing 和 Yandex 仍支持,小成本试错值得考虑。
在页面 <head> 里加:
<meta name="fragment" content="!">
Google 爬虫看到后,会自动用 ?_escaped_fragment_= 参数重新请求一次,比如:https://yoursite.com/product/123?_escaped_fragment_=
你需要在服务端检测这个参数,返回预渲染好的 HTML(可以用 prerender-node 这类中间件自动处理)。
我在一个旅游攻略站试过,加完两天内,目的地详情页就开始陆续被收录。虽然 Google 已转向更现代的 JS 渲染能力,但如果你主要面向中文或海外多引擎流量,这个标签还能当个备用弹药。
今天就能执行的3个操作步骤
打开 Google Search Console,用“URL检查工具”测你最重要的3个 AJAX 页面
输入https://yoursite.com/product/123这类地址,点“测试实时网址”。如果右侧预览区是空的,或者提示“未找到主要内容”,说明爬虫确实看不到你的数据——这就是你要优先修的第一个洞。给每个核心 AJAX 页面,手动补一个服务端静态 fallback
不用立刻上 SSR。比如你现在用fetch('/api/product?id=123')加载商品,那就同步在后端加一个路由:GET /product/123,返回一个含<title>、<h1>和简要描述的 HTML。哪怕数据是写死的,也比空白强。今天就去改服务器配置,解决 404 问题
如果你用 Nginx,打开你的 site 配置文件,在location / { ... }块里加一行:try_files $uri $uri/ /index.html;
然后运行nginx -t && nginx -s reload。Apache 用户打开.htaccess,加上FallbackResource /index.html。改完立刻用隐身窗口访问/product/123,确认页面能正常打开。