SEO-Friendly URLs with Rails
I had a chance to work on a project that used friendly_id. The gem was mostly great, but there were a few situations when it didn’t work as expected and I spent hours fixing it. I’m not a fan of reinventing the wheel either, but for my latest project I tried to implement SEO-friendly URLs just with what Rails has to offer. It turned out really easy and with many advantages over the gem.
There are two common requirements for SEO-friendly URLs:
- URL is more descriptive than just an id (e.g. /courses/123-search-engine-optimization-best-practices is better than /courses/123)
- URL is canonical (/courses/123-old-course-title redirects to /courses/123-search-engine-optimization-best-practices rather than showing a duplicate page or 404)
Descriptive URLs
ActiveSupport::Inflector provides a method to convert any string into a URL-friendly format.
[irb(main):001:0> "Course title".parameterize
=> "course-title"
It also removes special characters and accents.
[irb(main):002:0> "Course title ~ 2 $$".parameterize
=> "course-title-2"
[irb(main):003:0> "Názov v slovenčine".parameterize
=> "nazov-v-slovencine"
ActiveRecord::Base has to_param method, which can be overridden.
class Course < ActiveRecord::Base
def to_param
"#{id}-#{title.parameterize}"
end
end
And to_param is used to generate URLs.
[irb(main):004:0> course.to_param
=> "123-search-engine-optimization-best-practices"
[irb(main):005:0> course_path(course)
=> "/courses/123-search-engine-optimization-best-practices"
ActiveRecord find ignores extra characters after numeric id. That's why we include id right at the beginning.
[irb(main):006:0> Course.find("123-search-engine-optimization-best-practices")
=> #<Course id: 123, title: "Search Engine Optimization Best Practices">
This is great, because you don't need to change a line of code in your controllers or routes :) This code works perfectly with SEO-friendly URLs:
class CoursesController < ApplicationController
def show
@course = Course.find(params[:id])
end
end
Three lines of code in your model is really all you need to meet the first SEO requirement.
Canonical URLs
Imagine that we are updating our resource and the attribute used in to_param is changed.
[irb(main):007:0> course.update_attributes(title: 'New title')
=> true
[irb(main):008:0> course_path(course)
=> "/courses/123-new-title"
Extra characters after numeric id are ignored by find and now both URLs work and serve duplicate content. There's a simple solution again, but this time, we'll need to touch our controllers.
class CoursesController < ApplicationController
before_action :load_course
before_action :ensure_canonical_url def show
end private def load_course
@course = Course.find(params[:id])
end def ensure_canonical_url
redirect_to @course if @course.to_param != params[:id]
end
end
We added ensure_canonical_url before action. Let's say a new request for 123-search-engine-optimization-best-practices comes in. The 123 course is loaded in spite of having a changed title. However, course.to_param is currently 123-new-title, not 123-search-engine-optimization-best-practices as requested. The condition in ensure_canonical_url is true and the user is redirected to the current course URL (/courses/123-new-title).
When you decide to add more attributes to your URLs, just include them in to_param. All the URLs change and the outdated ones will redirect to the current ones, which become new canonical URLs (as long as you keep id at the beginning, of course).
Conclusion
to_param method in your models and ensure_canonical_url before action to your controllers are really all you need for SEO-friendly URLs. You don't have to write a single migration, change your routes config or rely on yet another gem. Moreover, now it's your application code — DRY it up with or without concerns and write tests for it!