Skip to content

feat(seo): IndexNow bulk submission (pnpm indexnow) + per-host key files#782

Merged
choraria merged 5 commits into
mainfrom
worktree-indexnow-bulk-submit
Jul 23, 2026
Merged

feat(seo): IndexNow bulk submission (pnpm indexnow) + per-host key files#782
choraria merged 5 commits into
mainfrom
worktree-indexnow-bulk-submit

Conversation

@choraria

Copy link
Copy Markdown
Contributor

Why

Founder authorised a one-time bulk IndexNow push (previously held pending explicit approval, per the SEO-indexation lane brief).

Scope, stated plainly: IndexNow reaches Bing, Yandex, Seznam, Naver, Yep — not Google. Google tested it in 2021 and never shipped support, so nothing here moves Google indexation. The reason to run it is Bing, where we currently have ~2 URLs indexed.

Two key files, not one

indexnow.org's FAQ is explicit: "Each subdomain is treated as a separate host, which means you must create and manage individual key files for each one." A key on the apex does not cover subdomains — so www.webhook.co and docs.webhook.co each get their own copy at their own root.

The key is public by design (it is served at a URL) and carries no privilege beyond "may submit URLs for this host". That is why it is committed rather than kept in a secret store.

Guards, each tested

  • submitBatch verifies the key file is live and serves the key before it will POST. An ok response whose body is not the key (a soft-404 HTML page) counts as failure — so a host that 200s everything cannot look verified.
  • Every URL must be on the submitted host. IndexNow answers a mismatch with a bare 422; we fail locally and name the offender.
  • Host must be webhook.co or a subdomain, refused before any network call, with lookalikes (evilwebhook.co, webhook.co.evil.com) covered.
  • Batching at the protocol's 10,000-URL ceiling.

apps/docs is deliberately an experiment

Mintlify documents .txt as Enterprise-only, so the docs key file may 404 after deploy. verifyKeyLive turns that into a clean refusal rather than a wasted submission — and it converts an unknown into a measured fact rather than an assumption. If it 404s, the file gets removed and docs goes to Bing via Webmaster Tools' GSC import instead (no key file needed).

Test plan

  • 16 new tests; full lint gate green (630 script tests).
  • Verified the key file lands in the www static export at the root with exact content (out/<key>.txt, 36 bytes).
  • Post-merge: confirm deploy-www ran, verify both key URLs live, then run pnpm indexnow <host> and report the HTTP result per host.

No human UI verification needed — a CLI script and two static text files, no rendered surface.

Founder authorised a one-time bulk IndexNow push. This adds the key
files and the submitter.

Scope, stated plainly: IndexNow reaches Bing, Yandex, Seznam, Naver and
Yep. NOT Google -- Google tested it in 2021 and never shipped support, so
nothing here moves Google indexation. The reason to run it is Bing, where
we are close to absent.

Two key files, not one: indexnow.org's FAQ is explicit that each
subdomain is a separate host needing its own key file at its own root --
a key on the apex does not cover subdomains. So www.webhook.co and
docs.webhook.co each get a copy.

The key is PUBLIC BY DESIGN (it is served at a URL) and carries no
privilege beyond "may submit URLs for this host", which is why it is
committed rather than kept in a secret store.

Guards, each tested:
- submitBatch verifies the key file is LIVE and serves the key before it
  will POST -- an `ok` response whose body is not the key (a soft-404
  HTML page) counts as failure, so a host that 200s everything cannot
  look verified.
- every URL must be on the submitted host (IndexNow answers a mismatch
  with a bare 422; we fail locally, naming the offender).
- host must be webhook.co or a subdomain, refused before any network
  call, with lookalikes (evilwebhook.co, webhook.co.evil.com) covered.
- batching at the protocol's 10,000-URL ceiling.

apps/docs is an EXPERIMENT: Mintlify documents .txt as Enterprise-only,
so the docs key file may 404. verifyKeyLive turns that into a clean
refusal rather than a wasted submission, and the answer is then measured
rather than assumed.

