包包baobaolin.com
date
entry
038
topic
infrastructure
rev
—

三支 IndexNow 腳本預設送整份 sitemap,被拒絕也回 0

同一套提交邏輯出現在我三支腳本裡:不帶參數就送整份 sitemap,202 印成「submitted」,被拒絕也回 0。對照 IndexNow 的文件和一台假伺服器。

IndexNow 是一個推送協定:網站把新增、修改、刪除的網址 送到一個端點(我的腳本用 JSON 批次 POST),參與的搜尋引擎(Bing、Yandex 等)彼此分享。它不包含 Google,這在〈Search Console 的兩個時間戳〉寫過。

我四個站各有一支提交腳本。這次把四支並排讀,發現有三支沿用同一套提交邏輯:2026 年 7 月 16 日寫第一支(同一天從 curl 改成這個 Python 版本),8 月 22 日、10 月 9 日各沿用一次,細節(sitemap 路徑、輸出的箭頭)略有不同,三支都帶著同樣的三個問題。這篇要談的就是這三個問題:不帶參數時送了什麼、202 被印成什麼、被拒絕時結束碼是多少;最後是第四支的寫法和我會怎麼改。

不帶參數會送什麼

範本的核心是一支內嵌在 shell 裡的 Python(節錄,省略 import 與金鑰宣告):

urls = sys.argv[1:] or re.findall(r'<loc>(.*?)</loc>', open("public/sitemap.xml").read())
payload = {"host": "www.yutuo-tech.com", "key": key,
           "keyLocation": f"https://www.yutuo-tech.com/{key}.txt", "urlList": urls}

有給網址就送那幾個,沒給就送 sitemap 裡符合這個 regex 的每一個 <loc>。用法說明的第一行就是不帶參數的寫法,註解寫「提交 sitemap 全部 URL」。2026 年 10 月 13 日三個站 repo 裡已 commit 的 sitemap 大小:

站腳本寫成不帶參數時送出
yutuo-tech.com2026-07-16413 個網址
一個外銷站2026-08-221,920 個網址
一個新站2026-10-0915 個網址

IndexNow 的 FAQ 有一題就是「可以用 IndexNow 送全站網址嗎」,回答是:它是用來通知最近新增、修改或刪除的網址,「not designed for submitting every URL on your site at once」;如果整站內容最近都更新過,例如搬家或改版之後,一次送全部可以接受;平常的發現與長期收錄用 XML sitemap。另一題說同一個網址一天不要送很多次,除非內容有實質改變。

所以範本的預設值不篩選變更,容易導向 FAQ 不建議的日常全站重送。假設在 1,920 個網址的那個站,每發一篇文章就跑一次不帶參數的版本,那一次真正更新的可能只有新文章和幾個列表頁,其餘一千九百多個網址也會一起送出(我沒有這三個站實際送了幾次的紀錄,這是照預設值推的)。

202 被印成「submitted」

範本處理成功的那一行:

r = urllib.request.urlopen(req)
print("HTTP", r.status, "→ submitted", len(urls), "URLs")

200 和 202 都會正常拿到回應、走到這裡(HTTP 錯誤會變成 HTTPError,連線失敗則是 URLError 之類的例外),印出來的狀態碼不同,標籤都是「submitted 413 URLs」。IndexNow 的文件把兩者分開:200 是「URL submitted successfully」,202 是「URL received. IndexNow key validation pending」。FAQ 補充說第一次提交可能拿到 202,搜尋引擎會先驗證金鑰再處理內容。

也就是說,看到 202 時,金鑰檔對不對還沒有答案,而共用的「submitted」標籤沒有說出這一點。FAQ 自己的範例程式也把 200、202 一起當成功,印的是「URLs submitted successfully」。另外,200 和 202 都不代表網址已經被抓取或收錄。

被拒絕也回 0

範本把 HTTPError 接住、印出狀態碼和回應,然後讓程式正常結束。我寫了一台十行左右的假伺服器,依參數回指定的狀態碼,把範本的端點換成它(提交與例外處理的邏輯不動,端點、host、金鑰和 sitemap 的讀法換成測試用的),送 yutuo-tech.com 那份 413 個網址的 sitemap(整理後的輸出與結束碼):

HTTP 200 -> submitted 413 URLs     exit=0
HTTP 202 -> submitted 413 URLs     exit=0
HTTP 403 …                         exit=0
HTTP 422 …                         exit=0
HTTP 429 …                         exit=0
(伺服器沒開)                       exit=1

