Canonical URLs in Rails applications

By Exequiel Rozas

Getting organic traffic is a nice and sustainable way to build a digital business.

But if we're not careful with the way we render our pages, we can harm our ability to gain traffic from search engines. The main issue with it is duplicate or near-identical content which can affect the way our pages are indexed and ranked.

In this article, we will learn how to handle these cases properly using canonical URLs in Rails applications and some scenarios we might run into.

Let's start by understanding what canonical URLs are:

What are canonical URLs?

A canonical URL is a URL that indicates the main or primary version of a set of pages that might have duplicate or near-duplicate content.

Imagine that we have an application where we show a banner if a given query parameter is present in the URL:

<% if params[:bfcm] %>
  <%= render "bfcm_discount_banner" %>
<% end %> 

If Google finds out about that version of the page via an internal or external link it will consider both of them to be two different URLs and it will decide which version to index based on many internal factors and the page it chooses might not be the one we want.

Duplicate pages query parameter

In this example, we don't really want the URL with the banner to be indexed because it would be confusing for customers, so we add an appropriate canonical tag to our blog index page:

<link href="/blog" rel="canonical" />

As you can see, a canonical tag is just a <link> tag with rel="canonical", there's nothing really fancy about it. The main difficulty is to determine how to use them for different types of scenarios which we will explore below.

Why are canonical URLs important?

Regardless of what we do, Google will pick a canonical URL using their best judgment through a process called “deduplication”.

If we don't explicitly set the canonical URL for each page, we are at risk of undesirable outcomes like search engines picking a variation we don't want or having duplicate content issues.

Another reason to add canonical URLs is that they are useful to consolidate link equity, meaning that if we receive links to a page that defines a canonical URL, the authority will flow to the canonical version.

Canonicalization link authority flow example

Furthermore, properly setting canonical URLs helps us manage our crawl budget by minimizing the amount of time crawlers spend processing pages that we don't need to be indexed which means that they can crawl the pages we actually want indexed more frequently.

Even though we can influence search engines into considering a given page the canonical version for it, there are no actual guarantees that they will agree with us. Occasionally, they decide that what we consider the alternate page is actually the canonical. Check for “Duplicate, Google chose different canonical than user” errors in the Google Search Console to identify this issue.

Application setup

For this tutorial, all we need is to add the meta-tags gem as we will use it to add canonical tags with it.

bundle add meta-tags

Then, we run the installation command in case we need to customize anything:

bin/rails generate meta_tags:install

Finally, we add the display_meta_tags to our application layout's head tag:

<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
  <head>
    <!-- Rest of the content -->
    <%= yield :head %>
    <%= display_meta_tags site: false %>
  </head>
</html>

With this in place, let's see how to add canonical tags to our app:

Adding canonical URLs in Rails

There are basically two ways to add canonical tags in Rails applications: manually or using a gem like meta-tags or canonical-rails.

Normally, if you use the meta-tags gem, it makes sense to add canonical tags using it.

Let's start by seeing how we could add them manually:

Manually

We can add the canonical tag using the content_for helper in views:

<%= content_for(:head) do %>
  <%= tag.link rel: "canonical", href: posts_url %>
<% end %>

We can also create a helper to add the tag in one line:

module ApplicationHelper
  def canonical_tag(url)
    content_for(:head, tag.link(rel: "canonical", href: url))
  end
end

And we can call that in our views:

<%# app/views/posts/index.html.erb %>
<%= canonical_tag posts_url %>

With the meta-tags gem

With the meta-tags gem installed, we can easily add canonical tags with the canonical option:

<%# app/views/posts/show.html.erb %>
<% 
  tags = {
    title: @post.meta_title,
    description: @post.meta_description,
    canonical: posts_url(@post)
  }

  set_meta_tags(tags)
%>

Which produces the following:

Canonical tag with the meta-tags gem

There's really nothing more to it than this simple steps. However, the real challenge with canonicalization is facing the different scenarios where we need to decide how to do it:

Common scenarios

There are many situations where we should consider how to add canonical tags properly to avoid duplication issues but at the same time, make sure that things work as expected.

Paginated collections

Maybe one of the most common scenarios because most applications need to have paginated collections of resources.

Whether to add a self-referencing URL for each page or just pick the first page as the canonical URL has been discussed a lot in the SEO world because paginated resources can be considered not adding much content or making a difference in the search results page.

But, by definition, paginated collections don't produce duplicate content, so adding a self-referencing canonical URL for each page is considered a good practice as stated by John Mueller.

In Rails, this means that we generate the canonical URL for each page:

<% canonical_url = posts_url(page: params[:page]) %>

For our blog example, the first page would render https://avohq.io/blog and the second page would render https://avohq.io/blog?page=2.

Because this technique implies that each paginated collection can be indexed, it's better to add a unique title for each page:

