indexmap 0.6.0 → 0.7.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef452a5928b87f84f65ecb9ba2afcab5340d9d09c5d8c8dfd8c4ffb3217a68fd
4
- data.tar.gz: 03b11e1d9360bbd797d6b61886c99d1619247877dfd2c4676efeca9a1119764c
3
+ metadata.gz: 64a9d1c671519fea6ba6a46b7f67daed2784ad5aaccf2594dc3aa0a374f55854
4
+ data.tar.gz: 50f6bc13a8097194dec9736b96c8cc6baf0f76429edf0175372aee97991c63f0
5
5
  SHA512:
6
- metadata.gz: a04a772029e2438636df90b65004a27c54e1cf3360205fd556feff615768fd527668fe2a3e3063f2831b9af9afb4edd983e48e73b922278c4c10a63d936b938e
7
- data.tar.gz: 6ce68d9ee6343aaf7c4bbff6860a712d743899390b4a8a3a09c712694c95cff32cc0acd85f30a43e2ba37f0c8ff671d2af11028e63976649ae90e5100d5e0a1b
6
+ metadata.gz: 2ce92a1da076303265c03ff833585b069215c8417b134407850770c64ae04b3ab93fcf894484874e710e304c6b3c94f19247d70073aaa6ba5e170f70212e4992
7
+ data.tar.gz: 991f39cd9db707dd79e7a404e636aaea8c8a1db07ce5b59d2daf9d73c33a55d39c252d6edef2adf8fc98ea22e153f8619aee4f67f9fd11c3fccf56b473bb1ffa
data/CHANGELOG.md CHANGED
@@ -5,14 +5,124 @@ 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.7.1] - 2026-05-31
9
+
10
+
11
+ ### Documentation
12
+
13
+ - document google search console credentials
14
+
15
+
16
+
17
+
18
+ ### Fixed
19
+
20
+ - preserve release changelog history ([#13](https://github.com/ethos-link/indexmap/pull/13))
21
+
22
+ - improve sitemap create output ([#14](https://github.com/ethos-link/indexmap/pull/14))
23
+
24
+
25
+
26
+ ## [0.7.0] - 2026-05-08
27
+
28
+
29
+ ### Fixed
30
+
31
+ - avoid rewriting existing IndexNow key files ([#11](https://github.com/ethos-link/indexmap/pull/11))
32
+
33
+
34
+
8
35
  ## [0.6.0] - 2026-05-01
9
36
 
10
37
 
11
38
  ### Added
12
39
 
13
- - add the url count to google ping output (#9)
40
+ - add the url count to google ping output ([#9](https://github.com/ethos-link/indexmap/pull/9))
41
+
42
+ - support named sitemap outputs ([#10](https://github.com/ethos-link/indexmap/pull/10))
43
+
44
+
45
+
46
+ ## [0.5.0] - 2026-04-24
47
+
48
+
49
+ ### Fixed
50
+
51
+ - namespace rake tasks and harden sitemap validation ([#8](https://github.com/ethos-link/indexmap/pull/8))
52
+
53
+
54
+
55
+ ## [0.4.2] - 2026-04-23
56
+
57
+
58
+ ### Fixed
59
+
60
+ - harden sitemap pinging and indexnow key handling ([#7](https://github.com/ethos-link/indexmap/pull/7))
61
+
62
+
63
+
64
+ ## [0.4.1] - 2026-04-22
65
+
66
+
67
+ ### Added
68
+
69
+ - add output to ping tasks ([#6](https://github.com/ethos-link/indexmap/pull/6))
70
+
71
+
72
+
73
+ ## [0.4.0] - 2026-04-22
74
+
75
+
76
+ ### Documentation
77
+
78
+ - improve the task messages ([#4](https://github.com/ethos-link/indexmap/pull/4))
79
+
80
+
81
+
82
+ ## [0.3.1] - 2026-04-22
83
+
84
+
85
+ ### Fixed
86
+
87
+ - fix changelog generation ([#2](https://github.com/ethos-link/indexmap/pull/2))
88
+
89
+ - harden indexmap runtime defaults and test coverage ([#3](https://github.com/ethos-link/indexmap/pull/3))
90
+
91
+
92
+
93
+ ## [0.3.0] - 2026-04-22
94
+
95
+
96
+ ### Added
97
+
98
+ - expand indexmap with sitemap parsing, validation, and search engine pinging
99
+
100
+
101
+
102
+ ## [0.2.1] - 2026-04-21
103
+
104
+
105
+ ### Fixed
106
+
107
+ - publish built gem in release workflow
108
+
109
+
110
+
111
+ ## [0.2.0] - 2026-04-21
112
+
113
+
114
+ ### Added
115
+
116
+ - add single-file sitemap mode
117
+
118
+
119
+
120
+ ## [0.1.0] - 2026-04-21
121
+
122
+
123
+ ### Added
14
124
 
15
- - support named sitemap outputs (#10)
125
+ - bootstrap indexmap gem
16
126
 
17
127
 
18
128
 
data/README.md CHANGED
@@ -34,6 +34,17 @@ Or install it directly:
34
34
  gem install indexmap
35
35
  ```
36
36
 
37
+ Upgrading an existing app? Read [UPGRADE.md](UPGRADE.md) before deploying,
38
+ especially if the app uses custom storage or stores sitemap files under a
39
+ directory prefix such as `sitemaps/`.
40
+
41
+ ## Documentation
42
+
43
+ - [Search engine ping](docs/search-engine-ping.md): Google Search Console
44
+ credentials, Google property configuration, and IndexNow key setup.
45
+ - [UPGRADE.md](UPGRADE.md): host-app migration steps for public contract
46
+ changes.
47
+
37
48
  ## Ruby Usage
38
49
 
39
50
  ```ruby
@@ -51,7 +62,6 @@ sections = [
51
62
 
52
63
  Indexmap::Writer.new(
53
64
  sections: sections,
54
- public_path: Pathname("public"),
55
65
  base_url: "https://example.com"
56
66
  ).write
57
67
  ```
@@ -63,7 +73,12 @@ In an initializer:
63
73
  ```ruby
64
74
  Indexmap.configure do |config|
65
75
  config.base_url = -> { "https://example.com" }
66
- config.public_path = -> { Rails.public_path }
76
+ config.storage = -> do
77
+ Indexmap::Storage::Filesystem.new(
78
+ path: Rails.public_path,
79
+ public_url: config.base_url
80
+ )
81
+ end
67
82
  config.sections = -> do
68
83
  [
69
84
  Indexmap::Section.new(
@@ -85,26 +100,26 @@ bin/rails indexmap:sitemap:format
85
100
  bin/rails indexmap:sitemap:validate
86
101
  ```
87
102
 
88
- `indexmap:sitemap:create` is the main task. It writes sitemap files to a local
89
- temporary directory, formats them, validates the result, then replaces the final
90
- XML files. Existing sitemap files are left untouched if generation or validation
91
- fails.
103
+ `indexmap:sitemap:create` is the main task. It builds sitemap files in memory,
104
+ formats them, validates the result, then writes the final XML files to the
105
+ configured storage. Existing sitemap files are left untouched if generation or
106
+ validation fails.
92
107
 
93
108
  ### Default Index Mode
94
109
 
95
110
  This is the default behavior. `indexmap` writes:
96
111
 
97
- - `public/sitemap.xml` as a sitemap index
112
+ - `sitemap.xml` as a sitemap index
98
113
  - one or more child sitemap files from `config.sections`
99
114
 
100
115
  ### Single-File Mode
101
116
 
102
- For sites that only want one `public/sitemap.xml` file:
117
+ For sites that only want one `sitemap.xml` file:
103
118
 
104
119
  ```ruby
105
120
  Indexmap.configure do |config|
106
121
  config.base_url = -> { "https://example.com" }
107
- config.public_path = -> { Rails.public_path }
122
+ config.storage = -> { Indexmap::Storage::Filesystem.new(path: Rails.public_path, public_url: config.base_url) }
108
123
  config.format = :single_file
109
124
  config.entries = -> do
110
125
  [
@@ -122,13 +137,12 @@ In `:single_file` mode, `indexmap` writes a `urlset` directly to `sitemap.xml` a
122
137
  Most apps only need the default output. Use named outputs when one part of the
123
138
  sitemap must be generated separately, for example when static pages can be
124
139
  generated during deploy but database-heavy pages should refresh later. Named
125
- outputs still write normal sitemap XML files to a filesystem path; storage and
126
- serving are application concerns.
140
+ outputs write through the same configured storage as the default output.
127
141
 
128
142
  ```ruby
129
143
  Indexmap.configure do |config|
130
144
  config.base_url = -> { "https://example.com" }
131
- config.public_path = -> { Rails.root.join("storage/sitemaps") }
145
+ config.storage = -> { Indexmap::Storage::Filesystem.new(path: Rails.root.join("storage/sitemaps"), public_url: config.base_url) }
132
146
  config.sections = -> { Sitemap.sections }
133
147
 
134
148
  config.output :insights_data do |output|
@@ -151,12 +165,57 @@ Generate only the named output:
151
165
  Indexmap.create(:insights_data)
152
166
  ```
153
167
 
154
- Named outputs inherit `base_url`, `public_path`, and `format` from the main
155
- configuration unless you override them.
168
+ Named outputs inherit `base_url` and `format` from the main configuration unless
169
+ you override them. Storage is configured once and shared by every output.
170
+
171
+ `Indexmap.create` uses the same safe publish flow as the rake task: build,
172
+ format, validate, and then write the final XML file or files to storage.
173
+
174
+ ### Storage
175
+
176
+ Every `indexmap` operation reads and writes through `config.storage`. The storage
177
+ object is the source of truth for generation, validation, parsing, Google
178
+ submission, IndexNow submission, and IndexNow verification files.
179
+
180
+ The filesystem adapter stores files in a directory and exposes public URLs from
181
+ the same filenames:
182
+
183
+ ```ruby
184
+ Indexmap.configure do |config|
185
+ config.base_url = "https://example.com"
186
+ config.storage = Indexmap::Storage::Filesystem.new(
187
+ path: Rails.public_path,
188
+ public_url: "https://example.com"
189
+ )
190
+ end
191
+ ```
192
+
193
+ Rails apps that store sitemap files in Active Storage can use the optional
194
+ adapter. `indexmap` does not depend on `activestorage`; this adapter only uses
195
+ the model and attachment object you pass in.
196
+
197
+ ```ruby
198
+ Indexmap.configure do |config|
199
+ config.base_url = "https://example.com"
200
+ config.storage = Indexmap::Storage::ActiveStorage.new(
201
+ model: SitemapArtifact,
202
+ filename_column: :filename,
203
+ attachment: :file,
204
+ public_url: "https://example.com"
205
+ )
206
+ end
207
+ ```
208
+
209
+ Custom storage backends can implement the same small interface:
156
210
 
157
- `Indexmap.create` uses the same safe local publish flow as the rake task:
158
- generate in a temporary directory, format, validate, and then replace the final
159
- XML file or files.
211
+ ```ruby
212
+ storage.write(filename, body, content_type:)
213
+ storage.read(filename)
214
+ storage.exist?(filename)
215
+ storage.list(prefix:, suffix:)
216
+ storage.delete(filename)
217
+ storage.public_url(filename)
218
+ ```
160
219
 
161
220
  ### Deferred Dynamic Sections
162
221
 
@@ -168,7 +227,7 @@ replaced successfully.
168
227
  ```ruby
169
228
  Indexmap.configure do |config|
170
229
  config.base_url = -> { "https://example.com" }
171
- config.public_path = -> { Rails.root.join("storage/sitemaps") }
230
+ config.storage = -> { Indexmap::Storage::Filesystem.new(path: Rails.root.join("storage/sitemaps"), public_url: config.base_url) }
172
231
  config.sections = -> { Sitemap.sections }
173
232
 
174
233
  config.output :insights_data do |output|
@@ -201,7 +260,7 @@ while database-dependent output is refreshed by the job backend.
201
260
  `indexmap` also includes small utilities for working with generated sitemap files:
202
261
 
203
262
  ```ruby
204
- parser = Indexmap::Parser.new(path: Rails.public_path.join("sitemap.xml"))
263
+ parser = Indexmap::Parser.new(source: "sitemap.xml")
205
264
  parser.paths
206
265
  # => ["/", "/about", "/articles/example"]
207
266
 
@@ -224,6 +283,8 @@ The built-in validator checks for:
224
283
  ## Search Engine Ping
225
284
 
226
285
  `indexmap` can ping Google Search Console and IndexNow after sitemap generation.
286
+ See [Search engine ping](docs/search-engine-ping.md) for Google credential
287
+ setup, Search Console property configuration, and IndexNow key provisioning.
227
288
 
228
289
  Available rake tasks:
229
290
 
@@ -235,59 +296,9 @@ bin/rails indexmap:index_now:write_key
235
296
  bin/rails indexmap:ping
236
297
  ```
237
298
 
238
- ### Google Search Console
239
-
240
- Google pinging requires service account credentials:
241
-
242
- ```ruby
243
- Indexmap.configure do |config|
244
- config.google.credentials = -> { ENV["GOOGLE_SITEMAP"] }
245
- end
246
- ```
247
-
248
- If `config.google.credentials` is blank, `indexmap:google:ping` skips Google submission.
249
-
250
- You can optionally override the Search Console property identifier:
251
-
252
- ```ruby
253
- Indexmap.configure do |config|
254
- config.google.credentials = -> { ENV["GOOGLE_SITEMAP"] }
255
- config.google.property = -> { "sc-domain:example.com" }
256
- end
257
- ```
258
-
259
- If `config.google.property` is not set, `indexmap` defaults to `sc-domain:<host>`.
260
-
261
- ### IndexNow
262
-
263
- IndexNow submission requires a key. `indexmap` supports two ways to provide it:
264
-
265
- - set `config.index_now.key`
266
- - or keep a valid verification file at `public/<key>.txt`
267
-
268
- Configured-key example:
269
-
270
- ```ruby
271
- Indexmap.configure do |config|
272
- config.index_now.key = -> { ENV["INDEXNOW_KEY"] }
273
- end
274
- ```
275
-
276
- If `config.index_now.key` is set, `indexmap:sitemap:create` also writes the matching `public/<key>.txt` verification file automatically.
277
-
278
- If you prefer the file-based flow, run:
279
-
280
- ```bash
281
- bin/rails indexmap:index_now:write_key
282
- ```
283
-
284
- That task:
285
-
286
- - reuses an existing valid key file when present
287
- - otherwise generates a new key in `public/<key>.txt`
288
- - makes that key available to `indexmap:index_now:ping` without adding `config.index_now.key`
289
-
290
- If neither a configured key nor a valid key file is present, `indexmap:index_now:ping` skips IndexNow submission.
299
+ If Google credentials are blank, `indexmap:google:ping` skips Google
300
+ submission. If neither a configured IndexNow key nor a valid key file is
301
+ present, `indexmap:index_now:ping` skips IndexNow submission.
291
302
 
292
303
  ## Development
293
304
 
@@ -315,7 +326,12 @@ Note: `Gemfile.lock` is intentionally not tracked for this gem, following normal
315
326
 
316
327
  ### Git hooks
317
328
 
318
- We use [lefthook](https://lefthook.dev/) with the Ruby [commitlint](https://github.com/arandilopez/commitlint) gem to enforce Conventional Commits on every commit. We also use [Standard Ruby](https://standardrb.com/) to keep code style consistent. CI validates commit messages, Standard Ruby, tests, and git-cliff changelog generation on pull requests and pushes to main/master.
329
+ We use [lefthook](https://lefthook.dev/) with the Ruby
330
+ [commitlint](https://github.com/arandilopez/commitlint) gem to enforce
331
+ Conventional Commits on every commit. We also use
332
+ [Standard Ruby](https://standardrb.com/) to keep code style consistent. CI
333
+ validates commit messages, Standard Ruby, tests, and git-cliff changelog
334
+ generation on pull requests and pushes to main/master.
319
335
 
320
336
  Run the hook installer once per clone:
321
337
 
@@ -325,11 +341,16 @@ bundle exec lefthook install
325
341
 
326
342
  ## Release
327
343
 
328
- Releases are tag-driven and published by GitHub Actions to RubyGems. Local release commands never publish directly.
344
+ Releases are tag-driven and published by GitHub Actions to RubyGems.
345
+ Local release commands never publish directly.
329
346
 
330
- Install [git-cliff](https://git-cliff.org/) locally before preparing a release. The release task regenerates `CHANGELOG.md` from Conventional Commits.
347
+ Install [git-cliff](https://git-cliff.org/) locally before preparing a
348
+ release. The release task prepends the next `CHANGELOG.md` section from
349
+ Conventional Commits.
331
350
 
332
- Before preparing a release, make sure you are on `main` or `master` with a clean worktree.
351
+ Before preparing a release, make sure you are on `main` or `master` with a
352
+ clean worktree. If the release contains a breaking public-contract change,
353
+ update `UPGRADE.md` with the host-app migration steps first.
333
354
 
334
355
  Then run one of:
335
356
 
@@ -342,12 +363,13 @@ bundle exec rake 'release:prepare[0.1.0]'
342
363
 
343
364
  The task will:
344
365
 
345
- 1. Regenerate `CHANGELOG.md` with `git-cliff`.
366
+ 1. Prepend the next `CHANGELOG.md` section with `git-cliff`.
346
367
  1. Update `lib/indexmap/version.rb`.
347
368
  1. Commit the release changes.
348
369
  1. Create and push the `vX.Y.Z` tag.
349
370
 
350
- The `Release` workflow then runs tests, publishes the gem to RubyGems, and creates the GitHub release from the changelog entry.
371
+ The `Release` workflow then runs tests, publishes the gem to RubyGems,
372
+ and creates the GitHub release from the changelog entry.
351
373
 
352
374
  ## License
353
375
 
@@ -4,7 +4,7 @@ module Indexmap
4
4
  class Configuration
5
5
  VALID_FORMATS = %i[index single_file].freeze
6
6
 
7
- attr_writer :base_url, :entries, :format, :index_filename, :public_path, :sections
7
+ attr_writer :base_url, :entries, :format, :index_filename, :sections, :storage
8
8
 
9
9
  def initialize
10
10
  @format = :index
@@ -38,11 +38,8 @@ module Indexmap
38
38
  @index_now ||= IndexNowConfiguration.new
39
39
  end
40
40
 
41
- def public_path
42
- value = resolve(@public_path)
43
- return Pathname("public") if value.nil?
44
-
45
- Pathname(value)
41
+ def storage
42
+ resolve(@storage) || Storage::Filesystem.new(path: "public", public_url: base_url)
46
43
  end
47
44
 
48
45
  def sections
@@ -1,74 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nokogiri"
4
- require "tmpdir"
5
4
 
6
5
  module Indexmap
7
6
  class Creator
8
- ValidationConfiguration = Struct.new(:base_url, keyword_init: true)
7
+ ValidationConfiguration = Struct.new(:base_url, :index_filename, :storage, keyword_init: true)
9
8
 
10
9
  def initialize(output:)
11
10
  @output = output
12
11
  end
13
12
 
14
13
  def create
15
- FileUtils.mkdir_p(output.public_path.dirname)
16
-
17
- Dir.mktmpdir("indexmap", output.public_path.dirname) do |dir|
18
- staging_path = Pathname(dir)
19
- written_files = write_to(staging_path)
20
- sitemap_files = sitemap_files_in(staging_path)
21
-
22
- format(sitemap_files)
23
- validate(staging_path.join(output.index_filename))
24
-
25
- publish(sitemap_files)
26
- written_files.map { |path| output.public_path.join(path.basename) }
27
- end
14
+ files = format(write)
15
+ validate(files)
16
+ publish(files)
17
+ files.map(&:filename)
28
18
  end
29
19
 
30
20
  private
31
21
 
32
22
  attr_reader :output
33
23
 
34
- def write_to(staging_path)
35
- output.writer.tap do |writer|
36
- writer.public_path = staging_path
37
- end.write
38
- end
39
-
40
- def sitemap_files_in(path)
41
- path.glob("sitemap*.xml").sort
24
+ def write
25
+ output.writer.write
42
26
  end
43
27
 
44
28
  def format(files)
45
- files.each do |file_path|
29
+ files.map do |file|
46
30
  document = Nokogiri::XML(
47
- file_path.read,
31
+ file.body,
48
32
  nil,
49
33
  nil,
50
34
  Nokogiri::XML::ParseOptions::DEFAULT_XML | Nokogiri::XML::ParseOptions::NOBLANKS
51
35
  )
52
36
  save_options = Nokogiri::XML::Node::SaveOptions::FORMAT | Nokogiri::XML::Node::SaveOptions::AS_XML
53
37
 
54
- file_path.write(document.to_xml(indent: 2, save_with: save_options))
38
+ Storage::File.new(
39
+ filename: file.filename,
40
+ body: document.to_xml(indent: 2, save_with: save_options),
41
+ content_type: file.content_type
42
+ )
55
43
  end
56
44
  end
57
45
 
58
- def validate(index_path)
46
+ def validate(files)
59
47
  Validator.new(
60
- configuration: ValidationConfiguration.new(base_url: output.base_url),
61
- path: index_path
48
+ configuration: ValidationConfiguration.new(
49
+ base_url: output.base_url,
50
+ index_filename: output.index_filename,
51
+ storage: Storage::Memory.new(files)
52
+ )
62
53
  ).validate!
63
54
  end
64
55
 
65
56
  def publish(files)
66
- FileUtils.mkdir_p(output.public_path)
67
-
68
- files.map do |file_path|
69
- final_path = output.public_path.join(file_path.basename)
70
- File.rename(file_path, final_path)
71
- final_path
57
+ files.each do |file|
58
+ output.storage.write(file.filename, file.body, content_type: file.content_type)
72
59
  end
73
60
  end
74
61
  end
@@ -5,7 +5,7 @@ module Indexmap
5
5
  DEFAULT_ENDPOINT = "https://api.indexnow.org"
6
6
  DEFAULT_MAX_URLS_PER_REQUEST = 500
7
7
 
8
- attr_writer :dry_run, :endpoint, :key, :key_path, :max_urls_per_request
8
+ attr_writer :dry_run, :endpoint, :key, :key_filename, :max_urls_per_request, :write_key_file
9
9
 
10
10
  def dry_run?
11
11
  value = resolve(@dry_run)
@@ -21,12 +21,19 @@ module Indexmap
21
21
  resolve(@key)
22
22
  end
23
23
 
24
- def key_path(public_path:, key: self.key)
25
- configured_path = resolve(@key_path)
26
- return Pathname(configured_path) unless configured_path.to_s.strip.empty?
24
+ def write_key_file?
25
+ value = resolve(@write_key_file)
26
+ return !key.to_s.strip.empty? if value.nil?
27
+
28
+ value == true || value.to_s == "1"
29
+ end
30
+
31
+ def key_filename(key: self.key)
32
+ configured_filename = resolve(@key_filename)
33
+ return configured_filename unless configured_filename.to_s.strip.empty?
27
34
  return if key.to_s.strip.empty?
28
35
 
29
- Pathname(public_path).join("#{key}.txt")
36
+ "#{key}.txt"
30
37
  end
31
38
 
32
39
  def max_urls_per_request
@@ -4,7 +4,7 @@ module Indexmap
4
4
  class Output
5
5
  VALID_FORMATS = %i[index single_file].freeze
6
6
 
7
- attr_writer :base_url, :entries, :format, :index_filename, :public_path, :sections
7
+ attr_writer :base_url, :entries, :format, :index_filename, :sections
8
8
 
9
9
  def initialize(configuration:)
10
10
  @configuration = configuration
@@ -29,9 +29,8 @@ module Indexmap
29
29
  resolve(@index_filename) || configuration.index_filename
30
30
  end
31
31
 
32
- def public_path
33
- value = resolve(@public_path) || configuration.public_path
34
- Pathname(value)
32
+ def storage
33
+ configuration.storage
35
34
  end
36
35
 
37
36
  def sections
@@ -57,7 +56,6 @@ module Indexmap
57
56
  entries: entries,
58
57
  format: format,
59
58
  sections: sections,
60
- public_path: public_path,
61
59
  base_url: base_url,
62
60
  index_filename: index_filename
63
61
  )