前陣子在看網站管理工具的報表,翻到一批「標題重複」的警告,數量是三位數。點進去才發現那些全是短網址頁面——使用者每建立一條短連結就是一個網址,而那些頁面對每一條來說內容都長得一樣。
另外一份查詢報表更麻煩。裡面夾著一整批跟服務完全無關的關鍵字,是某間小吃店的店名和菜色。推測是有人拿 Mork 分享了那間店的菜單照片,那個短網址頁面就這樣被收錄了,於是站上替別人的店在搜尋結果裡佔了位置。粗略估過,那類查詢大概佔了總曝光的一成。
短網址頁面本來就不該出現在搜尋結果裡。有價值的是轉址的目的地,而那是別人的網站。
robots.txt 擋不掉
要把一批網址移出搜尋結果,我們第一個想到的也是 robots.txt:
User-agent: *
Disallow: /
寫下去之後爬蟲確實不再來抓,看起來像是生效了。但索引裡那些頁面一個都沒少。
收錄跟抓取是兩件事。Disallow 擋的是抓取,noindex 管的才是收錄,而 noindex 是寫在頁面裡的——爬蟲得先抓到頁面,才讀得到它。
所以擋掉之後會有兩個後果。已經在索引裡的那些會留著,因為爬蟲不再來,就永遠不會發現你加了 noindex;那些頁面繼續待在搜尋結果裡,只是搜尋引擎不再知道內容,於是顯示成沒有描述的空殼。而還沒被收錄的,只要有外部連結指向它,搜尋引擎可以只憑連結的錨點文字把網址本身收進索引,並不需要讀過內容。
順序得反過來,先讓爬蟲抓得到、看得到 noindex,等索引清乾淨了,再考慮擋抓取來省檢索預算。
meta 標籤只蓋得到一半
知道要用 noindex 之後,下一個坑是把它放在 HTML 裡。
短網址頁面其實有兩條路。圖片、影片、音檔會算繪出一個播放頁,那裡放得下 <meta>;但純短連結不會,它走的是轉址。302 回應沒有內容,沒有 <head>,也就沒有地方放 <meta>。而搜尋引擎照樣可以把那個短網址本身收進索引,它是一個回應 302 的合法網址,這樣就夠了。
站上原本的樣板還多錯一層,noindex 只在非連結型別時才輸出:
{% if page_type != "Link" %}
<meta name="robots" content="noindex">
{% endif %}
也就是會算繪 HTML 的那一半有 noindex,走轉址的那一半沒有,而連結型別剛好是數量最多的那一種。
改用回應標頭
X-Robots-Tag 跟 <meta name="robots"> 的效力一樣,差別在它是標頭——轉址帶得上,410 帶得上,直接回圖片的回應也帶得上。
後來改成放在中介層,依路由名稱判斷:
match = getattr(request, 'resolver_match', None)
if match is not None and match.url_name in ('view', 'manage'):
response.setdefault('X-Robots-Tag', 'noindex')
這裡有幾個地方是踩過才知道的。
判斷條件用路由名稱而不是路徑前綴,因為短網址代碼是根層的單一路段,沒有前綴可以比對;路由本來就已經知道這個請求是不是短網址頁面了,再拿字串猜一次只會多一個猜錯的機會。
resolver_match 在某些錯誤路徑上會是 None,直接取屬性會炸掉,所以用 getattr 而不是 request.resolver_match。
最後是用 setdefault 而不是指派。視圖如果自己已經送了更嚴格的值,中介層不該蓋掉它。
這一段有測試守著,而且測兩個方向:短網址頁面與管理頁要有這個標頭,內容頁一定不能有。後者其實比前者重要,這個標頭一旦外溢到一般頁面,整個網站會從搜尋結果裡消失,而且大概要好幾週才會有人發現。
後續
索引不會馬上乾淨。搜尋引擎要重新抓過每一個已收錄的網址、讀到標頭,才會把它移除,這批有幾百個,估計要幾週。
期間曝光數會下降,不過那是應該的。少掉的那些從來就不代表有人在找 Mork。