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
data/UPGRADE.md ADDED
@@ -0,0 +1,361 @@
1
+ # Upgrade Guide
2
+
3
+ Use this guide when upgrading an application that already generates, validates,
4
+ or pings sitemaps with `indexmap`.
5
+
6
+ ## Unreleased
7
+
8
+ ### Host loader and rendered artifacts
9
+
10
+ The install generator now defines `Indexmap::HostConfiguration.apply` instead of
11
+ `IndexmapConfiguration.apply`. When adopting the new template, move the existing
12
+ loader into `module Indexmap; module HostConfiguration; ...; end; end` and update
13
+ its bottom-of-file apply call, route mounts, jobs, scripts, and tests. Keep the
14
+ existing configure block, mutex, and idempotent `@applied` guard. Inside the new
15
+ namespace, qualify host constants that overlap gem names (for example `::Entry`
16
+ for a host model, versus `Indexmap::Entry` for a sitemap value). Use:
17
+
18
+ ```ruby
19
+ Indexmap::HostConfiguration.apply if defined?(Indexmap::Configuration)
20
+ ```
21
+
22
+ An existing host-owned loader can continue running while you migrate: the gem
23
+ never calls it by name. Do not mix old and new loader modules, as each has its own
24
+ application guard. Apply the host configuration before creating an operation.
25
+
26
+ The rendered file value is now `Indexmap::Artifact`, replacing
27
+ `Indexmap::Storage::File`. Its `filename`, `body`, `content_type`, and `basename`
28
+ interface is unchanged. Update custom adapters, type checks, and direct
29
+ constructor calls; replace internal `require "indexmap/storage/file"` with
30
+ `require "indexmap"`. The old constant and require path are removed. `::File`
31
+ always means Ruby's filesystem class.
32
+
33
+ ### Storage resolution and publication lifecycle
34
+
35
+ `Creator`, `TaskRunner`, `Validator`, and pinger instances now resolve a storage
36
+ callable lazily once and retain that adapter. Nested generation, validation,
37
+ IndexNow key handling, and submission share it. Create a fresh operation instance
38
+ after changing storage configuration or tenant/context; do not reuse a runner or
39
+ pinger across different destinations. `Indexmap.create` already creates a new
40
+ creator each time. The optional `storage:` constructor keyword selects an explicit
41
+ adapter. Direct reads of `config.storage` still resolve the callable each time.
42
+
43
+ Filesystem publication now copies validated files and the previous root into
44
+ private `.indexmap-builds/` staging on the destination filesystem, outside the
45
+ per-root lock. Short locked sections create the staging lease and activate files
46
+ with same-filesystem renames. Keep that private directory writable and exclude
47
+ hidden paths from static HTTP serving. Allow disk space for source staging,
48
+ destination staging, and retained generations. Do not remove active staging
49
+ directories manually. Pruning reclaims abandoned staging after its grace period
50
+ and skips directories whose build lock is held; returned deleted paths can now
51
+ include these private directories. Normal failures remove staging immediately.
52
+
53
+ Filesystem listing now scopes traversal by the literal prefix's directory and
54
+ leading filename fragment. It excludes symlinks and hidden paths, including
55
+ private staging, and requires normalized relative prefixes (no traversal,
56
+ absolute paths, or redundant separators). Replace any reliance on symlink entries with actual keys under the storage root. A listing without a
57
+ prefix still intentionally scans the complete visible tree.
58
+
59
+ Atomic Active Storage checks the expected revision before uploading and again
60
+ before each upload. A failed or superseded build destroys its newly created child
61
+ records and purges its blobs immediately, including a partial upload whose blob
62
+ was saved before the service failed. Uploads and cleanup stay outside the root
63
+ row lock. A committed root is preserved even if a host `after_commit` callback
64
+ raises. Activation also checks that children still exist, so pruning an unusually
65
+ old in-flight build aborts it rather than publishing missing children.
66
+
67
+ Concurrent builds can still perform some speculative uploads before one wins;
68
+ optimistic publication cannot avoid work already in flight. Process death and
69
+ storage outages can also prevent immediate cleanup. Keep scheduled pruning,
70
+ unattached-blob cleanup, and working attachment purge jobs. A
71
+ `Indexmap::PublicationCleanupError` reports cleanup failures through `failures`
72
+ (each contains `artifact` and `error`), with the original publication exception as
73
+ its cause. Retain those private diagnostics; a failed service deletion can require
74
+ cleanup by object key even when Active Storage already removed the blob row.
75
+
76
+ ### Maintainer release workflow
77
+
78
+ `release:prepare[...]` now runs from clean, updated `main` matching
79
+ `origin/main`. It updates the version and changelog, creates a Conventional
80
+ Commit with hooks enabled, and pushes `main`. Replace scripts that prepare on a
81
+ topic branch or separately commit those files. The branch hook permits only a
82
+ release-task commit containing the bumped version and matching changelog; other
83
+ changes still require a topic branch. Ensure remote branch protections permit
84
+ the maintainer to push this release commit. Then run `bundle exec rake release:tag`
85
+ to create and push the tag. If pushing preparation fails, push the existing
86
+ release commit before tagging rather than preparing the same version again.
87
+ See the [release instructions](README.md#release).
88
+
89
+ ### Task loading, parsing, and documentation
90
+
91
+ For an existing lazy installation, edit the generated Rakefile loader to check
92
+ Rake's collected tasks as well as `ARGV` (Rails removes the command from `ARGV`
93
+ before loading the Rakefile):
94
+
95
+ ```ruby
96
+ indexmap_tasks_requested = Rake.application.options.show_tasks ||
97
+ ARGV.any? { |argument| ["-T", "--tasks"].include?(argument) }
98
+ require "indexmap/tasks" if indexmap_tasks_requested ||
99
+ (Rake.application.top_level_tasks + ARGV).any? { |argument| argument.start_with?("indexmap:") }
100
+ ```
101
+
102
+ Keep this block before `require_relative "config/application"`. The installer
103
+ preserves existing task loaders, so rerunning it does not replace a stale block.
104
+ Verify `bin/rails indexmap:sitemap:create`, `bin/rails --tasks`, and
105
+ `bundle exec rake indexmap:sitemap:validate`. The generated initializer now checks
106
+ `defined?(Indexmap::Configuration)` before applying settings and constructs the
107
+ filesystem adapter once inside `apply`. Apply those edits to existing initializers
108
+ if needed, together with the host-loader rename above.
109
+
110
+ `Parser` remote children and redirects must now keep the explicit source URL's
111
+ scheme, host, and port; userinfo is rejected. Cross-origin references raise
112
+ `Indexmap::ValidationError` before their target is requested. To inspect a deployed
113
+ sitemap through a local origin, use `rebase_remote_children: true` deliberately;
114
+ this rebases child references, not redirects. Pass only trusted remote root URLs:
115
+ the parser is not an SSRF filter for arbitrary user-supplied roots or DNS results.
116
+ Remote responses and local files are capped at 52,428,800 bytes per document, and
117
+ DTDs are rejected. Streaming XML parsing can raise `Nokogiri::XML::SyntaxError`
118
+ for malformed XML that the older DOM parser recovered silently. Fix malformed or
119
+ oversized/DTD-bearing documents and cross-origin references before upgrading.
120
+ Storage-root parsing resolves children through storage and does not fetch remote
121
+ child URLs.
122
+
123
+ Use `Parser#each_entry` for incremental enumeration without the `entries` cache;
124
+ `entries` and `paths` still collect results. Google URL counting now streams XML
125
+ and uses temporary disk for exact deduplication. Provision temporary space for
126
+ that operation as well as generation/validation.
127
+
128
+ Create stages, validates, and publishes; it does not pretty-print. Only
129
+ `TaskRunner#create` and its rake task run callbacks and IndexNow key handling.
130
+ The optional format task remains for legacy output. See the
131
+ [deployment guide](docs/deployment.md) for static/dynamic sources and delivery,
132
+ robots policy, and development/production checks. Main documentation describes
133
+ unreleased features; use tagged documentation while consuming 0.8.2.
134
+
135
+ ### Generation, publication, and delivery
136
+
137
+ Generation now stages XML on temporary disk and partitions index sections by
138
+ `max_urls` and `max_bytes`. Defaults are the sitemap protocol limits. The first
139
+ legacy partition keeps its configured name; later files append `-002.xml`, etc.
140
+ Single-file mode still writes one file and fails if splitting would be required.
141
+ Locations must be shorter than 2,048 characters; repeated `loc`/`lastmod` fields
142
+ are rejected. Provision temporary disk for XML, validation spools, and duplicate
143
+ sorting. The full-document Nokogiri formatting pass is removed; XML meaning is preserved but
144
+ serialized bytes/checksums can change. Generation reports no longer download
145
+ published artifacts to count URLs.
146
+
147
+ Set `config.atomic = true` to opt into immutable generation children and guarded
148
+ root activation. Filesystem and Active Storage implement the required revision
149
+ and publish capabilities. Custom six-method adapters retain legacy behavior;
150
+ unsupported atomic mode fails explicitly. Generated child keys use a reserved
151
+ `sitemap-<owner>-<generation>-<section>-<part>.xml` namespace and cannot be
152
+ rewritten by built-in adapters. Update fixed filename route constraints, remove
153
+ static files that shadow new routes, and replace app cleanup callbacks that
154
+ recompute configured filenames. They would delete generated children.
155
+
156
+ Atomic Active Storage requires an unconditional unique filename index. Inspect
157
+ existing data/schema and use a generated host migration if needed. The host
158
+ artifact model remains app-owned. Retention uses archive record timestamps and
159
+ the model's normal attachment cleanup, which requires working purge-job workers.
160
+ Filesystem atomic mode requires shared lock/rename semantics; a worker-local
161
+ container directory is insufficient for multiple serving instances.
162
+
163
+ `Indexmap::SupersededPublication` means another build activated the root after
164
+ this build began. Retry by rebuilding; do not fall back to sequential writes.
165
+ Use native bounded Active Job retries in host jobs for this transient condition.
166
+ The same entrypoints support GoodJob and Solid Queue. The opt-in sitemap
167
+ controller serves configured roots and current/retained atomic children using
168
+ storage metadata and streaming reads. See [dynamic sitemap usage](docs/dynamic-sitemaps.md)
169
+ for routes, cache policy, adapter capabilities, and adoption steps.
170
+
171
+ Schedule `indexmap:sitemap:prune` separately. `RETENTION_HOURS` defaults to 48 and
172
+ must be at least 1; choose a grace period covering cached roots and rollbacks.
173
+ The format task refuses to mutate atomic publications; regenerate instead.
174
+
175
+ IndexNow selection now spools validated entries before sending, without retaining
176
+ all selected URLs. Optional bounded retries use `config.index_now.max_attempts`
177
+ (1..5, default 1) and `max_wait` (0..60 seconds, default 30). Attempt exports now
178
+ include attempt numbers. Long `Retry-After` values defer retries rather than
179
+ being shortened; this is not durable scheduling or exactly-once delivery.
180
+
181
+ Section entries are now consumed once per generation, including calculation of
182
+ the index's `lastmod`. Existing array sources keep the same XML meaning. Apps may
183
+ return fresh lazy enumerables from `config.sections`; do not reuse exhausted
184
+ one-shot sources between refreshes. Streaming generation and opt-in atomic
185
+ publication are described above.
186
+
187
+ Sitemap validation now rejects files larger than 52,428,800 uncompressed bytes,
188
+ URL sets and indexes with more than 50,000 entries, missing or incorrect sitemap
189
+ namespaces, DTDs, missing URL locations, cyclic indexes, unsafe child paths, and
190
+ child URLs outside the configured scheme, host, or port. Relative local child
191
+ references remain supported. Fix invalid existing files before upgrading.
192
+ Generation still validates before publication; sequential storage writes do not
193
+ provide atomic publication of the entire sitemap set.
194
+
195
+ IndexNow now validates the reachable sitemap before planning or sending. Duplicate
196
+ or otherwise invalid entries fail validation instead of being silently filtered.
197
+ Index sections partition automatically; use index format when a single-file
198
+ output would exceed its limits.
199
+
200
+ Date-only `lastmod` values now participate in IndexNow cutoff selection. Dates on
201
+ or after the cutoff's UTC calendar day are included, even when the cutoff has a
202
+ later time on that day. Missing dates remain included. Invalid `SINCE` still
203
+ raises and does not fall back to `INDEXNOW_RECENT_HOURS`. Batch sizes must be
204
+ integers from 1 through 10,000; malformed configuration is no longer truncated.
205
+
206
+ The new `indexmap:index_now:plan` task exports selection as JSON Lines without a
207
+ key or network submission. Existing dry runs still require a key. Set
208
+ `INDEXNOW_REPORT=1` on the ping task for JSON Lines containing attempt-started,
209
+ attempt-finished, and summary records. These are non-durable exports: save them
210
+ to private operator storage if needed, never public sitemap storage. HTTP 202
211
+ is reported as `validation_pending` in the attempt receipt. The summary retains
212
+ its existing `submitted` status for successful HTTP responses; it does not prove
213
+ indexing. Transport failures now appear in partial/failed results with reason
214
+ `unknown_delivery`, preserving earlier successful batches. Retries are opt-in as
215
+ described above; there is no durable delivery history.
216
+
217
+ Robots delivery is opt-in. Require `indexmap/robots_controller`, apply the host
218
+ configuration, and mount the controller explicitly as documented in the README.
219
+ Remove `public/robots.txt` when mounting it; static files can shadow Rails routes.
220
+ Crawling defaults to disabled. Configure an explicit predicate that also handles
221
+ staging running in production mode. The endpoint does not change host routes,
222
+ queue adapters, refresh jobs, or sitemap storage.
223
+
224
+ Search-engine ping tasks now treat `config.index_filename` as the authoritative
225
+ sitemap entrypoint. Google submits that sitemap once, and IndexNow recursively
226
+ reads the child sitemaps it references. Unreferenced `sitemap*.xml` files in the
227
+ configured storage are no longer submitted or included in IndexNow URL batches.
228
+
229
+ Before upgrading, ensure every current sitemap is reachable from the configured
230
+ root sitemap. If an application intentionally maintains an independent named
231
+ output, submit that output separately instead of relying on storage discovery.
232
+
233
+ ## Upgrading To 0.6.x
234
+
235
+ Version 0.6.x routes every sitemap operation through the configured storage
236
+ backend. Generation, formatting, validation, parsing, Google submission,
237
+ IndexNow submission, and IndexNow verification files should all read and write
238
+ through the same storage object.
239
+
240
+ ### 1. Configure Storage Explicitly
241
+
242
+ Set `config.storage` in the application initializer instead of relying on
243
+ implicit filesystem behavior.
244
+
245
+ For a Rails app that serves sitemap files from `public/`:
246
+
247
+ ```ruby
248
+ Indexmap.configure do |config|
249
+ config.base_url = -> { "https://example.com" }
250
+ config.storage = -> do
251
+ Indexmap::Storage::Filesystem.new(
252
+ path: Rails.public_path,
253
+ public_url: config.base_url
254
+ )
255
+ end
256
+ end
257
+ ```
258
+
259
+ For a production app that stores sitemap files elsewhere, point the storage
260
+ adapter at that production location and keep `public_url` on the public origin
261
+ where search engines fetch the files.
262
+
263
+ ```ruby
264
+ Indexmap.configure do |config|
265
+ config.base_url = -> { "https://example.com" }
266
+ config.storage = -> do
267
+ Indexmap::Storage::Filesystem.new(
268
+ path: Rails.root.join("storage/sitemaps"),
269
+ public_url: config.base_url
270
+ )
271
+ end
272
+ end
273
+ ```
274
+
275
+ ### 2. Keep Relative Storage Keys Stable
276
+
277
+ If your app stores sitemap files under a directory, keep those directory
278
+ prefixes in `config.index_filename`, section filenames, and named output
279
+ filenames.
280
+
281
+ ```ruby
282
+ Indexmap.configure do |config|
283
+ config.index_filename = "sitemaps/sitemap.xml"
284
+ config.sections = -> do
285
+ [
286
+ Indexmap::Section.new(
287
+ filename: "sitemaps/sitemap-pages.xml",
288
+ entries: Sitemap.pages
289
+ )
290
+ ]
291
+ end
292
+ end
293
+ ```
294
+
295
+ Do not work around older versions by stripping paths down to basenames. The
296
+ parser, validator, pingers, and filesystem adapter now preserve relative storage
297
+ keys such as `sitemaps/sitemap.xml`.
298
+
299
+ ### 3. Update Custom Storage Backends
300
+
301
+ Custom storage backends must accept the documented `content_type:` keyword on
302
+ `write`. The format task rewrites XML files and passes the XML content type.
303
+
304
+ ```ruby
305
+ class SitemapStorage
306
+ def write(filename, body, content_type:)
307
+ # Persist body under filename and store content_type when the backend
308
+ # uses it.
309
+ end
310
+
311
+ def read(filename)
312
+ end
313
+
314
+ def exist?(filename)
315
+ end
316
+
317
+ def list(prefix:, suffix:)
318
+ end
319
+
320
+ def delete(filename)
321
+ end
322
+
323
+ def public_url(filename)
324
+ end
325
+ end
326
+ ```
327
+
328
+ ### 4. Verify Development And Production
329
+
330
+ Run the same lifecycle tasks against the storage configuration each environment
331
+ will use.
332
+
333
+ Development:
334
+
335
+ ```bash
336
+ bin/rails indexmap:sitemap:create
337
+ bin/rails indexmap:sitemap:validate
338
+ bin/rails indexmap:sitemap:format
339
+ ```
340
+
341
+ Production or staging:
342
+
343
+ ```bash
344
+ RAILS_ENV=production bin/rails indexmap:sitemap:create
345
+ RAILS_ENV=production bin/rails indexmap:sitemap:validate
346
+ RAILS_ENV=production bin/rails indexmap:ping
347
+ ```
348
+
349
+ Before deploying, confirm:
350
+
351
+ - `config.base_url` is the public origin for the environment.
352
+ - `config.index_filename` identifies the public root sitemap and exists in the
353
+ configured storage.
354
+ - The root sitemap references every current child sitemap. Unreferenced sitemap
355
+ files in storage are intentionally ignored by the pingers.
356
+ - `config.storage.public_url(config.index_filename)` returns the fetchable root
357
+ sitemap URL.
358
+ - `indexmap:sitemap:validate` passes against the same storage used by
359
+ production.
360
+ - If IndexNow is enabled, the `<key>.txt` verification file is present in the
361
+ configured storage or `config.index_now.key` is set.
@@ -0,0 +1,55 @@
1
+ # How Indexmap works
2
+
3
+ > This describes unreleased main. See [release status](../README.md#release-status).
4
+
5
+ The host supplies eligible URLs and content dates. Indexmap generates, validates,
6
+ and publishes XML. A web server, CDN, or explicitly mounted Rails controller
7
+ serves those artifacts. Crawlers do not trigger generation or query section sources.
8
+
9
+ ```mermaid
10
+ flowchart TD
11
+ manifest["Host manifest: static entries or database enumeration"] --> writer
12
+ ruby["Indexmap.create(output)"] --> creator["Creator: selected output"]
13
+ task["Rake create task"] --> runner["TaskRunner#create: default output"]
14
+ job["Host refresh job"] --> runner
15
+ runner --> creator
16
+ creator --> writer["Writer: consume sources once and partition XML"]
17
+ writer --> staging["Temporary disk staging"]
18
+ staging --> validate["Validator: complete graph and disk-backed duplicate checks"]
19
+ validate --> mode{"Publication mode"}
20
+ mode -->|legacy| sequential["Write children, then stable root sequentially"]
21
+ mode -->|atomic| prepare["Prepare children outside the root lock"]
22
+ prepare --> atomic["Check revision and activate root under a short lock"]
23
+ sequential --> storage["Configured storage"]
24
+ atomic --> storage
25
+ creator -->|successful return to TaskRunner| hooks["IndexNow key handling, then after_create callbacks"]
26
+ storage --> static["Static server or CDN"]
27
+ storage --> controller["Opt-in Rails sitemap controller"]
28
+ static --> crawler["Crawler fetches published XML"]
29
+ controller --> crawler
30
+ config["Host robots policy"] --> robots["Static robots.txt or opt-in Rails robots controller"]
31
+ robots --> crawler
32
+ storage --> ping["Separate Google or IndexNow submission"]
33
+ ```
34
+
35
+ Generation and validation errors occur before publication. Legacy writes can
36
+ leave mixed files if publication itself fails. Atomic mode retains the previous
37
+ root until activation succeeds; a competing winner causes
38
+ `Indexmap::SupersededPublication`. Built-in adapters clean up ordinary failed
39
+ builds; retention covers artifacts left by crashes. Cleanup errors are reported
40
+ explicitly. Callbacks and key handling run after publication,
41
+ so their failures do not undo it. Neither generation entrypoint submits to search
42
+ engines automatically. A host can choose to submit from a callback, but separate
43
+ submission makes refresh retries easier to operate.
44
+
45
+ | Entrypoint | Purpose |
46
+ | --- | --- |
47
+ | `Indexmap.create(:name)` | Generate, validate, and publish one output; defaults to `:default`. |
48
+ | `Indexmap::TaskRunner.new.create` | Generate the default output, handle the IndexNow key, then invoke callbacks. |
49
+ | `indexmap:sitemap:create` | Run `TaskRunner#create` and print the writer's recorded counts. |
50
+ | `Writer#each_file` / `Writer#write` | Render XML files; callers own validation and publication. `write` collects all files. |
51
+ | `indexmap:sitemap:format` | Optionally pretty-print legacy stored files; atomic outputs reject mutation. |
52
+ | Sitemap / robots controllers | Serve artifacts or configuration; never generate a sitemap. |
53
+
54
+ See [domain terms](domain-terms.md), [deployment choices](deployment.md), and the
55
+ [storage contract](storage.md).
@@ -0,0 +1,223 @@
1
+ # Choose a deployment path
2
+
3
+ > This guide describes unreleased main, not RubyGems 0.8.2.
4
+ > See [release status](../README.md#release-status) before installing.
5
+
6
+ Source data and HTTP delivery are separate decisions. A database-backed sitemap
7
+ can be served as a static file after a scheduled refresh. Static marketing URLs
8
+ can be served by a Rails controller. Neither choice generates XML on crawler
9
+ requests. Robots policy is independent of both.
10
+
11
+ | Situation | Generate | Store and serve | Robots |
12
+ | --- | --- | --- | --- |
13
+ | Static marketing site | Build or release step from an explicit manifest; no database required by the source | Include `public/` artifacts in the deployed image/site | Static file, or Rails endpoint if environment policy varies |
14
+ | Database content, one server | Refresh after database preparation and on a host schedule | Persistent filesystem shared by the refresh and web processes; static server or Rails | Static or dynamic policy |
15
+ | Database content, multiple containers | Host job on GoodJob/Solid Queue or a release task | Shared persistent filesystem with working locks, or Active Storage with shared backing storage | Dynamic policy is useful for production-mode previews |
16
+ | Development or staging | Run the same generation/validation lifecycle with test content | Local filesystem or isolated staging storage | Disallow crawling; do not submit to search engines |
17
+
18
+ See the [pipeline diagram](architecture.md) for the generation/publication boundary.
19
+
20
+ ## Install and configure
21
+
22
+ Follow the [Rails installation](../README.md#rails-usage), including
23
+ `require: false` and `bin/rails generate indexmap:install`. Edit the configure
24
+ block inside `Indexmap::HostConfiguration.apply`, keeping its idempotent wrapper.
25
+ Construct storage once there when its settings are fixed:
26
+
27
+ ```ruby
28
+ config.base_url = ENV.fetch("INDEXMAP_BASE_URL", "http://localhost:3000")
29
+ config.storage = Indexmap::Storage::Filesystem.new(
30
+ path: Rails.public_path, public_url: config.base_url
31
+ )
32
+ ```
33
+
34
+ Always set `INDEXMAP_BASE_URL` to the actual public origin in deployment. For
35
+ private filesystem storage change `path` to `Rails.root.join("storage/sitemaps")`
36
+ and explicitly serve it through the Rails controller or web server. That directory
37
+ does not become publicly reachable merely because it has a `public_url` setting.
38
+ Use separate storage namespaces for development, staging, and production.
39
+
40
+ ### Static sources
41
+
42
+ For a small, database-free manifest, add to the same configure block:
43
+
44
+ ```ruby
45
+ config.format = :single_file
46
+ config.entries = -> do
47
+ ["/", "/about", "/pricing"].map do |path|
48
+ Indexmap::Entry.new(loc: "#{config.base_url}#{path}")
49
+ end
50
+ end
51
+ ```
52
+
53
+ Use an origin without a trailing slash for this example. Supply actual content
54
+ modification dates when available; omit `lastmod` when they are unknown. For
55
+ larger or grouped manifests use `config.sections` and index mode as in the
56
+ [README](../README.md#default-index-mode). Single-file mode fails if it needs to
57
+ split; index mode partitions automatically.
58
+
59
+ The gem does not require a database for a static source. The host Rails boot may
60
+ still require services through unrelated initializers. Keep its static build
61
+ entrypoint free of database queries, or generate through a plain Ruby script
62
+ with `require "indexmap"`, `Indexmap.configure`, and `Indexmap.create`.
63
+
64
+ ### Database-backed sources
65
+
66
+ Use the [database-backed guide](dynamic-sitemaps.md#configure-generation) for lazy
67
+ eligible-record enumeration. Generate after migrations and source transactions
68
+ commit. Refresh on a recurring schedule when eligibility depends on time, and
69
+ on meaningful publishing events if needed. Hosts own queries, routes, meaningful
70
+ dates, authorization context, jobs, and schedules.
71
+
72
+ For independent static and database outputs, use
73
+ [named outputs](../README.md#named-outputs). They do not automatically form a
74
+ combined index. Advertise every independent public root in robots.txt and submit
75
+ each separately if needed. Generate a deferred output successfully before
76
+ advertising it.
77
+
78
+ ## Development verification
79
+
80
+ ```sh
81
+ INDEXMAP_BASE_URL=http://localhost:3000 bin/rails indexmap:sitemap:create
82
+ INDEXMAP_BASE_URL=http://localhost:3000 bin/rails indexmap:sitemap:validate
83
+ bin/rails server
84
+ ```
85
+
86
+ From another terminal, fetch the chosen endpoints:
87
+
88
+ ```sh
89
+ curl -i http://localhost:3000/sitemap.xml
90
+ curl -i http://localhost:3000/robots.txt
91
+ ```
92
+
93
+ Expect XML with the configured localhost URLs and robots policy that disallows
94
+ crawling. If the root is an index, fetch one referenced child too. Check HEAD
95
+ and conditional responses when using controllers. Creation validates before
96
+ publishing; the separate validate command checks the stored result. Formatting
97
+ is optional for legacy output and unavailable for atomic publications.
98
+
99
+ ## Production static files
100
+
101
+ For static manifests, generate with the final public origin during a build or
102
+ release step, then include every generated root and child in the deployed
103
+ artifacts:
104
+
105
+ ```sh
106
+ RAILS_ENV=production INDEXMAP_BASE_URL=https://example.com \
107
+ bin/rails indexmap:sitemap:create
108
+ RAILS_ENV=production INDEXMAP_BASE_URL=https://example.com \
109
+ bin/rails indexmap:sitemap:validate
110
+ ```
111
+
112
+ Do not generate database content during an image build unless database access
113
+ and snapshot semantics are deliberate. Use a post-migration release step or job
114
+ instead. A release container's generated files must reach the serving instances;
115
+ files left on its private filesystem are unavailable to web containers.
116
+
117
+ Serve `public/` through the deployment's web server/CDN, or explicitly enable
118
+ Rails static serving. If the host uses an environment flag, configure its
119
+ production environment accordingly:
120
+
121
+ ```ruby
122
+ config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"] == "1"
123
+ ```
124
+
125
+ Set the flag only where Rails should serve these files. A reverse proxy or CDN
126
+ can serve them without Rails static serving. Keep stable roots on a short TTL,
127
+ and use a long immutable TTL only for atomic generated children. Legacy child
128
+ names are overwritten and must not receive immutable caching. Keep previously
129
+ referenced children available through the cached-root and rollback period.
130
+
131
+ A complete image deployed to one server can be enough for a static manifest.
132
+ Rolling deployments across different images can expose roots and children from
133
+ different releases; use shared storage or deploy all artifacts before switching
134
+ the root. Atomic mode protects publication in its configured store, not an
135
+ arbitrary deployment system's separate file-copy step.
136
+
137
+ ## Production shared storage and Rails delivery
138
+
139
+ For refresh jobs across containers, use shared persistent storage. Configure
140
+ `config.atomic = true` for revision-checked activation and immutable children.
141
+ Choose Filesystem only when locks and rename work across processes, or use the
142
+ [Active Storage adapter](../README.md#storage) with a unique artifact filename
143
+ index. A shared Active Storage database alone is insufficient if each container
144
+ uses a different local disk service.
145
+
146
+ Mount the root and generated child routes from
147
+ [Serve published XML](dynamic-sitemaps.md#serve-published-xml). Store XML outside
148
+ `public/` when the controller should own delivery, and remove old static files
149
+ that would shadow its routes. The shared controller serves configured roots and
150
+ atomic children; legacy index children require deliberate static or host delivery.
151
+ A single-file output needs only its root route.
152
+
153
+ Use one host refresh owner and the host's actual queue adapter; see
154
+ [background generation](../README.md#background-generation). Apply the generated
155
+ configuration in jobs. `TaskRunner#create` includes key-file handling and
156
+ callbacks; `Indexmap.create(:name)` only generates an output. Callbacks can fail
157
+ after files are published, so make them safe to repeat and monitor job failures.
158
+
159
+ Schedule pruning separately for atomic output:
160
+
161
+ ```sh
162
+ RAILS_ENV=production RETENTION_HOURS=48 bin/rails indexmap:sitemap:prune
163
+ ```
164
+
165
+ Supply the same storage/origin settings as generation. Retain children longer
166
+ than cached roots, stale responses, and rollback windows can reference them.
167
+ Replace old callbacks that delete files absent from configured section names.
168
+
169
+ ## Static or dynamic robots.txt
170
+
171
+ Choose one owner for `/robots.txt`:
172
+
173
+ - **Static:** manage `public/robots.txt` or a web-server file. For a public
174
+ production site, it can contain the following. Deploy a separate disallow file
175
+ for development, staging, and previews.
176
+ - **Dynamic:** mount the [robots controller](../README.md#dynamic-robots), remove
177
+ `public/robots.txt`, and configure an explicit environment predicate. It works
178
+ with static or controller-served sitemap XML and does not read sitemap storage.
179
+
180
+ Public production file:
181
+
182
+ ```text
183
+ User-agent: *
184
+ Allow: /
185
+ Sitemap: https://example.com/sitemap.xml
186
+ ```
187
+
188
+ Development, staging, or preview file:
189
+
190
+ ```text
191
+ User-agent: *
192
+ Disallow: /
193
+ ```
194
+
195
+ Dynamic policy inside the configure block:
196
+
197
+ ```ruby
198
+ config.robots.indexable = -> { Rails.env.production? && ENV["PUBLIC_INDEXING"] == "1" }
199
+ config.robots.groups = [{user_agent: "*", allow: ["/"], disallow: ["/admin/", "/search"]}]
200
+ ```
201
+
202
+ Set `PUBLIC_INDEXING=1` only on the intended public deployment. A preview can
203
+ run with `RAILS_ENV=production`; that alone must not enable crawling. When the
204
+ predicate is false, Indexmap emits disallow-all without sitemap advertisements.
205
+ Static robots files ignore this predicate. Keep host-specific exclusions in the
206
+ chosen policy. Robots instructions are not access control and do not ensure a
207
+ page is removed from an index; protect private routes independently.
208
+
209
+ ## Verify the deployed result
210
+
211
+ Fetch the actual public root, a referenced child (in index mode), and robots.txt.
212
+ Check content, content types, redirects, cache headers, and intended allow/disallow
213
+ rules from outside the container. Confirm that job writes are visible from every
214
+ serving instance and that a refresh failure retains the old root in atomic mode.
215
+ A storage validation pass does not prove HTTP delivery.
216
+
217
+ Keep search-engine submission separate. Test with
218
+ `INDEXNOW_DRY_RUN=1 bin/rails indexmap:index_now:ping` against the correct
219
+ environment before live submission; a dry run still needs an IndexNow key.
220
+ [Provision the verification key](search-engine-ping.md) at its public URL,
221
+ especially when XML lives outside `public/`. Do not submit development or staging
222
+ URLs. Google credentials and property setup, IndexNow selection previews, and
223
+ attempt reports are covered in the [search-engine guide](search-engine-ping.md).
@@ -0,0 +1,27 @@
1
+ # Domain terms
2
+
3
+ | Term | Meaning |
4
+ | --- | --- |
5
+ | Configuration | `Indexmap::Configuration` holds gem settings, sources, outputs, callbacks, and robots policy. |
6
+ | Configuration loader | Generated host module `Indexmap::HostConfiguration.apply`; applies the settings once per process. It is separate from the gem's configuration object. |
7
+ | Output | One independently generated target with a stable root filename. Named outputs share storage and are not automatically linked into the default output. |
8
+ | Section | A host-defined filename and enumerable of page entries, used in index mode. |
9
+ | Page entry | `Indexmap::Entry`: the host's `loc` and optional meaningful content `lastmod`. |
10
+ | Parsed entry | `Indexmap::Parser::Entry`: `loc`, `lastmod`, and the source sitemap key/URL. Returned by parsing and validation. |
11
+ | Partition | A bounded child XML document produced from a section. One section may produce several partitions. |
12
+ | Root | The configured `index_filename`: an index of children, or a URL set in single-file mode. |
13
+ | Publication | One generated output made visible in storage. Atomic publications have immutable children and a guarded root activation. |
14
+ | Generation | The unique identifier used for an atomic build's child keys and retained root archive. |
15
+ | Artifact | A stored XML file, including a root, child, or retained root archive. `Indexmap::Artifact` is the writer's in-memory filename/body/content-type value, not Ruby's filesystem `::File`. |
16
+ | Storage key | A relative filename, possibly including directories, such as `sitemaps/sitemap.xml`. It is separate from its public HTTP URL. |
17
+ | Static source | Explicit routes or manifest entries that can be generated without querying a database. |
18
+ | Dynamic source | Host data selected at refresh time, often through a lazy database enumeration. It does not mean generation during HTTP requests. |
19
+ | Format | `:index` or `:single_file` selects XML shape. The separate `format` task changes whitespace in legacy artifacts. |
20
+ | Robots policy | Crawl instructions served as `/robots.txt`. It is independent of sitemap storage and refresh scheduling. |
21
+
22
+ Use these terms in host manifests and operational instructions. The generated
23
+ host loader is `Indexmap::HostConfiguration`; `Indexmap::Configuration` is the
24
+ gem's settings object. `Indexmap::Artifact` is rendered data and `::File` is Ruby's
25
+ filesystem class. The former `IndexmapConfiguration` and `Indexmap::Storage::File`
26
+ names are replaced by this unreleased contract change; see
27
+ [UPGRADE.md](../UPGRADE.md#host-loader-and-rendered-artifacts) for migration.