搜索抓取

带 # 的 URL 与哈希路由:蜘蛛看到的到底是哪一份页面

URL 里 # 后面的内容不会随请求发给服务器,所以无论用户从哪个哈希路由进来,蜘蛛拿到的往往是同一份文档。文章说明片段标识符与哈希路由的差别,并给出让每份内容拥有独立可抓取 URL 的几种做法与上线前检查项。

搜索抓取

带 # 的 URL 与哈希路由:蜘蛛看到的到底是哪一份页面

在浏览器地址栏里,https://example.com/list#page=3 看起来是一个新页面:用户点开它,看到的确实是第三页。但对服务器来说,这个请求的路径仍然是 /list。片段标识符(# 及其后面的部分)只在浏览器内部起作用,不会随 HTTP 请求发出去。

蜘蛛拿到的永远是对应路径的那一份文档

抓取时蜘蛛发出的请求只有协议、域名、路径和查询串,# 之后的内容在发请求之前就已经被去掉了。所以 /list#page=3、/list#page=8、/list#detail-42 在蜘蛛眼里是同一个地址,它抓一次就够了,也不会因为页面上挂着多个不同哈希的链接而多抓几遍。

更关键的是:蜘蛛拿到的是服务器返回的原始 HTML。如果页面完全依赖 JavaScript 在客户端读取哈希、再去请求数据拼出内容,那么蜘蛛首次拿到的文档里往往只有空壳。它是否愿意继续执行脚本、等到数据回来,取决于渲染能力与站点当时的抓取状态,这件事本身不可控,也不该被当成主要方案。

哈希路由与 History API 的差别

用 #!/item/42 这类哈希路由做前端路由,对用户体验不一定有影响,对 URL 发现却很不利。所有“页面”共用同一个真实路径,Sitemap 里没法为它们分别列出条目,列了也会被当作同一个地址处理;内链也无法把锚文本和线索分散到具体内容上。

相对稳妥的做法是使用 History API,让前端路由对应真实路径,例如 /item/42,同时保证服务器对这类路径直接返回包含对应内容的 HTML。哪怕内容是预渲染或服务端拼装出来的,也比只给一份空壳强得多。判断标准很简单:关掉 JavaScript,单独请求这个地址,看返回的 HTML 里有没有这条内容的标题和正文。

让每份内容都有独立可抓取地址的常见做法

  • 前端路由改用真实路径,服务端为每个路径返回对应内容,或至少返回可读的基础信息与元数据。
  • 分页、筛选、排序等状态尽量用查询串表达,例如 /list?page=3,虽然带来参数管理成本,但每条地址至少能被单独请求和判断。
  • 如果只能用哈希承载状态,就在站内另建一套指向真实路径的入口页,把它们放进内链和 Sitemap,别让哈希地址成为唯一线索。
  • 内链一律直接写真实地址,不要靠脚本二次拼接;锚文本写清目标内容,而不是“点击查看”。
  • Sitemap 里只放服务器能直接返回内容的地址,避免把哈希地址或参数拼出的无效组合塞进去。

锚点链接并没有白写

需要区分两种 # 用法。一种是页面内的锚点跳转,例如 /guide#setup,它指向同一篇文档里的某个小节。这种链接对抓取没有增量,但对用户和搜索结果里的“跳转到章节”仍有价值,合理使用没问题。另一种是把 # 当作路由来承载不同内容,这才是需要改掉的部分。

另外,服务器日志里不会出现 # 后面的内容。如果你在日志里反复看到同一个路径被大量请求,却找不到那些“看起来很多”的页面,先检查是不是哈希路由把几十个虚拟页面折叠成了一条真实地址。

判断一个地址是否真的独立,不看它在浏览器里显示成什么样,只看把它单独发给服务器时,返回的 HTML 是不是对应该内容的那一份。

上线前值得做的几项检查

  1. 关闭 JavaScript 请求几条核心详情页,确认正文与标题存在。
  2. 查看 Sitemap 与内链中的地址,是否含有 # 或依赖脚本跳转。
  3. 抽查服务器日志,确认详情类的真实路径有抓取记录,而不只是首页和列表页。
  4. 确认这些路径返回稳定的状态码,不要用 200 返回空内容。

改造的力气主要花在服务端路由与渲染上,短期看是麻烦,长期看是让 URL 发现回到一个可预期的轨道上。