16 new tests; full lint gate green (630 script tests). Verified the key
file lands in the www static export at the root with exact content.
@mintlify

mintlify Bot commented Jul 23, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
webhook-co 🟢 Ready View Preview Jul 23, 2026, 1:44 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

choraria added 2 commits July 23, 2026 14:52
…two test gaps

gitleaks: generic-api-key fires on the name "KEY" plus entropy alone.
The value is PUBLISHED by design at https://<host>/<key>.txt -- the
protocol requires it to be world-readable so engines can verify domain
ownership. Suppressed inline (the repo's established pattern), with a
.gitleaksignore fingerprint for this branch's frozen history.

Two gaps ai-review flagged, both real:

1. parseSitemapLocs was untested -- it decides which URLs get submitted.
   Added cases for plain locs, whitespace, CDATA unwrapping, empty locs
   and a document with none.

2. Nothing kept the deployed key FILES in sync with INDEXNOW_KEY.
   Rotating the constant without renaming the files would make
   verifyKeyLive refuse every submission against a 404, and nothing in
   CI would notice -- the files are inert static assets. Added a guard
   asserting each host's file exists, is named for the key, and contains
   it; plus one asserting every submittable host has a key file declared.
   Verified the guard fails RED with a key file removed, then restored.

21 tests pass.
…KeyLive

Second ai-review round, all three findings legitimate gaps in a
security-adjacent validator:

- submitBatch's URL-mismatch guard was untested (only the sibling host
  guard was). Now asserts it refuses before any network call.
- Malformed / empty entries took the catch path unasserted.
- Relative and non-http(s) URLs were untested. `javascript:` is the
  interesting one: it PARSES, with an empty hostname, so it fails the
  host comparison rather than the try/catch -- worth pinning explicitly.

Also took the non-blocking finding: verifyKeyLive now calls
assertHostAllowed. It is exported and issues an outbound GET built from
`host`, so leaving it unguarded made it a request primitive that could be
pointed at an arbitrary origin. submitBatch already guarded; this closes
the direct-call path.

25 tests pass.
Comment thread scripts/indexnow-submit.test.mjs Fixed
CodeQL flagged /evil\.com/ in the off-host test as
js/regex/missing-regexp-anchor, HIGH: an unanchored regex tested against
URL-ish text can match anywhere, so arbitrary hosts may precede or follow
it. Substantively a false positive -- it reads an error message, it does
not sanitise a URL -- but a test should not model the shape of a broken
host check, and the anchored form is a stronger assertion anyway.

Now matches the message SHAPE with an anchored regex and the offending
URL with a literal String#includes. Verified the matcher discriminates:
false against an unrelated error, true against the real one.

25 tests pass.
Comment thread scripts/indexnow-submit.test.mjs Fixed
CodeQL moved from js/regex/missing-regexp-anchor to
js/incomplete-url-substring-sanitization on the replacement: it treats
ANY substring test against a URL literal as a host check, because a
substring can match anywhere in a URL with arbitrary hosts before or
after it.

Both forms were the wrong shape. Now the offender is asserted by EXACT
EQUALITY on the message's trailing URL, with the host kept as a bare
hostname so no URL literal is ever compared. Verified the assertion
discriminates: true for the real URL, false for a different one.

25 tests pass; no URL-literal substring or regex comparisons remain.
@choraria
choraria merged commit 74e921e into main Jul 23, 2026
30 checks passed
@choraria
choraria deleted the worktree-indexnow-bulk-submit branch July 23, 2026 14:26
choraria added a commit that referenced this pull request Jul 23, 2026
…the key

The docs key file shipped in #782 as a deliberate EXPERIMENT: Mintlify
documents .txt as Enterprise-only, so whether docs could participate was
unknown rather than assumed.

Now measured. Deployed, then fetched:
  https://www.webhook.co/<key>.txt   -> 200, serves the key
  https://docs.webhook.co/<key>.txt  -> 404 "Asset not found"

So docs cannot host an IndexNow key file, and the committed
apps/docs/<key>.txt was dead weight. Removed.

