indexmap 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +287 -35
  4. data/UPGRADE.md +361 -0
  5. data/docs/architecture.md +55 -0
  6. data/docs/deployment.md +223 -0
  7. data/docs/domain-terms.md +27 -0
  8. data/docs/dynamic-sitemaps.md +302 -0
  9. data/docs/search-engine-ping.md +368 -0
  10. data/docs/storage.md +94 -0
  11. data/lib/generators/indexmap/install_generator.rb +1 -1
  12. data/lib/generators/indexmap/templates/initializer.rb.tt +18 -18
  13. data/lib/indexmap/artifact.rb +9 -0
  14. data/lib/indexmap/configuration.rb +12 -1
  15. data/lib/indexmap/creator.rb +45 -42
  16. data/lib/indexmap/index_now_configuration.rb +3 -1
  17. data/lib/indexmap/output.rb +19 -5
  18. data/lib/indexmap/parser.rb +112 -52
  19. data/lib/indexmap/pinger/base.rb +6 -5
  20. data/lib/indexmap/pinger/google.rb +12 -8
  21. data/lib/indexmap/pinger/index_now.rb +97 -52
  22. data/lib/indexmap/publication.rb +89 -0
  23. data/lib/indexmap/robots.rb +67 -0
  24. data/lib/indexmap/robots_controller.rb +13 -0
  25. data/lib/indexmap/sitemaps_controller.rb +57 -0
  26. data/lib/indexmap/storage/active_storage.rb +132 -1
  27. data/lib/indexmap/storage/filesystem.rb +182 -11
  28. data/lib/indexmap/storage/memory.rb +1 -1
  29. data/lib/indexmap/task_runner.rb +21 -28
  30. data/lib/indexmap/validator/duplicates.rb +83 -0
  31. data/lib/indexmap/validator.rb +171 -59
  32. data/lib/indexmap/version.rb +1 -1
  33. data/lib/indexmap/writer.rb +89 -34
  34. data/lib/indexmap.rb +14 -1
  35. data/lib/tasks/indexmap_tasks.rake +21 -2
  36. data/test/indexmap/correctness_test.rb +153 -0
  37. data/test/indexmap/install_generator_test.rb +36 -4
  38. data/test/indexmap/parser_test.rb +77 -0
  39. data/test/indexmap/pinger/google_test.rb +22 -0
  40. data/test/indexmap/pinger/index_now_test.rb +168 -0
  41. data/test/indexmap/publication_test.rb +280 -0
  42. data/test/indexmap/robots_test.rb +73 -0
  43. data/test/indexmap/sitemaps_controller_test.rb +50 -0
  44. data/test/indexmap/storage_test.rb +27 -0
  45. data/test/indexmap/task_runner_test.rb +24 -0
  46. data/test/indexmap/validator_test.rb +14 -0
  47. data/test/indexmap/writer_test.rb +42 -0
  48. data/test/integration/queue_checks.rb +244 -0
  49. data/test/release_task_test.rb +153 -0
  50. metadata +22 -5
  51. data/lib/indexmap/storage/file.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3d31112daf5ee0b501338d564a4e7e9ef30487c88e65439a67543faac070d64
4
- data.tar.gz: 550713d5205e26bc9c42a4a37660d0c72e493898b47f7bf5dbb4ef92e2488e6b
3
+ metadata.gz: 1b4cefe268685e0e7558e8265a10c195369ae24eb117a31f9f880104db68a593
4
+ data.tar.gz: c10e968aff2cd046d825723656b29c1f05bde39ce956c55a34e2df49d6293b70
5
5
  SHA512:
