🇫🇷 French version available here !

At la revanche des sites, we believe there is a better way to do marketing. A more valuable, less invasive way where customers are earned rather than bought. That’s why we focus on search engine optimization (SEO) to help you to drive customers to your website or directly to your shop.

Many of our client websites are built with Ruby On Rails, a web application framework designed to work with the Ruby programming language. In this article we regroup the most common techniques we used to optimise your ranking with some examples & tips for your Rails application.

View source code on Github.

SEO-Friendly URL

Let’s start with the URL. Rails use by default the primary key (also called id) to generate routes and URL for your resources. For example, if you create a new post template, by default, the URL that accesses the resource will be of the type /posts/1 where 1 is the identifier. However, for a better user experience (UX) but also to perform in the search engines, we generally advise that the URL are composed of keywords associated with the resource. These URL must be valid and unique!

You can develop a system for managing this yourself quite easily but for the sake of speed and maintainability, we will use here a gem (or library) called friendly_id. This allows you to generate the link from a string associated with the resource, also called slug, which will not contain special characters (accents, etc.) and avoid duplication (two resources can not be accessed via the same URL). In our example, /posts/1 will become /posts/your-title where your-title is a chosen field of the template.

After following the instructions for installing the gem, this gives you:

# app/models/post.rb

class Post < ApplicationRecord

  # friendly url
  extend FriendlyId
  friendly_id :title, use: :slugged

# app/controllers/posts_controller.rb

def show
  @post = Post.friendly.find(params[:id])

Dynamic Error Pages

The error pages are used to indicate to the user but also to the robots that a resource is not available. Technically, your Rails server will respond with a different HTTP code depending on the state of the resource. These pages are important for your SEO since they are used by search engines, including Google, to better understand the behaviour and architecture of your site.

By default, Rails offers pages 404 (page does not exist or more), 422 (incomplete or incorrect resource) and 500 (server error) available in the / public directory. You will need to customize these pages to reflect the overall design of your site (with your layout / application.html.erb) or to dynamically return content based on the user.

To customize them, you will first need to specify routes to these error pages:

# config/routes.rb

match "/404", to: "errors#not_found", via: :all
match "/422", to: "errors#unacceptable", via: :all
match "/500", to: "errors#internal_server_error", via: :all

Then create the associated controller:

# app/controllers/errors_controller.rb

# do not forget to delete public/{404, 422, 500}.html
# rm public/{404, 422, 500}.html
class ErrorsController < ApplicationController

  def not_found
    render status: 404

  def unacceptable
    render status: 422

  def internal_server_error
    render status: 500


Do not forget to delete the old pages present in public:

rm public / {404, 422, 500} .html

Finally, specify to your application that errors are now generated dynamically:

# config/application.rb

config.exceptions_app = self.routes


After putting your application into production, you may be adding, modifying or deleting some pages of your site or changing the name of one of your posts and thus its slug. The architecture of your site will be modified, as well as your URLs. However, if a search engine has already indexed your site, you will have to redirect your old pages, and therefore old URLs, to new ones so as not to lose your visitors on your 404 pages. In this case, you must create 301 redirects to specify that one page has been moved permanently to another.

The Rails routing system makes these 301 redirects quite easily in the routes.rb file:

# 301 redirect from old URLs
match "/old_path_to_posts/:id", to: redirect("/posts/%{id}s")

You can also define regular expressions (or regexp) to automate some type of redirects.

Meta Tags

Meta Tags allow search engines and social networks to better understand the content of your page and thus gain visibility, so they are essential in SEO. They must be dynamic since they are specific to each page and must be unique to avoid any duplicate content conflicts.

Ruby On Rails does not propose a default management system for Meta Tags, so we will put one in place.

Start by creating a meta.yml file in the config directory that will contain the default values:

# config/meta.yml