docs.webhook.co is now absent from SITEMAP_FOR_HOST and the test's
KEY_FILE_DIRS, with a comment recording WHY and saying not to re-add it,
so the next person does not repeat the experiment. The stale usage line
in the header is gone too -- a comment that outruns the code is worse
than none. Bing coverage for the docs host comes from Bing Webmaster
Tools' Search Console import instead, which needs no key file.

www is unaffected and already submitted: 36 URLs accepted, HTTP 202.

25 tests pass; verified the CLI refuses docs cleanly and www --dry-run
still resolves 36 URLs.
choraria added a commit that referenced this pull request Jul 23, 2026
TWO fixes. The first is urgent: main is currently RED on gitleaks at
74e921e (#782), and I caused it.

1. gitleaks. In #782 I put `// gitleaks:allow` on the lines ABOVE the
   INDEXNOW_KEY constant. That does nothing -- the directive must sit on
   the SAME LINE as the finding. #782 went green only because the
   .gitleaksignore fingerprint covered its BRANCH commit, and a
   squash-merge mints a NEW commit SHA, so the suppression evaporated the
   moment it landed and main went red. The file's own header warns that
   fingerprints are commit-bound; the squash case is the same trap.

   Fixed by moving the directive onto the line, and adding a fingerprint
   for the squash commit that is now frozen in main's history. Both are
   needed: the directive for any future commit that touches the line, the
   fingerprint for the one already written.

   Verified with the gitleaks CLI locally, reproducing RED on the old
   tree first and confirming clean after -- not by watching the check
   flip colour.

2. docs.webhook.co dropped from IndexNow. The key file shipped in #782 as
   a deliberate experiment, since Mintlify documents .txt as
   Enterprise-only. Measured after deploy:
     https://www.webhook.co/<key>.txt   -> 200, serves the key
     https://docs.webhook.co/<key>.txt  -> 404 "Asset not found"
   So docs cannot host a key file and apps/docs/<key>.txt was dead
   weight. Removed, and docs dropped from SITEMAP_FOR_HOST and the test's
   KEY_FILE_DIRS with a comment recording why, so nobody repeats it. Bing
   coverage for docs comes from Bing Webmaster Tools' GSC import instead.

www is unaffected and already submitted: 36 URLs, HTTP 202.
choraria added a commit that referenced this pull request Jul 23, 2026
TWO fixes. The first is urgent: main is currently RED on gitleaks at
74e921e (#782), and I caused it.

1. gitleaks. In #782 I put `// gitleaks:allow` on the lines ABOVE the
   INDEXNOW_KEY constant. That does nothing -- the directive must sit on
   the SAME LINE as the finding. #782 went green only because the
   .gitleaksignore fingerprint covered its BRANCH commit, and a
   squash-merge mints a NEW commit SHA, so the suppression evaporated the
   moment it landed and main went red. The file's own header warns that
   fingerprints are commit-bound; the squash case is the same trap.

   Fixed by moving the directive onto the line, and adding a fingerprint
   for the squash commit that is now frozen in main's history. Both are
   needed: the directive for any future commit that touches the line, the
   fingerprint for the one already written.

   Verified with the gitleaks CLI locally, reproducing RED on the old
   tree first and confirming clean after -- not by watching the check
   flip colour.

2. docs.webhook.co dropped from IndexNow. The key file shipped in #782 as
   a deliberate experiment, since Mintlify documents .txt as
   Enterprise-only. Measured after deploy:
     https://www.webhook.co/<key>.txt   -> 200, serves the key
     https://docs.webhook.co/<key>.txt  -> 404 "Asset not found"
   So docs cannot host a key file and apps/docs/<key>.txt was dead
   weight. Removed, and docs dropped from SITEMAP_FOR_HOST and the test's
   KEY_FILE_DIRS with a comment recording why, so nobody repeats it. Bing
   coverage for docs comes from Bing Webmaster Tools' GSC import instead.

www is unaffected and already submitted: 36 URLs, HTTP 202.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants