在浏览器地址栏里,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 是不是對應该内容的那一份。
上线前值得做的几項检查
- 關閉 JavaScript 請求几條核心詳情頁,確認正文與标题存在。
- 查看 Sitemap 與内鏈中的地址,是否含有 # 或依赖脚本跳轉。
- 抽查服務器日誌,確認詳情類的真實路径有抓取记錄,而不只是首頁和列表頁。
- 確認這些路径返回稳定的狀態碼,不要用 200 返回空内容。
改造的力气主要花在服務端路由與渲染上,短期看是麻烦,長期看是让 URL 發現回到一個可预期的轨道上。