Skip to content

Sitemaps for sites in many languages, rebuilt only where they changed - #1

Merged
czj merged 4 commits into
mainfrom
initial-version
Nov 11, 2026
Merged

czj merged 4 commits into
mainfrom
initial-version

Conversation

@czj

@czj czj commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

A site of 30,000 pages in 24 languages has 720,000 URLs to list. Regenerating all of them on every run is time spent on pages that did not change, and writing the hreflang links by hand is easy to get wrong: each language of a page must list all the others and itself.

This is the first version of Mapitaine. A source class names the records of one kind of page, their URLs and their languages; a build writes the files, the index and a manifest, and the next build rewrites only the files whose records changed.

  • Sources in app/sitemaps with scope, url, locales, lastmod, images; the blocks have the app's route helpers, set to the configured host.
  • Hreflang by construction: one <url> per language, each listing every language of the page and x-default.
  • Stable files: each file holds a fixed range of ids, sized from the number of languages so it stays under 50,000 URLs and 50 MiB, and continues in a second file if it still outgrows one.
  • Incremental builds without callbacks: one grouped COUNT and MAX query per source, compared with the manifest of the last build, so update_all, imports and deletions are seen too.
  • Around it: local or Active Storage storage, IndexNow for the URLs that changed, mapitaine:check for duplicate, foreign or future-dated URLs (no HTTP request), build.mapitaine / shard.mapitaine notifications, and the website in docs/ (home page, guide, benchmarks) in the layout of the other gems.
Tests and benchmark

47 tests, 122 assertions, green on Rails 8.1 and 8.0 with Ruby 3.3.6; RuboCop clean; rake docs:highlight:check passes. The Railtie, the rake tasks and the Active Storage storage were also run in a minimal Rails 8.1 app. Not exercised: a real cloud bucket, a real IndexNow submission, and these workflows, which run here for the first time.

bundle exec ruby bench/build.rb (SQLite in memory, Ruby 3.3.6 without YJIT), 30,000 records in 24 languages:

Build Time Files written
Everything, 720,000 URLs 31.9 s 55 (17 MiB gzipped)
Nothing new 0.01 s 0
3 records changed 1.0 s 2
  • 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

…hanged

A source class names the records of one kind of page, their URLs and their
languages. A build writes one <url> per language with the hreflang links of
all of them, cuts each source into files by ranges of ids, and rewrites only
the ranges whose count or latest change moved since the manifest of the last
build. Changed URLs go to IndexNow.

30,000 records in 24 languages (720,000 URLs) build in 32 s, a build with
nothing new takes 10 ms, and three changed records rewrite 2 of 55 files in
1 s (bench/build.rb, SQLite in memory, Ruby 3.3.6 without YJIT).

Assisted-by: Claude Code
An app that already stores files with Active Storage had to give Mapitaine
its bucket and credentials a second time. Storage::ActiveStorage takes the
name of a service of config/storage.yml and uploads through it, without
creating blobs: a service of its own puts the sitemaps in another bucket.

Assisted-by: Claude Code
Storage::S3 did what an Active Storage service already does, with a second
copy of the bucket and credentials and an SDK to keep up with. It is
removed: the sitemaps reach S3, Google Cloud Storage or Azure through a
service of config/storage.yml.

Assisted-by: Claude Code
Mapitaine had a README and no site, unlike the other gems of the family.
docs/ holds one in their layout, served by GitHub Pages: what the gem does
on the home page, the guide, and the numbers of bench/build.rb. Its code
blocks are highlighted with Rouge, and CI fails when one is not.

Assisted-by: Claude Code
@czj
czj merged commit 8a4c373 into main Nov 11, 2026
@czj
czj deleted the initial-version branch November 11, 2026 20:40
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