meta_title: "SEO & Ruby On Rails"
meta_description: "The 2018 comprehensive guide on SEO in Rails"
meta_image: "logo.png" # Une image dans votre dossier app/assets/images/
twitter_account: "@RevancheSites"

We will then need to initialize the DEFAULT_META Ruby constant to be able to use the values anywhere in your application:

# config/initializers/default_meta.rb

# Initialize default meta tags.
DEFAULT_META = YAML.load_file(Rails.root.join("config/meta.yml"))

Then create the methods that will retrieve these values:

# app/helpers/meta_tags_helper.rb

module MetaTagsHelper
  def meta_title
    content_for?(:meta_title) ? content_for(:meta_title) : DEFAULT_META["meta_title"]

  def meta_description
    content_for?(:meta_description) ? content_for(:meta_description) : DEFAULT_META["meta_description"]

  def meta_image
    meta_image = (content_for?(:meta_image) ? content_for(:meta_image) : DEFAULT_META["meta_image"])
    # ajoutez la ligne ci-dessous pour que le helper fonctionne indifféremment
    # avec une image dans vos assets ou une url absolue
    meta_image.starts_with?("http") ? meta_image : image_url(meta_image)

Now add Meta Tags to your layout:

/ application.html.slim
title = meta_title

meta name="description" content="#{meta_description}"

/ Facebook Open Graph data
meta property="og:title" content="#{meta_title}"
meta property="og:type" content="website"
meta property="og:url" content="#{request.original_url}"
meta property="og:image" content="#{meta_image}"
meta property="og:description" content="#{meta_description}"
meta property="og:site_name" content="#{meta_title}"

/ Twitter Card data
meta name="twitter:card" content="summary_large_image"
meta name="twitter:site" content="#{DEFAULT_META["twitter_account"]}"
meta name="twitter:title" content="#{meta_title}"
meta name="twitter:description" content="#{meta_description}"
meta name="twitter:creator" content="#{DEFAULT_META["twitter_account"]}"
meta name="twitter:image:src" content="#{meta_image}"

And that’s it, you can now simply dynamically set these variables according to your resource, for example here for posts#show:

# app/views/posts/show.html.slim

- content_for :meta_title, "#{@post.title} | #{DEFAULT_META["meta_title"]}"

Structured Data

Structured data helps send the right signals to search engines about your app and content, helps to increase search engine understanding of your site’s content, and improve visibility through rich snippets and results. Also increases probability of being selected in Knowledge Graph. You really have to make the effort to implement a markup scheme on your site because this data is increasingly important for Google.

To know which schema is used on your site, we recommend this series of articles from Google.

There are different formats to use them: JSON-LD, Microdata or RDFA. It is advisable to use JSON-LD by Google but for the sake of simplicity, we will generally use Microdata.

The green_monkey gem makes it easy to integrate Microdata properties directly into your application:

# app/models/post.rb

class Post < ActiveRecord::Base
  html_schema_type :BlogPosting

Post.html_schema_type # => Mida::SchemaOrg::BlogPosting
Post.new.html_schema_type # => Mida::SchemaOrg::BlogPosting
/ show.html.haml
  = link_to "/posts/#{post.id}", :itemprop => "url" do
    %h3[:name]>= post.title
  .post_body[:articleBody]= post.body.html_safe
  = time_tag(post.created_at, :itemprop => "datePublished")

The breadcrumb allows you to remember the location of a page on your site in the global tree. It is important in SEO since Google officially recommends to offer a breadcrumb on your website.

There exists some gems to automatically generate a breadcrumb on your application : gretel, loaf or breadcrumbs_on_rails. We will use the latter, easily customizable and compatible with Bootstrap.

To install it, simply add the gem to your Gemfile.

# Gemfile
gem "breadcrumbs_on_rails"

And then add to the controllers concerned by the breadcrumb display:

# app/controllers/posts_controller.rb

