Default Routes Considered Harmful, and Other Rails SEO Tips

There is much to love about the Ruby on Rails framework. Don't Repeat Yourself. It Just Works. Massive productivity gains, happiness returning to the world of boring CRUD apps, and a certain sense of panache in programming. However, while Rails has sensible defaults it doesn't get everything right out of the box. This article focuses on how you can improve the search engine optimization (SEO) of your Rails site the Ruby way and get a

  • more usable,
  • more popular,
  • and more profitable application -- with less work!

It's conveniently divided up into bite-sized chunks for you -- try the sidebar on your right, or just start scrolling down.

The article presumes a basic familiarity with SEO. If you don't have that yet, I recommend starting with SEOMoz, who write lots of easily comprehensible beginner-oriented oriented articles, or perhaps reading my first foray into giving advice to small businesses on SEO.





Rails SEO Tips By Topic





Case Study: Daily Bingo Cards

Throughout this article I'll be using this site, Daily Bingo Cards, as an example. Basically, it exists to sell a Java program which makes bingo cards for teachers. Understanding the business case for running the site is not strictly necessary to understand the following tips, but it helps if you know that:

  • Category has_many :cards       #cards are grouped in categories like Holidays
  • Card belongs_to :category       #shows one set of bingo cards, like Valentine's Day bingo

Back to the Table of Contents





Sexy URLs, or, Default Routes Considered Harmful

Most Rails programmers are smart enough to use scaffolding for development purposes and abandon it before going live. This leverages the framework to increase productivity and decrease the amount of time it takes to see results. This is a good Rails practice. Many Rails developers, sadly, think that leaving the default routes.rb file in place saves them time and effort. This is a bad Rails practice.

URLs are first sight of your app's GUI -- If every Rails developer said that to themselves, there wouldn't be a deployed default routes.rb left on the Internet. A URL is not just an arbitrary string of characters which identifies a resource on the Internet. A URL does not merely identify, it also describes what comes behind it, both to end users and, critically, to search engine crawlers. Take a look at these two URL pairs:

  • www.dailybingocards.com/card/show/116
  • www.dailybingocards.com/category/show/16

What is behind those URLs? I don't know -- and I wrote the app! Let's rewrite so that visitors and Google know what they're getting:

Much better! Now anyone knows what those two URLs are about -- the first offeres European Capitals bingo cards, the second offers geography bingo cards. What is more, these URLs map to the problem domain. You and Googlebot can both clearly tell by the folder metaphor that European Capitals belongs_to Geography, whereas before that was buried in our user-invisible Ruby code. Additionally, while you have almost certainly chosen your identifiers for convenience for you, your publicly visible URLs should be convenient to your users. You wouldn't use variable names as UI labels, why would you print them in URLs? I would have gone spare typing BingoCard.find_by_bingo_card_name all the time while writing my controllers, but there is no reason my laziness should result in URLs which don't tell the user what kind of "card" they are getting. Although, to be fair, with the domain they should have a pretty good idea.

How I Did It: This is simplicity itself. Either use the Permalink-fu plugin and do some mild hackery, or add the following method to your models:

def to_param
  url #replace with anything that makes a URL-encoded string
end
  

This will make Rails default to searching for your objects, and writing routes to them, by their slugs (whatever you wrote to_param as) instead of their IDs. Note that if you put an index on whatever column is used to generate the slug, this is no slower than searching on numeric IDs, but it sure looks nicer. If you're lazy you can prepend the ID to the slug, which will cause Rails to search on the ID.

Note that you will have to write custom routes to go with this, and you will have to be explicit about creating URLs with url_for and the like. I like naming mine so that I can generate them quickly and never get an ugly route by mistake:

#goes in routes.rb
  map.showCategory 'bingo-cards/:category', :controller => 'category', :action => 'show'
  map.showCard 'bingo-cards/:category/:url', :controller => 'card', :action => 'show'
  

Since I'm lazy I gave Card a helper method which writes a show URL for the appropriate instance, but the canonical way is to pass everything that shows up in the route, like so

link_to "Put My Anchor Text Here", showCard_url(:url => @card.url, :category => @card.category)
  

Performance Note: Generating anonymous routes is very freaking expensive because Rails has to try generating every possible route you have listed. Use either named routes or helper/model methods if you've got lots of links on a page and are worried about performance.

Back to the Table of Contents





Taming Your Metadata

The most important bit of real-estate on your page for SEO purposes is the title. Many Rails applications simply let layouts/application.rhtml fill in something useless like "Name Of My Site" as the title. Slightly smarter Rails applications, like the one generated by the scaffold, detect the name of the controller and use that to construct the title -- better, but we're showing programmer-optimized identifiers to the end-user again. Bad idea!