<%
  title = "Ruby on Rails Articles" 
  tags = { 
    title: params[:page] ? "#{title} | Page #{params[:page]} - Avo" : "#{title} - Avo",
    description: "Discover comprehensive Ruby on Rails tutorials, best practices, and insights.",
    canonical: params[:page] ? posts_url(page: params[:page]) : posts_url
  }

  set_meta_tags(tags)
%>

This produces the following when the params[:page] is present in the URL:

Canonical pagination with page param present

Infinite pagination without progressive enhancement can discourage crawlers from indexing our paginated content. Unless you have a good reason to add it to your application, traditional pagination with a set of navigation links is preferable.

Product variants

A very common scenario that comes up with canonicalization is handling URL-accessible variants.

For example, we might sell shirts that come in four sizes: blue, red, green, and yellow. And have the following URL structure:

https://example.com/products/awesome-shirt/blue

Which renders the product page with the blue shirt selected as a variant, its image as a default, and its unique description, if it applies.

Deciding how to canonicalize the product page depends greatly on three factors:

  • The value of indexation: doing keyword research should tell us if there's value in having the variant pages indexed. If users are searching for “awesome shirt blue”, indexing variants is probably a good idea. Otherwise, if users tend to search for the product name without the variant, we might want to avoid indexing those pages.
  • The uniqueness: if the only thing we change about the page for each variant is a couple of words and the default image we might want to canonicalize each variant to the product page because search engines might consider the product page as the canonical URL anyway.
  • The potential amount of URLs: if we are building an e-commerce with few SKUs, indexing every variant won't hurt us in any way because we won't be exceeding our crawl budget. On the other hand, if we envision our site to have thousands of variants, we probably need to be more conservative about our approach.

Considering this, if we want to index the variants, we could define a URL structure like the following:

# config/routes
get "products/:id/:variant_id", to: "products#show", as: :product_variant

Now, if we decide to index each variant, we add a self-referencing canonical URL:

<% canonical_url = product_variant_url(@product, @variant) %>

And in the sitemap, we add something like this:

Product.available.find_each do |product|
  product.variants.each do |variant|
    add product_variant_url(product, variant)
  end
end

Otherwise, if we would rather not index variants, we add a route for the product:

resources :products, only: [:show]

We make the canonical URL point to the product URL:

<%# app/views/products/show.html.erb %>
<% canonical_url = product_url(@product) %>

This implies that we can add a variant picker that chooses the default variant using query params like /products/awesome-shirt?color=red so users can share a URL to a given variant without the need to have search engines index every variant. If the picker uses links, consider adding rel=nofollow to them.

And in the sitemap, something like the following:

Product.available.find_each do |product|
  add product_path(product), changefreq: "daily"
end

To keep things consistent, if we decide not to index the variants, links to the product page from collections shouldn't include query parameters because that indicates to search engines that they are separate URLs.

Filtered or faceted URLs

Another common canonicalization scenario happens when we implement filtering and faceted search functionality.

For example, an e-commerce site might allow users to filter products by category, brand, and price resulting in URLs that look like this:

https://example.com/products?category=electronics&brand=apple&price_max=999

As these URLs show product collections that match the filtering criteria, there are many combinations that can result in duplicate pages and indexing a potentially massive amount of URLs.

This scenario is very similar to the variant pages: to decide whether to index the collection pages or not we should try to do keyword research to find out what people are searching for. For example, if we find that people are searching for “apple laptops under 1000 dollars”, we might consider indexing the collection but not indexing other collections with lower or no value.

Besides, to rank for certain terms, we would need to customize the title, description, and content for each generated collection and also add a feature to match the desired keywords with the filters.

Of course, we could add the feature, but it might be more convenient to make the collection page the canonical one without the filters and add specific collections with a different route to match the keywords that we know have SEO value.

The following is an example from the Best Buy site where they do this:

Best Buy shop example

In addition to these specific shop pages that are keyword-oriented, they have a search page with faceted filtering where they canonicalize to the root URL:

Best Buy search page with faceted filtering canonicalization

You might notice that they are excluding the facets from the URL, but the actual canonical has the st=macbook+m4 query parameter which means they want this page to be indexed for that search term.

This is part of their individual strategy, but as a good rule of thumb, if we need to index keyword-related collections, it's probably better to add them separately.

The direction we take is highly dependent on the amount of URLs we expect to have, the way we handle our crawl budget, and the specific strategy we want to pursue which also depends on the size and authority of our site.

Translated content

Handling canonicalization for multilingual sites presents unique challenges, as content exists in multiple languages while often serving the same fundamental purpose. Consider a blog post available in both English and Spanish:

https://example.com/en/articles/canonical-urls-rails
https://example.com/es/articulos/urls-canonicas-en-rails

These pages contain essentially the same information but target different language audiences, requiring careful canonicalization strategy.

