indexmap 0.2.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 +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +174 -0
- data/lib/indexmap/configuration.rb +71 -0
- data/lib/indexmap/entry.rb +5 -0
- data/lib/indexmap/railtie.rb +9 -0
- data/lib/indexmap/section.rb +5 -0
- data/lib/indexmap/task_runner.rb +45 -0
- data/lib/indexmap/version.rb +5 -0
- data/lib/indexmap/writer.rb +138 -0
- data/lib/indexmap.rb +36 -0
- data/lib/tasks/indexmap_tasks.rake +13 -0
- data/test/indexmap/configuration_test.rb +69 -0
- data/test/indexmap/writer_test.rb +80 -0
- data/test/test_helper.rb +9 -0
- metadata +150 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c3d316abdab0a6a55f613b44a0481bdf98d7d041d3d5b60b8523524450494333
|
|
4
|
+
data.tar.gz: a18dd5ad2e6db70ecef78719d8bae1a6d249fd393fe7dc363a8862fedd2faede
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8dfd199b1b978991b703870f4eb08091b8ce4689156840b7f30a985f951d55d163dce89ba71d7d8d4645931b2cb6980a4134f9b593bd8bde3b97cf272fdfbe4c
|
|
7
|
+
data.tar.gz: 875b0e45a6d3ad629f03456f62b8311f6379f3d8cde8a308ec66e6bf5233cb122a34d39531d7a245b5b7e3a3e2bc051ee7fd16d5f2981fb5e5eb6b600ef01fa8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.1] - 2026-04-21
|
|
9
|
+
|
|
10
|
+
### <!-- 1 -->🐛 Bug Fixes
|
|
11
|
+
- publish built gem in release workflow
|
|
12
|
+
|
|
13
|
+
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ethos Link
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Indexmap
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/indexmap)
|
|
4
|
+
[](https://github.com/ethos-link/indexmap/actions/workflows/ruby.yml)
|
|
5
|
+
|
|
6
|
+
`indexmap` is a small Ruby gem for generating XML sitemap indexes and child sitemaps from explicit section definitions.
|
|
7
|
+
|
|
8
|
+
It is designed for Rails apps that want:
|
|
9
|
+
|
|
10
|
+
- deterministic sitemap output
|
|
11
|
+
- plain Ruby configuration
|
|
12
|
+
- first-party rake tasks instead of a large DSL
|
|
13
|
+
- easy extraction of sitemap logic into app-owned manifests
|
|
14
|
+
|
|
15
|
+
The default output mode is a sitemap index plus one or more child sitemap files. For simpler sites, `indexmap` also supports an explicit single-file mode that writes a single `urlset` directly to `sitemap.xml`.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add this line to your application's Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem "indexmap"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
And then execute:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or install it directly:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
gem install indexmap
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Ruby usage
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require "indexmap"
|
|
41
|
+
|
|
42
|
+
sections = [
|
|
43
|
+
Indexmap::Section.new(
|
|
44
|
+
filename: "sitemap-marketing.xml",
|
|
45
|
+
entries: [
|
|
46
|
+
Indexmap::Entry.new(loc: "https://example.com/"),
|
|
47
|
+
Indexmap::Entry.new(loc: "https://example.com/pricing", lastmod: Date.new(2026, 4, 21))
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
Indexmap::Writer.new(
|
|
53
|
+
sections: sections,
|
|
54
|
+
public_path: Pathname("public"),
|
|
55
|
+
base_url: "https://example.com"
|
|
56
|
+
).write
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Rails configuration
|
|
60
|
+
|
|
61
|
+
In an initializer:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Indexmap.configure do |config|
|
|
65
|
+
config.base_url = -> { "https://example.com" }
|
|
66
|
+
config.public_path = -> { Rails.public_path }
|
|
67
|
+
config.sections = -> do
|
|
68
|
+
[
|
|
69
|
+
Indexmap::Section.new(
|
|
70
|
+
filename: "sitemap-marketing.xml",
|
|
71
|
+
entries: [
|
|
72
|
+
Indexmap::Entry.new(loc: "https://example.com/")
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This enables:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bin/rails sitemap:create
|
|
84
|
+
bin/rails sitemap:format
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Single-file mode
|
|
88
|
+
|
|
89
|
+
For sites that only want one `public/sitemap.xml` file:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
Indexmap.configure do |config|
|
|
93
|
+
config.base_url = -> { "https://example.com" }
|
|
94
|
+
config.public_path = -> { Rails.public_path }
|
|
95
|
+
config.format = :single_file
|
|
96
|
+
config.entries = -> do
|
|
97
|
+
[
|
|
98
|
+
Indexmap::Entry.new(loc: "https://example.com/"),
|
|
99
|
+
Indexmap::Entry.new(loc: "https://example.com/about", lastmod: Date.new(2026, 4, 21))
|
|
100
|
+
]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
In `:single_file` mode, `indexmap` writes a `urlset` directly to `sitemap.xml`. In the default `:index` mode, it writes a sitemap index plus child sitemap files from `sections`.
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
Run tests:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bundle exec rake test
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Run lint:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
bundle exec rake standard
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Run the full default task:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
bundle exec rake
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Note: `Gemfile.lock` is intentionally not tracked for this gem, following normal Ruby library conventions.
|
|
128
|
+
|
|
129
|
+
### Git hooks
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
Run the hook installer once per clone:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bundle exec lefthook install
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Release
|
|
140
|
+
|
|
141
|
+
Releases are tag-driven and published by GitHub Actions to RubyGems. Local release commands never publish directly.
|
|
142
|
+
|
|
143
|
+
Install [git-cliff](https://git-cliff.org/) locally before preparing a release. The release task regenerates `CHANGELOG.md` from Conventional Commits.
|
|
144
|
+
|
|
145
|
+
Before preparing a release, make sure you are on `main` or `master` with a clean worktree.
|
|
146
|
+
|
|
147
|
+
Then run one of:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
bundle exec rake 'release:prepare[patch]'
|
|
151
|
+
bundle exec rake 'release:prepare[minor]'
|
|
152
|
+
bundle exec rake 'release:prepare[major]'
|
|
153
|
+
bundle exec rake 'release:prepare[0.1.0]'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The task will:
|
|
157
|
+
|
|
158
|
+
1. Regenerate `CHANGELOG.md` with `git-cliff`.
|
|
159
|
+
1. Update `lib/indexmap/version.rb`.
|
|
160
|
+
1. Commit the release changes.
|
|
161
|
+
1. Create and push the `vX.Y.Z` tag.
|
|
162
|
+
|
|
163
|
+
The `Release` workflow then runs tests, publishes the gem to RubyGems, and creates the GitHub release from the changelog entry.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT License, see [LICENSE.txt](LICENSE.txt)
|
|
168
|
+
|
|
169
|
+
## About
|
|
170
|
+
|
|
171
|
+
Made by the team at [Ethos Link](https://www.ethos-link.com) — practical software for growing businesses. We build tools for hospitality operators who need clear workflows, fast onboarding, and real human support.
|
|
172
|
+
|
|
173
|
+
We also build [Reviato](https://www.reviato.com), “Capture. Interpret. Act.”.
|
|
174
|
+
Turn guest feedback into clear next steps for your team. Collect private appraisals, spot patterns across reviews, and act before small issues turn into public ones.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Indexmap
|
|
4
|
+
class Configuration
|
|
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
|
|
10
|
+
@format = :index
|
|
11
|
+
@index_filename = "sitemap.xml"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def base_url
|
|
15
|
+
resolve(@base_url)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def entries
|
|
19
|
+
Array(resolve(@entries))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def format
|
|
23
|
+
value = resolve(@format)
|
|
24
|
+
value.nil? ? :index : value.to_sym
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def index_filename
|
|
28
|
+
resolve(@index_filename)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def public_path
|
|
32
|
+
value = resolve(@public_path)
|
|
33
|
+
return Pathname("public") if value.nil?
|
|
34
|
+
|
|
35
|
+
Pathname(value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def sections
|
|
39
|
+
Array(resolve(@sections))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def writer
|
|
43
|
+
raise ConfigurationError, "Indexmap base_url is not configured" if base_url.to_s.strip.empty?
|
|
44
|
+
|
|
45
|
+
unless VALID_FORMATS.include?(format)
|
|
46
|
+
raise ConfigurationError, "Indexmap format must be one of: #{VALID_FORMATS.join(", ")}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if format == :single_file
|
|
50
|
+
raise ConfigurationError, "Indexmap entries are not configured" if entries.empty?
|
|
51
|
+
elsif sections.empty?
|
|
52
|
+
raise ConfigurationError, "Indexmap sections are not configured" if sections.empty?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Writer.new(
|
|
56
|
+
entries: entries,
|
|
57
|
+
format: format,
|
|
58
|
+
sections: sections,
|
|
59
|
+
public_path: public_path,
|
|
60
|
+
base_url: base_url,
|
|
61
|
+
index_filename: index_filename
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def resolve(value)
|
|
68
|
+
value.respond_to?(:call) ? value.call : value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Indexmap
|
|
6
|
+
class TaskRunner
|
|
7
|
+
def initialize(configuration: Indexmap.configuration)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
remove_existing_sitemap_files
|
|
13
|
+
configuration.writer.write
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def format
|
|
17
|
+
sitemap_files.each do |file_path|
|
|
18
|
+
content = File.read(file_path)
|
|
19
|
+
document = Nokogiri::XML(
|
|
20
|
+
content,
|
|
21
|
+
nil,
|
|
22
|
+
nil,
|
|
23
|
+
Nokogiri::XML::ParseOptions::DEFAULT_XML | Nokogiri::XML::ParseOptions::NOBLANKS
|
|
24
|
+
)
|
|
25
|
+
save_options = Nokogiri::XML::Node::SaveOptions::FORMAT | Nokogiri::XML::Node::SaveOptions::AS_XML
|
|
26
|
+
|
|
27
|
+
File.write(file_path, document.to_xml(indent: 2, save_with: save_options))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :configuration
|
|
34
|
+
|
|
35
|
+
def sitemap_files
|
|
36
|
+
Dir.glob(configuration.public_path.join("sitemap*.xml"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def remove_existing_sitemap_files
|
|
40
|
+
Dir.glob(configuration.public_path.join("sitemap*.xml*")).each do |file_path|
|
|
41
|
+
File.delete(file_path)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Indexmap
|
|
4
|
+
class Writer
|
|
5
|
+
VALID_FORMATS = %i[index single_file].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(public_path:, base_url:, sections: nil, entries: nil, index_filename: "sitemap.xml", format: :index)
|
|
8
|
+
@entries = normalize_entries(entries)
|
|
9
|
+
@format = normalize_format(format)
|
|
10
|
+
@sections = normalize_sections(sections)
|
|
11
|
+
@public_path = Pathname(public_path)
|
|
12
|
+
@base_url = base_url
|
|
13
|
+
@index_filename = index_filename
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write
|
|
17
|
+
FileUtils.mkdir_p(public_path)
|
|
18
|
+
|
|
19
|
+
return public_path.join(index_filename).write(urlset_xml(entries)) if single_file?
|
|
20
|
+
|
|
21
|
+
sections.each do |section|
|
|
22
|
+
public_path.join(section.filename).write(urlset_xml(section.entries))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
public_path.join(index_filename).write(index_xml(sections))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :base_url, :entries, :format, :index_filename, :public_path, :sections
|
|
31
|
+
|
|
32
|
+
def normalize_entries(raw_entries)
|
|
33
|
+
Array(raw_entries).map { |entry| normalize_entry(entry) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def normalize_format(value)
|
|
37
|
+
normalized = value.nil? ? :index : value.to_sym
|
|
38
|
+
return normalized if VALID_FORMATS.include?(normalized)
|
|
39
|
+
|
|
40
|
+
raise ConfigurationError, "Indexmap format must be one of: #{VALID_FORMATS.join(", ")}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_sections(raw_sections)
|
|
44
|
+
Array(raw_sections).map do |section|
|
|
45
|
+
next section if section.is_a?(Section)
|
|
46
|
+
|
|
47
|
+
Section.new(
|
|
48
|
+
filename: section.fetch(:filename),
|
|
49
|
+
entries: section.fetch(:entries)
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def single_file?
|
|
55
|
+
format == :single_file
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def urlset_xml(entries)
|
|
59
|
+
lines = [
|
|
60
|
+
%(<?xml version="1.0" encoding="UTF-8"?>),
|
|
61
|
+
%(<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
entries.each do |entry|
|
|
65
|
+
sitemap_entry = normalize_entry(entry)
|
|
66
|
+
lines << " <url>"
|
|
67
|
+
lines << " <loc>#{escape(sitemap_entry.loc)}</loc>"
|
|
68
|
+
lines << " <lastmod>#{format_lastmod(sitemap_entry.lastmod)}</lastmod>" if sitemap_entry.lastmod
|
|
69
|
+
lines << " </url>"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
lines << "</urlset>"
|
|
73
|
+
lines.join("\n") + "\n"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def index_xml(sitemap_sections)
|
|
77
|
+
lines = [
|
|
78
|
+
%(<?xml version="1.0" encoding="UTF-8"?>),
|
|
79
|
+
%(<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">)
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
sitemap_sections.each do |section|
|
|
83
|
+
lines << " <sitemap>"
|
|
84
|
+
lines << " <loc>#{escape(index_loc(section.filename))}</loc>"
|
|
85
|
+
lines << " <lastmod>#{format_lastmod(section_lastmod(section))}</lastmod>"
|
|
86
|
+
lines << " </sitemap>"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
lines << "</sitemapindex>"
|
|
90
|
+
lines.join("\n") + "\n"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def normalize_entry(entry)
|
|
94
|
+
return entry if entry.is_a?(Entry)
|
|
95
|
+
|
|
96
|
+
Entry.new(loc: entry.fetch(:loc), lastmod: entry[:lastmod])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def index_loc(filename)
|
|
100
|
+
File.join(base_url.sub(%r{/\z}, ""), filename)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def section_lastmod(section)
|
|
104
|
+
timestamps = Array(section.entries).map { |entry| comparable_lastmod(normalize_entry(entry).lastmod) }.compact
|
|
105
|
+
timestamps.max || Time.now.utc
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def format_lastmod(value)
|
|
109
|
+
timestamp = parsed_lastmod(value)
|
|
110
|
+
|
|
111
|
+
escape(timestamp.iso8601)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def comparable_lastmod(value)
|
|
115
|
+
parsed = parsed_lastmod(value)
|
|
116
|
+
return parsed.to_time.utc if parsed.is_a?(Date)
|
|
117
|
+
|
|
118
|
+
parsed
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parsed_lastmod(value)
|
|
122
|
+
case value
|
|
123
|
+
when String
|
|
124
|
+
Time.parse(value)
|
|
125
|
+
when Date
|
|
126
|
+
value
|
|
127
|
+
when Time, DateTime
|
|
128
|
+
value
|
|
129
|
+
else
|
|
130
|
+
value.respond_to?(:to_time) ? value.to_time : value
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def escape(value)
|
|
135
|
+
CGI.escapeHTML(value.to_s)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/indexmap.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "date"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "time"
|
|
8
|
+
|
|
9
|
+
require_relative "indexmap/version"
|
|
10
|
+
require_relative "indexmap/configuration"
|
|
11
|
+
require_relative "indexmap/entry"
|
|
12
|
+
require_relative "indexmap/section"
|
|
13
|
+
require_relative "indexmap/task_runner"
|
|
14
|
+
require_relative "indexmap/writer"
|
|
15
|
+
|
|
16
|
+
module Indexmap
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
class ConfigurationError < Error; end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def configuration
|
|
23
|
+
@configuration ||= Configuration.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def configure
|
|
27
|
+
yield(configuration)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset!
|
|
31
|
+
@configuration = Configuration.new
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
require_relative "indexmap/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
namespace :sitemap do
|
|
2
|
+
desc "Create sitemap files"
|
|
3
|
+
task create: :environment do
|
|
4
|
+
runner = Indexmap::TaskRunner.new
|
|
5
|
+
runner.create
|
|
6
|
+
runner.format
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
desc "Format sitemap files for better readability"
|
|
10
|
+
task format: :environment do
|
|
11
|
+
Indexmap::TaskRunner.new.format
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class IndexmapConfigurationTest < Minitest::Test
|
|
6
|
+
def teardown
|
|
7
|
+
Indexmap.reset!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_writer_builds_from_configured_callables
|
|
11
|
+
Indexmap.configure do |config|
|
|
12
|
+
config.base_url = -> { "https://example.com" }
|
|
13
|
+
config.public_path = -> { Pathname("tmp/public") }
|
|
14
|
+
config.sections = -> do
|
|
15
|
+
[Indexmap::Section.new(filename: "sitemap-pages.xml", entries: [Indexmap::Entry.new(loc: "https://example.com/")])]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
writer = Indexmap.configuration.writer
|
|
20
|
+
|
|
21
|
+
assert_equal Pathname("tmp/public"), writer.instance_variable_get(:@public_path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_writer_builds_single_file_writer_from_configured_entries
|
|
25
|
+
Indexmap.configure do |config|
|
|
26
|
+
config.base_url = "https://example.com"
|
|
27
|
+
config.format = :single_file
|
|
28
|
+
config.entries = -> { [Indexmap::Entry.new(loc: "https://example.com/")] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
writer = Indexmap.configuration.writer
|
|
32
|
+
|
|
33
|
+
assert_equal :single_file, writer.instance_variable_get(:@format)
|
|
34
|
+
assert_equal [Indexmap::Entry.new(loc: "https://example.com/")], writer.instance_variable_get(:@entries)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_writer_raises_without_base_url
|
|
38
|
+
Indexmap.configure do |config|
|
|
39
|
+
config.sections = [Indexmap::Section.new(filename: "sitemap-pages.xml", entries: [])]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
error = assert_raises(Indexmap::ConfigurationError) { Indexmap.configuration.writer }
|
|
43
|
+
|
|
44
|
+
assert_equal "Indexmap base_url is not configured", error.message
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_writer_raises_without_entries_in_single_file_mode
|
|
48
|
+
Indexmap.configure do |config|
|
|
49
|
+
config.base_url = "https://example.com"
|
|
50
|
+
config.format = :single_file
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
error = assert_raises(Indexmap::ConfigurationError) { Indexmap.configuration.writer }
|
|
54
|
+
|
|
55
|
+
assert_equal "Indexmap entries are not configured", error.message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_writer_raises_for_invalid_format
|
|
59
|
+
Indexmap.configure do |config|
|
|
60
|
+
config.base_url = "https://example.com"
|
|
61
|
+
config.format = :unsupported
|
|
62
|
+
config.sections = [Indexmap::Section.new(filename: "sitemap-pages.xml", entries: [])]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
error = assert_raises(Indexmap::ConfigurationError) { Indexmap.configuration.writer }
|
|
66
|
+
|
|
67
|
+
assert_equal "Indexmap format must be one of: index, single_file", error.message
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class IndexmapWriterTest < Minitest::Test
|
|
6
|
+
def test_writes_sitemap_index_and_child_sitemap
|
|
7
|
+
Dir.mktmpdir do |directory|
|
|
8
|
+
sections = [
|
|
9
|
+
Indexmap::Section.new(
|
|
10
|
+
filename: "sitemap-pages.xml",
|
|
11
|
+
entries: [
|
|
12
|
+
Indexmap::Entry.new(loc: "https://example.com/", lastmod: Date.new(2026, 4, 21)),
|
|
13
|
+
Indexmap::Entry.new(loc: "https://example.com/pricing", lastmod: Time.utc(2026, 4, 22, 10, 30, 0))
|
|
14
|
+
]
|
|
15
|
+
)
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
Indexmap::Writer.new(
|
|
19
|
+
sections: sections,
|
|
20
|
+
public_path: directory,
|
|
21
|
+
base_url: "https://example.com"
|
|
22
|
+
).write
|
|
23
|
+
|
|
24
|
+
index_xml = File.read(File.join(directory, "sitemap.xml"))
|
|
25
|
+
child_xml = File.read(File.join(directory, "sitemap-pages.xml"))
|
|
26
|
+
|
|
27
|
+
assert_includes index_xml, "<loc>https://example.com/sitemap-pages.xml</loc>"
|
|
28
|
+
assert_includes child_xml, "<loc>https://example.com/</loc>"
|
|
29
|
+
assert_includes child_xml, "<loc>https://example.com/pricing</loc>"
|
|
30
|
+
assert_includes child_xml, "<lastmod>2026-04-21</lastmod>"
|
|
31
|
+
assert_includes child_xml, "<lastmod>2026-04-22T10:30:00Z</lastmod>"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_accepts_hash_based_sections_and_entries
|
|
36
|
+
Dir.mktmpdir do |directory|
|
|
37
|
+
Indexmap::Writer.new(
|
|
38
|
+
sections: [
|
|
39
|
+
{
|
|
40
|
+
filename: "sitemap-pages.xml",
|
|
41
|
+
entries: [
|
|
42
|
+
{loc: "https://example.com/about", lastmod: "2026-04-20T09:15:00Z"}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
public_path: directory,
|
|
47
|
+
base_url: "https://example.com"
|
|
48
|
+
).write
|
|
49
|
+
|
|
50
|
+
child_xml = File.read(File.join(directory, "sitemap-pages.xml"))
|
|
51
|
+
|
|
52
|
+
assert_includes child_xml, "<loc>https://example.com/about</loc>"
|
|
53
|
+
assert_includes child_xml, "<lastmod>2026-04-20T09:15:00Z</lastmod>"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_writes_single_file_urlset
|
|
58
|
+
Dir.mktmpdir do |directory|
|
|
59
|
+
Indexmap::Writer.new(
|
|
60
|
+
format: :single_file,
|
|
61
|
+
entries: [
|
|
62
|
+
Indexmap::Entry.new(loc: "https://example.com/", lastmod: Date.new(2026, 4, 21)),
|
|
63
|
+
{loc: "https://example.com/about", lastmod: "2026-04-22T09:15:00Z"}
|
|
64
|
+
],
|
|
65
|
+
public_path: directory,
|
|
66
|
+
base_url: "https://example.com"
|
|
67
|
+
).write
|
|
68
|
+
|
|
69
|
+
sitemap_xml = File.read(File.join(directory, "sitemap.xml"))
|
|
70
|
+
|
|
71
|
+
assert_includes sitemap_xml, "<urlset"
|
|
72
|
+
assert_includes sitemap_xml, "<loc>https://example.com/</loc>"
|
|
73
|
+
assert_includes sitemap_xml, "<loc>https://example.com/about</loc>"
|
|
74
|
+
assert_includes sitemap_xml, "<lastmod>2026-04-21</lastmod>"
|
|
75
|
+
assert_includes sitemap_xml, "<lastmod>2026-04-22T09:15:00Z</lastmod>"
|
|
76
|
+
refute_includes sitemap_xml, "<sitemapindex"
|
|
77
|
+
refute File.exist?(File.join(directory, "sitemap-pages.xml"))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: indexmap
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Paulo Fidalgo
|
|
8
|
+
- Ethos Link
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: nokogiri
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.16'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.16'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: railties
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '7.1'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '7.1'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: minitest
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '5.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '5.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rake
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '13.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '13.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: standard
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.0'
|
|
97
|
+
description: A small Ruby gem for generating sitemap indexes and child sitemaps from
|
|
98
|
+
explicit section definitions, with optional Rails rake task integration.
|
|
99
|
+
email:
|
|
100
|
+
- devel@ethos-link.com
|
|
101
|
+
executables: []
|
|
102
|
+
extensions: []
|
|
103
|
+
extra_rdoc_files: []
|
|
104
|
+
files:
|
|
105
|
+
- CHANGELOG.md
|
|
106
|
+
- LICENSE.txt
|
|
107
|
+
- README.md
|
|
108
|
+
- lib/indexmap.rb
|
|
109
|
+
- lib/indexmap/configuration.rb
|
|
110
|
+
- lib/indexmap/entry.rb
|
|
111
|
+
- lib/indexmap/railtie.rb
|
|
112
|
+
- lib/indexmap/section.rb
|
|
113
|
+
- lib/indexmap/task_runner.rb
|
|
114
|
+
- lib/indexmap/version.rb
|
|
115
|
+
- lib/indexmap/writer.rb
|
|
116
|
+
- lib/tasks/indexmap_tasks.rake
|
|
117
|
+
- test/indexmap/configuration_test.rb
|
|
118
|
+
- test/indexmap/writer_test.rb
|
|
119
|
+
- test/test_helper.rb
|
|
120
|
+
homepage: https://www.ethos-link.com/opensource/indexmap
|
|
121
|
+
licenses:
|
|
122
|
+
- MIT
|
|
123
|
+
metadata:
|
|
124
|
+
homepage_uri: https://www.ethos-link.com/opensource/indexmap
|
|
125
|
+
source_code_uri: https://github.com/ethos-link/indexmap
|
|
126
|
+
bug_tracker_uri: https://github.com/ethos-link/indexmap/issues
|
|
127
|
+
changelog_uri: https://github.com/ethos-link/indexmap/blob/main/CHANGELOG.md
|
|
128
|
+
documentation_uri: https://github.com/ethos-link/indexmap/blob/main/README.md
|
|
129
|
+
funding_uri: https://www.reviato.com/
|
|
130
|
+
github_repo: ssh://github.com/ethos-link/indexmap
|
|
131
|
+
allowed_push_host: https://rubygems.org
|
|
132
|
+
rubygems_mfa_required: 'true'
|
|
133
|
+
rdoc_options: []
|
|
134
|
+
require_paths:
|
|
135
|
+
- lib
|
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - ">="
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: 3.2.0
|
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
requirements: []
|
|
147
|
+
rubygems_version: 4.0.6
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: Generate sitemap indexes and child sitemaps with plain Ruby
|
|
150
|
+
test_files: []
|