The better way, mostly DRY with fine-tuned configuration when necessary

#in your application.rhtml
<title>@title? @title: "My Default Sitewide Title"</title>

#in each of your controllers    
before_filter :set_title
def set_title
  @title = "My Default Title For This Controller"    
end   
                       
def someAction    
  # some code goes here    
  @title = "My Incredibly Fine-Grained Title"    
end 
    

Presto-changeo -- hierarchial titles with no pain and sensible defaults should you not want to specify anything at the action level. You can be as sophisticated as you want at any of these levels -- my action titles, for example, look at the instance variable I'm show-ing and construct a keyword-rich, human-readable title based on it.

Other places you can use the same trick

  • meta keywords (mostly useless, but hey, its free to write something there)
  • meta description, which may control the excerpt Google shows when your page pops up on organic search. I'd pay as much attention to this text as I do to my PPC ad text, as it has essentially the same function (beat out 9 competitors to win the click!)
  • internal calls to action

Back to the Table of Contents





Don't Repeat Yourself (In Your Content)

Search engines penalize sites for internally duplicated content, which means multiple URLs on your website resolving to the same or strikingly similar pages. For example, if you have a blog,

/posts/2007/January/1/Happy-New-Year

and

/posts/2007/January/1

are likely to be strikingly similar. That isn't a positive thing.

The easiest way to take care of this is to decide, in your design phase, which way to access any particular bit of data "wins" as the canonical way. If you were designing a blog, that would probably be the individual post page, as it is most likely to be linked. Then, for the other pages, put <meta name="robots" content="noindex,nofollow" /> somewhere between their head tags. This tells search engine crawlers that they can feel free to use the links from these pages, but that they shouldn't put them in the index (and should, instead, prefer the pages that don't have this restriction).

Another way is to edit your robot.txt file to totally excude any duplicated pages. This is particularly an issue for certain forms of search and pagination. Frankly, the default Rails pagination is very lackluster from a SEO perspective. Either roll your own or look for a plugin which produces clean, consistent, unique URLs.

While we're on the subject of not copying like an out of control Xerox machine, stuffing the same keyword into your page a million times is likely to get you slapped down. I'll be perfectly honest -- the prospect of this terrifies me on Daily Bingo Cards, as each page is laser-focused on the card at issue and that results in repeating the title about eight times. To the maximum extent possible, switch up the way you phrase your keyword throughout your text. One way you can do this is by using partials or helpers which switch, randomly, between several synonyms. For example, I could have had my templates built so that all non-heading uses of "bingo cards" were replaced with one from the set of ["bingo cards", "bingo boards", "classroom activities", "classroom games"].

You can also get around this by creative use of pulling data from the database, particularly when you are writing in paragraphs. For example, I break up many of my paragraphs of boilerplate text by using example words from the word list at issue. It looks like a human wrote that, even though it is just Rails spitting out wordList[12]. As a bonus, that word is guaranteed by construction to be semantically related to the main page keywords, which also helps you to rank. (I rank for many of the individual words in the lists for bingo cards, on the strength of a single textual mention and the general page topic).

Back to the Table of Contents





URL Canonicalization

Strongly related to the above topic, Google/Yahoo/MSN have recently released a URL canonicalization standard. All you have to do is generate a tag that looks like <link rel="canonical" href="http://www.example.com/canonical/url/goes/here" /> and multiple copies of your content, which might exist, will get "merged" if they have the same canonical URL. This is great for times when you have substantially similar content on the website which you still want to present to give human users a better experience -- for example, printer friendly pages, which typically duplicate content pages.

I use this because my site has a page where users see a new card every day. I'd like that card to not get treated as duplicate content of the version of the same page in the archives. Thus, with a little Rails magic in the head section for application.rb:

    <%= yield :canonical %>
  

and a quick helper in application_helper.rb:

def canonical(url)
  %Q|<link name="canonical" href="#{url}" >|
end
  

all that is needed is the following code in any template:

<% content_for :canonical do %>
 <%= canonical("any/url/i/want") %>
<% end %>
  

I find that content_for and yield is one of the great mysteries of Rails, but using it is extraordinarily powerful.

Back to the Table of Contents





acts_as_linkbait

If you're reading this, you're a Rails developer, a budding SEO, and almost by definition a power-user of the Internet. Most people aren't. Quite possibly, most people in your niche aren't, either. Even if they wanted to show their support for what you are doing, they might not know how. You can tell them what to do and tell them how to do it and, if your site is indeed worthy of action, they'll go right ahead and do what you tell them.