The approach depends on several considerations:

  • Content relationship: if pages are direct translations of each other, they should use hreflang annotations rather than canonical tags. However, if content is substantially different or adapted for local markets, each version should have its own canonical URL.
  • URL structure: whether you use subdirectories (/en/, /es/), subdomains (en.example.com, es.example.com), or separate domains (example.com, example.es) affects how you handle canonicalization.
  • Default language handling: if your site serves a default language without a language prefix (like /articles/rails-best-practices for English), you need to decide whether this canonicalizes to itself or to the explicit language version.

For true translations where content serves the same purpose across languages, avoid using canonical tags and instead implement hreflang annotations:

<%# app/views/layouts/application.html.erb %>
<% I18n.available_locales.each do |locale| %>
  <% if article_translation_exists?(@article, locale) %>
    <link rel="alternate" hreflang="<%= locale %>" 
          href="<%= article_url(@article, locale: locale) %>" />
  <% end %>
<% end %>

<%# Self-referencing canonical for the current language version %>
<% canonical_url = article_url(@article, locale: I18n.locale) %>

In your routes, ensure consistent URL structure:

# config/routes.rb
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
  resources :articles, only: [:show, :index]
end

For the sitemap, generate separate entries for each language version:

I18n.available_locales.each do |locale|
  Article.available.find_each do |article|
    if article.translation_exists?(locale)
      add article_url(article, locale: locale), 
          changefreq: "weekly",
          alternates: build_alternates(article)
    end
  end
end

def build_alternates(article)
  I18n.available_locales.map do |locale|
    next unless article.translation_exists?(locale)

    {
      href: article_url(article, locale: locale),
      lang: locale
    }
  end.compact
end

However, if you have content that's substantially adapted for different markets rather than direct translations, treat each version as independent:

<%# Each market-specific version gets its own canonical %>
<% canonical_url = article_url(@article, locale: I18n.locale) %>

Never use canonical tags to point from one language version to another, as this signals to search engines that one version is duplicate content. Use hreflang annotations to indicate the relationship between equivalent content in different languages, allowing each version to rank appropriately for its target audience.

Best practices

Adding canonicalization to a Rails app without much consideration can result in more harm than good in some scenarios. Consider the following best practices:

  • Always add a canonical tag: even if we don't envision needing a canonical for a given page, adding a self-referential canonical tag for pages that we want search engines to index is completely safe and can help us to avoid surprises down the line.
  • Only add one per page: if you're adding the tag manually, make sure that every page on your site has no more than one canonical tag.
  • Only add canonical URLs to the sitemap: make sure that you only add the canonical version of your URLs to the Rails sitemap. Otherwise, we're sending contradictory signals to search engines.
  • Beware of redirections: just like it happens with sitemaps, it's not recommended to add URLs that redirect as canonicals.
  • Always link to canonical URLs: whenever adding internal links within your application, make sure to always link to the canonical version.
  • Use absolute URLs: never use relative URLs for your canonical tags.
  • Add params when it makes sense: if your site uses query params to construct important parts of the content, don't hesitate to add them to the canonical tag. For example, if you have a route setup like /products?category=electronics&sub_category=computers to render the computer subcategory, make sure that you're adding the query params. Furthermore, consider using friendly URLs for a simpler structure.
  • WWW or non-WWW subdomain: decide whether you want to use the www subdomain for your site or not and keep it consistent across the canonical tags. Rails handles this for us with the default_url_options configuration where we can set the host and the protocol. Make sure to add the appropriate redirect to avoid indexation issues.
  • Trailing slash: decide whether you want URLs to have a trailing slash and stick to one style. Otherwise, search engines might consider URLs with and without a trailing slash as different.
  • Use https: this tip is probably redundant as most apps use https by default nowadays, but don't forget to check that canonical tags stay consistent with the protocol.

Besides taking these tips into consideration, don't forget to add your site to the Google Search Console, where we can see indexing issues and issues with canonical URLs.

Summary

Canonical URLs are URLs that point to the authoritative or primary version of a page that might produce duplicate or near-duplicate content.

Adding them to a Rails application can be as simple as adding a <link> tag manually or using a library like meta-tags or canonical-rails.

The most challenging part for large applications or sites that have many indexable URLs is to decide what should be the canonical URL.

We listed common scenarios and how to solve them:

  • Paginated collections: add a self-referencing link to the current page.
  • Product variants: if the variants don't get many searches canonicalize the main product or master variant. If the variants have distinct search demand, make each view indexable with its canonical URL.
  • Faceted search: identify filters that might be get traffic and add a canonical URL for the filters that don't. For example, you might generate a unique URL for /shoes?color=red&size=12 that targets the red shoes size 12 query but ignore things like sort criteria or other less significant query params.
  • Translated content:

I hope this article helps you add canonical URLs to your Rails application and that it leads you to better SEO results.

Have a good one and happy coding!


Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.