indexmap 0.5.0 → 0.6.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 +5 -3
- data/README.md +83 -1
- data/lib/indexmap/configuration.rb +24 -20
- data/lib/indexmap/creator.rb +75 -0
- data/lib/indexmap/output.rb +74 -0
- data/lib/indexmap/parser.rb +0 -1
- data/lib/indexmap/path.rb +0 -1
- data/lib/indexmap/pinger/google.rb +16 -5
- data/lib/indexmap/task_runner.rb +9 -9
- data/lib/indexmap/version.rb +1 -1
- data/lib/indexmap/writer.rb +13 -5
- data/lib/indexmap.rb +6 -1
- data/lib/tasks/indexmap_tasks.rake +3 -5
- data/test/indexmap/configuration_test.rb +134 -0
- data/test/indexmap/pinger/google_test.rb +52 -1
- data/test/indexmap/task_runner_test.rb +29 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef452a5928b87f84f65ecb9ba2afcab5340d9d09c5d8c8dfd8c4ffb3217a68fd
|
|
4
|
+
data.tar.gz: 03b11e1d9360bbd797d6b61886c99d1619247877dfd2c4676efeca9a1119764c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a04a772029e2438636df90b65004a27c54e1cf3360205fd556feff615768fd527668fe2a3e3063f2831b9af9afb4edd983e48e73b922278c4c10a63d936b938e
|
|
7
|
+
data.tar.gz: 6ce68d9ee6343aaf7c4bbff6860a712d743899390b4a8a3a09c712694c95cff32cc0acd85f30a43e2ba37f0c8ff671d2af11028e63976649ae90e5100d5e0a1b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,12 +5,14 @@ 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.6.0] - 2026-05-01
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### Added
|
|
12
12
|
|
|
13
|
-
-
|
|
13
|
+
- add the url count to google ping output (#9)
|
|
14
|
+
|
|
15
|
+
- support named sitemap outputs (#10)
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
|
data/README.md
CHANGED
|
@@ -85,7 +85,10 @@ bin/rails indexmap:sitemap:format
|
|
|
85
85
|
bin/rails indexmap:sitemap:validate
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
`indexmap:sitemap:create` is the main task. It writes sitemap files
|
|
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.
|
|
89
92
|
|
|
90
93
|
### Default Index Mode
|
|
91
94
|
|
|
@@ -114,6 +117,85 @@ end
|
|
|
114
117
|
|
|
115
118
|
In `:single_file` mode, `indexmap` writes a `urlset` directly to `sitemap.xml` and reads entries from `config.entries` instead of `config.sections`.
|
|
116
119
|
|
|
120
|
+
### Named Outputs
|
|
121
|
+
|
|
122
|
+
Most apps only need the default output. Use named outputs when one part of the
|
|
123
|
+
sitemap must be generated separately, for example when static pages can be
|
|
124
|
+
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.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
Indexmap.configure do |config|
|
|
130
|
+
config.base_url = -> { "https://example.com" }
|
|
131
|
+
config.public_path = -> { Rails.root.join("storage/sitemaps") }
|
|
132
|
+
config.sections = -> { Sitemap.sections }
|
|
133
|
+
|
|
134
|
+
config.output :insights_data do |output|
|
|
135
|
+
output.format = :single_file
|
|
136
|
+
output.index_filename = "sitemap-insights-data.xml"
|
|
137
|
+
output.entries = -> { Sitemap.insights_data_entries }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Generate the default output:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
Indexmap.create
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Generate only the named output:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
Indexmap.create(:insights_data)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Named outputs inherit `base_url`, `public_path`, and `format` from the main
|
|
155
|
+
configuration unless you override them.
|
|
156
|
+
|
|
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.
|
|
160
|
+
|
|
161
|
+
### Deferred Dynamic Sections
|
|
162
|
+
|
|
163
|
+
Use `after_create` when `indexmap:sitemap:create` should publish the default
|
|
164
|
+
sitemap first, then schedule slower dynamic sections for the background. The
|
|
165
|
+
callback runs only after the generated files have been formatted, validated, and
|
|
166
|
+
replaced successfully.
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
Indexmap.configure do |config|
|
|
170
|
+
config.base_url = -> { "https://example.com" }
|
|
171
|
+
config.public_path = -> { Rails.root.join("storage/sitemaps") }
|
|
172
|
+
config.sections = -> { Sitemap.sections }
|
|
173
|
+
|
|
174
|
+
config.output :insights_data do |output|
|
|
175
|
+
output.format = :single_file
|
|
176
|
+
output.index_filename = "sitemap-insights-data.xml"
|
|
177
|
+
output.entries = -> { Sitemap.insights_data_entries }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
config.after_create do
|
|
181
|
+
Insights::SitemapRefreshJob.perform_later
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Then the job can stay small:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
class Insights::SitemapRefreshJob < ApplicationJob
|
|
190
|
+
def perform
|
|
191
|
+
Indexmap.create(:insights_data)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This keeps deploys fast: the deploy only waits for `indexmap:sitemap:create`,
|
|
197
|
+
while database-dependent output is refreshed by the job backend.
|
|
198
|
+
|
|
117
199
|
## Validation And Parsing
|
|
118
200
|
|
|
119
201
|
`indexmap` also includes small utilities for working with generated sitemap files:
|
|
@@ -9,6 +9,8 @@ module Indexmap
|
|
|
9
9
|
def initialize
|
|
10
10
|
@format = :index
|
|
11
11
|
@index_filename = "sitemap.xml"
|
|
12
|
+
@after_create_callbacks = []
|
|
13
|
+
@outputs = {}
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def base_url
|
|
@@ -47,27 +49,29 @@ module Indexmap
|
|
|
47
49
|
Array(resolve(@sections))
|
|
48
50
|
end
|
|
49
51
|
|
|
52
|
+
def output(name)
|
|
53
|
+
output = output_for(name)
|
|
54
|
+
yield(output) if block_given?
|
|
55
|
+
output
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def output_for(name = :default)
|
|
59
|
+
normalized_name = name.to_sym
|
|
60
|
+
@outputs[normalized_name] ||= Output.new(configuration: self)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def after_create(&block)
|
|
64
|
+
raise ArgumentError, "after_create requires a block" unless block
|
|
65
|
+
|
|
66
|
+
@after_create_callbacks << block
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def run_after_create_callbacks
|
|
70
|
+
@after_create_callbacks.each(&:call)
|
|
71
|
+
end
|
|
72
|
+
|
|
50
73
|
def writer
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
unless VALID_FORMATS.include?(format)
|
|
54
|
-
raise ConfigurationError, "Indexmap format must be one of: #{VALID_FORMATS.join(", ")}"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
if format == :single_file
|
|
58
|
-
raise ConfigurationError, "Indexmap entries are not configured" if entries.empty?
|
|
59
|
-
elsif sections.empty?
|
|
60
|
-
raise ConfigurationError, "Indexmap sections are not configured" if sections.empty?
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
Writer.new(
|
|
64
|
-
entries: entries,
|
|
65
|
-
format: format,
|
|
66
|
-
sections: sections,
|
|
67
|
-
public_path: public_path,
|
|
68
|
-
base_url: base_url,
|
|
69
|
-
index_filename: index_filename
|
|
70
|
-
)
|
|
74
|
+
output_for(:default).writer
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
private
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Indexmap
|
|
7
|
+
class Creator
|
|
8
|
+
ValidationConfiguration = Struct.new(:base_url, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def initialize(output:)
|
|
11
|
+
@output = output
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
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
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :output
|
|
33
|
+
|
|
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
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def format(files)
|
|
45
|
+
files.each do |file_path|
|
|
46
|
+
document = Nokogiri::XML(
|
|
47
|
+
file_path.read,
|
|
48
|
+
nil,
|
|
49
|
+
nil,
|
|
50
|
+
Nokogiri::XML::ParseOptions::DEFAULT_XML | Nokogiri::XML::ParseOptions::NOBLANKS
|
|
51
|
+
)
|
|
52
|
+
save_options = Nokogiri::XML::Node::SaveOptions::FORMAT | Nokogiri::XML::Node::SaveOptions::AS_XML
|
|
53
|
+
|
|
54
|
+
file_path.write(document.to_xml(indent: 2, save_with: save_options))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate(index_path)
|
|
59
|
+
Validator.new(
|
|
60
|
+
configuration: ValidationConfiguration.new(base_url: output.base_url),
|
|
61
|
+
path: index_path
|
|
62
|
+
).validate!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
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
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Indexmap
|
|
4
|
+
class Output
|
|
5
|
+
VALID_FORMATS = %i[index single_file].freeze
|
|
6
|
+
|
|
7
|
+
attr_writer :base_url, :entries, :format, :index_filename, :public_path, :sections
|
|
8
|
+
|
|
9
|
+
def initialize(configuration:)
|
|
10
|
+
@configuration = configuration
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def base_url
|
|
14
|
+
resolve(@base_url) || configuration.base_url
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def entries
|
|
18
|
+
resolved_entries = resolve(@entries)
|
|
19
|
+
|
|
20
|
+
Array(resolved_entries.nil? ? configuration.entries : resolved_entries)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def format
|
|
24
|
+
value = resolve(@format) || configuration.format
|
|
25
|
+
value.nil? ? :index : value.to_sym
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def index_filename
|
|
29
|
+
resolve(@index_filename) || configuration.index_filename
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def public_path
|
|
33
|
+
value = resolve(@public_path) || configuration.public_path
|
|
34
|
+
Pathname(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def sections
|
|
38
|
+
resolved_sections = resolve(@sections)
|
|
39
|
+
|
|
40
|
+
Array(resolved_sections.nil? ? configuration.sections : resolved_sections)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def writer
|
|
44
|
+
raise ConfigurationError, "Indexmap base_url is not configured" if base_url.to_s.strip.empty?
|
|
45
|
+
|
|
46
|
+
unless VALID_FORMATS.include?(format)
|
|
47
|
+
raise ConfigurationError, "Indexmap format must be one of: #{VALID_FORMATS.join(", ")}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if format == :single_file
|
|
51
|
+
raise ConfigurationError, "Indexmap entries are not configured" if entries.empty?
|
|
52
|
+
elsif sections.empty?
|
|
53
|
+
raise ConfigurationError, "Indexmap sections are not configured" if sections.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Writer.new(
|
|
57
|
+
entries: entries,
|
|
58
|
+
format: format,
|
|
59
|
+
sections: sections,
|
|
60
|
+
public_path: public_path,
|
|
61
|
+
base_url: base_url,
|
|
62
|
+
index_filename: index_filename
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
attr_reader :configuration
|
|
69
|
+
|
|
70
|
+
def resolve(value)
|
|
71
|
+
value.respond_to?(:call) ? value.call : value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/indexmap/parser.rb
CHANGED
data/lib/indexmap/path.rb
CHANGED
|
@@ -20,10 +20,11 @@ module Indexmap
|
|
|
20
20
|
return {status: :skipped, reason: :missing_credentials}
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
files = sitemap_files
|
|
24
|
+
results = files.map { |sitemap_file| ping_sitemap(sitemap_file) }
|
|
24
25
|
return {status: :skipped, reason: :no_sitemaps} if results.empty?
|
|
25
26
|
|
|
26
|
-
summarize_results(results)
|
|
27
|
+
summarize_results(results, url_count: sitemap_url_count(files))
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
private
|
|
@@ -90,16 +91,26 @@ module Indexmap
|
|
|
90
91
|
)
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def
|
|
94
|
+
def sitemap_url_count(files)
|
|
95
|
+
files.each_with_object(Set.new) do |sitemap_file, urls|
|
|
96
|
+
Parser.new(path: sitemap_file).entries.each do |entry|
|
|
97
|
+
loc = entry.loc.to_s.strip
|
|
98
|
+
urls.add(loc) unless loc.empty?
|
|
99
|
+
end
|
|
100
|
+
end.count
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def summarize_results(results, url_count:)
|
|
94
104
|
submitted = results.select { |result| result[:status] == :submitted }
|
|
95
105
|
failures = results.select { |result| result[:status] == :failed }
|
|
96
106
|
|
|
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?
|
|
107
|
+
return {status: :submitted, sitemap_count: submitted.count, url_count: url_count, submitted: submitted} if failures.empty?
|
|
108
|
+
return {status: :failed, sitemap_count: 0, url_count: 0, failures: failures} if submitted.empty?
|
|
99
109
|
|
|
100
110
|
{
|
|
101
111
|
status: :partial,
|
|
102
112
|
sitemap_count: submitted.count,
|
|
113
|
+
url_count: url_count,
|
|
103
114
|
submitted: submitted,
|
|
104
115
|
failures: failures
|
|
105
116
|
}
|
data/lib/indexmap/task_runner.rb
CHANGED
|
@@ -9,9 +9,11 @@ module Indexmap
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def create
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
written_files = Indexmap.create(configuration: configuration)
|
|
13
|
+
index_now_key_path = write_index_now_key
|
|
14
|
+
configuration.run_after_create_callbacks
|
|
15
|
+
|
|
16
|
+
{files: written_files.map(&:to_s), written_files: written_files, index_now_key_path: index_now_key_path}
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def format
|
|
@@ -44,21 +46,19 @@ module Indexmap
|
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
def public_path
|
|
47
|
-
|
|
49
|
+
default_output.public_path
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
private
|
|
51
53
|
|
|
52
54
|
attr_reader :configuration
|
|
53
55
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
File.delete(file_path)
|
|
57
|
-
end
|
|
56
|
+
def default_output
|
|
57
|
+
configuration.output_for(:default)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def sitemap_files
|
|
61
|
-
Dir.glob(
|
|
61
|
+
Dir.glob(public_path.join("sitemap*.xml")).sort
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
end
|
data/lib/indexmap/version.rb
CHANGED
data/lib/indexmap/writer.rb
CHANGED
|
@@ -16,18 +16,20 @@ module Indexmap
|
|
|
16
16
|
def write
|
|
17
17
|
FileUtils.mkdir_p(public_path)
|
|
18
18
|
|
|
19
|
-
return
|
|
19
|
+
return [write_file(index_filename, urlset_xml(entries))] if single_file?
|
|
20
20
|
|
|
21
|
-
sections.
|
|
22
|
-
|
|
21
|
+
paths = sections.map do |section|
|
|
22
|
+
write_file(section.filename, urlset_xml(section.entries))
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
paths + [write_file(index_filename, index_xml(sections))]
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
attr_accessor :public_path
|
|
29
|
+
|
|
28
30
|
private
|
|
29
31
|
|
|
30
|
-
attr_reader :base_url, :entries, :format, :index_filename, :
|
|
32
|
+
attr_reader :base_url, :entries, :format, :index_filename, :sections
|
|
31
33
|
|
|
32
34
|
def normalize_entries(raw_entries)
|
|
33
35
|
Array(raw_entries).map { |entry| normalize_entry(entry) }
|
|
@@ -55,6 +57,12 @@ module Indexmap
|
|
|
55
57
|
format == :single_file
|
|
56
58
|
end
|
|
57
59
|
|
|
60
|
+
def write_file(filename, body)
|
|
61
|
+
path = public_path.join(filename)
|
|
62
|
+
path.write(body)
|
|
63
|
+
path
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
def urlset_xml(entries)
|
|
59
67
|
lines = [
|
|
60
68
|
%(<?xml version="1.0" encoding="UTF-8"?>),
|
data/lib/indexmap.rb
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
require "cgi"
|
|
4
4
|
require "date"
|
|
5
5
|
require "fileutils"
|
|
6
|
-
require "pathname"
|
|
7
6
|
require "time"
|
|
8
7
|
|
|
9
8
|
require_relative "indexmap/version"
|
|
10
9
|
require_relative "indexmap/google_configuration"
|
|
11
10
|
require_relative "indexmap/index_now_configuration"
|
|
12
11
|
require_relative "indexmap/configuration"
|
|
12
|
+
require_relative "indexmap/creator"
|
|
13
13
|
require_relative "indexmap/entry"
|
|
14
|
+
require_relative "indexmap/output"
|
|
14
15
|
require_relative "indexmap/path"
|
|
15
16
|
require_relative "indexmap/parser"
|
|
16
17
|
require_relative "indexmap/pinger/base"
|
|
@@ -39,6 +40,10 @@ module Indexmap
|
|
|
39
40
|
def reset!
|
|
40
41
|
@configuration = Configuration.new
|
|
41
42
|
end
|
|
43
|
+
|
|
44
|
+
def create(output_name = :default, configuration: self.configuration)
|
|
45
|
+
Creator.new(output: configuration.output_for(output_name)).create
|
|
46
|
+
end
|
|
42
47
|
end
|
|
43
48
|
end
|
|
44
49
|
|
|
@@ -4,10 +4,8 @@ namespace :indexmap do
|
|
|
4
4
|
task create: :environment do
|
|
5
5
|
runner = Indexmap::TaskRunner.new
|
|
6
6
|
create_result = runner.create
|
|
7
|
-
runner.format
|
|
8
|
-
validated_files = runner.validate
|
|
9
7
|
|
|
10
|
-
puts "Created, formatted, and validated #{file_count(
|
|
8
|
+
puts "Created, formatted, and validated #{file_count(create_result[:files])} in #{public_directory(runner)}."
|
|
11
9
|
puts "IndexNow key file: #{create_result[:index_now_key_path]}" if create_result[:index_now_key_path]
|
|
12
10
|
end
|
|
13
11
|
|
|
@@ -41,9 +39,9 @@ namespace :indexmap do
|
|
|
41
39
|
|
|
42
40
|
case result[:status]
|
|
43
41
|
when :submitted
|
|
44
|
-
puts "Submitted #{result[:sitemap_count]} sitemap #{(result[:sitemap_count] == 1) ? "file" : "files"} to Google Search Console."
|
|
42
|
+
puts "Submitted #{result[:sitemap_count]} sitemap #{(result[:sitemap_count] == 1) ? "file" : "files"} with #{result[:url_count]} URL#{"s" unless result[:url_count] == 1} to Google Search Console."
|
|
45
43
|
when :partial
|
|
46
|
-
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}."
|
|
44
|
+
puts "Submitted #{result[:sitemap_count]} sitemap #{(result[:sitemap_count] == 1) ? "file" : "files"} with #{result[:url_count]} URL#{"s" unless result[:url_count] == 1} to Google Search Console, with #{result[:failures].count} failure#{"s" unless result[:failures].count == 1}."
|
|
47
45
|
result[:failures].each { |failure| puts format_google_ping_failure(failure) }
|
|
48
46
|
when :failed
|
|
49
47
|
result[:failures].each { |failure| puts format_google_ping_failure(failure) }
|
|
@@ -91,4 +91,138 @@ class IndexmapConfigurationTest < Minitest::Test
|
|
|
91
91
|
assert_equal "example-key", Indexmap.configuration.index_now.key
|
|
92
92
|
assert_equal 250, Indexmap.configuration.index_now.max_urls_per_request
|
|
93
93
|
end
|
|
94
|
+
|
|
95
|
+
def test_named_outputs_inherit_configuration_defaults
|
|
96
|
+
Dir.mktmpdir do |dir|
|
|
97
|
+
public_path = Pathname(dir)
|
|
98
|
+
|
|
99
|
+
Indexmap.configure do |config|
|
|
100
|
+
config.base_url = "https://example.com"
|
|
101
|
+
config.public_path = public_path
|
|
102
|
+
config.output :reports do |output|
|
|
103
|
+
output.sections = [
|
|
104
|
+
Indexmap::Section.new(
|
|
105
|
+
filename: "sitemap-reports.xml",
|
|
106
|
+
entries: [Indexmap::Entry.new(loc: "https://example.com/reports")]
|
|
107
|
+
)
|
|
108
|
+
]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
files = Indexmap.create(:reports)
|
|
113
|
+
|
|
114
|
+
assert_equal [
|
|
115
|
+
public_path.join("sitemap-reports.xml"),
|
|
116
|
+
public_path.join("sitemap.xml")
|
|
117
|
+
], files
|
|
118
|
+
assert_includes public_path.join("sitemap.xml").read, "https://example.com/sitemap-reports.xml"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_create_writes_named_output_to_public_path
|
|
123
|
+
Dir.mktmpdir do |dir|
|
|
124
|
+
public_path = Pathname(dir)
|
|
125
|
+
|
|
126
|
+
Indexmap.configure do |config|
|
|
127
|
+
config.base_url = "https://example.com"
|
|
128
|
+
config.public_path = public_path
|
|
129
|
+
config.output :dynamic do |output|
|
|
130
|
+
output.sections = [
|
|
131
|
+
Indexmap::Section.new(
|
|
132
|
+
filename: "sitemap-dynamic.xml",
|
|
133
|
+
entries: [Indexmap::Entry.new(loc: "https://example.com/dynamic")]
|
|
134
|
+
)
|
|
135
|
+
]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
files = Indexmap.create(:dynamic)
|
|
140
|
+
|
|
141
|
+
assert_equal [
|
|
142
|
+
public_path.join("sitemap-dynamic.xml"),
|
|
143
|
+
public_path.join("sitemap.xml")
|
|
144
|
+
], files
|
|
145
|
+
assert_includes public_path.join("sitemap-dynamic.xml").read, "https://example.com/dynamic"
|
|
146
|
+
assert_includes public_path.join("sitemap.xml").read, "https://example.com/sitemap-dynamic.xml"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_create_preserves_existing_files_when_validation_fails
|
|
151
|
+
Dir.mktmpdir do |dir|
|
|
152
|
+
public_path = Pathname(dir)
|
|
153
|
+
public_path.join("sitemap.xml").write("old index")
|
|
154
|
+
public_path.join("sitemap-pages.xml").write("old child")
|
|
155
|
+
|
|
156
|
+
Indexmap.configure do |config|
|
|
157
|
+
config.base_url = "https://example.com"
|
|
158
|
+
config.public_path = public_path
|
|
159
|
+
config.sections = [
|
|
160
|
+
Indexmap::Section.new(
|
|
161
|
+
filename: "sitemap-pages.xml",
|
|
162
|
+
entries: [Indexmap::Entry.new(loc: "https://example.com/about?utm_source=test")]
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
error = assert_raises(Indexmap::ValidationError) { Indexmap.create }
|
|
168
|
+
|
|
169
|
+
assert_match "Parameterized sitemap URLs detected", error.message
|
|
170
|
+
assert_equal "old index", public_path.join("sitemap.xml").read
|
|
171
|
+
assert_equal "old child", public_path.join("sitemap-pages.xml").read
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def test_create_writes_single_file_named_output_without_default_index
|
|
176
|
+
Dir.mktmpdir do |dir|
|
|
177
|
+
public_path = Pathname(dir)
|
|
178
|
+
|
|
179
|
+
Indexmap.configure do |config|
|
|
180
|
+
config.base_url = "https://example.com"
|
|
181
|
+
config.public_path = public_path
|
|
182
|
+
config.output :dynamic do |output|
|
|
183
|
+
output.format = :single_file
|
|
184
|
+
output.index_filename = "sitemap-dynamic.xml"
|
|
185
|
+
output.entries = [
|
|
186
|
+
Indexmap::Entry.new(loc: "https://example.com/dynamic")
|
|
187
|
+
]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
files = Indexmap.create(:dynamic)
|
|
192
|
+
|
|
193
|
+
assert_equal [public_path.join("sitemap-dynamic.xml")], files
|
|
194
|
+
refute public_path.join("sitemap.xml").exist?
|
|
195
|
+
assert_includes public_path.join("sitemap-dynamic.xml").read, "https://example.com/dynamic"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_create_preserves_existing_named_output_when_validation_fails
|
|
200
|
+
Dir.mktmpdir do |dir|
|
|
201
|
+
public_path = Pathname(dir)
|
|
202
|
+
public_path.join("sitemap-dynamic.xml").write("old dynamic")
|
|
203
|
+
|
|
204
|
+
Indexmap.configure do |config|
|
|
205
|
+
config.base_url = "https://example.com"
|
|
206
|
+
config.public_path = public_path
|
|
207
|
+
config.output :dynamic do |output|
|
|
208
|
+
output.format = :single_file
|
|
209
|
+
output.index_filename = "sitemap-dynamic.xml"
|
|
210
|
+
output.entries = [
|
|
211
|
+
Indexmap::Entry.new(loc: "https://example.com/dynamic?utm_source=test")
|
|
212
|
+
]
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
error = assert_raises(Indexmap::ValidationError) { Indexmap.create(:dynamic) }
|
|
217
|
+
|
|
218
|
+
assert_match "Parameterized sitemap URLs detected", error.message
|
|
219
|
+
assert_equal "old dynamic", public_path.join("sitemap-dynamic.xml").read
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_after_create_requires_a_block
|
|
224
|
+
error = assert_raises(ArgumentError) { Indexmap.configuration.after_create }
|
|
225
|
+
|
|
226
|
+
assert_equal "after_create requires a block", error.message
|
|
227
|
+
end
|
|
94
228
|
end
|
|
@@ -8,10 +8,11 @@ class IndexmapPingerGoogleTest < Minitest::Test
|
|
|
8
8
|
|
|
9
9
|
class FakeWebmastersService
|
|
10
10
|
attr_accessor :authorization
|
|
11
|
-
attr_reader :submitted, :list_sites_calls
|
|
11
|
+
attr_reader :submitted, :submissions, :list_sites_calls
|
|
12
12
|
|
|
13
13
|
def initialize(site_urls:)
|
|
14
14
|
@site_urls = site_urls
|
|
15
|
+
@submissions = []
|
|
15
16
|
@list_sites_calls = 0
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -22,6 +23,7 @@ class IndexmapPingerGoogleTest < Minitest::Test
|
|
|
22
23
|
|
|
23
24
|
def submit_sitemap(property, sitemap_url)
|
|
24
25
|
@submitted = [property, sitemap_url]
|
|
26
|
+
@submissions << @submitted
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -53,10 +55,59 @@ class IndexmapPingerGoogleTest < Minitest::Test
|
|
|
53
55
|
assert_equal ["sc-domain:example.com", "https://www.example.com/sitemap.xml"], service.submitted
|
|
54
56
|
assert_equal :submitted, result[:status]
|
|
55
57
|
assert_equal 1, result[:sitemap_count]
|
|
58
|
+
assert_equal 0, result[:url_count]
|
|
56
59
|
assert_equal 1, service.list_sites_calls
|
|
57
60
|
end
|
|
58
61
|
end
|
|
59
62
|
|
|
63
|
+
def test_reports_unique_url_count_from_submitted_sitemaps
|
|
64
|
+
Dir.mktmpdir do |dir|
|
|
65
|
+
public_path = Pathname(dir)
|
|
66
|
+
public_path.join("sitemap.xml").write(<<~XML)
|
|
67
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
68
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
69
|
+
<sitemap><loc>https://www.example.com/sitemap-pages.xml</loc></sitemap>
|
|
70
|
+
<sitemap><loc>https://www.example.com/sitemap-posts.xml</loc></sitemap>
|
|
71
|
+
</sitemapindex>
|
|
72
|
+
XML
|
|
73
|
+
public_path.join("sitemap-pages.xml").write(<<~XML)
|
|
74
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
75
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
76
|
+
<url><loc>https://www.example.com/</loc></url>
|
|
77
|
+
<url><loc>https://www.example.com/about</loc></url>
|
|
78
|
+
</urlset>
|
|
79
|
+
XML
|
|
80
|
+
public_path.join("sitemap-posts.xml").write(<<~XML)
|
|
81
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
82
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
83
|
+
<url><loc>https://www.example.com/about</loc></url>
|
|
84
|
+
<url><loc>https://www.example.com/blog</loc></url>
|
|
85
|
+
</urlset>
|
|
86
|
+
XML
|
|
87
|
+
|
|
88
|
+
configuration = Indexmap::Configuration.new
|
|
89
|
+
configuration.base_url = "https://www.example.com"
|
|
90
|
+
configuration.public_path = public_path
|
|
91
|
+
configuration.google.credentials = "{\"type\":\"service_account\"}"
|
|
92
|
+
|
|
93
|
+
service = FakeWebmastersService.new(site_urls: ["sc-domain:example.com"])
|
|
94
|
+
result = Indexmap::Pinger::Google.new(
|
|
95
|
+
configuration: configuration,
|
|
96
|
+
service: service,
|
|
97
|
+
credentials_builder: ->(**) { :fake_authorizer }
|
|
98
|
+
).ping
|
|
99
|
+
|
|
100
|
+
assert_equal :submitted, result[:status]
|
|
101
|
+
assert_equal 3, result[:sitemap_count]
|
|
102
|
+
assert_equal 3, result[:url_count]
|
|
103
|
+
assert_equal [
|
|
104
|
+
["sc-domain:example.com", "https://www.example.com/sitemap-pages.xml"],
|
|
105
|
+
["sc-domain:example.com", "https://www.example.com/sitemap-posts.xml"],
|
|
106
|
+
["sc-domain:example.com", "https://www.example.com/sitemap.xml"]
|
|
107
|
+
], service.submissions
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
60
111
|
def test_skips_google_ping_when_credentials_are_missing
|
|
61
112
|
Dir.mktmpdir do |dir|
|
|
62
113
|
public_path = Pathname(dir)
|
|
@@ -5,11 +5,12 @@ require "test_helper"
|
|
|
5
5
|
class IndexmapTaskRunnerTest < Minitest::Test
|
|
6
6
|
VALID_KEY = "1234567890abcdef1234567890abcdef"
|
|
7
7
|
|
|
8
|
-
def
|
|
8
|
+
def test_create_writes_new_sitemap_and_key_file_without_deleting_unrelated_files
|
|
9
9
|
Dir.mktmpdir do |dir|
|
|
10
10
|
public_path = Pathname(dir)
|
|
11
11
|
public_path.join("sitemap.xml").write("old")
|
|
12
12
|
public_path.join("sitemap-pages.xml.gz").write("old")
|
|
13
|
+
public_path.join("sitemap-extra.xml").write("existing")
|
|
13
14
|
|
|
14
15
|
configuration = Indexmap::Configuration.new
|
|
15
16
|
configuration.base_url = "https://example.com"
|
|
@@ -24,14 +25,40 @@ class IndexmapTaskRunnerTest < Minitest::Test
|
|
|
24
25
|
|
|
25
26
|
result = Indexmap::TaskRunner.new(configuration: configuration).create
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
assert public_path.join("sitemap-pages.xml.gz").exist?
|
|
29
|
+
assert_equal "existing", public_path.join("sitemap-extra.xml").read
|
|
28
30
|
assert_includes public_path.join("sitemap.xml").read, "<sitemapindex"
|
|
29
31
|
assert_equal VALID_KEY, public_path.join("#{VALID_KEY}.txt").read
|
|
30
32
|
assert_equal [public_path.join("sitemap-pages.xml").to_s, public_path.join("sitemap.xml").to_s], result[:files]
|
|
33
|
+
assert_equal [public_path.join("sitemap-pages.xml"), public_path.join("sitemap.xml")], result[:written_files]
|
|
31
34
|
assert_equal public_path.join("#{VALID_KEY}.txt"), result[:index_now_key_path]
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
|
|
38
|
+
def test_create_runs_after_create_callbacks_after_validation
|
|
39
|
+
Dir.mktmpdir do |dir|
|
|
40
|
+
calls = []
|
|
41
|
+
public_path = Pathname(dir)
|
|
42
|
+
configuration = Indexmap::Configuration.new
|
|
43
|
+
configuration.base_url = "https://example.com"
|
|
44
|
+
configuration.public_path = public_path
|
|
45
|
+
configuration.sections = [
|
|
46
|
+
Indexmap::Section.new(
|
|
47
|
+
filename: "sitemap-pages.xml",
|
|
48
|
+
entries: [Indexmap::Entry.new(loc: "https://example.com/about")]
|
|
49
|
+
)
|
|
50
|
+
]
|
|
51
|
+
configuration.after_create do
|
|
52
|
+
calls << :called
|
|
53
|
+
calls << public_path.join("sitemap.xml").read.include?("<sitemapindex")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Indexmap::TaskRunner.new(configuration: configuration).create
|
|
57
|
+
|
|
58
|
+
assert_equal [:called, true], calls
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
35
62
|
def test_write_index_now_key_returns_nil_when_key_is_not_configured
|
|
36
63
|
Dir.mktmpdir do |dir|
|
|
37
64
|
configuration = Indexmap::Configuration.new
|
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
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Fidalgo
|
|
@@ -177,9 +177,11 @@ files:
|
|
|
177
177
|
- README.md
|
|
178
178
|
- lib/indexmap.rb
|
|
179
179
|
- lib/indexmap/configuration.rb
|
|
180
|
+
- lib/indexmap/creator.rb
|
|
180
181
|
- lib/indexmap/entry.rb
|
|
181
182
|
- lib/indexmap/google_configuration.rb
|
|
182
183
|
- lib/indexmap/index_now_configuration.rb
|
|
184
|
+
- lib/indexmap/output.rb
|
|
183
185
|
- lib/indexmap/parser.rb
|
|
184
186
|
- lib/indexmap/path.rb
|
|
185
187
|
- lib/indexmap/pinger/base.rb
|