文件裡 403 是金鑰無效(找不到金鑰檔,或檔案裡沒有那個金鑰),422 是網址不屬於這個 host 或金鑰格式不對,429 是請求太多、可能被當成垃圾。這三種被拒絕的情況,結束碼都是 0;這組測試裡只有伺服器沒開的那次是 1(sitemap 讀不到之類其他沒被接住的例外,也會是非零)。假伺服器回的 body 是我自己寫的,只能證明腳本的控制流程,不代表 IndexNow 實際回什麼。

目前三個站的部署腳本都沒有呼叫它(我 grep 過),也沒有執行紀錄能說明實際怎麼用;如果是手動跑、看輸出,風險就在「看」:一行 HTTP 403 夾在一長串輸出裡,結束碼又是 0,可能被略過。哪天有人把它接進只檢查結束碼的部署流程,提交失敗就會被判成成功。

當天抓得到四站的金鑰檔

既然 403 可能被略過,我順手確認了金鑰檔。IndexNow 的文件要求金鑰 8 到 128 個字元,只能有大小寫字母、數字和連字號;建議的做法是在網站根目錄放一個 UTF-8 的 {key}.txt,內容就是金鑰本身(也可以用 keyLocation 指到同一個 host 的其他位置,但那樣只能提交該路徑底下的網址)。四個站的金鑰檔,10 月 13 日用 curl 抓:都回 200,內容和檔名一致。這只說明當天抓得到,不代表搜尋引擎已經驗證成功。只有一個細節:yutuo-tech.com 的金鑰檔在 www 底下,apex 網域的同一個路徑回 301;範本的 host 和 keyLocation 都寫 www,對得上。

第四支的寫法

2026 年 9 月 12 日寫的第四支(一個資料庫站,sitemap 索引底下約兩萬個網址,大部分由資料庫產生)沒有沿用範本:

  • 預設只印、不送。有網址時預設只列出來,要加 --submit 才真的送出,因為通知外部服務是收不回的動作;一個網址都沒有就直接報錯。
  • 沒有「送全部」的選項。要送什麼得自己給網址(--url 或 --stdin),或用 --since <git ref> 讓 git 列出從那個 ref 到 HEAD 新增或修改過的文章(不含刪除、不含未 commit 的改動),再依改動的語言換算成網址;資料庫產生的頁面不走這條路。
  • 金鑰從檔案讀。程式去 web/public/ 找唯一一個金鑰檔(它只認 16 到 64 個小寫十六進位字元的檔名,比協定允許的窄),檢查內容和檔名相同,不在程式裡另寫一份。
  • 真的送出時,只有 200、202 回 0。其他狀態碼回 1;分批送時只要一批失敗,整體就回 1。

它的註解裡有一句「送沒有變動的 URL 會被降低信任」。我這次回去讀 IndexNow 的文件與 FAQ,沒有找到這個說法;FAQ 寫的是「不要一天送很多次」「不是設計來一次送全站」。那句話應該改成照文件的寫法,不要比原文強。

範本要改的三件事:不帶參數時不要讀 sitemap,改成報錯並印出用法;202 印成「已收到,金鑰驗證中」而不是「submitted」;200、202 以外的狀態碼都 sys.exit(1)。下面這段取代範本裡從 urls = 到結尾的部分,key、payload、req 的設定照舊放在中間;用法那行的腳本路徑各站不同,照各自的改。

urls = sys.argv[1:]          # 不再退回讀 sitemap
if not urls:
    sys.exit("用法:bash scripts/indexnow-ping.sh https://… [https://…]\n"
             "這支只送明確指定的新增、修改、刪除網址,不讀 sitemap。")
# …(key、payload、req 的設定同原本)
try:
    r = urllib.request.urlopen(req, timeout=30)
    label = {200: "submitted", 202: "received, key validation pending"}.get(r.status)
    print("HTTP", r.status, label or "unexpected", len(urls), "URLs")
    sys.exit(0 if label else 1)
except urllib.error.HTTPError as e:
    print("HTTP", e.code, e.read().decode(errors="replace")[:300])
    sys.exit(1)

如果只記得一件事

抄範本會連它的預設值一起抄。開頭那三個問題,一個是預設就做 FAQ 不建議的日常全站重送,一個是用「送出了」的標籤蓋過「還在驗證」,一個是把拒絕變成結束碼 0;三支腳本各帶了一份,第四支用了不同的預設值、輸出和錯誤處理。三個站的腳本我還沒改,這篇是改之前的對照。

修訂紀錄

  1. 首次發布