indexmap 0.4.0 → 0.4.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: 7a655a2fa64beda7c2724d6fb4f283a13a5baf47519863c489e982efa86f2c32
4
- data.tar.gz: 42dac8a1312d05bba81df9534bba52eaa69d55c236688473757252a1038dae06
3
+ metadata.gz: 513c19d20a8373690c66fa99a0251db934298589a19139c2b77654340ce7006a
4
+ data.tar.gz: '087bd725e6ecaf81ff07510f78f7c5fa1d613730cd7b735c9f0ed60ecb3f4676'
5
5
  SHA512:
6
- metadata.gz: d0615154aef9cf51613b73c46f834efb16c53b77dfb3611f8f980a8ae5c1e8eedaa653ca84ca5610a27b58377717e7c3acbd0fc1cdb162df90fd84d660912ed6
7
- data.tar.gz: 796aeed197f26684ebbd74bb7b47d37c878fea855c02c4ec0636cd7ce28c44f32fb73a3c2a911955d194d175c8c80ce351c8b2f955e03707543a88e4ab740763
6
+ metadata.gz: 2f4065163991559ca5a1df38f1f01aafbdc539936f40aa81d78a3c5aa4abe34bdbc7cdb0df25d970a8a243e1b552c1946ec97c640f8d64350a46ef0d8de36006
7
+ data.tar.gz: d0ebff2036cefb30689a22ebddf313c89ac45a6dda11277a4a5322b6c39c6257e6d05e3615e0167ca876f985b14dc5ac21eba096f947876ab72be32bc6a73c33
data/CHANGELOG.md CHANGED
@@ -5,12 +5,12 @@ 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.4.0] - 2026-04-22
8
+ ## [0.4.1] - 2026-04-22
9
9
 
10
10
 
11
- ### Documentation
11
+ ### Added
12
12
 
