indexmap builds XML sitemaps from explicit Ruby data.
It keeps sitemap rules in app code. Teams can review them, test them, and change them without learning a custom DSL.
What
- Builds sitemap indexes and child sitemap files from plain Ruby
SectionandEntryobjects. - Supports single-file mode for sites that only need one public
sitemap.xml. - Supports named outputs for sitemap files that need a separate refresh path.
- Stages generated XML, formats it, validates it, then replaces the final files only after a successful run.
- Provides Rails tasks for creating, formatting, validating, and pinging sitemap URLs.
How
Most sitemap failures are not XML failures. They are ownership failures.
The hard questions are about ownership:
- Which routes belong in the sitemap?
- Which parameterized URLs stay out?
- Which records are indexable?
- How do those rules stay visible as the app grows?
indexmap keeps those decisions in normal Ruby.
For a larger app, group entries by route family or content type. indexmap can then write a sitemap index plus child sitemap files.
Indexmap.configure do |config|
routes = Rails.application.routes.url_helpers
site_url_options = Rails.application.config.x.site_url_options
config.base_url = -> { Rails.application.config.x.site_url }
config.public_path = -> { Rails.public_path }
config.sections = -> do
posts = Post.published.order(published_at: :desc)
[
Indexmap::Section.new(
filename: "sitemap-marketing.xml",
entries: [
Indexmap::Entry.new(loc: routes.root_url(**site_url_options)),
Indexmap::Entry.new(loc: routes.pricing_url(**site_url_options)),
Indexmap::Entry.new(loc: routes.features_url(**site_url_options))
]
),
Indexmap::Section.new(
filename: "sitemap-posts.xml",
entries: posts.map do |post|
Indexmap::Entry.new(
loc: routes.post_url(post, **site_url_options),
lastmod: post.updated_at
)
end
)
]
end
end
That is the shape of a larger app sitemap. Sections follow the public surface.
Marketing pages, posts, reports, and directory pages each have their own inclusion rules. Those rules are built from route helpers and app-owned content repositories.
For a smaller site, single-file mode keeps the same pattern but writes one urlset directly to public/sitemap.xml.
Indexmap.configure do |config|
routes = Rails.application.routes.url_helpers
site_url_options = Rails.application.config.x.site_url_options
config.base_url = -> { Rails.application.config.x.site_url }
config.public_path = -> { Rails.public_path }
config.format = :single_file
config.entries = -> do
posts = Post.published.order(published_at: :desc)
[
Indexmap::Entry.new(loc: routes.root_url(**site_url_options)),
Indexmap::Entry.new(loc: routes.about_url(**site_url_options)),
Indexmap::Entry.new(loc: routes.posts_url(**site_url_options), lastmod: posts.first&.updated_at),
*posts.map do |post|
Indexmap::Entry.new(
loc: routes.post_url(post, **site_url_options),
lastmod: post.updated_at
)
end
]
end
end
This is the same pattern a small marketing site can use. The site only needs one public sitemap file, but the sitemap is still route-driven and explicit.
Some apps also have sitemap sections that should not run during deployment. For example, report pages might depend on production data.
For that case, indexmap supports named outputs and post-create hooks:
Indexmap.configure do |config|
config.after_create do
Sitemaps::ReportsRefreshJob.perform_later
end
config.output :reports do |output|
output.format = :single_file
output.index_filename = "sitemap-reports.xml"
output.entries = -> { Sitemap::Reports.entries }
end
end
class Sitemaps::ReportsRefreshJob < ApplicationJob
def perform
Indexmap.create(:reports)
end
end
The default sitemap can stay fast and deployment-safe. The database-backed sitemap can refresh later in the app job system, using the same entry objects and validation rules.
Before publishing files, indexmap writes to a local temporary directory, formats the XML, validates the generated sitemap, and only then replaces the final files. A broken generation run leaves the previous sitemap in place.
Why
Sitemaps are a contract with search engines. They should not quietly become a dumping ground for every route an app can render.
indexmap is designed around that constraint:
- route helpers keep public URLs tied to real application routes.
- model scopes and content repositories decide which records belong there.
- named outputs separate fast static generation from slower data-backed refreshes.
- validation catches malformed URLs, duplicate entries, parameterized URLs, fragments, invalid
lastmodvalues, and URLs outside the configured site. - safe publishing prevents a failed generation run from deleting the last good sitemap.
The result is intentionally small. The gem generates, formats, validates, and pings.
Storage, serving, and higher-level inclusion rules stay in the app, where they belong.
About EthosLink
We build focused tools for hospitality and retail teams at EthosLink. indexmap comes from production sitemap work.
Search visibility depends on keeping many page types explicit, reviewable, and operationally safe.
If you are working on customer feedback operations, Reviato turns scattered reviews and private feedback into clear next steps for your team.