MetaGreg

writes code that writes code for food

How to create Google-friendly URLs in Rails

with 4 comments

If you are building content-management systems or any applications with pages that you want to appear in a Google search, it is very important that you employ search engine optimization techniques or SEO. One such technique involves URLs.

Given a default URL in Rails, e.g. http://cia.gov/users/123, we know as developers that this request involves the controller ‘Users’, an action ‘show’, and a User with an ID ’123′. However, Google doesn’t care about controllers actions, no ‘id’s – it only cares what the URL represents. To tell Google what the URL represents, we need to add more information like a name.

The most common approach in the Rails community is create URLs like this ‘/users/123-greg-moreno’. Hyphens are highly recommended because it is the king of word separation. To accomplish this, we modify our User model:

def to_params
  "#{id}-#{name.parameterize}"
end

I think this became prevalent because it is the easiest and involves the least amount of code. There’s no need to change all your controller code because this format, i.e ‘users/123-greg-moreno’, works well with User.find(params[:id]) thanks to Ruby’s string to numeric conversion.

>> id_s = params[:id]
=> "123-greg-moreno"
>> id = id_s.to_i
=> 123

One principle in SEO is that the relevant keywords should appear first, which is ‘name’ in our case. As an alternative, we can change our URL to ‘/users/greg-moreno-123′. Let’s see if this works.

>> id_s = params[:id]
=> "greg-moreno-123"
>> id = id_s.to_i
=> 0

This won’t work as is because Ruby looks at the first digits it can recognize. If we go by this route, we need to build a method to extract the ‘id’.

def extract_id(s)
  s.gsub(/.+-(\d+)$/, '\1') if s
end

User.find extract_id(params[:id])

This isnt’ nice because you need to make sure you call the ‘extract_id’ method every time we need the ‘id’.

What we want is a URL that is SEO-friendly and at the same time requires minimal changes to our code. My friend who maintains a site with 4M visitors a month, recommends this format ‘users/greg-moreno/123′.

This is better but it will fail because it will match the route ‘users/:id/:action’. To work around this, we need to change the format and add a new route. Before we got into the technical details, let’s discuss our goals first:

1. The URL we are preparing is for use by Google and other search engines.
2. We also expect this URL to be pasted in blogs, forums, and other websites.

The URL we are preparing is for external use. Pages that need authentication doesn’t need to be in a SEO-friendly format because after all we don’t intend Google to see them.

If you agree with me, let’s continue :)

Here are the things we need to do then.
1. Remove ‘to_params’ from our models. Use Rails’ default.
2. Create a new route to distinguish our Google-friendly URL with Rails’ default
3. Use the Google-friendly URLs in pages that we want Google to see (e.g. public forum pages)

Here’s the new route:

map.user_permalink 'u/:name/:id', :controller => 'users', :action => 'show'
# => u/greg-moreno/1

# in  template
link_to user.name, user_permalink_path(:name => user.name.parameterize, :id => user.id)

I’m sure you would be using this URL in at least one 1 page so you better move this to a helper method.

module UrlsHelper
  def permalink(object)
    case object
    when User
      user_permalink_path(user_permalink(object))
    when Book
      book_permalink_path(book_permalink(object))
    end
  end

  def user_permalink(user)
    {:name => "#{user.name.parameterize}",
     :id  => user.id,
    }
  end

  def book_permalink(book)
    # your code here
  end
end

# in your template
link_to user.name, permalink(user)

Wait, we’re not done yet. Let’s improve the code a little bit. In our UrlsHelper, we can do away with the case/when statements by checking the object’s class.

def permalink(object)
  name = object.class.name.downcase
  params = send("#{name}_permalink", object)
  send("#{name}_permalink_path", params)
end

But wait there’s more!

Wouldn’t it be nice if instead of another helper, we could just use the generated route names instead? What if we can do this?

user_path(user, :permalink => true)
# => u/greg-moreno/123
user_path(user)
# => users/123
user_path(user, :permalink => true, :sort => 'votes')
# => u/greg-moreno/123?sort=votes

That would be cool! To do this we need to dig deeper into Rails. We still use the same technique as our UrlsHelper but we need to modify Rails’s default URL generation code:

module ActionController
  class UrlRewriter
    private

    def rewrite_url_with_permalink(options)
      debugger
      if options.delete(:permalink)
        case options[:use_route]
        when :user
          options.merge!(user_permalink(options[:id]))
        when :book
          options.merge!(workspace_permalink(options[:id]))
        end
      end

      rewrite_url_without_permalink(options)
    end
    alias_method_chain :rewrite_url, :permalink

    def user_permalink(id)
      object = id.is_a?(Fixnum) ? User.find(id) : id
      { :use_route => :user_permalink,
        :name      => object.profile ? object.profile.name.parameterize : object.name.parameterize,
        :subdomain  => false
      }
    end

    def book_permalink(id)
    end
  end
