How to Generate a Simple Sitemap with Ruby on Rails

I wanted to share how I created a dynamic sitemap that I don't have to manually update. Below is an explanation of why and how I set it up but you can also skip to the final code here.


What is a Sitemap and Why Do You Need One?

Your sitemap tells Google where to find all of your content.

Without one, some of your blog posts and website pages might not be indexed and won't show up in search results.

I built this website with Ruby on Rails and have started to build up a good number of blog posts.

And I definitely want to make sure Google can find them and share them with people!

You can manually generate a sitemap pretty easily, but that can lead to a lot of work depending on how frequently you update your website.

So, I think it's about time that I generate a sitemap dynamically.

Since I'm using Ruby on Rails I could use an existing Gem, but I found a lot of them to be overly complex.

Instead, I'm going to show you how I created a dynamic sitemap with just one new controller action and a new view.

You can check out the resulting sitemap here.


How to Create a Dynamic Sitemap

We need to give Google an endpoint that tells their crawlers where to find all of our content.

One of the easiest ways to do this is to generate an XML file with our URLs and the date they were last updated.

We can start with our routing.

Adding a Sitemap Route

We need to expose our sitemap.xml to Google. We can do this by adding a route to our routes.rb.

get '/sitemap', to: 'testsuite#sitemap'

This tells our app to direct requests made to testsuite.io/sitemap to our TestsuiteController class and to then call the sitemap method.

Let's add that code next.

Adding the Sitemap to the Controller

In my TestsuiteController I need to define the sitemap action now.

It should look like this.

class TestsuiteController < ApplicationController
  def sitemap
    # We'll add to this next...
  end
end

Now the actual XML output will be handled in the view.

Rails will automatically look for a view matching the name of our method here, so we need to create the matching sitemap view.

Creating the Sitemap View

In our app/views we need to make sure we have a directory corresponding to our controller with a file matching the method the controller is calling.

In this case, that means Rails will look for app/views/testsuite/sitemap.

We know we want to return the sitemap in XML and we are going to want to generate the output with Ruby so we can make this an XML file with the ERB templating engine.

So we end up with app/views/testsuite/sitemap.xml.erb.

Here's the exact code I use in my sitemap view.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://testsuite.io</loc>
    <lastmod><%= @posts[0].updated_at.strftime("%Y-%m-%d") %></lastmod>
  </url>
  <% @posts.each do |post| %>
    <url>
      <loc>https://testsuite.io/<%= post.slug %></loc>
      <lastmod><%= post.updated_at.strftime("%Y-%m-%d") %></lastmod>
    </url>
  <% end %>
</urlset>

A couple of things are going on here.

The first <url> entry is actually just my homepage. There's nothing dynamic here, except that I'm updating the last modified date or <lastmod> to whenever I last published a new article.

Next, we are mapping through all of my posts with:

<% @posts.each do |post| %>

This lets us create a new <url> record where I can add the custom slug and last updated date based on each individual post.

From here, my app will automatically generate the new sitemap every time I add or update a post.

This wraps up the view, but we still need to cover how we get our posts to the view.


Wiring Everything Together

The final step here is to revisit our controller so that we fetch all our blog posts from the database.

I already have a BlogPost model defined so I can query all my blog posts from that model.

I also only want my published blog posts to appear in my sitemap, and none that are reposts with canonical links.

I created a scope to find published posts and make sure the canonical link field is empty.

There are fields specific to my blog post model, but you can customize your query depending on your own website content.

My query looks like this.

@posts = BlogPost.published.where(canonical_link: [nil, ''])


Final Code for a Dynamic Rails Sitemap

Addition in our config/routes.rb

get '/sitemap', to: 'testsuite#sitemap'

app/controllers/testsuite_controller.rb

class TestsuiteController < ApplicationController
  def sitemap
    @posts = BlogPost.published.where(canonical_link: [nil, ''])
  end
end

app/views/testsuite/sitemap.xml.erb

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://testsuite.io</loc>
    <lastmod><%= @posts[0].updated_at.strftime("%Y-%m-%d") %></lastmod>
  </url>
  <% @posts.each do |post| %>
    <url>
      <loc>https://testsuite.io/<%= post.slug %></loc>
      <lastmod><%= post.updated_at.strftime("%Y-%m-%d") %></lastmod>
    </url>
  <% end %>
</urlset>

Takeaways

This is how my dynamic sitemap works at Testsuite.io so that I never have to manually update it when I publish new content.

Thanks to a few lines of code in my routes.rb, a short method in my controller, and a straightforward view that maps through as much content as I want, Google will always have access to all of my content as well as be able to tell when each individual article was last updated.

If you're interested in more of the technical SEO behind sitemaps, check out my related post on how search engines index websites.

Featured
Level up faster
Recommended Books
Check out my list.
One on Ones: 101
Leveraging Other People's Experience
Hey, I'm Nicholas Dill.

I help people become better software developers with daily tips, tricks, and advice.

Level up
Related Articles
More like this
Understanding the #any? Method in Ruby
The Differences Between #nil?, #empty?, #blank?, and #present?
Routing Multiple Domains in a Ruby on Rails App