Wednesday, January 16, 2008

Hyphenated URLs in Rails

The Problem

Rails, by default, produces underscored URLs for multi-word methods and controllers. For example, a controller by the name of ContactUs with the method about_advertising will respond, with the default routing, to the path /contact_us/about_advertising. This is all well and good except when it comes to search engines. Underscores are seen, at least by Google at the moment, as part of a word, whereas hyphens (dashes) are seen as word separators. This means that our above path will hit for searches on "contact_us" and "about_advertising" but not for "contact" or "advertising". That's no good! How can we get Rails to use /contact-us/about-advertising so that our site might get indexed for the keyword "advertising"?

Action Names

Fixing the action name is easy enough as hyphens are valid characters in method names. Well, almost-- hyphens may be valid in method names, but they are not valid in symbols, which means that we can't use the standard "def" syntax to declare methods with hyphens in their names. To get around that, we turn to define_method. To change /contact_us/about_advertising to /contact_us/about-advertising, we can change the method declaration from:
def about_advertising
...
end
to:
define_method('about-advertising') do
...
end

Controller Paths

It looks like we are halfway to where we want to go, but the controller part of the path turns out to be a more difficult part of the problem. There is no way, at least that I know of, to use an underscore in a class name. Therefore, we turn to Rails routing for some help. The easiest solution is to create a special route for the ContactUs controller in config/routes.rb as follows:


ActionController::Routing::Routes.draw do |map|
map.connect '/contact-us/:action/:id', :controller => 'contact_us'
end

and we get /contact-us/about-advertising. And we're done! Well, not quite. Having to add route entries for every multi-word controller is no fun, certainly not very DRY, and doesn't handle the RESTful map.resources. Enter hyphenated_controller_routes, the plugin I wrote to address those problems, and the reason for this blog post.

Hyphenated Controller Routes

The plugin, located at http://svn.vickeryj.com/public/hyphenated_controller_routes/trunk can automatically create hyphenated routes for all multi-word controllers, and can update map.resources to generate hyphenated URLs for multi-word resources. After installing the plugin, update config/routes.rb to make use of map.add_hyphenated_routes() and map.use_hyphenated_resources() as desired.

An example config/routes.rb:

ActionController::Routing::Routes.draw do |map|

#a resource that will be mapped with an underscore
map.resources :non_hyphenated_resources

#turn on hyphenated resources
map.use_hyphenated_resources()

#all following resources will use hyphens (contact-us)
map.resource :contact_us

# add hyphenated routes with higher priority than the default routes
map.add_hyphenated_routes

# Install the default routes
map.connect ':controller/:action/:id.:format'
map.connect ':controller/:action/:id'

end

8 comments:

Unknown said...

Hi Josh,

This is wonderful - thank you!

I can't believe this is not perceived to be a more serious issue with Rails. No SEO person will encourage underscores in URLs and for any commercial website SEO considerations are paramount.

I could fix the actions problem (using define_method instead of def) but until I found this I could not fix controllers without using Apache Rewrites.

Have you done any performance testing with this? I'm about to launch a site destined to be BIG (I hope!) so I'm very wary of any performance hits.

Thanks again.

:)

Chris

Josh Vickery said...

Hi Chris,

We haven't done any performance testing on this particular plugin specifically, but I don't anticipate there being any performance slowdowns with it for the following reasons:

1. When using "use_hyphenated_resources" there should be no performance difference whatsoever as it only changes the routes that are generated at startup, so there is no run-time overhead.

2. When using "add_hyphenated_routes" those routes are created at startup, not scanned at runtime as they are with the default routing. This might actually lead to a performance improvement, rather than a slowdown.

Unknown said...

What about if you have a dir say: app/controllers/product_category/ then a controller called product_items.rb. Can it update those underscores in the dir? Right now I've got a work around in my routes.rb using :path_prefix and :name_prefix in my map.resources, but it'd be cool if I didn't have to. Any suggestions?

Josh Vickery said...

Hi Darren,

I've never tried to put controllers inside subdirectories, mostly because I've switched from using the default routing (map.connect ':controller/:action/:id.:format') to ActionController::Resources based routing, with nested resources. This plugin does handle the case of nesting underscored resources, in your case that would be something like:

map.resource :product_category do |pc|
:pc.resources product_items
end

Which should send the request http://yoursite.com/product-category/product-items to the ProductItems controller.

Unfortunately this plugin does not currently handle the case of controller subdirectories with the default routing. However, looking at the source right now I think it would be pretty straightforward to add that functionality. If you would like to submit a patch I would be happy to accept it.

Josh

Unknown said...

@Josh... thanks for the reply. I also use the resource based routing. Your example should be(without the : in the pc):

map.resource :product_category do |pc|
pc.resources product_items
end

However that didn't work with using sub dirs. It would work I think?.. if I had product_category_controller.rb and product_items_controller.rb in the app/controllers/

My problem was that I have a site that has 5 sites within that I was trying to keep organized into sub dirs. I didn't want to use subdomains cause of SEO and having the urls look like this: site.com/sub-site/pages/some-page-name would have looked really good for when google bots crawled them. I'm going to have a look at your plugin some more when I have a few extra hours. It's still a great plugin I use on every site anyhow.

Unknown said...

I've ported your svn repos of this plugin to github: http://github.com/darrenterhune/hyphenated_controller_routes/tree/master I stopped using svn cause git is oh so wonderful! You should give it a try!

Josh Vickery said...

Hi Darren,

I do like git so I went ahead and imported the SVN repository to github.

I left the SVN repository up for now, but I'll do any future development (or accept patches/pull requests) on github and push them over to SVN as necessary.

Josh

Unknown said...

Seems to be broken in Rails 2.3+ I've added an Issue to your github repos of this plugin.