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 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
+ [![Gem Version](https://badge.fury.io/rb/indexmap.svg)](https://badge.fury.io/rb/indexmap)
4
+ [![Ruby](https://github.com/ethos-link/indexmap/actions/workflows/ruby.yml/badge.svg)](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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Indexmap
4
+ Entry = Struct.new(:loc, :lastmod, keyword_init: true)
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Indexmap
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load File.expand_path("../tasks/indexmap_tasks.rake", __dir__)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Indexmap
4
+ Section = Struct.new(:filename, :entries, keyword_init: true)
5
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Indexmap
4
+ VERSION = "0.2.1"
5
+ 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "minitest/autorun"
6
+ require "tmpdir"
7
+ require "date"
8
+
9
+ require "indexmap"
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: []