文章分析了网站列表页改为无限滚动加载后,导致深层文章收录大幅下降的问题。原因包括爬虫无法触发滚动加载、误用已废弃的rel=next标签、以及分页Canonical标签误指向第一页,导致搜索引擎错误剔除深层内容。作者通过修复这些问题,提高了网站的SEO效果。
去年上半年给团队维护的一个技术资讯社区重构列表页。当时为了让移动端和桌面端的滑动体验更顺畅,前端团队把原来底部规规矩矩的“第1页、第2页...末页”这种传统数字分页器全部撤掉了,换成了主流的无限滚动加载:页面底部埋一个观察节点,用户滚到底部就通过接口异步拉取下一批20条数据塞进列表容器,顺便用 pushState 把浏览器地址栏悄悄改成对应的页码地址。
刚上线那会儿大家都很兴奋。前端埋点显示列表页的滚动深度增加了三成多,跳出率也下降了几个百分点。
好状态只维持了一个月。到第二个月中旬查月度 SEO 报表,全站已收录的索引量出现断崖式下跌,从原本的十几万条直接缩水到不足四万条。很多发布了几个月甚至几年的优质历史文章,只要权重掉出分类页前两屏,就在搜索引擎的索引库里悄无声息地消失了。
调出服务器的 Nginx 抓取日志和百度、谷歌两边的爬虫抓取记录一对比,原因很直接:所有爬虫对分类和标签列表页的抓取深度,全都被死死卡在第一屏。第二页往后的数万篇历史内容,内部链接通道全部断裂,彻底沦为了孤岛页面。
前后花了两周时间把这套无限滚动方案彻底翻新,顺带把里面埋藏的分页与抓取硬伤一个个拔了出来。
爬虫拿到的是静态快照,不会主动滚屏触发异步加载
很多开发者潜意识里觉得,现在的搜索引擎爬虫都内置了无头浏览器渲染引擎,能够正常执行 JavaScript 代码,所以异步加载肯定不在话下。
事实是,爬虫虽然具备渲染能力,但它的计算资源非常昂贵。无论百度蜘蛛还是谷歌爬虫,抓取器在请求页面时,执行初始脚本、等待首屏渲染完成并触发 load 事件后,就会立刻抽取 DOM 结构生成快照。它绝不会像真实用户那样,闲着没事把页面向下滚动几千像素去触发各种滚动监听事件。
当时前端写的最典型的一段监听代码长这样:
<div id="article-list">
<!-- 首屏20条文章由服务端直出 -->
<article class="post-item">...</article>
</div>
<!-- 底部哨兵节点 -->
<div id="scroll-sentinel" style="height: 10px;"></div>
<script>
const sentinel = document.getElementById('scroll-sentinel');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 爬虫根本不会主动滚动到底部,这里的异步请求永远不会被触发
loadMoreArticles();
}
});
}, { rootMargin: '200px' });
observer.observe(sentinel);
</script>
在这套逻辑下,真实用户只要手指滑一滑,数据源源不断;但在爬虫眼中,这个列表永远只有开头的20条。
对于资讯站或者博客系统来说,文章详情页的入口主要依赖首页、分类归档和标签页。当列表页无法自然翻页,第2页到第500页的文章链接直接在 HTML 里蒸发了。爬虫从首页顺藤摸瓜抓取时,走到分类第一页就撞到了死胡同。
盲信 rel="next" 与 rel="prev",没注意到这项规则早就成了历史遗迹
发现爬虫不滚屏之后,有同事建议:既然异步加载抓不到,那在页面的 head 头里补上标准的分页关系标签,告诉爬虫下一页的地址在哪儿:
<link rel="next" href="https://www.example.com/tech/?page=2">
<link rel="prev" href="https://www.example.com/tech/">
很多老牌建站插件甚至不少国内的 SEO 文章,到现在还在煞有其事地强调这项配置。
但实际上,谷歌官方搜索团队在几年前就公开确认,其核心抓取索引系统早就废弃了对 rel="next" 和 rel="prev" 标签的支持,不再将它们作为合并分页权重或识别连续翻页的信号。百度蜘蛛在面对纯粹写在 head 里的翻页 link 标签时,也基本不会将其当成有效的链接爬取入口。
现代搜索引擎爬虫发现新 URL 的第一铁律依然没变:必须在正文 HTML 结构中存在合法的、带有 href 属性的 <a> 标签。
如果页面可见区域里完全没有 <a href="/tech/?page=2">下一页</a> 这种实体锚文本链接,指望爬虫去读 head 里的辅助元数据来完成深度爬取,完全是一厢情愿。
分页 Canonical 标签误指向第一页,给所有翻页判了死刑
这是我们在排查过程中揪出来的最严重的自残式配置。
做无限滚动时,前端配合服务端搞了动态 URL 替换。用户往下滑动加载了第2页的内容,地址栏变成 /tech/?page=2。为了防止被搜索引擎判定为“内容相似度过高的重复页面”,模板渲染引擎里写了一句极其要命的通用代码:
<!-- 在 /tech/?page=2 的源代码里赫然写着: -->
<link rel="canonical" href="https://www.example.com/tech/">
当时是为了防重复页面,可 Canonical 标签的真实含义是:告诉搜索引擎当前页面是个影子副本,请忽略当前 URL,直接去收录规范版本。
这个指令直接导致了两个灾难性后果:
- 搜索引擎遵照指示,把所有
/tech/?page=2、/tech/?page=3从搜索索引库中彻底剔除。 - 搜索引擎顺带认定这些分页页面没有独立存在的必要,大幅降低对这些分页 URL 上所携带内部超链接的抓取优先级。
在分页场景中,规范标签必须坚持自引用原则。每一页的内容既然不同,它的规范地址就必须是它自己:
<!-- /tech/ 首页 -->
<link rel="canonical" href="https://www.example.com/tech/">
<!-- /tech/?page=2 翻页 -->
<link rel="canonical" href="https://www.example.com/tech/?page=2">
pushState 换了地址栏,服务端却根本无法直出该页
很多团队做无限滚动,前端用 history.pushState 把地址栏改得很漂亮:用户滚到哪一屏,地址栏就对应变成 /tech/page/3/。
问题出在服务端。我们当时随手测试了几十个被 pushState 刷出来的分页地址,直接复制粘贴到无痕浏览器窗口回车打开,结果直接报错 404,或者直接被 Nginx 的重写规则回滚到了第一页的内容。
原因很简单:前端团队在客户端本地算好了偏移量渲染,但后端的路由配置压根没有针对 /tech/page/3/ 做过页面数据直出支持。
当外部用户把某个具体分页分享到社交平台,或者爬虫顺着某些外部引荐链接直接抓取这个分页 URL 时,服务端返回的要么是 404 错误页,要么是与第一页一模一样的重复 HTML。
爬虫对这种“点进去和标出来的 URL 内容不符”或者大量 404 的行为极其敏感,很快就会给站点的整体技术质量打上低分,收缩分配给该站点的抓取配额。
游标分页与动态过滤参数引发抓取预算风暴
为了解决传统分页在 MySQL 数据量很大时 OFFSET 10000 LIMIT 20 查询变慢的问题,后端在做无限滚动时,顺便把接口改成了根据时间戳或自增主键游标的机制:
GET /api/posts?cursor=17123456789&limit=20
前端不仅把这个游标地址直接挂到了页面 URL 上,还顺带把各种默认筛选条件也拼了进去:
https://www.example.com/tech/?cursor=1712345678&sort=newest&filter=all&view=card
对人来说,这是同一个分类列表;但对搜索引擎爬虫来说,每一个微小的时间戳游标、每一个参数组合,全都是一个全新的网页地址。
更要命的是,由于时间戳随时在变,列表里只要有一篇新文章发布,所有计算出来的游标全部重新洗牌。爬虫进入分类页之后,顺着这些带游标的链接疯狂递归爬取,每天几十万次抓取配额,全部在这些参数黑洞里原地转圈。真正需要被收录的高价值文章详情页,反而因为抓取预算耗尽而排不上队。
兼顾体验与收录的落地解法:渐进增强式混合分页
踩完这一圈坑,我们最终把列表页做成了一套兼顾客户端体验与搜索引擎抓取的混合分页架构。
核心原则就一条:无脚本环境保底,有脚本环境增强。
1. 服务端必须直出真实的语义化超链接
在列表容器的最下方,无论是否准备启用无限滚动,服务端直出的原生 HTML 结构里必须包含纯粹的、可点击的翻页链接导航:
<nav class="pagination-nav" aria-label="文章分页">
<a href="/tech/?page=1" class="page-num">1</a>
<span class="current">2</span>
<a href="/tech/?page=3" class="page-num">3</a>
<a href="/tech/?page=3" class="next-page" rel="next">下一页</a>
</nav>
当爬虫抓取这一页时,无需运行复杂的滚动交互,顺着底部的这些 <a> 标签就能一层层顺畅地抓取下去。
2. 客户端运行时优雅接管分页行为
真实用户的浏览器在加载了 JavaScript 之后,脚本在 DOM 就绪时会自动接管底部的分页栏,将其替换为“加载更多”按钮或无限滚动的监听锚点:
document.addEventListener('DOMContentLoaded', () => {
const paginationNav = document.querySelector('.pagination-nav');
if (!paginationNav) return;
// 脚本正常加载时隐藏原生分页,挂载无限滚动触发器
paginationNav.style.display = 'none';
const loadMoreBtn = document.createElement('button');
loadMoreBtn.className = 'load-more-btn';
loadMoreBtn.textContent = '加载更多';
paginationNav.parentNode.insertBefore(loadMoreBtn, paginationNav);
// 后续滚动或点击加载更多时,执行 pushState 并在当前页追加节点
setupInfiniteScroll(loadMoreBtn);
});
不管是爬虫抓取,还是用户网络卡顿导致脚本没跑起来,页面底部的原生翻页导航始终都在。
3. 每个分页 URL 都能独立直出与访问
确保任何一个形如 /tech/?page=3 的地址被独立请求时,后端服务端都能准确查询对应偏移量的文章列表,并在 HTML 里把这一页的内容、正确的页码标题以及自引用的 canonical 标签一次性吐出来。
# Nginx 层面做好规范路径 rewrite,避免出现多套分页 URL 格式
location ~ ^/tech/page/([0-9]+)/?$ {
rewrite ^/tech/page/([0-9]+)/?$ /tech/?page=$1 last;
}
并在响应头部带上清晰的缓存控制,防止爬虫抓到由于客户端状态污染导致的错误快照。
4. 规范化参数并屏蔽动态游标
对外部和爬虫只暴露语义干净、结构固定的页码参数(比如 ?page=3),绝对不要把临时游标参数写入正文链接。
在 robots.txt 中主动封堵那些可能引发参数组合爆炸的无意义筛选:
User-agent: *
Disallow: /*?*cursor=*
Disallow: /*?*sort=*
Disallow: /*?*filter=*
把分类列表改回这套渐进增强的混合分页结构后,配合站长平台重新提交分类入口。两到三周左右,爬虫在服务器日志里的抓取深度明显回升,那些沉寂多日的深层历史文章也陆续重新回到百度的收录结果中。技术改造千万不能想当然,任何影响页面链接拓扑结构的改动,上线前都得先用爬虫的视角看一眼。
文章标题:网站列表页换成无限滚动加载,深层文章收录却直接腰斩?排查 rel=next 废弃后的爬虫死胡同、Canonical 误标与抓取配额耗尽,我踩过的几个深坑
文章链接:https://www.llbbs.cn/jishujaocheng/267.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?