搜尋抓取

带 # 的 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 發現回到一個可预期的轨道上。