end

The method ‘url_rewrite’ handles the generation of the URL after Rails has decided what route to use. For example, in ‘user_path(user, :permalink => true)’, options would have the following values:

{:action=>"show", :controller=>"user", :only_path=>true,
:permalink=>true, :id=>#<User id: 123,  name: "Greg Moreno"...>,
:use_route=>:user}

What we do then is tell Rails the route to use, i.e. :user_route => :user_permalink, and pass the extra options to generate our Google-friendly URL. If the code looks a bit strange, that’s OK. This code uses method chaining to add functionality to ‘url_rewrite’

alias_method_chain :rewrite_url, :permalink

# above line is a shortcut for 2 method aliases:

alias_method :url_rewrite_without_permalink, :url_rewrite
alias_method :url_rewrite, :url_rewrite_with_permalink

For further reading, you can check Jamis Buck’s detailed post on Rails route generation and recognition.

Limitations: This doesn’t work with nested resources and additional member/collection methods. If you have a solution, I would love to try your code :)

Update: The friendly_id plugin uses slugs and even has redirect options. subdomain_fu uses ‘url_rewrite’ to add a subdomain to your URL. You can also this approach to modify the protocol like change it to ‘https’ for some accounts.

Share and Enjoy:
  • Digg
  • Facebook
  • StumbleUpon
  • TwitThis

Related posts:

  • How to create a class on the fly in Ruby “So what if Ruby is dynamic?” This is often the reaction I get whenever I tell friends that Ruby allows you to fiddle with your program at runtime; followed by...
  • Rails 3 upgrade part 2: Routes In the previous post, I outlined the steps I took to upgrade and boot a Rails 3 application. This time, I share my experience upgrading the routes file. By the...
  • How to share code between Javascript and Rails Rails’ validations is great because it allows you to quickly implement the valid states of your models and at the same time have a ready-made way of displaying the errors...
  • Rails 3 upgrade part 4: Prototype helpers and Javascript Rails 3 is embracing the unobtrusive Javascript (or UJS) mantra which is good because it is the right way; at the same time, it is bad because many applications will...
  • How to read Google buzz updates in Ruby require 'rubygems' require 'nokogiri' require 'open-uri' require 'feedzirra' profile_name = 'dave.winer' page = Nokogiri::HTML(open("http://www.google.com/profiles/#{profile_name}")) feed_url = page.search('//head/link[@type="application/atom+xml"]').first['href'] feed = Feedzirra::Feed.fetch_and_parse(feed_url) puts feed.title puts feed.url puts feed.last_modified feed.entries.each do |entry| puts...

Written by Greg Moreno

August 24th, 2009 at 9:52 pm

Posted in Geekiness

Tagged with ,

4 Responses to 'How to create Google-friendly URLs in Rails'

Subscribe to comments with RSS or TrackBack to 'How to create Google-friendly URLs in Rails'.

  1. Hey Greg,

    Your solution looks good. I’ve used plugins for this in the past, friendly_id in particular. I haven’t looked recently, there may be newer/better plugins on the scene.

    One nice feature of the plugin is that if the user changes his/her name, their URL changes as well, and the plugin will automatically do a 301 redirect so none of your links break or google juice. It keeps a “slugs” table in the database with past and current urls.

    Good post,
    Brian

    Brian Armstrong

    25 Aug 09 at 6:30 am

  2. Another good post…
    You are right about hyphens..
    Have written more than 2 apps so far but I’m still very SEO-challenged.
    My pages cannot be googled yet:( Just replaced my lousy gsub code with stringex method to_url 2 days ago (so I have title.to_url instead of title.gsub(….) ). I am still working on improving a Sinatra app.
    The slug is saved on database. I always opted for solutions wherein slugs are saved on db like friendly_id. So far I like with friendly_id. It handles duplicate url’s well. but you can just imagine having links like about–2 if there are duplicates. That’s still fine I think.
    You solution is also ok.. having the id before the name.

    katz

    25 Aug 09 at 6:46 am

  3. @brian, @katz Thanks for sharing the plugin. That would definitely a time-saver. I resisted looking for plugins because I knew it would affect my line of thinking while I write :)

    The slugs/redirect are very useful indeed especially for SEO purposes.

    I am also toying with the idea of adding functionality in Rails route recognition so I can drop the ID just like in friendly_id but your controller sees params[:id]. Hmmn. But I guess that’s just the same with friendly_id :)

    Thanks for the feedback guys

    Greg Moreno

    25 Aug 09 at 10:51 am

  4. In addition to the solution shown here you might consider using the friendly_id gem.
    A profound step-by-step guide can be found here: http://doblock.com/articles/seo-friendly-urls-for-your-rails-app-with-friendly_id

Leave a Reply