6
- metadata.gz: 10da855e190aab04ccd1e3dcc4fe5e49125bd431eb9831c9ea0e983169f11c08d0ce1beb093bd16d8576d637b850fa3d8da74c013b65cd5982bab0452097d272
7
- data.tar.gz: 6e4c5cdd0a014d1e897ba94d79f22f1acb69f0eb6ff1ff4f23ef756fb1884b1ef27df96f5181b665bdefd9e80f4925b7fe985a1ddc51bdd91e1f9d9e47af98af
6
+ metadata.gz: 81179a3e30e5c4717e431aa6b4a27c7844bd4c3d57a9676d28e26603b9872a7972a889fc1474c2a4c627f4869b838c8d102b1de3069a061e321e7b6c90489c01
7
+ data.tar.gz: 0720dc59bb0adeee8529266aff3c01f69f766a6602eec4253e51918897e9c2123784a2ff8a357f87d1f4e5ade869f2da7f2c36cd6037857aa41e945d7879d2db
data/CHANGELOG.md CHANGED
@@ -5,6 +5,71 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.0] - 2026-11-04
9
+
10
+
11
+ ### Added
12
+
13
+ - validate and expose public crawler output
14
+
15
+ - validate and expose public crawler output ([#21](https://github.com/ethos-link/indexmap/pull/21))
16
+
17
+ - stream and atomically publish dynamic inventories
18
+
19
+ - stream and atomically publish dynamic inventories ([#23](https://github.com/ethos-link/indexmap/pull/23))
20
+
21
+
22
+
23
+
24
+ ### Changed
25
+
26
+ - reconcile writer branch with merged sitemap fixes
27
+
28
+ - synchronize publication branch with resolved writer base
29
+
30
+
31
+
32
+
33
+ ### Documentation
34
+
35
+ - explain background sitemap generation ([#19](https://github.com/ethos-link/indexmap/pull/19))
36
+
37
+ - explain public key setup in Rails ([#20](https://github.com/ethos-link/indexmap/pull/20))
38
+
39
+ - clarify usage limits and implementation status
40
+
41
+ - clarify retries are disabled by default
42
+
43
+ - document unreleased APIs and sitemap deployment choices
44
+
45
+
46
+
47
+
48
+ ### Fixed
49
+
50
+ - consume sitemap section entries once
51
+
52
+ - consume sitemap section entries once ([#22](https://github.com/ethos-link/indexmap/pull/22))
53
+
54
+ - separate review preparation from release tagging
55
+
56
+ - prepare and push releases from main
57
+
58
+ - install git-cliff before running release tests
59
+
60
+ - prepare and push releases from main ([#25](https://github.com/ethos-link/indexmap/pull/25))
61
+
62
+
63
+
64
+
65
+ ### fix
66
+
67
+ - harden publication and Rails task loading
68
+
69
+ - harden sitemap publication and Rails integration ([#24](https://github.com/ethos-link/indexmap/pull/24))
70
+
71
+
72
+
8
73
  ## [0.8.2] - 2026-07-22
9
74
 
10
75
 
data/README.md CHANGED
@@ -14,6 +14,16 @@ It is designed for Rails apps that want:
14
14
 
15
15
  By default, `indexmap` writes a sitemap index plus one or more child sitemap files. For simpler sites, it also supports `:single_file` mode, which writes a single `urlset` directly to `sitemap.xml`.
16
16
 
17
+ ## Release status
18
+
19
+ This branch documents **unreleased main**, including partitioned generation,
20
+ atomic publication, sitemap and robots controllers, and IndexNow reports/retries.
21
+ The latest version recorded in this repository is **0.8.2**. For that release,
22
+ use the [0.8.2 README](https://github.com/ethos-link/indexmap/blob/v0.8.2/README.md)
23
+ and [changelog](CHANGELOG.md). Installing from RubyGems does not provide the
24
+ unreleased features below. Use a source checkout for evaluation until the next
25
+ release, and read [UPGRADE.md](UPGRADE.md) before adopting it.
26
+
17
27
  ## Installation
18
28
 
19
29
  Add this line to your application's Gemfile:
@@ -44,6 +54,14 @@ directory prefix such as `sitemaps/`.
44
54
 
45
55
  ## Documentation
46
56
 
57
+ - [How Indexmap works](docs/architecture.md): pipeline diagram and entrypoints.
58
+ - [Domain terms](docs/domain-terms.md): sections, partitions, outputs, publications,
59
+ roots, and entry types.
60
+ - [Deployment guide](docs/deployment.md): development and production, static and
61
+ database-backed sources, static files or Rails delivery, and robots.txt.
62
+ - [Storage contract](docs/storage.md): legacy, streaming, atomic, and pruning APIs.
63
+ - [Dynamic sitemaps](docs/dynamic-sitemaps.md): streaming and partitions, atomic
64
+ publication, Rails delivery, retention, jobs, and host adoption.
47
65
  - [Search engine ping](docs/search-engine-ping.md): Google Search Console
48
66
  credentials, Google property configuration, and IndexNow key setup.
49
67
  - [UPGRADE.md](UPGRADE.md): host-app migration steps for public contract
@@ -64,10 +82,15 @@ sections = [
64
82
  )
65
83
  ]
66
84
 
67
- Indexmap::Writer.new(
68
- sections: sections,
69
- base_url: "https://example.com"
70
- ).write
85
+ Indexmap.configure do |config|
86
+ config.base_url = "https://example.com"
87
+ config.storage = Indexmap::Storage::Filesystem.new(
88
+ path: "public", public_url: config.base_url
89
+ )
90
+ config.sections = sections
91
+ end
92
+
93
+ Indexmap.create
71
94
  ```
72
95
 
73
96
  ## Rails Usage
@@ -79,7 +102,7 @@ bin/rails generate indexmap:install
79
102
  ```
80
103
 
81
104
  The generator creates `config/initializers/indexmap.rb` with an idempotent
82
- `IndexmapConfiguration.apply` loader and adds conditional `indexmap/tasks`
105
+ `Indexmap::HostConfiguration.apply` loader and adds conditional `indexmap/tasks`
83
106
  loading to the application's `Rakefile`. This preserves `bin/rails --tasks` and
84
107
  the `indexmap:*` tasks without loading Indexmap during normal Rails boot.
85
108
 
@@ -88,12 +111,10 @@ Customize the `Indexmap.configure` block inside the generated initializer:
88
111
  ```ruby
89
112
  Indexmap.configure do |config|
90
113
  config.base_url = -> { "https://example.com" }
91
- config.storage = -> do
92
- Indexmap::Storage::Filesystem.new(
93
- path: Rails.public_path,
94
- public_url: config.base_url
95
- )
96
- end
114
+ config.storage = Indexmap::Storage::Filesystem.new(
115
+ path: Rails.public_path,
116
+ public_url: config.base_url
117
+ )
97
118
  config.sections = -> do
98
119
  [
99
120
  Indexmap::Section.new(
@@ -111,7 +132,7 @@ Runtime callers and jobs must apply the generated configuration before using
111
132
  Indexmap:
112
133
 
113
134
  ```ruby
114
- IndexmapConfiguration.apply
135
+ Indexmap::HostConfiguration.apply
115
136
  Indexmap.create
116
137
  ```
117
138
 
@@ -122,14 +143,188 @@ Then run:
122
143
 
123
144
  ```bash
124
145
  bin/rails indexmap:sitemap:create
125
- bin/rails indexmap:sitemap:format
126
146
  bin/rails indexmap:sitemap:validate
127
147
  ```
128
148
 
129
- `indexmap:sitemap:create` is the main task. It builds sitemap files in memory,
130
- formats them, validates the result, then writes the final XML files to the
131
- configured storage. Existing sitemap files are left untouched if generation or
132
- validation fails.
149
+ Creation already validates. `indexmap:sitemap:format` optionally pretty-prints
150
+ legacy XML after publication; it rejects atomic outputs.
151
+
152
+ `indexmap:sitemap:create` is the main task. It stages partitioned XML on temporary
153
+ disk, validates the complete result, then publishes to configured storage.
154
+ Generation and validation failures leave published files untouched. Legacy
155
+ publication remains sequential; enable atomic mode for immutable children and
156
+ revision-checked root activation. See [dynamic sitemaps](docs/dynamic-sitemaps.md).
157
+
158
+ ### Background Generation
159
+
160
+ Keep the refresh job and its scheduling in the host application. Indexmap does
161
+ not select a queue adapter, run workers, or deduplicate jobs. Apply the generated
162
+ configuration inside the job. Use `Indexmap::TaskRunner.new.create` for the
163
+ rake task's complete lifecycle, including `after_create` callbacks and IndexNow
164
+ key-file handling; `Indexmap.create` only generates the selected output.
165
+
166
+ #### GoodJob
167
+
168
+ With GoodJob 4.22.1 or newer, use its public labelled concurrency rules:
169
+
170
+ ```ruby
171
+ class SitemapRefreshJob < ApplicationJob
172
+ include GoodJob::ActiveJobExtensions::Concurrency
173
+
174
+ good_job_concurrency_rule(
175
+ perform_limit: 1,
176
+ label: "sitemap-refresh",
177
+ apply_label: true
178
+ )
179
+
180
+ good_job_concurrency_rule(
181
+ total_limit: 1,
182
+ label: -> { "sitemap-refresh:#{arguments.first}" },
183
+ apply_label: true
184
+ )
185
+
186
+ def perform(_version)
187
+ Indexmap::HostConfiguration.apply
188
+ Indexmap::TaskRunner.new.create
189
+ end
190
+ end
191
+ ```
192
+
193
+ Enqueue with a release identifier: `SitemapRefreshJob.perform_later(version)`.
194
+ The version label limits unfinished work for that release; the shared label
195
+ limits generation across releases to one execution at a time. A newer release
196
+ can enqueue while an older refresh is unfinished. A repeat enqueue is allowed
197
+ after completion: these controls do not provide permanent once-per-release
198
+ tracking. Use `perform_later`; direct `perform_now` calls bypass GoodJob's limits.
199
+ See [GoodJob concurrency controls](https://github.com/bensheldon/good_job#concurrency-controls).
200
+
201
+ #### Solid Queue
202
+
203
+ With Solid Queue 1.7 or newer, serialize writes to the shared sitemap output:
204
+
205
+ ```ruby
206
+ class SitemapRefreshJob < ApplicationJob
207
+ limits_concurrency to: 1, key: "sitemap-refresh",
208
+ duration: 30.minutes, on_conflict: :block
209
+
210
+ def perform(_version)
211
+ Indexmap::HostConfiguration.apply
212
+ Indexmap::TaskRunner.new.create
213
+ end
214
+ end
215
+ ```
216
+
217
+ Choose a duration longer than the expected generation time. `:block` retains
218
+ every refresh and runs them serially; it does not suppress duplicate enqueues.
219
+ Using `on_conflict: :discard` coalesces conflicting requests, including requests
220
+ for newer releases when the key is shared. A version-based key such as
221
+ `->(version) { "sitemap-refresh:#{version}" }` scopes conflicts to that release,
222
+ but permits different releases to write concurrently. Select that tradeoff
223
+ explicitly; it is not equivalent to GoodJob's two rules above. Direct
224
+ `perform_now` calls bypass queue coordination.
225
+ See [Solid Queue concurrency controls](https://github.com/rails/solid_queue#concurrency-controls).
226
+
227
+ #### Triggering Refreshes
228
+
229
+ Enqueue from one release task, a recurring schedule, or application startup.
230
+ Rails' `after_initialize` hook can enqueue from a worker application without
231
+ depending on a backend's worker callbacks:
232
+
233
+ ```ruby
234
+ Rails.application.config.after_initialize do
235
+ if Rails.env.production? && ENV["PROCESS_ROLE"] == "worker"
236
+ SitemapRefreshJob.perform_later(ENV.fetch("RELEASE_VERSION")) do |job|
237
+ raise job.enqueue_error if job.enqueue_error
238
+ end
239
+ end
240
+ end
241
+ ```
242
+
243
+ `PROCESS_ROLE` and `RELEASE_VERSION` are application-defined environment values.
244
+ Configure them in the deployment, with the worker role flag limited to worker
245
+ processes. `after_initialize` runs for every Rails process, including commands;
246
+ it indicates application initialization, not worker readiness. Complete database
247
+ preparation before starting workers. Suppressed enqueues are expected; report
248
+ actual enqueue errors. Avoid scheduling the same refresh from both startup and
249
+ a release task.
250
+
251
+ Use storage shared by all serving containers, such as object storage through
252
+ the Active Storage adapter. A job container's local filesystem is not shared
253
+ with web containers unless the deployment mounts the same persistent storage.
254
+
255
+ #### Operating Practices And Current Limits
256
+
257
+ Generate database-backed content after database preparation, through one
258
+ host-owned refresh job. Load the existing configuration inside that job and
259
+ query eligible records when it runs. Pass release or output identifiers rather
260
+ than relations, URL inventories, or credentials. Keep queue selection, recurring
261
+ schedules, and any application authorization context in the host.
262
+
263
+ Use a shared concurrency key or label for jobs writing the same destination,
264
+ across releases. Prefer blocking or rescheduling conflicts to discarding a newer
265
+ refresh. Backend controls do not protect manual rake invocations, and Solid
266
+ Queue's concurrency duration can expire before an unusually long job finishes.
267
+ Avoid concurrent writers through all entrypoints in legacy mode: an interrupted
268
+ sequential publication can leave mixed generations. Opt-in atomic mode checks
269
+ root revisions under a storage lock and retains previous generations. See the
270
+ [publication and retention contract](docs/dynamic-sitemaps.md#atomic-publication-and-storage).
271
+
272
+ Keep generation and search-engine submission separate. A refresh retry should
273
+ not automatically resend successful IndexNow batches. Make `after_create`
274
+ callbacks safe to repeat: they run after publication, and a callback failure does
275
+ not roll published files back. Validate the deployed sitemap and preview IndexNow
276
+ selection before submitting. See [selection and attempt reports](docs/search-engine-ping.md#inspect-selection-and-actual-attempts).
277
+
278
+ Batch source queries with native Rails APIs and select the fields needed for
279
+ URLs and dates. Supply meaningful content dates; do not use the refresh time as
280
+ `lastmod`. Each sitemap must stay within 50,000 entries and 52,428,800 uncompressed
281
+ bytes. Index mode partitions automatically; single-file mode rejects an output
282
+ that needs splitting. Measure the complete refresh before adding parallel jobs or
283
+ caches. Supply lazy sources to benefit from incremental generation and validation;
284
+ app-owned arrays and explicit inventory-returning APIs still retain their data.
285
+
286
+ Generation uses the same synchronous entrypoints with GoodJob and Solid Queue;
287
+ the examples above describe their different concurrency behavior. Verify the
288
+ host's actual adapter, workers, shared storage, failure reporting, and intended
289
+ system context in integration tests. Gem tests alone do not establish those
290
+ deployment guarantees.
291
+
292
+ ### Dynamic Robots
293
+
294
+ Indexmap can serve `/robots.txt` from configuration without querying content or
295
+ reading sitemap storage. Crawling defaults to disabled. Inside the generated
296
+ `Indexmap.configure` block, configure the site's policy:
297
+
298
+ ```ruby
299
+ config.robots.indexable = -> { Rails.env.production? && ENV["PUBLIC_INDEXING"] == "1" }
300
+ config.robots.groups = [
301
+ {user_agent: "*", allow: ["/"], disallow: ["/admin/", "/search"]}
302
+ ]
303
+ ```
304
+
305
+ The sitemap URL defaults to `base_url` plus `index_filename`. Override it with
306
+ `config.robots.sitemaps = ["https://example.com/sitemap.xml"]` when needed.
307
+ Groups and sitemap lists also accept callables. Keep them configuration-only;
308
+ do not query models in these callables.
309
+
310
+ In the application's `config/routes.rb`, load and mount the endpoint explicitly:
311
+
312
+ ```ruby
313
+ require "indexmap/robots_controller"
314
+ Indexmap::HostConfiguration.apply
315
+
316
+ Rails.application.routes.draw do
317
+ get "/robots.txt", to: Indexmap::RobotsController.action(:show)
318
+ end
319
+ ```
320
+
321
+ If the configuration loader lives outside an initializer, require that file
322
+ before applying it. Remove `public/robots.txt` so it cannot shadow the route.
323
+ The controller supports GET, HEAD, and conditional requests, uses a one-hour
324
+ public cache, and does not inherit application authentication or browser gates.
325
+ Set `PUBLIC_INDEXING` only for the intended public deployment; production-mode
326
+ previews should remain disabled. Robots rules do not replace authorization or
327
+ page-level indexing directives.
133
328
 
134
329
  ### Default Index Mode
135
330
 
@@ -138,6 +333,24 @@ This is the default behavior. `indexmap` writes:
138
333
  - `sitemap.xml` as a sitemap index
139
334
  - one or more child sitemap files from `config.sections`
140
335
 
336
+ Section entries can be enumerables. Indexmap consumes each section once and
337
+ calculates its index `lastmod` in that same pass. For database sources, return a
338
+ fresh enumerable from the configured sections callable on every generation:
339
+
340
+ ```ruby
341
+ config.sections = -> do
342
+ entries = PublicPage.indexable.find_each.lazy.map do |page|
343
+ Indexmap::Entry.new(loc: page.canonical_url, lastmod: page.updated_at)
344
+ end
345
+ [Indexmap::Section.new(filename: "sitemap-pages.xml", entries: entries)]
346
+ end
347
+ ```
348
+
349
+ `PublicPage`, its eligibility scope, and canonical URL method are host-app
350
+ examples. Keep queries and routing decisions in the host. Generation stages
351
+ bounded partitions and validates incrementally, using temporary disk for global
352
+ duplicate checks. See [memory and disk limits](docs/dynamic-sitemaps.md#configure-generation).
353
+
141
354
  ### Single-File Mode
142
355
 
143
356
  For sites that only want one `sitemap.xml` file:
@@ -145,7 +358,7 @@ For sites that only want one `sitemap.xml` file:
145
358
  ```ruby
146
359
  Indexmap.configure do |config|
147
360
  config.base_url = -> { "https://example.com" }
148
- config.storage = -> { Indexmap::Storage::Filesystem.new(path: Rails.public_path, public_url: config.base_url) }
361
+ config.storage = Indexmap::Storage::Filesystem.new(path: Rails.public_path, public_url: config.base_url)
149
362
  config.format = :single_file
150
363
  config.entries = -> do
151
364
  [
@@ -168,7 +381,7 @@ outputs write through the same configured storage as the default output.
168
381
  ```ruby
169
382
  Indexmap.configure do |config|
170
383
  config.base_url = -> { "https://example.com" }
171
- config.storage = -> { Indexmap::Storage::Filesystem.new(path: Rails.root.join("storage/sitemaps"), public_url: config.base_url) }
384
+ config.storage = Indexmap::Storage::Filesystem.new(path: Rails.root.join("storage/sitemaps"), public_url: config.base_url)
172
385
  config.sections = -> { Sitemap.sections }
173
386
 
174
387
  config.output :insights_data do |output|
@@ -194,8 +407,10 @@ Indexmap.create(:insights_data)
194
407
  Named outputs inherit `base_url` and `format` from the main configuration unless
195
408
  you override them. Storage is configured once and shared by every output.
196
409
 
197
- `Indexmap.create` uses the same safe publish flow as the rake task: build,
198
- format, validate, and then write the final XML file or files to storage.
410
+ `Indexmap.create` stages XML, validates it, then publishes the selected output.
411
+ Only `Indexmap::TaskRunner#create` (also used by the create task) handles the
412
+ IndexNow key file and `after_create` callbacks. Formatting is a separate optional
413
+ operation for legacy outputs; atomic outputs must be regenerated.
199
414
 
200
415
  ### Storage
201
416
 
@@ -243,17 +458,26 @@ storage.delete(filename)
243
458
  storage.public_url(filename)
244
459
  ```
245
460
 
461
+ Atomic publication, pruning, and streamed HTTP delivery require additional
462
+ capabilities. See the [storage contract](docs/storage.md) for signatures,
463
+ return values, and adapter requirements. Each operation instance resolves a
464
+ storage callable once and shares the adapter with its collaborators. Create a
465
+ fresh runner/pinger after changing destinations or tenant context. Direct reads
466
+ of `config.storage` still resolve the callable; construct an adapter once in the
467
+ host loader when its settings are fixed for the process.
468
+
246
469
  ### Deferred Dynamic Sections
247
470
 
248
471
  Use `after_create` when `indexmap:sitemap:create` should publish the default
249
472
  sitemap first, then schedule slower dynamic sections for the background. The
250
- callback runs only after the generated files have been formatted, validated, and
251
- replaced successfully.
473
+ callback runs only after the default output has been validated and published
474
+ and configured IndexNow key-file handling has succeeded. It also runs for
475
+ `Indexmap::TaskRunner#create`; `Indexmap.create` does not run callbacks.
252
476
 
253
477
  ```ruby
254
478
  Indexmap.configure do |config|
255
479
  config.base_url = -> { "https://example.com" }
256
- config.storage = -> { Indexmap::Storage::Filesystem.new(path: Rails.root.join("storage/sitemaps"), public_url: config.base_url) }
480
+ config.storage = Indexmap::Storage::Filesystem.new(path: Rails.root.join("storage/sitemaps"), public_url: config.base_url)
257
481
  config.sections = -> { Sitemap.sections }
258
482
 
259
483
  config.output :insights_data do |output|
@@ -273,13 +497,17 @@ Then the job can stay small:
273
497
  ```ruby
274
498
  class Insights::SitemapRefreshJob < ApplicationJob
275
499
  def perform
500
+ Indexmap::HostConfiguration.apply
276
501
  Indexmap.create(:insights_data)
277
502
  end
278
503
  end
279
504
  ```
280
505
 
281
- This keeps deploys fast: the deploy only waits for `indexmap:sitemap:create`,
282
- while database-dependent output is refreshed by the job backend.
506
+ The default output and named output are independent roots. List both public
507
+ root URLs in `config.robots.sitemaps` (or the static robots file) and submit each
508
+ separately if needed; named outputs are not automatically linked into the default
509
+ index. Publish the named output before advertising its URL. A deferred job can
510
+ fail after the default root is published, so monitor it separately.
283
511
 
284
512
  ## Validation And Parsing
285
513
 
@@ -312,6 +540,11 @@ The built-in validator checks for:
312
540
  See [Search engine ping](docs/search-engine-ping.md) for Google credential
313
541
  setup, Search Console property configuration, and IndexNow key provisioning.
314
542
 
543
+ For Rails apps with sitemaps outside `public/`, see
544
+ [serving an IndexNow key from `public/`](docs/search-engine-ping.md#rails-public-key-file-with-separate-sitemap-storage).
545
+ Key discovery searches the configured sitemap storage, so an existing public
546
+ key file needs explicit configuration in that setup.
547
+
315
548
  Available rake tasks:
316
549
 
317
550
  ```bash
@@ -328,6 +561,13 @@ present, `indexmap:index_now:ping` skips IndexNow submission.
328
561
 
329
562
  ## Development
330
563
 
564
+ The gem requires Ruby 3.2+ and Active Support/railties 7.1+. CI exercises the
565
+ Rails 7.1 floor on Ruby 3.2 and Rails 8.1.4 integration on Ruby 4.0, alongside the
566
+ normal Ruby matrix. See [integration checks](docs/dynamic-sitemaps.md#integration-checks-from-a-source-checkout).
567
+
568
+ Install [git-cliff](https://git-cliff.org/) before running tests; the release
569
+ tests use it to generate changelogs in temporary repositories.
570
+
331
571
  Run tests:
332
572
 
333
573
  ```bash
@@ -374,9 +614,9 @@ Install [git-cliff](https://git-cliff.org/) locally before preparing a
374
614
  release. The release task prepends the next `CHANGELOG.md` section from
375
615
  Conventional Commits.
376
616
 
377
- Before preparing a release, make sure you are on `main` or `master` with a
378
- clean worktree. If the release contains a breaking public-contract change,
379
- update `UPGRADE.md` with the host-app migration steps first.
617
+ Before preparing a release, switch to updated `main` with a clean worktree
618
+ matching `origin/main`. If the release changes a public contract, merge the
619
+ `UPGRADE.md` migration steps before preparing the release.
380
620
 
381
621
  Then run one of:
382
622
 
@@ -384,15 +624,27 @@ Then run one of:
384
624
  bundle exec rake 'release:prepare[patch]'
385
625
  bundle exec rake 'release:prepare[minor]'
386
626
  bundle exec rake 'release:prepare[major]'
387
- bundle exec rake 'release:prepare[0.1.0]'
627
+ bundle exec rake 'release:prepare[0.9.0]'
388
628
  ```
389
629
 
390
- The task will:
630
+ The task prepends the changelog and updates the version, preserving existing
631
+ history. It commits those two files on `main` with a Conventional Commit and
632
+ `What:`, `Why:`, and `Where:` in the body, then pushes `main`. Review any
633
+ release-status notices before running it. Hooks remain enabled; the branch
634
+ protection hook permits only the task's version and changelog commit on `main`.
635
+ Ordinary changes still require a topic branch. Remote branch protections must
636
+ permit the maintainer to push this release commit.
637
+
638
+ After preparation succeeds, create and push the annotated release tag:
639
+
640
+ ```bash
641
+ bundle exec rake release:tag
642
+ ```
391
643
 
392
- 1. Prepend the next `CHANGELOG.md` section with `git-cliff`.
393
- 1. Update `lib/indexmap/version.rb`.
394
- 1. Commit the release changes.
395
- 1. Create and push the `vX.Y.Z` tag.
644
+ Tagging requires a clean `main` matching `origin/main`, an unused version tag,
645
+ and a matching changelog section. Gem metadata links documentation to that
646
+ version's tag. If a push fails, resolve the failure and push the existing release
647
+ commit before tagging; do not rerun preparation with the same version.
396
648
 
397
649
  The `Release` workflow then runs tests, publishes the gem to RubyGems,
398
650
  and creates the GitHub release from the changelog entry.