Titles and SEO Tags for Rails 5: A Better Way

Years ago, when I first started coding in Rails, I wrote a blog post about handling titles and meta tags in a Rails app. While it was a good solution for the time, it is a woefully outdated approach for the world of Rails 5. So what is a better way?

Splitting the Contexts

It is crucial to understand that there are two different contexts for titles and meta tags in a Rails app.

For 50% of your app - every index action and every new action - your title will always be the same. As an example, let's think about a project management app. The projects index page will always have the title "Your Projects" regardless of which user is logged in. And the new project page will always have the title "Create a Project".

But the other 50% of the app - every show action and every edit action - will have a changing title. The project show pages should display the name of the particular project being viewed as the page's title: "Food Delivery App", "Human Resources Tool". Just as the user show pages should have the user's name as the title: "David Lesches's Profile", "Khaleesi's Profile". And your edit pages should also have a particular name in the title: "Edit David Lesches's Profile", "Edit Khaleesi's Profile".

Phrased another way, 50% of our app has static titles and metatags, and other 50% has dynamic title and metatags.

Great, so how to proceed?

Starting with the Static 50%

For the static titles, our approach should be a no-brainer. Let's hook into Rails' locales translation mechanism.

Under config/locales, add a new file named meta.en.yml

config/locales/meta.en.yml
en:
  meta:

Let's use a simple, logical structure to list our titles and metatags. Let's group them by combining the controller name with the action name. Like so.

config/locales/meta.en.yml
en:
  meta:
    projects_new:
      title: Create a Project
      keywords: 'projects, secure, project management, tools'
      description: 'Create your secure, personal project on AwesomeAppX.'
    projects_index: 
      title: Your Projects
      keywords: 'projects, project management, tools, listing'
      description: 'All of your AwesomeAppX projects.'
    users_new:
      title: Add a User
    users_index:
      title: All Users

I know the above isn't a great example of using SEO keywords, and that for user-login-required parts of your app you don't even need to have any keywords or description, as search engines can't scrape it anyway. But I'm just giving an example of how to handle keywords and descriptions should you want to. You can skip them when you like, as I did for the users_new and users_index actions.

I also like to add a default title to render when no other one can be found. Like so.

config/locales/meta.en.yml
en:
  meta:
    default:
      title: "*Title Missing*. Add it in locales/meta"
      keywords: ''
      description: ''
    projects_new:
      title: Create a Project
      keywords: 'projects, secure, project management, tools'
      description: 'Create your secure, personal project on AwesomeAppX.'
    projects_index: 
      title: Your Projects
      keywords: 'projects, project management, tools, listing'
      description: 'All of your AwesomeAppX projects.'
    users_new:
      title: Add a User
    users_index:
      title: All Users

OK, so we've listed out our static titles. It's all nicely organized in a single file for our entire app. Neat. So how do we get it to actually show up in our HTML?

Patience, buster. We'll get there, I promise. For now, let's shift gears to the dynamic titles.

The Dynamic 50%

For our pages with dynamic titles, we will need to define the titles within the views. There is simply no other way; it is only within the view that we know what the title should be.

Here, I will start backwards. How do I want the implementation to look like?

Simple. I want very clear, easy helpers to use in my view files.

Like this.

app/views/projects/show.html.haml
- title @project.name
- meta_description @project.description

(... then the other show page HTML output goes here ...)

The same for our edit pages.

app/views/projects/edit.html.haml
- title "Edit #{ @project.name }"

(... then the other edit page HTML and form output goes here ...)

Making the Magic: The Helpers

So now for the last step. Let's make some helpers.

app/helpers/application_helper.rb
module ApplicationHelper

  def title override = nil
    if override
      content_for(:title, override)
      return
    end

    if content_for?(:title)
      content_for(:title)
    else
      t "meta.#{page_key}.title", default: t("meta.default.title")
    end
  end

  def page_key
    [ controller_name, action_name ].join('_').to_sym
  end

end

Our title helper method takes an optional argument called override. If you opt to pass in this argument, as we do in our show and edit pages when we call - title @project.name, the title method sets that text into memory, via content_for, so we can access it later in our layout.

But should you call the title method without any argument, it'll retrieve the title for us. First it'll look for any dynamic title we set, and if there is none, it'll fetch the static one from meta.en.yml

All that is left is to call the title method in our layout.

app/helpers/views/layouts/application.html.haml
!!!
%html{ :lang => "en", :class => 'no-js' }
  %head
    %title= title

And that's it! It will fetch your dynamic and static titles perfectly.

Now For The Metatags

For the metatags, we do the same thing. Let's refactor our application_helper.rb.

app/helpers/application_helper.rb
module ApplicationHelper

  def title override = nil
    meta_or_override :title, override
  end

  def meta_keywords override = nil
    meta_or_override :keywords, override
  end

  def meta_description override = nil
    meta_or_override :description, override
  end


  private

  def page_key
    [ controller_name, action_name ].join('_').to_sym
  end

  def meta_or_override type, override
    if override
      content_for(type, override)
      return
    end

    if content_for?(type)
      content_for(type)
    else
      t "meta.#{page_key}.#{type}", default: t("meta.default.#{type}")
    end
  end

end

And update out layout.

app/helpers/views/layouts/application.html.haml
!!!
%html{ :lang => "en", :class => 'no-js' }
  %head
    %title= title_with_site_name
    %meta{ :name => "keywords", :content => meta_keywords }
    %meta{ :name => "description", :content => meta_description }

And our layout can seamlessly fetch our titles, meta keywords, and meta descriptions, whether we set static ones in meta.en.yml or dynamic ones in our view files.

I like this approach because it means that for 50% of our app - all pages with static titles and tags - we can define those titles and tags in a single place instead of littering those titles across all our view files. In addition, all our pages that are truly static - like our homepage, terms and conditions, privacy policy, FAQ, contact us, about our team, and other such pages can also have their titles and tags defined in this one meta.yml file.

Even better, a fantastic side benefit of this approach its easy support for multiple languages. Because we hooked into Rails' locales mechanism, you can add titles and tags for other languages by simply adding additional meta.yml files, like meta.fr.yml for French.

Tip: If your site is well structured, every page will have a similar look and start with a h1 tag. You can now move that h1 tag out of all your many views into your layout, with a simple %h1= title and it'll fetch the page title for your h1 tag as well.