13
- - improve the task messages (#4)
13
+ - add output to ping tasks (#6)
14
14
 
15
15
 
16
16
 
@@ -17,10 +17,13 @@ module Indexmap
17
17
  def ping
18
18
  if google_configuration.credentials.to_s.strip.empty?
19
19
  logger.debug("Google sitemap credentials not configured.")
20
- return
20
+ return {status: :skipped, reason: :missing_credentials}
21
21
  end
22
22
 
23
- super
23
+ results = sitemap_files.map { |sitemap_file| ping_sitemap(sitemap_file) }
24
+ return {status: :skipped, reason: :no_sitemaps} if results.empty?
25
+
26
+ summarize_results(results)
24
27
  end
25
28
 
26
29
  private
@@ -36,13 +39,26 @@ module Indexmap
36
39
 
37
40
  unless authorized?
38
41
  logger.error("Google Search Console does not have access to the site: #{root_domain}")
39
- return
42
+ return {
43
+ status: :failed,
44
+ reason: :unauthorized,
45
+ property: property_identifier,
46
+ root_domain: root_domain
47
+ }
40
48
  end
41
49
 
42
50
  webmasters_service.submit_sitemap(property_identifier, sitemap_url)
43
51
  logger.debug { "Successfully pinged Google with sitemap: #{sitemap_url}" }
52
+ {status: :submitted, sitemap_url: sitemap_url}
44
53
  rescue ::Google::Apis::ClientError => e
45
54
  logger.debug { "Failed to ping Google for #{sitemap_url}. Status: #{e.status_code}, Body: #{e.body}" }
55
+ {
56
+ status: :failed,
57
+ reason: :client_error,
58
+ sitemap_url: sitemap_url,
59
+ status_code: e.status_code,
60
+ body: e.body
61
+ }
46
62
  end
47
63
 
48
64
  def authorized?
@@ -73,6 +89,21 @@ module Indexmap
73
89
  scope: scope
74
90
  )
75
91
  end
92
+
93
+ def summarize_results(results)
94
+ submitted = results.select { |result| result[:status] == :submitted }
95
+ failures = results.select { |result| result[:status] == :failed }
96
+
97
+ return {status: :submitted, sitemap_count: submitted.count, submitted: submitted} if failures.empty?
98
+ return {status: :failed, sitemap_count: 0, failures: failures} if submitted.empty?
99
+
100
+ {
101
+ status: :partial,
102
+ sitemap_count: submitted.count,
103
+ submitted: submitted,
104
+ failures: failures
105
+ }
106
+ end
76
107
  end
77
108
  end
78
109
  end
@@ -17,25 +17,27 @@ module Indexmap
17
17
  api_key = read_api_key
18
18
  unless api_key
19
19
  logger.debug("IndexNow API key is not configured.")
20
- return
20
+ return {status: :skipped, reason: :missing_key}
21
21
  end
22
22
 
23
23
  entries = entries_to_ping
24
24
  if entries.empty?
25
25
  logger.debug("IndexNow: no URLs matched the current filter.")
26
- return
26
+ return {status: :skipped, reason: :no_urls}
27
27
  end
28
28
 
29
- entries.each_slice(max_urls_per_request) do |batch|
29
+ results = entries.each_slice(max_urls_per_request).map do |batch|
30
30
  urls = batch.map(&:loc)
31
31
 
32
32
  if dry_run?
33
33
  logger.debug { "IndexNow dry-run: would ping #{urls.count} URLs." }
34
- next
34
+ next({status: :dry_run, url_count: urls.count})
35
35
  end
36
36
 
37
37
  submit_batch(api_key: api_key, urls: urls)
38
38
  end
39
+
40
+ summarize_results(results)
39
41
  end
40
42
 
41
43
  def write_key_file(key: index_now_configuration.key, path: nil)
@@ -148,10 +150,10 @@ module Indexmap
148
150
 
149
151
  if response.success?
150
152
  logger.debug { "Successfully pinged IndexNow with #{urls.count} URLs." }
151
- true
153
+ {status: :submitted, url_count: urls.count}
152
154
  else
153
155
  logger.debug { "Failed to ping IndexNow. Status: #{response.status}, Body: #{response.body}" }
154
- false
156
+ {status: :failed, url_count: urls.count, status_code: response.status, body: response.body}
155
157
  end
156
158
  end
157
159
 
@@ -191,6 +193,44 @@ module Indexmap
191
193
  def generated_key
192
194
  SecureRandom.uuid
193
195
  end
196
+
197
+ def summarize_results(results)
198
+ dry_runs = results.select { |result| result[:status] == :dry_run }
199
+ submitted = results.select { |result| result[:status] == :submitted }
200
+ failures = results.select { |result| result[:status] == :failed }
201
+
202
+ if dry_runs.any?
203
+ return {
204
+ status: :dry_run,
205
+ url_count: dry_runs.sum { |result| result[:url_count] },
206
+ batch_count: dry_runs.count
207
+ }
208
+ end
209
+
210
+ if failures.empty?
211
+ return {
212
+ status: :submitted,
213
+ url_count: submitted.sum { |result| result[:url_count] },
214
+ batch_count: submitted.count
215
+ }
216
+ end
217
+
218
+ if submitted.empty?
219
+ return {
220
+ status: :failed,
221
+ url_count: 0,
222
+ batch_count: 0,
223
+ failures: failures
224
+ }
225
+ end
226
+
227
+ {
228
+ status: :partial,
229
+ url_count: submitted.sum { |result| result[:url_count] },
230
+ batch_count: submitted.count,
231
+ failures: failures
232
+ }
233
+ end
194
234
  end
195
235
  end
196
236
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Indexmap
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -35,14 +35,40 @@ namespace :sitemap do
35
35
  namespace :google do
36
36
  desc "Ping Google Search Console"
37
37
  task ping: :environment do
38
- Indexmap::Pinger::Google.new.ping
38
+ result = Indexmap::Pinger::Google.new.ping
39
+
40
+ case result[:status]
41
+ when :submitted
42
+ puts "Submitted #{result[:sitemap_count]} sitemap #{(result[:sitemap_count] == 1) ? "file" : "files"} to Google Search Console."
43
+ when :partial
44
+ puts "Submitted #{result[:sitemap_count]} sitemap #{(result[:sitemap_count] == 1) ? "file" : "files"} to Google Search Console, with #{result[:failures].count} failure#{"s" unless result[:failures].count == 1}."
45
+ result[:failures].each { |failure| puts format_google_ping_failure(failure) }
46
+ when :failed
47
+ result[:failures].each { |failure| puts format_google_ping_failure(failure) }
48
+ when :skipped
49
+ puts format_google_ping_skip(result)
50
+ end
39
51
  end
40
52
  end
41
53
 
42
54
  namespace :index_now do
43
55
  desc "Ping IndexNow. ENV: SINCE=2026-04-18T10:30:00Z or INDEXNOW_RECENT_HOURS=24"
44
56
  task ping: :environment do
45
- Indexmap::Pinger::IndexNow.new.ping
57
+ result = Indexmap::Pinger::IndexNow.new.ping
58
+
59
+ case result[:status]
60
+ when :submitted
61
+ puts "Submitted #{result[:url_count]} URL#{"s" unless result[:url_count] == 1} to IndexNow in #{result[:batch_count]} request#{"s" unless result[:batch_count] == 1}."
62
+ when :partial
63
+ puts "Submitted #{result[:url_count]} URL#{"s" unless result[:url_count] == 1} to IndexNow in #{result[:batch_count]} request#{"s" unless result[:batch_count] == 1}, with #{result[:failures].count} failure#{"s" unless result[:failures].count == 1}."
64
+ result[:failures].each { |failure| puts format_index_now_ping_failure(failure) }
65
+ when :failed
66
+ result[:failures].each { |failure| puts format_index_now_ping_failure(failure) }
67
+ when :dry_run
68
+ puts "IndexNow dry-run: would submit #{result[:url_count]} URL#{"s" unless result[:url_count] == 1} in #{result[:batch_count]} request#{"s" unless result[:batch_count] == 1}."
69
+ when :skipped
70
+ puts format_index_now_ping_skip(result)
71
+ end
46
72
  end
47
73
 
48
74
  desc "Ensure the IndexNow key file exists in public/"
@@ -64,4 +90,46 @@ namespace :sitemap do
64
90
  def public_directory(runner)
65
91
  runner.public_path
66
92
  end
93
+
94
+ def format_google_ping_failure(failure)
95
+ case failure[:reason]
96
+ when :unauthorized
97
+ "Google Search Console does not have access to #{failure[:property]}."
98
+ when :client_error
99
+ "Google ping failed for #{failure[:sitemap_url]} (status #{failure[:status_code]})."
100
+ else
101
+ "Google ping failed."
102
+ end
103
+ end
104
+
105
+ def format_google_ping_skip(result)
106
+ case result[:reason]
107
+ when :missing_credentials
108
+ "Google sitemap credentials are not configured; skipped Google submission."
109
+ when :no_sitemaps
110
+ "No sitemap files found; skipped Google submission."
111
+ else
112
+ "Skipped Google submission."
113
+ end
114
+ end
115
+
116
+ def format_index_now_ping_failure(failure)
117
+ case failure[:status_code]
118
+ when nil
119
+ "IndexNow submission failed."
120
+ else
121
+ "IndexNow submission failed for #{failure[:url_count]} URL#{"s" unless failure[:url_count] == 1} (status #{failure[:status_code]})."
122
+ end
123
+ end
124
+
125
+ def format_index_now_ping_skip(result)
126
+ case result[:reason]
127
+ when :missing_key
128
+ "IndexNow key is not configured and no valid key file was found; skipped IndexNow submission."
129
+ when :no_urls
130
+ "No sitemap URLs matched the current IndexNow filter; skipped IndexNow submission."
131
+ else
132
+ "Skipped IndexNow submission."
133
+ end
134
+ end
67
135
  end
@@ -40,7 +40,7 @@ class IndexmapPingerGoogleTest < Minitest::Test
40
40
  :fake_authorizer
41
41
  end
42
42
 
43
- Indexmap::Pinger::Google.new(
43
+ result = Indexmap::Pinger::Google.new(
44
44
  configuration: configuration,
45
45
  service: service,
46
46
  credentials_builder: credentials_builder
@@ -49,6 +49,8 @@ class IndexmapPingerGoogleTest < Minitest::Test
49
49
  assert_equal [["{\"type\":\"service_account\"}", "https://www.googleapis.com/auth/webmasters"]], builder_calls
50
50
  assert_equal :fake_authorizer, service.authorization
51
51
  assert_equal ["sc-domain:example.com", "https://www.example.com/sitemap.xml"], service.submitted
52
+ assert_equal :submitted, result[:status]
53
+ assert_equal 1, result[:sitemap_count]
52
54
  end
53
55
  end
54
56
 
@@ -63,9 +65,53 @@ class IndexmapPingerGoogleTest < Minitest::Test
63
65
 
64
66
  service = FakeWebmastersService.new(site_urls: ["sc-domain:example.com"])
65
67
 
66
- Indexmap::Pinger::Google.new(configuration: configuration, service: service).ping
68
+ result = Indexmap::Pinger::Google.new(configuration: configuration, service: service).ping
67
69
 
68
70
  assert_nil service.submitted
71
+ assert_equal({status: :skipped, reason: :missing_credentials}, result)
72
+ end
73
+ end
74
+
75
+ def test_reports_missing_sitemap_files
76
+ Dir.mktmpdir do |dir|
77
+ configuration = Indexmap::Configuration.new
78
+ configuration.base_url = "https://www.example.com"
79
+ configuration.public_path = Pathname(dir)
80
+ configuration.google.credentials = "{\"type\":\"service_account\"}"
81
+
82
+ service = FakeWebmastersService.new(site_urls: ["sc-domain:example.com"])
83
+ result = Indexmap::Pinger::Google.new(
84
+ configuration: configuration,
85
+ service: service,
86
+ credentials_builder: ->(**) { :fake_authorizer }
87
+ ).ping
88
+
89
+ assert_equal({status: :skipped, reason: :no_sitemaps}, result)
90
+ assert_nil service.submitted
91
+ end
92
+ end
93
+
94
+ def test_reports_google_authorization_failure
95
+ Dir.mktmpdir do |dir|
96
+ public_path = Pathname(dir)
97
+ public_path.join("sitemap.xml").write("<sitemapindex/>")
98
+
99
+ configuration = Indexmap::Configuration.new
100
+ configuration.base_url = "https://www.example.com"
101
+ configuration.public_path = public_path
102
+ configuration.google.credentials = "{\"type\":\"service_account\"}"
103
+
104
+ service = FakeWebmastersService.new(site_urls: ["sc-domain:not-example.org"])
105
+ result = Indexmap::Pinger::Google.new(
106
+ configuration: configuration,
107
+ service: service,
108
+ credentials_builder: ->(**) { :fake_authorizer }
109
+ ).ping
110
+
111
+ assert_equal :failed, result[:status]
112
+ assert_equal 1, result[:failures].count
113
+ assert_equal :unauthorized, result[:failures].first[:reason]
114
+ assert_nil service.submitted
69
115
  end
70
116
  end
71
117
  end
@@ -48,12 +48,15 @@ class IndexmapPingerIndexNowTest < Minitest::Test
48
48
  indexnow_url = "https://api.indexnow.org/indexnow"
49
49
  stub_request(:post, indexnow_url).to_return(status: 200, body: "", headers: {})
50
50
 
51
- Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
51
+ result = Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
52
52
 
53
53
  assert_requested(:post, indexnow_url, times: 1) do |request|
54
54
  payload = JSON.parse(request.body)
55
55
  assert_equal "test-key-123", payload.fetch("key")
56
56
  end
57
+ assert_equal :submitted, result[:status]
58
+ assert_equal 2, result[:url_count]
59
+ assert_equal 1, result[:batch_count]
57
60
  end
58
61
  end
59
62
 
@@ -74,7 +77,7 @@ class IndexmapPingerIndexNowTest < Minitest::Test
74
77
  indexnow_url = "https://api.indexnow.org/indexnow"
75
78
  stub_request(:post, indexnow_url).to_return(status: 200, body: "", headers: {})
76
79
 
77
- Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
80
+ result = Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
78
81
 
79
82
  assert_requested(:post, indexnow_url, times: 1) do |request|
80
83
  payload = JSON.parse(request.body)
@@ -83,6 +86,9 @@ class IndexmapPingerIndexNowTest < Minitest::Test
83
86
  "https://www.example.com/insights/us/restaurants/overview"
84
87
  ].sort, payload.fetch("urlList").sort
85
88
  end
89
+ assert_equal :submitted, result[:status]
90
+ assert_equal 2, result[:url_count]
91
+ assert_equal 1, result[:batch_count]
86
92
  end
87
93
  end
88
94
 
@@ -104,7 +110,11 @@ class IndexmapPingerIndexNowTest < Minitest::Test
104
110
  stub_request(:post, indexnow_url).to_return(status: 200, body: "", headers: {})
105
111
 
106
112
  with_env("SINCE" => "2026-04-15T00:00:00Z") do
107
- Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
113
+ result = Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
114
+
115
+ assert_equal :submitted, result[:status]
116
+ assert_equal 1, result[:url_count]
117
+ assert_equal 1, result[:batch_count]
108
118
  end
109
119
 
110
120
  assert_requested(:post, indexnow_url, times: 1) do |request|
@@ -114,6 +124,74 @@ class IndexmapPingerIndexNowTest < Minitest::Test
114
124
  end
115
125
  end
116
126
 
127
+ def test_skips_indexnow_ping_when_key_is_missing
128
+ Dir.mktmpdir do |dir|
129
+ public_path = Pathname(dir)
130
+ write_sitemap_files(
131
+ public_path,
132
+ marketing_lastmod: "2026-04-18T00:00:00Z",
133
+ insights_lastmod: "2026-04-10T00:00:00Z"
134
+ )
135
+
136
+ configuration = Indexmap::Configuration.new
137
+ configuration.base_url = "https://www.example.com"
138
+ configuration.public_path = public_path
139
+
140
+ result = Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
141
+
142
+ assert_equal({status: :skipped, reason: :missing_key}, result)
143
+ end
144
+ end
145
+
146
+ def test_reports_indexnow_dry_run
147
+ Dir.mktmpdir do |dir|
148
+ public_path = Pathname(dir)
149
+ write_sitemap_files(
150
+ public_path,
151
+ marketing_lastmod: "2026-04-18T00:00:00Z",
152
+ insights_lastmod: "2026-04-10T00:00:00Z"
153
+ )
154
+
155
+ configuration = Indexmap::Configuration.new
156
+ configuration.base_url = "https://www.example.com"
157
+ configuration.public_path = public_path
158
+ configuration.index_now.key = "test-key"
159
+
160
+ with_env("INDEXNOW_DRY_RUN" => "1") do
161
+ result = Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
162
+
163
+ assert_equal :dry_run, result[:status]
164
+ assert_equal 2, result[:url_count]
165
+ assert_equal 1, result[:batch_count]
166
+ end
167
+ end
168
+ end
169
+
170
+ def test_reports_failed_indexnow_submission
171
+ Dir.mktmpdir do |dir|
172
+ public_path = Pathname(dir)
173
+ write_sitemap_files(
174
+ public_path,
175
+ marketing_lastmod: "2026-04-18T00:00:00Z",
176
+ insights_lastmod: "2026-04-10T00:00:00Z"
177
+ )
178
+
179
+ configuration = Indexmap::Configuration.new
180
+ configuration.base_url = "https://www.example.com"
181
+ configuration.public_path = public_path
182
+ configuration.index_now.key = "test-key"
183
+
184
+ indexnow_url = "https://api.indexnow.org/indexnow"
185
+ stub_request(:post, indexnow_url).to_return(status: 500, body: "boom", headers: {})
186
+
187
+ result = Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
188
+
189
+ assert_equal :failed, result[:status]
190
+ assert_equal 1, result[:failures].count
191
+ assert_equal 500, result[:failures].first[:status_code]
192
+ end
193
+ end
194
+
117
195
  private
118
196
 
119
197
  def with_env(overrides)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: indexmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Fidalgo