class PostsController < ApplicationController

  add_breadcrumb "Blog", :posts_path

  def index
    @posts = Post.all

  def show
    @post = Post.friendly.find(params[:id])
    add_breadcrumb @post, post_path(@post)


NB : If you want to add the structured data features on your breadcrumb, you define a constructor :

# lib/breadcrumbs/builders/structured_data_breadcrumbs_builder.rb

class Breadcrumbs::Builders::StructuredDataBreadcrumbsBuilder < BreadcrumbsOnRails::Breadcrumbs::Builder
  def render
    @elements.collect do |element|
    end.join(@options[:separator] || " &raquo; ")

  def render_element(element)
    if element.path == nil
      content = compute_name(element)
      content = @context.link_to_unless_current(compute_name(element), compute_path(element), element.options.merge({
        itemscope: "",
        itemtype: "http://schema.org/Thing",
        itemprop: "item"
    if @options[:tag]
        {itemprop:"itemListElement", itemscope: "", itemtype:"http://schema.org/ListItem"}

Then display your breadcrumb in your layout :

/ application.html.slim
ul.breadcrumb itemscope="" itemtype="http://schema.org/BreadcrumbList"
  = render_breadcrumbs tag: 'li', separator: '',
builder: Breadcrumbs::Builders::StructuredDataBreadcrumbsBuilder


The sitemap is the file that contains the list of your website URL. This file is in XML format. It gives search engines information about your URL and the architecture of your site. It allows sites with a substantial content and a complex mesh to be better understood by search engines and thus be better referenced.

Some gems can generate it as sitemap_generator, or dynamic_sitemaps but it is nevertheless possible to build it by yourself thanks to the XML builder library.

First, create the route to specify the controller that will handle the request:

# config/routes.rb

get '/sitemap.xml' => 'sitemaps#index', defaults: { format: 'xml' }

Then let’s take care of building the associated controller:

# app/controllers/sitemaps_controller.rb

class SitemapsController < ApplicationController

  layout :false
  before_action :init_sitemap

  def index
    @posts = Post.all


  def init_sitemap
    headers['Content-Type'] = 'application/xml'


And the associated view :

# app/views/sitemaps/index.xml.builder

xml.instruct! :xml, version: '1.0'
xml.tag! 'sitemapindex', 'xmlns' => "http://www.sitemaps.org/schemas/sitemap/0.9" do

  xml.tag! 'url' do
    xml.tag! 'loc', root_url

  xml.tag! 'url' do
    xml.tag! 'loc', contact_url

  @posts.each do |post|
    xml.tag! 'url' do
      xml.tag! 'loc', post_url(post)
      xml.lastmod post.updated_at.strftime("%F")



The robots.txt file is a text file used for the natural referencing of your site, containing instructions for search engine indexing robots in order to specify which pages may or may not be indexed. You can visit robots-txt.com for more information about its contents.

Ruby On Rails proposes by default a robots.txt present in the public directory. You can, as with the management of the error pages or the sitemap, make it dynamic and allow to define directives specific to your environment, development, pre-production or production for example.

# config/routes.rb

get "/robots.:format", to: "pages#robots"
# app/controllers/pages_controller.rb

class PagesController < ApplicationController
  def robots
    # Don't forget to delete /public/robots.txt
    respond_to :text


Also specify the URL of your sitemap to help robots find it easier.

/ robots.txt.slim
- if Rails.env == "production"
  = "User-Agent: *\n"
  = "Disallow: \n"
- else
  = "User-Agent: *\n"
  = "Noindex: /\n"
= "\nSitemap: #{root_url}sitemap.xml"


Google AMP, for “Accelerated Mobile Pages” is a protocol designed by Google to enable a “faster web”. AMP pages are a variation of classic web pages with simplified HTML and JavaScript. AMP only keeps what is needed to display the information very quickly.

First, specify the new mime type for your application :

# config/initializers/mime_types.rb

# Be sure to restart your server when you modify this file.

# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf

Mime::Type.register 'text/html', :amp

You will then need to create a new AMP layout:

<!-- application.amp.erb -->
<!doctype html>
<html >
    <meta charset="utf-8">
    <link rel="canonical" href="<%= url_for(format: :html, only_path: false) %>" >
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
    <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
    <script async src="https://cdn.ampproject.org/v0.js"></script>
    <script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-iframe-0.1.js"></script>
    <script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>
    <% if Rails.application.assets && Rails.application.assets['amp/application'] %>
      <style amp-custom><%= Rails.application.assets['amp/application'].to_s.html_safe %></style>
    <% else %>
      <style amp-custom><%= File.read "#{Rails.root}/public#{stylesheet_path('amp/application', host: nil)}" %></style>
    <% end %>
    <div class="amp">
      <%= yield %>

The AMP needs to include only CSS tags and not external tags. The trick to continue using the Rails asset pipeline and compiled CSS in views is to create a new SASS style file under app/assets/stylesheets/amp/application.scss containing the style you want for your pages AMP. Then save this file to the precompilation pipe by adding it to the config/application.rb file.

# config/application.rb

config.assets.precompile << 'amp/application.scss'

The last problem handled is that AMP limits the use of certain tags and requires modifications for others to work. For example, the img tag becomes amp-img, the same for iframes. The solution is to implement a scrubber to customize the Rails ActionView sanitize method. This scrubber will try to convert the original DOM to make it “AMP valid”. That’s what it looks like:

# app/scrubbers/amp_scrubber.rb

class AmpScrubber < Rails::Html::PermitScrubber
    'img' => lambda { |node|
      if node['width'] && node['height']
        node.name = 'amp-img'
        node['layout'] = 'responsive'
        node['srcset'] = node['src']
    'iframe' => lambda { |node|

      node['src'] = node['src'].gsub(%r{^(\/\/|http:\/\/)}, 'https://')
      url = URI(node['src'])
      node['layout'] = 'responsive'

      if url.host.include?('youtube.com')
        node.name = 'amp-youtube'
        node['data-videoid'] = node['src'].match(%r{(\/embed\/|watch?v=)(.*)})[2]
        node.name = 'amp-iframe'

  def initialize
    @tags = %w(a em p span h1 h2 h3 h4 h5 h6 div strong s u br blockquote)
    @attributes = %w(style contenteditable frameborder allowfullscreen)

  def self.find_parent(node)
    node = node.parent while node.parent


  def scrub_attribute?(name)

  def scrub_node(node)
    if node.name.in?(TAG_MAPPINGS.keys)
      remap_node! node, TAG_MAPPINGS[node.name]

  def remap_node!(node, filter)
    case filter
    when String
      node.name = filter
    when Proc

Finally, you can create the views you want to make available in AMP for example:

# app/views/posts/show.amp.slim

h1 = @post.title
div = sanitize @post.content, scrubber: AmpScrubber.new

Do not forget to specify the URLs of your AMP pages in your layout to allow Google to index them:

# app/views/layouts/application.html.erb
<link rel="canonical" href="<%= url_for(format: :html, only_path: false) %>" >
<link rel="amphtml" href="<%= url_for(format: :amp, only_path: false) %>" >


By default, if you have configured your server correctly with your SSL certificate, you should automatically have your Ruby On Rails application in HTTPS. To enable the feature for a specific environment, you will need to enable the following option:

# config/environments/production.rb 

Rails.application.configure do 
  # ... 
  # force HTTPS on production 
  config.force_ssl = true 


www to non-www redirection

In SEO, one of the recurring problems is the duplicate content, which corresponds to two different pages containing the same content. This problem is very often caused by the duplication of content between the non-www http://example.com and the www version http://www.example.com.

To work around this problem, a simple technique:

# Force www redirect
# Start server with rails s -p 3000 -b lvh.me
# Then go to http://www.lvh.me:3000
constraints(host: /^(?!www\.)/i) do
  match '(*any)' => redirect { |params, request|
    URI.parse(request.url).tap { |uri| uri.host = "www.#{uri.host}" }.to_s


The other way to avoid duplicate content is to use the rel = canonical link tag. It allows to indicate to the search engines the canonical URL on a given page.

If any of your site’s pages are accessible through multiple URLs, or if different pages on your site have similar content (for example, a page with a mobile version and a classic version), you must explicitly tell Google what is the canonical URL for these pages, in other words the authoritative URL, otherwise Google will choose the canonical page for you or consider all similar pages equal.

The trick to set it up on your Rails application:

/ application.html.slim
= yield :canonical
# app/helpers/application_helper.rb

def canonical(url)
  content_for(:canonical, tag(:link, rel: :canonical, href: url)) if url
/ show.html.slim
- canonical(blog_post_url(@blog_post))


We explained in a previous article on the implementation strategies for SEO, the cache is not negligible especially if your site contains a lot of URLS. For each crawl of a site, Google’s robot dedicates a specific time. The faster your pages load, the more Google crawls pages on your site.

Rails provides a set of caching features. We could devote a whole article to this subject, so I advise you to go to the official documentation.


When you put a site online, you usually test its technical status with tools like PageSpeed that allows you to know the rating that Google assigns to your site in technical terms. It is said that rating awarded by Google PageSpeed, is a ranking factor for your SEO, therefore you must aim for the best rating.

Generally, the problems that PageSpeed will raise will be the position of your JavaScript resources as well as the compression of your assets. For the first problem, all you have to do is move calls from them to your layout. For compression, nothing more simple, just activate it with the middleware Rack :: Deflater. Inserted correctly in your application, it can significantly reduce the size of your responses.

# config/application.rb

module SeoRubyOnRails
  class Application < Rails::Application
    # Deflater
    # See also : https://robots.thoughtbot.com/content-compression-with-rack-deflater
    config.middleware.use Rack::Deflater


You have already understood, in SEO, links (or anchors) are essential, they allow your visitors and robots to navigate your site. It is really important to know the nofollow directive. By default, if a bot looks at a link, it follows it to try toindex the entire page recursively. This applies to the internal links of your site but also external. It will be necessary to pay attention to all the fields likely to be used for spammers to come to improve the visibility of their own sites by adding links on yours. For this we can use the Nokogiri gem. Just inspect the sent fields that can contain links before saving them.

# app/models/post.rb

class Post < ApplicationRecord
  # ...
  # callbacks
  before_save :anti_spam

  def anti_spam
    doc = Nokogiri::HTML::DocumentFragment.parse(self.content)
    doc.css('a').each do |a|
      a[:rel] = 'nofollow'
      a[:target] = '_blank'
    self.content = doc.to_s
  # ...

The Nokogiri gem can also be used to create a crawler.

Homepage H1

In SEO, a page corresponds to a positioning goal. We usually include the company logo in the <h1> tag for the home page only (which uses the brand name as the alt attribute). All other pages on the site will have a relevant <h1> text tag on the page.

/ app/views/layouts/_header.html.slim
        = link_to root_path do
          - if current_page?(root_url)
            h1 = image_tag 'lrds_logo.svg', alt: "SEO & Ruby On Rails", height: "20px"
          - else
            = image_tag 'lrds_logo.svg', alt: "SEO & Ruby On Rails", height: "20px"

      li = link_to "Blog", posts_path
      li = link_to "Contact", contact_path

To conclude, Ruby On Rails being a very flexible fast development framework, it is quite easy to set up all kinds of recommendations for SEO. There are many factors to be considered when positioning yourself on search engines, this guide lists only those related directly to development, but we must not forget that original, quality and organized content with a architecture that meets the needs and expectations of your visitors remain the keys to reach the first position!

Feel free to share and ask any questions or remarks in comment, we will be happy to answer!

Special thanks