Skip to content

Send IndexNow what it refused again, and pages that appear or go away - #4

Merged
czj merged 10 commits into
mainfrom
index-now-that-retries
Nov 12, 2026
Merged

czj merged 10 commits into
mainfrom
index-now-that-retries

Conversation

@czj

@czj czj commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

A refusal (429, 403) or an unreachable endpoint lost its URLs for good, because the next build compared lastmods with its own start. Pages that went away were never sent, nor new pages whose lastmod predates the last build, nor pages dated by a day that was still today somewhere. One URL on another host made the endpoint refuse its whole batch, and with the sitemaps on a CDN the key went to the CDN, where IndexNow does not read it.

The manifest now keeps the URLs IndexNow has yet to take, each rewritten file is compared with the one it replaces, and the key is published only where IndexNow reads it.

  • Nothing lost: the manifest is written with the URLs still to send, then again once IndexNow answered; refused or unreachable batches warn and go out with the next build (up to 10,000 URLs kept).
  • Appeared or gone: the URLs of a rewritten or removed file are compared with the previous version, read from the storage before it is replaced; a new configuration or new ranges skip this and send only what the lastmods say.
  • Dates alone: a day counts as changed while it may still be the day of the last build somewhere (UTC-12); a month or a year alone does not.
  • Right host, right key: only URLs on config.host are sent; with sitemaps_url elsewhere, or a key location on another host, builds and mapitaine:check warn instead of uploading the key where IndexNow never looks.
  • ruby-head: the two tests that rewrite a manifest no longer go through Active Support 8.0's JSON encoder, the last ruby-head · Rails 8.0 failure on Catch more changes between builds, and date files by their last change #3.
Proof

73 runs, 202 assertions, green on Rails 8.1 and 8.0; RuboCop clean. The new tests fail on main: a 429 reported and sent again with the next build, a removed and an added page with old lastmods submitted, a date of the day of the last build, a foreign host filtered out, a key the site does not serve reported by builds and by the check, one reply per batch, a build dying while submitting, a new configuration and new ranges sending only edited pages, and a year-only lastmod.

The ruby-head · Rails 8.0 failure on #3 was ArgumentError: unknown keyword: quirks_mode at test/build_test.rb:166 and :202, from Hash#to_json; those lines now call JSON.generate.

  • Tests cover the change (a bug fix comes with a test that fails without it)
  • Output unchanged, or every difference is intended and listed above

Generated by Claude Code

A URL built by hand could put a space, an accent or a control character
in a file, which made it unreadable. Rails locales such as pt_BR went out
as hreflang="pt_BR", which search engines ignore. A lastmod string was
written whatever it held, and the index took the greatest string rather
than the latest time. x_default was missed when the locales block returned
strings, and two languages sharing a URL listed it twice.

URLs are now percent-encoded outside RFC 3986's characters, locales are
written with a hyphen, lastmods are read as W3C dates and written in UTC,
and a page lists each of its URLs once. The check reports URLs over 2,048
characters, hreflang values that are not language codes, lastmods that are
not dates, and an index with no file or more than 50,000.

Assisted-by: Claude Code
An impossible date such as 2026-13-01 crashed the build, and 2026-02-30
was written out. A date alone counted from UTC midnight, so today's date
looked like the future east of UTC. A url block that forgot the locale no
longer warned once its URLs were listed once per page. A protocol-relative
URL got the host in front, and a % not followed by two hex digits stayed.

Dates are validated, a date alone starts at UTC+14, a page whose languages
share one URL is reported once per source, as is a lastmod that is not a
date, and the lastmod is read once per page. An empty site no longer fails
the check.

Assisted-by: Claude Code
A record deleted and another inserted with an older timestamp left a
shard's count and latest change alone, so its file kept the deleted page.
A new shard size kept files cut for the old ranges. update_columns, a
change in another table or a new url block went unseen until someone ran
a rebuild. In the index, a file that lost a record kept its date or went
back in time, and a build that died after uploading a new file left it
there for good.

The fingerprint adds the sum of the ids and the shard size. A file that
comes out the same is not uploaded and keeps its date; one that changed
without a later lastmod takes the build's time. A full build comes every
week by itself (config.rebuild_every). The names a build is about to add
are recorded in the manifest first, so the next build can remove them.

Assisted-by: Claude Code
…builds

SQLite's SUM overflows on ids near 2^63, so the fingerprint sums offsets
within each range instead. A lastmod from an older manifest is compared as
a time, not as text. A build that turned full because rebuild_every had
passed says so in the result and in the build.mapitaine payload.

Assisted-by: Claude Code
# Conflicts:
#	lib/mapitaine/builder.rb
On ruby-head, Active Support 8.0's encoder passes json an option it no
longer takes (quirks_mode), so the configuration digest and the IndexNow
body raised. Both hold plain values: JSON.generate writes the same bytes.

Assisted-by: Claude Code
A refusal (429, 403) or an unreachable endpoint lost the URLs for good: the
next build compared lastmods with its own start. Pages that went away were
never sent, nor new pages whose lastmod predates the last build, nor pages
dated by a day that was still today. One URL on another host made the
endpoint refuse its whole batch, and with sitemaps on a CDN the key went to
the CDN, where IndexNow does not read it.

URLs the endpoint did not take stay in the manifest and go out with the
next build, with a warning. For each rewritten file, the URLs of the file
it replaces are compared with the new ones; removed files count too. A
date alone counts as changed while it may still be the day of the last
build. Only URLs on the host are sent, and the key is published only where
IndexNow reads it, with a warning otherwise.

Assisted-by: Claude Code
Submitting before the manifest meant an unexpected error or a slow
endpoint held back the manifest after files were already published. The
manifest is now written with the URLs still to send, then again once
IndexNow answered, so a build that dies while submitting loses none.

A new configuration or new ranges no longer read every previous file to
send the whole site: only the lastmods count then. URLs are compared right
after each shard renders, so no list of every URL waits for the others,
a month or a year alone no longer counts as a change, the check reports a
key the site does not serve, and Storage::Local reads files as bytes.

Assisted-by: Claude Code
# Conflicts:
#	AGENTS.md
#	lib/mapitaine/builder.rb
#	lib/mapitaine/index_now.rb
#	lib/mapitaine/manifest.rb
The ruby-head job still failed on the two tests that rewrite a manifest
with Hash#to_json, which Active Support 8.0 routes through its encoder.

Assisted-by: Claude Code
@czj
czj enabled auto-merge (squash) November 12, 2026 08:04
@czj
czj merged commit 9f0cc47 into main Nov 12, 2026
11 checks passed
@czj
czj deleted the index-now-that-retries branch November 12, 2026 08:04
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