indexmap 0.4.1 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -3
- data/README.md +21 -14
- data/lib/indexmap/pinger/google.rb +6 -2
- data/lib/indexmap/pinger/index_now.rb +36 -10
- data/lib/indexmap/validator.rb +120 -0
- data/lib/indexmap/version.rb +1 -1
- data/lib/indexmap/writer.rb +3 -2
- data/lib/tasks/indexmap_tasks.rake +26 -24
- data/test/indexmap/configuration_test.rb +27 -16
- data/test/indexmap/pinger/google_test.rb +27 -1
- data/test/indexmap/pinger/index_now_test.rb +65 -12
- data/test/indexmap/task_runner_test.rb +7 -5
- data/test/indexmap/validator_test.rb +114 -0
- data/test/indexmap/writer_test.rb +19 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38fa952c31358e79a900d348041a2475d5f597d19652837b50d27713f2004250
|
|
4
|
+
data.tar.gz: 6889768f6be1e01b6de1938687a757b050a52f7f9dbf2af58d1818cca2fdc977
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 612858ebdac07d01107af653411182685e161b1e501670e569d84265e1dbcd455dbb6a7e55615f802d112037875d9eb15d94c11747a2c15e6cbd1b9f63b804de
|
|
7
|
+
data.tar.gz: 512df6e55dbad711516e9558cda07b0b90b65f7f950441e36699fe696965296690c52a478247b707ff9e15801b49b73a5d3428cea92dd1e0b49e9861c14580d2
|
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.
|
|
8
|
+
## [0.5.0] - 2026-04-24
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### Fixed
|
|
12
12
|
|
|
13
|
-
-
|
|
13
|
+
- namespace rake tasks and harden sitemap validation (#8)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
data/README.md
CHANGED
|
@@ -80,12 +80,12 @@ end
|
|
|
80
80
|
Then run:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
bin/rails sitemap:create
|
|
84
|
-
bin/rails sitemap:format
|
|
85
|
-
bin/rails sitemap:validate
|
|
83
|
+
bin/rails indexmap:sitemap:create
|
|
84
|
+
bin/rails indexmap:sitemap:format
|
|
85
|
+
bin/rails indexmap:sitemap:validate
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
`sitemap:create` is the main task. It writes sitemap files, formats them, and validates the result.
|
|
88
|
+
`indexmap:sitemap:create` is the main task. It writes sitemap files, formats them, and validates the result.
|
|
89
89
|
|
|
90
90
|
### Default Index Mode
|
|
91
91
|
|
|
@@ -129,8 +129,15 @@ Indexmap::Validator.new.validate!
|
|
|
129
129
|
The built-in validator checks for:
|
|
130
130
|
|
|
131
131
|
- missing sitemap files
|
|
132
|
+
- malformed sitemap XML
|
|
133
|
+
- empty sitemap files
|
|
134
|
+
- missing or duplicate child sitemap references
|
|
132
135
|
- duplicate sitemap URLs
|
|
133
136
|
- parameterized URLs in sitemap entries
|
|
137
|
+
- fragment URLs in sitemap entries
|
|
138
|
+
- non-HTTP or relative URLs
|
|
139
|
+
- URLs outside the configured `base_url`
|
|
140
|
+
- invalid `lastmod` values
|
|
134
141
|
|
|
135
142
|
## Search Engine Ping
|
|
136
143
|
|
|
@@ -139,11 +146,11 @@ The built-in validator checks for:
|
|
|
139
146
|
Available rake tasks:
|
|
140
147
|
|
|
141
148
|
```bash
|
|
142
|
-
bin/rails sitemap:validate
|
|
143
|
-
bin/rails
|
|
144
|
-
bin/rails
|
|
145
|
-
bin/rails
|
|
146
|
-
bin/rails
|
|
149
|
+
bin/rails indexmap:sitemap:validate
|
|
150
|
+
bin/rails indexmap:google:ping
|
|
151
|
+
bin/rails indexmap:index_now:ping
|
|
152
|
+
bin/rails indexmap:index_now:write_key
|
|
153
|
+
bin/rails indexmap:ping
|
|
147
154
|
```
|
|
148
155
|
|
|
149
156
|
### Google Search Console
|
|
@@ -156,7 +163,7 @@ Indexmap.configure do |config|
|
|
|
156
163
|
end
|
|
157
164
|
```
|
|
158
165
|
|
|
159
|
-
If `config.google.credentials` is blank, `
|
|
166
|
+
If `config.google.credentials` is blank, `indexmap:google:ping` skips Google submission.
|
|
160
167
|
|
|
161
168
|
You can optionally override the Search Console property identifier:
|
|
162
169
|
|
|
@@ -184,21 +191,21 @@ Indexmap.configure do |config|
|
|
|
184
191
|
end
|
|
185
192
|
```
|
|
186
193
|
|
|
187
|
-
If `config.index_now.key` is set, `sitemap:create` also writes the matching `public/<key>.txt` verification file automatically.
|
|
194
|
+
If `config.index_now.key` is set, `indexmap:sitemap:create` also writes the matching `public/<key>.txt` verification file automatically.
|
|
188
195
|
|
|
189
196
|
If you prefer the file-based flow, run:
|
|
190
197
|
|
|
191
198
|
```bash
|
|
192
|
-
bin/rails
|
|
199
|
+
bin/rails indexmap:index_now:write_key
|
|
193
200
|
```
|
|
194
201
|
|
|
195
202
|
That task:
|
|
196
203
|
|
|
197
204
|
- reuses an existing valid key file when present
|
|
198
205
|
- otherwise generates a new key in `public/<key>.txt`
|
|
199
|
-
- makes that key available to `
|
|
206
|
+
- makes that key available to `indexmap:index_now:ping` without adding `config.index_now.key`
|
|
200
207
|
|
|
201
|
-
If neither a configured key nor a valid key file is present, `
|
|
208
|
+
If neither a configured key nor a valid key file is present, `indexmap:index_now:ping` skips IndexNow submission.
|
|
202
209
|
|
|
203
210
|
## Development
|
|
204
211
|
|
|
@@ -38,7 +38,7 @@ module Indexmap
|
|
|
38
38
|
sitemap_url = URI.join(host, File.basename(sitemap_file)).to_s
|
|
39
39
|
|
|
40
40
|
unless authorized?
|
|
41
|
-
logger.
|
|
41
|
+
logger.debug("Google Search Console does not have access to the site: #{root_domain}")
|
|
42
42
|
return {
|
|
43
43
|
status: :failed,
|
|
44
44
|
reason: :unauthorized,
|
|
@@ -62,7 +62,7 @@ module Indexmap
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def authorized?
|
|
65
|
-
|
|
65
|
+
@authorized ||= accessible_site_urls.include?(property_identifier)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def property_identifier
|
|
@@ -104,6 +104,10 @@ module Indexmap
|
|
|
104
104
|
failures: failures
|
|
105
105
|
}
|
|
106
106
|
end
|
|
107
|
+
|
|
108
|
+
def accessible_site_urls
|
|
109
|
+
@accessible_site_urls ||= Array(webmasters_service.list_sites.site_entry).map(&:site_url)
|
|
110
|
+
end
|
|
107
111
|
end
|
|
108
112
|
end
|
|
109
113
|
end
|
|
@@ -8,6 +8,8 @@ require "time"
|
|
|
8
8
|
module Indexmap
|
|
9
9
|
module Pinger
|
|
10
10
|
class IndexNow < Base
|
|
11
|
+
KEY_FORMAT = /\A[a-f0-9]{32}\z/
|
|
12
|
+
|
|
11
13
|
def initialize(configuration: Indexmap.configuration, connection: nil)
|
|
12
14
|
super(configuration: configuration)
|
|
13
15
|
@connection = connection
|
|
@@ -41,20 +43,21 @@ module Indexmap
|
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def write_key_file(key: index_now_configuration.key, path: nil)
|
|
44
|
-
key = key
|
|
46
|
+
key = normalized_configured_key(key)
|
|
45
47
|
return if key.empty?
|
|
46
48
|
|
|
47
49
|
path ||= index_now_configuration.key_path(public_path: configuration.public_path, key: key)
|
|
48
50
|
FileUtils.mkdir_p(path.dirname)
|
|
49
|
-
File.write(path,
|
|
51
|
+
File.write(path, key)
|
|
50
52
|
path
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
def ensure_key_file
|
|
54
|
-
configured_key = index_now_configuration.key
|
|
56
|
+
configured_key = normalized_configured_key(index_now_configuration.key)
|
|
55
57
|
return write_key_file(key: configured_key) unless configured_key.empty?
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
existing_path = existing_key_file
|
|
60
|
+
return existing_path if existing_path
|
|
58
61
|
|
|
59
62
|
key = generated_key
|
|
60
63
|
write_key_file(key: key, path: configuration.public_path.join("#{key}.txt"))
|
|
@@ -143,6 +146,9 @@ module Indexmap
|
|
|
143
146
|
|
|
144
147
|
def submit_batch(api_key:, urls:)
|
|
145
148
|
payload = {host: hostname, key: api_key, urlList: urls}
|
|
149
|
+
location = key_location(api_key: api_key)
|
|
150
|
+
payload[:keyLocation] = location if location
|
|
151
|
+
|
|
146
152
|
response = index_now_connection.post("/indexnow") do |request|
|
|
147
153
|
request.headers["Content-Type"] = "application/json"
|
|
148
154
|
request.body = payload.to_json
|
|
@@ -168,30 +174,50 @@ module Indexmap
|
|
|
168
174
|
end
|
|
169
175
|
|
|
170
176
|
def read_api_key
|
|
171
|
-
configured_key = index_now_configuration.key
|
|
177
|
+
configured_key = normalized_configured_key(index_now_configuration.key)
|
|
172
178
|
return configured_key unless configured_key.empty?
|
|
173
179
|
|
|
174
|
-
existing_key_file&.read
|
|
180
|
+
existing_key_file&.read
|
|
175
181
|
end
|
|
176
182
|
|
|
177
183
|
def existing_key_file
|
|
178
184
|
configured_path = index_now_configuration.key_path(public_path: configuration.public_path)
|
|
179
185
|
return configured_path if valid_key_file?(configured_path)
|
|
180
186
|
|
|
181
|
-
configuration.public_path.glob("*.txt").find { |file| valid_key_file?(file) }
|
|
187
|
+
configuration.public_path.glob("*.txt").sort.find { |file| valid_key_file?(file) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def key_location(api_key:)
|
|
191
|
+
path = index_now_configuration.key_path(public_path: configuration.public_path, key: api_key) || existing_key_file
|
|
192
|
+
return unless path
|
|
193
|
+
|
|
194
|
+
public_path = configuration.public_path.expand_path
|
|
195
|
+
key_path = path.expand_path
|
|
196
|
+
relative_path = key_path.relative_path_from(public_path)
|
|
197
|
+
|
|
198
|
+
URI.join("#{host}/", relative_path.to_s).to_s
|
|
199
|
+
rescue ArgumentError
|
|
200
|
+
nil
|
|
182
201
|
end
|
|
183
202
|
|
|
184
203
|
def valid_key_file?(path)
|
|
185
204
|
return false unless path&.file?
|
|
186
205
|
|
|
187
206
|
filename = path.basename(".txt").to_s
|
|
188
|
-
return false unless filename.match?(
|
|
207
|
+
return false unless filename.match?(KEY_FORMAT)
|
|
189
208
|
|
|
190
|
-
path.read
|
|
209
|
+
path.read == filename
|
|
191
210
|
end
|
|
192
211
|
|
|
193
212
|
def generated_key
|
|
194
|
-
SecureRandom.
|
|
213
|
+
SecureRandom.hex(16)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def normalized_configured_key(value)
|
|
217
|
+
key = value.to_s.strip
|
|
218
|
+
return key if key.empty? || key.match?(KEY_FORMAT)
|
|
219
|
+
|
|
220
|
+
raise ConfigurationError, "IndexNow key must be a 32-character lowercase hexadecimal string"
|
|
195
221
|
end
|
|
196
222
|
|
|
197
223
|
def summarize_results(results)
|
data/lib/indexmap/validator.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "date"
|
|
5
|
+
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
3
8
|
module Indexmap
|
|
4
9
|
class Validator
|
|
5
10
|
def initialize(configuration: Indexmap.configuration, path: nil)
|
|
@@ -14,9 +19,15 @@ module Indexmap
|
|
|
14
19
|
)
|
|
15
20
|
raise ValidationError, "Missing sitemap file: #{sitemap_path}" unless File.exist?(sitemap_path)
|
|
16
21
|
|
|
22
|
+
validate_sitemap_file!(sitemap_path)
|
|
17
23
|
entries = Parser.new(path: sitemap_path).entries
|
|
24
|
+
validate_presence!(entries)
|
|
18
25
|
validate_duplicates!(entries)
|
|
19
26
|
validate_parameterized_urls!(entries)
|
|
27
|
+
validate_fragment_urls!(entries)
|
|
28
|
+
validate_absolute_http_urls!(entries)
|
|
29
|
+
validate_same_host_urls!(entries)
|
|
30
|
+
validate_lastmods!(entries)
|
|
20
31
|
true
|
|
21
32
|
end
|
|
22
33
|
|
|
@@ -24,6 +35,65 @@ module Indexmap
|
|
|
24
35
|
|
|
25
36
|
attr_reader :configuration, :path
|
|
26
37
|
|
|
38
|
+
def validate_sitemap_file!(sitemap_path)
|
|
39
|
+
document = read_xml_document(sitemap_path)
|
|
40
|
+
root_name = document.root&.name
|
|
41
|
+
|
|
42
|
+
case root_name
|
|
43
|
+
when "urlset"
|
|
44
|
+
validate_urlset_document!(document, sitemap_path)
|
|
45
|
+
when "sitemapindex"
|
|
46
|
+
validate_sitemap_index_document!(document, sitemap_path)
|
|
47
|
+
else
|
|
48
|
+
raise ValidationError, "Invalid sitemap root element in #{sitemap_path}: #{root_name || "none"}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def read_xml_document(file_path)
|
|
53
|
+
document = Nokogiri::XML(File.read(file_path, encoding: "UTF-8")) { |config| config.strict }
|
|
54
|
+
document.remove_namespaces!
|
|
55
|
+
document
|
|
56
|
+
rescue Nokogiri::XML::SyntaxError => error
|
|
57
|
+
raise ValidationError, "Invalid sitemap XML in #{file_path}: #{error.message.lines.first.strip}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_urlset_document!(document, sitemap_path)
|
|
61
|
+
return if document.xpath("/urlset/url/loc").any?
|
|
62
|
+
|
|
63
|
+
raise ValidationError, "Sitemap has no URLs: #{sitemap_path}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_sitemap_index_document!(document, sitemap_path)
|
|
67
|
+
child_locations = document.xpath("/sitemapindex/sitemap/loc").map { |node| node.text.to_s.strip }.reject(&:empty?)
|
|
68
|
+
raise ValidationError, "Sitemap index has no child sitemap URLs: #{sitemap_path}" if child_locations.empty?
|
|
69
|
+
|
|
70
|
+
duplicate_children = child_locations.group_by(&:itself).select { |_loc, values| values.size > 1 }.keys
|
|
71
|
+
unless duplicate_children.empty?
|
|
72
|
+
raise ValidationError, "Duplicate child sitemap URLs detected: #{duplicate_children.first(5).join(", ")}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
child_locations.each do |location|
|
|
76
|
+
child_path = local_child_path(sitemap_path, location)
|
|
77
|
+
raise ValidationError, "Missing child sitemap file: #{child_path}" unless File.exist?(child_path)
|
|
78
|
+
|
|
79
|
+
validate_sitemap_file!(child_path)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def local_child_path(sitemap_path, location)
|
|
84
|
+
uri = URI.parse(location)
|
|
85
|
+
filename = (uri.absolute? || location.start_with?("/")) ? File.basename(uri.path) : location
|
|
86
|
+
File.expand_path(filename, File.dirname(sitemap_path))
|
|
87
|
+
rescue URI::InvalidURIError
|
|
88
|
+
File.expand_path(location, File.dirname(sitemap_path))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_presence!(entries)
|
|
92
|
+
return unless entries.empty?
|
|
93
|
+
|
|
94
|
+
raise ValidationError, "Sitemap has no URLs"
|
|
95
|
+
end
|
|
96
|
+
|
|
27
97
|
def validate_duplicates!(entries)
|
|
28
98
|
duplicates = entries.map(&:loc).group_by(&:itself).select { |_url, values| values.size > 1 }.keys
|
|
29
99
|
return if duplicates.empty?
|
|
@@ -37,5 +107,55 @@ module Indexmap
|
|
|
37
107
|
|
|
38
108
|
raise ValidationError, "Parameterized sitemap URLs detected: #{param_urls.first(5).join(", ")}"
|
|
39
109
|
end
|
|
110
|
+
|
|
111
|
+
def validate_fragment_urls!(entries)
|
|
112
|
+
fragment_urls = entries.map(&:loc).select { |url| parse_uri(url)&.fragment }
|
|
113
|
+
return if fragment_urls.empty?
|
|
114
|
+
|
|
115
|
+
raise ValidationError, "Fragment sitemap URLs detected: #{fragment_urls.first(5).join(", ")}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_absolute_http_urls!(entries)
|
|
119
|
+
invalid_urls = entries.map(&:loc).reject do |url|
|
|
120
|
+
uri = parse_uri(url)
|
|
121
|
+
uri&.absolute? && %w[http https].include?(uri.scheme)
|
|
122
|
+
end
|
|
123
|
+
return if invalid_urls.empty?
|
|
124
|
+
|
|
125
|
+
raise ValidationError, "Invalid sitemap URLs detected: #{invalid_urls.first(5).join(", ")}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_same_host_urls!(entries)
|
|
129
|
+
base_uri = parse_uri(configuration.base_url)
|
|
130
|
+
return unless base_uri&.host
|
|
131
|
+
|
|
132
|
+
invalid_urls = entries.map(&:loc).reject do |url|
|
|
133
|
+
uri = parse_uri(url)
|
|
134
|
+
uri&.host == base_uri.host && uri&.scheme == base_uri.scheme && uri&.port == base_uri.port
|
|
135
|
+
end
|
|
136
|
+
return if invalid_urls.empty?
|
|
137
|
+
|
|
138
|
+
raise ValidationError, "Sitemap URLs outside configured base URL detected: #{invalid_urls.first(5).join(", ")}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def validate_lastmods!(entries)
|
|
142
|
+
invalid_entries = entries.select do |entry|
|
|
143
|
+
next false if entry.lastmod.nil?
|
|
144
|
+
|
|
145
|
+
Date.iso8601(entry.lastmod)
|
|
146
|
+
false
|
|
147
|
+
rescue ArgumentError
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
return if invalid_entries.empty?
|
|
151
|
+
|
|
152
|
+
raise ValidationError, "Invalid sitemap lastmod values detected: #{invalid_entries.first(5).map(&:loc).join(", ")}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def parse_uri(url)
|
|
156
|
+
URI.parse(url.to_s)
|
|
157
|
+
rescue URI::InvalidURIError
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
40
160
|
end
|
|
41
161
|
end
|
data/lib/indexmap/version.rb
CHANGED
data/lib/indexmap/writer.rb
CHANGED
|
@@ -82,7 +82,8 @@ module Indexmap
|
|
|
82
82
|
sitemap_sections.each do |section|
|
|
83
83
|
lines << " <sitemap>"
|
|
84
84
|
lines << " <loc>#{escape(index_loc(section.filename))}</loc>"
|
|
85
|
-
|
|
85
|
+
lastmod = section_lastmod(section)
|
|
86
|
+
lines << " <lastmod>#{format_lastmod(lastmod)}</lastmod>" if lastmod
|
|
86
87
|
lines << " </sitemap>"
|
|
87
88
|
end
|
|
88
89
|
|
|
@@ -102,7 +103,7 @@ module Indexmap
|
|
|
102
103
|
|
|
103
104
|
def section_lastmod(section)
|
|
104
105
|
timestamps = Array(section.entries).map { |entry| comparable_lastmod(normalize_entry(entry).lastmod) }.compact
|
|
105
|
-
timestamps.max
|
|
106
|
+
timestamps.max
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
def format_lastmod(value)
|
|
@@ -1,35 +1,37 @@
|
|
|
1
|
-
namespace :
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
namespace :indexmap do
|
|
2
|
+
namespace :sitemap do
|
|
3
|
+
desc "Create sitemap files"
|
|
4
|
+
task create: :environment do
|
|
5
|
+
runner = Indexmap::TaskRunner.new
|
|
6
|
+
create_result = runner.create
|
|
7
|
+
runner.format
|
|
8
|
+
validated_files = runner.validate
|
|
9
|
+
|
|
10
|
+
puts "Created, formatted, and validated #{file_count(validated_files)} in #{public_directory(runner)}."
|
|
11
|
+
puts "IndexNow key file: #{create_result[:index_now_key_path]}" if create_result[:index_now_key_path]
|
|
12
|
+
end
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
desc "Format sitemap files for better readability"
|
|
15
|
+
task format: :environment do
|
|
16
|
+
runner = Indexmap::TaskRunner.new
|
|
17
|
+
formatted_files = runner.format
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
puts "Formatted #{file_count(formatted_files)} in #{public_directory(runner)}."
|
|
20
|
+
end
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
desc "Validate sitemap shape and URL hygiene"
|
|
23
|
+
task validate: :environment do
|
|
24
|
+
runner = Indexmap::TaskRunner.new
|
|
25
|
+
validated_files = runner.validate
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
puts "Validated #{file_count(validated_files)} for sitemap shape and URL hygiene."
|
|
28
|
+
end
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
desc "Ping all configured search engines"
|
|
30
32
|
task ping: :environment do
|
|
31
|
-
Rake::Task["
|
|
32
|
-
Rake::Task["
|
|
33
|
+
Rake::Task["indexmap:index_now:ping"].invoke
|
|
34
|
+
Rake::Task["indexmap:google:ping"].invoke
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
namespace :google do
|
|
@@ -8,30 +8,41 @@ class IndexmapConfigurationTest < Minitest::Test
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def test_writer_builds_from_configured_callables
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
Dir.mktmpdir do |dir|
|
|
12
|
+
public_path = Pathname(dir)
|
|
13
|
+
|
|
14
|
+
Indexmap.configure do |config|
|
|
15
|
+
config.base_url = -> { "https://example.com" }
|
|
16
|
+
config.public_path = -> { public_path }
|
|
17
|
+
config.sections = -> do
|
|
18
|
+
[Indexmap::Section.new(filename: "sitemap-pages.xml", entries: [Indexmap::Entry.new(loc: "https://example.com/")])]
|
|
19
|
+
end
|
|
16
20
|
end
|
|
17
|
-
end
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
Indexmap.configuration.writer.write
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
assert_includes public_path.join("sitemap.xml").read, "<loc>https://example.com/sitemap-pages.xml</loc>"
|
|
25
|
+
assert_includes public_path.join("sitemap-pages.xml").read, "<loc>https://example.com/</loc>"
|
|
26
|
+
end
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def test_writer_builds_single_file_writer_from_configured_entries
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
Dir.mktmpdir do |dir|
|
|
31
|
+
public_path = Pathname(dir)
|
|
32
|
+
|
|
33
|
+
Indexmap.configure do |config|
|
|
34
|
+
config.base_url = "https://example.com"
|
|
35
|
+
config.public_path = public_path
|
|
36
|
+
config.format = :single_file
|
|
37
|
+
config.entries = -> { [Indexmap::Entry.new(loc: "https://example.com/")] }
|
|
38
|
+
end
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
Indexmap.configuration.writer.write
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
assert_includes public_path.join("sitemap.xml").read, "<urlset"
|
|
43
|
+
assert_includes public_path.join("sitemap.xml").read, "<loc>https://example.com/</loc>"
|
|
44
|
+
refute public_path.join("sitemap-pages.xml").exist?
|
|
45
|
+
end
|
|
35
46
|
end
|
|
36
47
|
|
|
37
48
|
def test_writer_raises_without_base_url
|
|
@@ -8,13 +8,15 @@ class IndexmapPingerGoogleTest < Minitest::Test
|
|
|
8
8
|
|
|
9
9
|
class FakeWebmastersService
|
|
10
10
|
attr_accessor :authorization
|
|
11
|
-
attr_reader :submitted
|
|
11
|
+
attr_reader :submitted, :list_sites_calls
|
|
12
12
|
|
|
13
13
|
def initialize(site_urls:)
|
|
14
14
|
@site_urls = site_urls
|
|
15
|
+
@list_sites_calls = 0
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def list_sites
|
|
19
|
+
@list_sites_calls += 1
|
|
18
20
|
SiteList.new(@site_urls.map { |site_url| SiteEntry.new(site_url) })
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -51,6 +53,7 @@ class IndexmapPingerGoogleTest < Minitest::Test
|
|
|
51
53
|
assert_equal ["sc-domain:example.com", "https://www.example.com/sitemap.xml"], service.submitted
|
|
52
54
|
assert_equal :submitted, result[:status]
|
|
53
55
|
assert_equal 1, result[:sitemap_count]
|
|
56
|
+
assert_equal 1, service.list_sites_calls
|
|
54
57
|
end
|
|
55
58
|
end
|
|
56
59
|
|
|
@@ -114,4 +117,27 @@ class IndexmapPingerGoogleTest < Minitest::Test
|
|
|
114
117
|
assert_nil service.submitted
|
|
115
118
|
end
|
|
116
119
|
end
|
|
120
|
+
|
|
121
|
+
def test_google_authorization_requires_exact_property_match
|
|
122
|
+
Dir.mktmpdir do |dir|
|
|
123
|
+
public_path = Pathname(dir)
|
|
124
|
+
public_path.join("sitemap.xml").write("<sitemapindex/>")
|
|
125
|
+
|
|
126
|
+
configuration = Indexmap::Configuration.new
|
|
127
|
+
configuration.base_url = "https://www.example.com"
|
|
128
|
+
configuration.public_path = public_path
|
|
129
|
+
configuration.google.credentials = "{\"type\":\"service_account\"}"
|
|
130
|
+
|
|
131
|
+
service = FakeWebmastersService.new(site_urls: ["sc-domain:myexample.com"])
|
|
132
|
+
result = Indexmap::Pinger::Google.new(
|
|
133
|
+
configuration: configuration,
|
|
134
|
+
service: service,
|
|
135
|
+
credentials_builder: ->(**) { :fake_authorizer }
|
|
136
|
+
).ping
|
|
137
|
+
|
|
138
|
+
assert_equal :failed, result[:status]
|
|
139
|
+
assert_equal :unauthorized, result[:failures].first[:reason]
|
|
140
|
+
assert_nil service.submitted
|
|
141
|
+
end
|
|
142
|
+
end
|
|
117
143
|
end
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
require "test_helper"
|
|
4
4
|
|
|
5
5
|
class IndexmapPingerIndexNowTest < Minitest::Test
|
|
6
|
+
VALID_KEY = "1234567890abcdef1234567890abcdef"
|
|
7
|
+
|
|
6
8
|
def test_writes_key_file_from_configuration
|
|
7
9
|
Dir.mktmpdir do |dir|
|
|
8
10
|
configuration = Indexmap::Configuration.new
|
|
9
11
|
configuration.base_url = "https://www.example.com"
|
|
10
12
|
configuration.public_path = Pathname(dir)
|
|
11
|
-
configuration.index_now.key =
|
|
13
|
+
configuration.index_now.key = VALID_KEY
|
|
12
14
|
|
|
13
15
|
path = Indexmap::Pinger::IndexNow.new(configuration: configuration).write_key_file
|
|
14
16
|
|
|
15
|
-
assert_equal Pathname(dir).join("
|
|
16
|
-
assert_equal
|
|
17
|
+
assert_equal Pathname(dir).join("#{VALID_KEY}.txt"), path
|
|
18
|
+
assert_equal VALID_KEY, path.read
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
|
|
@@ -25,16 +27,16 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
25
27
|
|
|
26
28
|
path = Indexmap::Pinger::IndexNow.new(configuration: configuration).ensure_key_file
|
|
27
29
|
|
|
28
|
-
assert_match(/\A[a-
|
|
29
|
-
assert_equal
|
|
30
|
+
assert_match(/\A[a-f0-9]{32}\.txt\z/, path.basename.to_s)
|
|
31
|
+
assert_equal path.basename(".txt").to_s, path.read
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def test_pings_using_existing_key_file_when_key_is_not_configured
|
|
34
36
|
Dir.mktmpdir do |dir|
|
|
35
37
|
public_path = Pathname(dir)
|
|
36
|
-
key_path = public_path.join("
|
|
37
|
-
key_path.write(
|
|
38
|
+
key_path = public_path.join("#{VALID_KEY}.txt")
|
|
39
|
+
key_path.write(VALID_KEY)
|
|
38
40
|
write_sitemap_files(
|
|
39
41
|
public_path,
|
|
40
42
|
marketing_lastmod: "2026-04-18T00:00:00Z",
|
|
@@ -52,7 +54,8 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
52
54
|
|
|
53
55
|
assert_requested(:post, indexnow_url, times: 1) do |request|
|
|
54
56
|
payload = JSON.parse(request.body)
|
|
55
|
-
assert_equal
|
|
57
|
+
assert_equal VALID_KEY, payload.fetch("key")
|
|
58
|
+
assert_equal "https://www.example.com/#{VALID_KEY}.txt", payload.fetch("keyLocation")
|
|
56
59
|
end
|
|
57
60
|
assert_equal :submitted, result[:status]
|
|
58
61
|
assert_equal 2, result[:url_count]
|
|
@@ -60,6 +63,24 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
60
63
|
end
|
|
61
64
|
end
|
|
62
65
|
|
|
66
|
+
def test_ignores_existing_key_file_with_trailing_newline
|
|
67
|
+
Dir.mktmpdir do |dir|
|
|
68
|
+
public_path = Pathname(dir)
|
|
69
|
+
invalid_key_path = public_path.join("1234567890abcdef1234567890abcdef.txt")
|
|
70
|
+
invalid_key_path.write("#{VALID_KEY}\n")
|
|
71
|
+
|
|
72
|
+
configuration = Indexmap::Configuration.new
|
|
73
|
+
configuration.base_url = "https://www.example.com"
|
|
74
|
+
configuration.public_path = public_path
|
|
75
|
+
|
|
76
|
+
path = Indexmap::Pinger::IndexNow.new(configuration: configuration).ensure_key_file
|
|
77
|
+
|
|
78
|
+
refute_equal invalid_key_path, path
|
|
79
|
+
assert_match(/\A[a-f0-9]{32}\.txt\z/, path.basename.to_s)
|
|
80
|
+
assert_equal path.basename(".txt").to_s, path.read
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
63
84
|
def test_pings_all_sitemap_urls_when_no_cutoff_is_provided
|
|
64
85
|
Dir.mktmpdir do |dir|
|
|
65
86
|
public_path = Pathname(dir)
|
|
@@ -72,7 +93,7 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
72
93
|
configuration = Indexmap::Configuration.new
|
|
73
94
|
configuration.base_url = "https://www.example.com"
|
|
74
95
|
configuration.public_path = public_path
|
|
75
|
-
configuration.index_now.key =
|
|
96
|
+
configuration.index_now.key = VALID_KEY
|
|
76
97
|
|
|
77
98
|
indexnow_url = "https://api.indexnow.org/indexnow"
|
|
78
99
|
stub_request(:post, indexnow_url).to_return(status: 200, body: "", headers: {})
|
|
@@ -85,6 +106,7 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
85
106
|
"https://www.example.com/pages/features",
|
|
86
107
|
"https://www.example.com/insights/us/restaurants/overview"
|
|
87
108
|
].sort, payload.fetch("urlList").sort
|
|
109
|
+
assert_equal "https://www.example.com/#{VALID_KEY}.txt", payload.fetch("keyLocation")
|
|
88
110
|
end
|
|
89
111
|
assert_equal :submitted, result[:status]
|
|
90
112
|
assert_equal 2, result[:url_count]
|
|
@@ -104,7 +126,7 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
104
126
|
configuration = Indexmap::Configuration.new
|
|
105
127
|
configuration.base_url = "https://www.example.com"
|
|
106
128
|
configuration.public_path = public_path
|
|
107
|
-
configuration.index_now.key =
|
|
129
|
+
configuration.index_now.key = VALID_KEY
|
|
108
130
|
|
|
109
131
|
indexnow_url = "https://api.indexnow.org/indexnow"
|
|
110
132
|
stub_request(:post, indexnow_url).to_return(status: 200, body: "", headers: {})
|
|
@@ -155,7 +177,7 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
155
177
|
configuration = Indexmap::Configuration.new
|
|
156
178
|
configuration.base_url = "https://www.example.com"
|
|
157
179
|
configuration.public_path = public_path
|
|
158
|
-
configuration.index_now.key =
|
|
180
|
+
configuration.index_now.key = VALID_KEY
|
|
159
181
|
|
|
160
182
|
with_env("INDEXNOW_DRY_RUN" => "1") do
|
|
161
183
|
result = Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
|
|
@@ -179,7 +201,7 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
179
201
|
configuration = Indexmap::Configuration.new
|
|
180
202
|
configuration.base_url = "https://www.example.com"
|
|
181
203
|
configuration.public_path = public_path
|
|
182
|
-
configuration.index_now.key =
|
|
204
|
+
configuration.index_now.key = VALID_KEY
|
|
183
205
|
|
|
184
206
|
indexnow_url = "https://api.indexnow.org/indexnow"
|
|
185
207
|
stub_request(:post, indexnow_url).to_return(status: 500, body: "boom", headers: {})
|
|
@@ -192,6 +214,37 @@ class IndexmapPingerIndexNowTest < Minitest::Test
|
|
|
192
214
|
end
|
|
193
215
|
end
|
|
194
216
|
|
|
217
|
+
def test_rejects_invalid_configured_key
|
|
218
|
+
Dir.mktmpdir do |dir|
|
|
219
|
+
configuration = Indexmap::Configuration.new
|
|
220
|
+
configuration.base_url = "https://www.example.com"
|
|
221
|
+
configuration.public_path = Pathname(dir)
|
|
222
|
+
configuration.index_now.key = "test-key"
|
|
223
|
+
|
|
224
|
+
error = assert_raises(Indexmap::ConfigurationError) do
|
|
225
|
+
Indexmap::Pinger::IndexNow.new(configuration: configuration).ping
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
assert_equal "IndexNow key must be a 32-character lowercase hexadecimal string", error.message
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def test_reuses_existing_key_file_deterministically
|
|
233
|
+
Dir.mktmpdir do |dir|
|
|
234
|
+
public_path = Pathname(dir)
|
|
235
|
+
public_path.join("ffffffffffffffffffffffffffffffff.txt").write("ffffffffffffffffffffffffffffffff")
|
|
236
|
+
public_path.join("00000000000000000000000000000000.txt").write("00000000000000000000000000000000")
|
|
237
|
+
|
|
238
|
+
configuration = Indexmap::Configuration.new
|
|
239
|
+
configuration.base_url = "https://www.example.com"
|
|
240
|
+
configuration.public_path = public_path
|
|
241
|
+
|
|
242
|
+
path = Indexmap::Pinger::IndexNow.new(configuration: configuration).ensure_key_file
|
|
243
|
+
|
|
244
|
+
assert_equal public_path.join("00000000000000000000000000000000.txt"), path
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
195
248
|
private
|
|
196
249
|
|
|
197
250
|
def with_env(overrides)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "test_helper"
|
|
4
4
|
|
|
5
5
|
class IndexmapTaskRunnerTest < Minitest::Test
|
|
6
|
+
VALID_KEY = "1234567890abcdef1234567890abcdef"
|
|
7
|
+
|
|
6
8
|
def test_create_removes_existing_sitemap_files_writes_new_sitemap_and_key_file
|
|
7
9
|
Dir.mktmpdir do |dir|
|
|
8
10
|
public_path = Pathname(dir)
|
|
@@ -18,15 +20,15 @@ class IndexmapTaskRunnerTest < Minitest::Test
|
|
|
18
20
|
entries: [Indexmap::Entry.new(loc: "https://example.com/about")]
|
|
19
21
|
)
|
|
20
22
|
]
|
|
21
|
-
configuration.index_now.key =
|
|
23
|
+
configuration.index_now.key = VALID_KEY
|
|
22
24
|
|
|
23
25
|
result = Indexmap::TaskRunner.new(configuration: configuration).create
|
|
24
26
|
|
|
25
27
|
assert_equal false, public_path.join("sitemap-pages.xml.gz").exist?
|
|
26
28
|
assert_includes public_path.join("sitemap.xml").read, "<sitemapindex"
|
|
27
|
-
assert_equal
|
|
29
|
+
assert_equal VALID_KEY, public_path.join("#{VALID_KEY}.txt").read
|
|
28
30
|
assert_equal [public_path.join("sitemap-pages.xml").to_s, public_path.join("sitemap.xml").to_s], result[:files]
|
|
29
|
-
assert_equal public_path.join("
|
|
31
|
+
assert_equal public_path.join("#{VALID_KEY}.txt"), result[:index_now_key_path]
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
@@ -56,8 +58,8 @@ class IndexmapTaskRunnerTest < Minitest::Test
|
|
|
56
58
|
|
|
57
59
|
result = Indexmap::TaskRunner.new(configuration: configuration).write_index_now_key(generate_if_missing: true)
|
|
58
60
|
|
|
59
|
-
assert_match(/\A[a-
|
|
60
|
-
assert_equal
|
|
61
|
+
assert_match(/\A[a-f0-9]{32}\.txt\z/, result.basename.to_s)
|
|
62
|
+
assert_equal result.basename(".txt").to_s, result.read
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
end
|
|
@@ -52,6 +52,120 @@ class IndexmapValidatorTest < Minitest::Test
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
def test_validate_raises_for_fragment_urls
|
|
56
|
+
Dir.mktmpdir do |directory|
|
|
57
|
+
path = Pathname(directory).join("sitemap.xml")
|
|
58
|
+
path.write(<<~XML)
|
|
59
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
60
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
61
|
+
<url><loc>https://example.com/about#team</loc></url>
|
|
62
|
+
</urlset>
|
|
63
|
+
XML
|
|
64
|
+
|
|
65
|
+
error = assert_raises(Indexmap::ValidationError) do
|
|
66
|
+
Indexmap::Validator.new(path: path).validate!
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
assert_equal "Fragment sitemap URLs detected: https://example.com/about#team", error.message
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_validate_raises_for_relative_urls
|
|
74
|
+
Dir.mktmpdir do |directory|
|
|
75
|
+
path = Pathname(directory).join("sitemap.xml")
|
|
76
|
+
path.write(<<~XML)
|
|
77
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
78
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
79
|
+
<url><loc>/about</loc></url>
|
|
80
|
+
</urlset>
|
|
81
|
+
XML
|
|
82
|
+
|
|
83
|
+
error = assert_raises(Indexmap::ValidationError) do
|
|
84
|
+
Indexmap::Validator.new(path: path).validate!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
assert_equal "Invalid sitemap URLs detected: /about", error.message
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_validate_raises_for_urls_outside_configured_base_url
|
|
92
|
+
Dir.mktmpdir do |directory|
|
|
93
|
+
path = Pathname(directory).join("sitemap.xml")
|
|
94
|
+
path.write(<<~XML)
|
|
95
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
96
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
97
|
+
<url><loc>https://other.example.com/about</loc></url>
|
|
98
|
+
</urlset>
|
|
99
|
+
XML
|
|
100
|
+
|
|
101
|
+
configuration = Indexmap::Configuration.new
|
|
102
|
+
configuration.base_url = "https://example.com"
|
|
103
|
+
|
|
104
|
+
error = assert_raises(Indexmap::ValidationError) do
|
|
105
|
+
Indexmap::Validator.new(configuration: configuration, path: path).validate!
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
assert_equal "Sitemap URLs outside configured base URL detected: https://other.example.com/about", error.message
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_validate_raises_for_invalid_lastmod_values
|
|
113
|
+
Dir.mktmpdir do |directory|
|
|
114
|
+
path = Pathname(directory).join("sitemap.xml")
|
|
115
|
+
path.write(<<~XML)
|
|
116
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
117
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
118
|
+
<url>
|
|
119
|
+
<loc>https://example.com/about</loc>
|
|
120
|
+
<lastmod>not-a-date</lastmod>
|
|
121
|
+
</url>
|
|
122
|
+
</urlset>
|
|
123
|
+
XML
|
|
124
|
+
|
|
125
|
+
error = assert_raises(Indexmap::ValidationError) do
|
|
126
|
+
Indexmap::Validator.new(path: path).validate!
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
assert_equal "Invalid sitemap lastmod values detected: https://example.com/about", error.message
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_validate_raises_for_empty_sitemaps
|
|
134
|
+
Dir.mktmpdir do |directory|
|
|
135
|
+
path = Pathname(directory).join("sitemap.xml")
|
|
136
|
+
path.write(<<~XML)
|
|
137
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
138
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
139
|
+
</urlset>
|
|
140
|
+
XML
|
|
141
|
+
|
|
142
|
+
error = assert_raises(Indexmap::ValidationError) do
|
|
143
|
+
Indexmap::Validator.new(path: path).validate!
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
assert_equal "Sitemap has no URLs: #{path}", error.message
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_validate_raises_for_missing_child_sitemap_files
|
|
151
|
+
Dir.mktmpdir do |directory|
|
|
152
|
+
path = Pathname(directory).join("sitemap.xml")
|
|
153
|
+
child_path = Pathname(directory).join("sitemap-pages.xml")
|
|
154
|
+
path.write(<<~XML)
|
|
155
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
156
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
157
|
+
<sitemap><loc>https://example.com/sitemap-pages.xml</loc></sitemap>
|
|
158
|
+
</sitemapindex>
|
|
159
|
+
XML
|
|
160
|
+
|
|
161
|
+
error = assert_raises(Indexmap::ValidationError) do
|
|
162
|
+
Indexmap::Validator.new(path: path).validate!
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
assert_equal "Missing child sitemap file: #{child_path}", error.message
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
55
169
|
def test_validate_passes_for_valid_sitemap
|
|
56
170
|
Dir.mktmpdir do |directory|
|
|
57
171
|
path = Pathname(directory).join("sitemap.xml")
|
|
@@ -77,4 +77,23 @@ class IndexmapWriterTest < Minitest::Test
|
|
|
77
77
|
refute File.exist?(File.join(directory, "sitemap-pages.xml"))
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
|
+
|
|
81
|
+
def test_omits_sitemap_index_lastmod_when_sections_have_no_lastmod
|
|
82
|
+
Dir.mktmpdir do |directory|
|
|
83
|
+
Indexmap::Writer.new(
|
|
84
|
+
sections: [
|
|
85
|
+
Indexmap::Section.new(
|
|
86
|
+
filename: "sitemap-pages.xml",
|
|
87
|
+
entries: [Indexmap::Entry.new(loc: "https://example.com/about")]
|
|
88
|
+
)
|
|
89
|
+
],
|
|
90
|
+
public_path: directory,
|
|
91
|
+
base_url: "https://example.com"
|
|
92
|
+
).write
|
|
93
|
+
|
|
94
|
+
index_xml = File.read(File.join(directory, "sitemap.xml"))
|
|
95
|
+
|
|
96
|
+
refute_includes index_xml, "<lastmod>"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
80
99
|
end
|