Generating a sitemap in Rails without a gem
You don't need a gem for this, and the version without one has a property the generated-file version can't match: it's never stale. Here's the whole thing, then the three numbers that decide whether it stays this simple.
The whole implementation
# config/routes.rb
get "/sitemap.xml", to: "sitemaps#show", defaults: { format: "xml" }
# app/controllers/sitemaps_controller.rb
class SitemapsController < ApplicationController
def show
@articles = Article.published.select(:slug, :updated_at)
expires_in 1.hour, public: true
end
end
<%# app/views/sitemaps/show.xml.builder %>
xml.instruct!
xml.urlset(xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9") do
xml.url do
xml.loc root_url
xml.changefreq "daily"
xml.priority "1.0"
end
@articles.each do |article|
xml.url do
xml.loc article_url(article.slug)
xml.lastmod article.updated_at.iso8601
xml.changefreq "monthly"
xml.priority "0.8"
end
end
end
That's it. article_url rather than article_path, because the spec wants absolute URLs and a
relative one is silently dropped. select only the columns you use, because this action loads
every published row and there's no reason to instantiate the bodies.
Three numbers off a real one
We measured this site's own live sitemap rather than a synthetic one:
| URLs | 243 |
| Uncompressed | 46,218 bytes |
| Per URL entry | 190 bytes |
| Gzipped | 3,122 bytes |
| Gzip ratio | 14.8x |
The spec caps a single sitemap at 50,000 URLs and 50 MB uncompressed. At 190 bytes an entry, 50 MB holds about 275,000 URLs, so the count limit arrives roughly five times sooner than the byte limit. Unless your URLs are unusually long, the only limit you'll ever meet is 50,000, and you can stop thinking about the other one.
That gzip ratio is worth a sentence too. 14.8x is extraordinary for text, and it's because a
sitemap is the same six tags repeated a few hundred times over a shared URL prefix. Rails will
gzip this for you if Rack::Deflater is in the stack, and it's close to the cheapest bandwidth win
in the app.
Why dynamic beats a generated file
The common alternative generates a file nightly and serves it from disk. It's faster to serve and it's wrong most of the day.
lastmod is the field this matters for. It's the one element in the spec that crawlers
demonstrably act on, because it tells them whether re-fetching a URL is worth a request. Publish an
article at ten in the morning and a file generated at midnight says nothing about it until
tomorrow. The action above reads updated_at at request time, so it's accurate by construction and
there's no job to fail quietly.
The cost is a database query per request, on a path that gets hit a handful of times a day.
expires_in 1.hour, public: true hands that to your CDN or your reverse proxy and the cost goes
away. If your sitemap is large enough that this stops being true, you're near the 50,000 limit and
you have a different job to do anyway.
Two fields in that template are mostly decoration. changefreq and priority are advisory,
Google has said publicly it ignores them, and priority in particular is relative within your own
site rather than a claim about the internet. They're harmless. They're also not where your effort
goes.
Past fifty thousand
The answer is a sitemap index, which is a sitemap whose entries are other sitemaps:
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://example.com/sitemaps/articles-1.xml</loc>
<lastmod>2026-10-17T09:00:00Z</lastmod>
</sitemap>
</sitemapindex>
The index itself is capped at 50,000 entries, which gives you two and a half billion URLs before
the structure runs out, and nobody reading this will meet that. Shard by something stable, like a
page number over an ordered scope, so a given URL keeps landing in the same child sitemap and
lastmod on the index stays meaningful.
The part a sitemap doesn't do
Worth ending on, because it's where the expectation goes wrong.
A sitemap asks for a crawl. An internal link argues for one. A page that appears in your sitemap and is linked from nowhere gets discovered, queued, and frequently left in whatever your search console calls "discovered, not indexed" indefinitely. We've watched exactly that happen on this site, to pages that were in the sitemap from the day they shipped.
So when a section isn't getting indexed, the sitemap is the first thing people check and almost never the problem. The question to ask instead is which page on the site links to it.
What this page doesn't cover
No crawler. Everything here is about producing a valid, accurate document, and nothing in it measures what Google then does with one, because that takes weeks and a property you control.
Image and video sitemap extensions aren't here, nor is hreflang for a multilingual site, which
is a genuinely fiddly corner where the alternates have to be reciprocal. And nothing above touches
robots.txt, which is where you announce the sitemap's location so a crawler that has never seen
your site can find it without being told.
Comments
No comments yet. Be the first.