IndexNow in 60 lines of bash — and the two traps that eat an afternoon
seoindexnowbashwebdev
IndexNow is the rare piece of SEO infrastructure that is actually just an HTTP request. You host a key file at your site root, POST a list of URLs, and one request reaches Bing, Yandex, Naver, Seznam and Yep — the API fans it out.
Google does not participate. Nothing in this post affects Google. Say that out loud before you start, because it is the single most common misunderstanding and it determines whether IndexNow is worth your time at all.
I submitted 22,818 URLs this way. Twelve requests, every one HTTP 200. Here is the whole thing, plus the two mechanical traps that cost me the first two attempts.
The protocol
- Generate a key: 8–128 hex characters.
- Serve it as plain text at
https://yoursite.com/<key>.txt, containing exactly the key and nothing else. - POST JSON to
https://api.indexnow.org/indexnow:
{
"host": "yoursite.com",
"key": "00cbf1ff17dc47824f373896bd042679",
"urlList": ["https://yoursite.com/a", "https://yoursite.com/b"]
}
Up to 10,000 URLs per request. That’s it. No account, no dashboard, no OAuth.
Trap 1: curl: Argument list too long
A batch of 2,000 URLs is roughly 150 KB of JSON. The obvious thing —
curl -X POST "$ENDPOINT" -H 'Content-Type: application/json' --data "$body"
— dies before a single request leaves the machine. $body becomes an argv entry,
and 150 KB blows past the exec argument limit. The error names curl, so you spend
twenty minutes suspecting curl.
Write the body to a file and hand curl the file:
curl -X POST "$ENDPOINT" \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary "@body.json"
Trap 2: a split glob that silently double-submits
split -d names its output chunk.00, chunk.01, … So you loop:
split -l 2000 -d urls.txt chunk.
for c in chunk.[0-9]*; do # <-- wrong
jq ... < "$c" > "$c.json"
curl ... --data-binary "@$c.json"
done
The loop writes chunk.00.json — which also matches chunk.[0-9]*. Depending
on glob expansion timing you submit some batches twice, and there is no error,
because double-submitting is perfectly valid. You just quietly burn your rate
limit and wonder why the counts don’t line up.
Glob exactly the digits split produces:
for c in chunk.[0-9][0-9]; do
Trap 3, the expensive one: an unserved key file
If the key file isn’t reachable, every batch returns 403. A 403 from IndexNow means “key invalid” — and a key that is perfectly valid but was never deployed produces the identical response. I regenerated the key twice before checking whether the file was actually being served.
Two things to get right:
- The file must contain the key and nothing else. A trailing newline makes it
33 bytes instead of 32. Write it with
printf '%s', notecho. - Verify it over HTTP before submitting anything, and refuse to proceed if it doesn’t match.
served="$(curl -fsS "$ORIGIN/$KEY.txt" || true)"
[ "$served" = "$KEY" ] || {
echo "key file not served at $ORIGIN/$KEY.txt (got: '${served:0:40}')" >&2
exit 1
}
That check turns a confusing afternoon into one clear line of output.
The script
#!/usr/bin/env bash
set -euo pipefail
HOST="yoursite.com"
ORIGIN="https://$HOST"
BATCH=2000 # protocol allows 10000; smaller bodies, clear of the rate limiter
# Derive the key from whatever <hex>.txt is in public/ — rotating the key is
# then "drop in a new file, delete the old one" with no code change.
KEY="$(basename "$(ls public/[0-9a-f]*.txt | head -1)" .txt)"
served="$(curl -fsS "$ORIGIN/$KEY.txt" || true)"
[ "$served" = "$KEY" ] || { echo "key file not served" >&2; exit 1; }
urls="$(mktemp)"; trap 'rm -f "$urls" "$urls".*' EXIT
curl -fsS "$ORIGIN/sitemap.xml" \
| grep -o '<loc>[^<]*</loc>' | sed 's|</\?loc>||g' > "$urls"
split -l "$BATCH" -d "$urls" "$urls".
for chunk in "$urls".[0-9][0-9]; do
jq -Rn --arg host "$HOST" --arg key "$KEY" \
'{host: $host, key: $key, urlList: [inputs]}' < "$chunk" > "$chunk.json"
code="$(curl -s -o /dev/null -w '%{http_code}' \
-X POST "https://api.indexnow.org/indexnow" \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary "@$chunk.json")"
printf '%5d urls HTTP %s\n' "$(wc -l < "$chunk")" "$code"
sleep 3
done
jq -Rn '... [inputs]' is the neat bit: -R reads raw lines, -n suppresses the
default input, and [inputs] slurps every line into an array — so the URL list is
built without any shell quoting of 2,000 strings.
Two design choices worth stealing
Don’t wire this into your deploy script. Mine reads the already-published sitemap over HTTP rather than the local build output. That means it runs independently of a deploy and cannot break one — and your deploy script is load-bearing in a way a submission script is not. Run it after a deploy, from cron, or by hand.
Derive the key from the filesystem, don’t hardcode it. ls public/[0-9a-f]*.txt
means rotating the key is dropping in a new file and deleting the old one. A
hardcoded key is a code change plus a deploy plus a chance to get them out of order.
If you’re on Cloudflare, check before you build this
Cloudflare has a “Crawler Hints” toggle that does IndexNow for you. It is free and
it is one click, so try it first — but measure it, because it derives its
signal from cache status: it reports on MISS. If your site deliberately doesn’t
edge-cache HTML (mine doesn’t — caching it would mean adding a purge step to the
deploy, or a deploy leaves the old release live worldwide), there may be nothing
for it to report. Check the IndexNow submitted-URL count in Bing Webmaster Tools
after a deploy. If it’s near zero, Crawler Hints isn’t working for you, and the
script above is the answer.
What to actually expect
IndexNow is a discovery push, not an indexing guarantee. It tells participating engines a URL exists. Whether they crawl it, and whether they index what they crawl, is entirely their call.
It is also worth being honest about who’s on the other end. Bing matters — it also feeds DuckDuckGo, Yahoo and Ecosia. Yandex matters if you have Russian-language traffic. Naver and Seznam both openly deprioritise foreign-language content, so if your site is English and you aren’t targeting Korea or Czechia, expect roughly nothing from them despite the free submission.
And Google, the one you actually care about, isn’t in the list at all.
From the deploy pipeline behind navyduck.com — 22,818 static pages of certification practice questions.