One fairly basic action you might want people to take on your behalf is to link to you. However, making links is scary for people who don't know what URL stands for and who don't breathe HTML. All links look like www.ohmygod.com/2342jkdjalsfdhwoirsnfasndklw to them. You should make linking to you as easy as a simple task, like copy pasting.

A trivial and effective way to do this is to just say so: "If you like this, link to it. Copy and paste <a href="http://www.dailybingocards.com/rails-seo-tips.htm">Rails SEO Tips</a> into your website. However, that gets long and ugly, and it could disrupt the aesthetics of your web pages. Plus, there are many failure modes: look how it splits into two lines, and imagine how a non-technical user might not copy all of the HTML and accidentally bork something. Make it easy for your users to succeed in helping you out. Instead, use a little bit of Prototype Javascript magic with an application helper.

#goes in application helper
#remember to include prototype.js in any view using this
#linkId should be unique per link on a single page, and ideally static
def hiddenLink(title, linkId = "hiddenLink", anchor = "Click here")
  url = "http://www.yourdomain.com#{request.request_uri}"
  %Q|
    <a href="#{url}" onClick="javascript: $(#{linkId}).toggle(); return false;">
    #{anchor}</a>
    <p id="#{linkId}" style="display: none;">
    Just copy and paste the blue text into your blog, email, MySpace,
    or web site to share it with folks: <br/>
    <font color="blue">&lt;a href="#{url}"&gt;#{title}&lt;/a&gt;</font></p>
    |
end
  

Trust me, that monstrosity looks better in your IDE than it does here. All it does is replace hiddenLink(title, linkId, anchorText) with a link that, when clicked on, magically shows clear and unambiguous instructions on how to link to you. You can sneak that almost anywhere on your website without having to uglify it. You can take the general idea behind this and expand on it -- say, copy the link directly to their clipboard (watch out, modern browsers may not approve) or show it already highlighted in a textbox.

If you want to see what it looks like in practice, click here to try it.

Back to the Table of Contents





Bookmarks, Chicklets, and Permalinks, Oh My!

Similarly to the above tip, if you want folks to visit your site frequently, make it easy for them to find their way back. The number one way is making sure that the URL in their address bar brings them back to what they are looking at. This conflicts directly with a lot of AJAX wizardry, so pick which one you want.

Many Internet users, particularly ones who only recently started using the Internet, navigate to favorite sites primarily based on bookmarks. Many sites take advantage of this and tell their users to "Hit Ctrl-D to bookmark us", and while that is a decent solution, you can do SO much better with some creative Javascript.

#goes in application helper, as usual
def bookmark
  title = @title? "'#{@title}'" : "'My Default Sitewide Bookmark Title'"
  url = "'http://www.mysite.com#{request.request_uri}'"
  %Q|<a href="javascript:bookmarksite(#{title}, #{url});">Bookmark this page</a>|
end        
  

Presto-changeo, all you have to do is sprinkle your views with at appropriate places and folks can do it with a simple mouseclick. You have total control over the contents of their bookmark by altering the title and url parameters to read whatever you want -- I just use sensible defaults.

That is nice for one user, but let's take it to the next level, with everyone's favorite social bookmarking service, del.icio.us. All you need to do is some creative URL rewriting and you can specify a link and title for a new delicious bookmark. Something like:

def deliciousButton
  #urlEncodedTitle = (@title? @title : "Daily Bingo Cards").gsub(/ /, "%20")
  #requestURL = "www.dailybingocards.com#{request.request_uri}"
  #%Q|<a href="http://del.icio.us/post" onclick="
    window.open('http://del.icio.us/post?v=4&noui&jump=close&url=#{requestURL}&title=#{urlEncodedTitle}',
    'delicious', 'toolbar=no,width=700,height=400');return false;">
  Save to del.icio.us</a>|
end
  

I used this technique for a month at Daily Bingo Cards. I love that it is really easy to blend the link in anywhere. However, I wanted something which gave social proof. Something which said hey buddy, other people liked this page too.

Something like:

Delicious has a page on their site which explains what you need to do to make that (they call it a tagogmeter). It is just copy/pasting some Javascript verbatim, so I'll omit the helper code I use.

Back to the Table of Contents





Conclusion

Thanks for reading this far. I hope you learned a trick or three you can put to good use on your own sites. I'd love to hear what you think of this article. Either shoot me an email at patrick@bingocardcreator.com, post a comment on my blog, or post something about this and I'll find you from my referrer logs.

Regards,
Patrick McKenzie