Styling Rails URLs

Published June 14, 2009

This article explains how you can alter the looks of your URLs in a Rails friendly way. I assume that you know how to use map.resources and named routes.

Adding .html to all pages

ActionController has a default_url_options method that it uses for all URL generation tasks. It returns an empty hash by default, but can be overridden.

class ApplicationController < ActionController::Base
  def default_url_options(options = nil)
    {:format => "html"}
  end
end

Note that the options to your individual route calls gets predesence, posts_path(:format => "xml") will append .xml, not .html.

Custom route names

For a projects controller’s show action, the default URL looks like this:

/projects/5

What if you want to localize it?

/prosjekter/5

One way is to name the controller “prosjekter” instead of “projects”. That isn’t very Rails friendly, though, you’ll end up fighting with pluralization conventions. Thankfully, there is a Rails friendly way to do this, via routes.rb.

map.resources :projects, :as => "prosjekter", :path_names => {:new => "nytt", :edit => "rediger"}

That will give you these URLs:

/prosjekter/5
/prosjekter/nytt
/prosjekter/5/rediger

You can also set :path_names globally, by adding the following to config/environment.rb.

config.action_controller.resources_path_names = { :new => 'ny', :edit => 'rediger' }

URLs without the ID of the record

Normaly, Rails URLs look like this:

/posts/5
/projects/239
/users/39/favorites/2

Perhaps you want more descriptive URLs, though, such as having the title of the post in the URL instead of just the ID. Here are a few altervatives.

The easy way with compromises

/posts/5-a-post-about-rockets
/projects/239-new-website
/users/39-augustl/favorites/2-that-book

The compromise is that the ID still is in the URL. It is unlikely that a visible ID is a problem, but if it for some reason is, you can learn how to do it without the ID in the next section.

What makes Rails use the ID alone in the first place? The answer is to_param. It’s an instance method in all models, and it looks like this:

def to_param
  id
end

We can override it to include the title of the post as well as the ID.

class Post < ActiveRecord::Base
  def to_param
    "#{id}-#{title.parameterize}"
  end
end

Rails will now use the id, a dash, and and URL friendly version of the title. We need to make it URL friendly with parameterize, so that "Foo and bar!" turns into "foo-and-bar". If we don’t, we’ll get invalid URLs, and browsers will blow up.

The reason this is the easy way, is that this is all there is to it. Our finds will still work, because of the way Ruby handles to_i on strings.

"5-a-post-about-rockets".to_i
# => 5

The find method that we use in the controllers calls to_i for us.

Post.find("5-a-post-about-rockets")
# equals
Post.find(5)

The hard way

/posts/a-post-about-rockets
/projects/new-website
/users/augustl/favorites-that-book

Because the ID is no longer in the URL, we have to change the code a bit.

class Post < ActiveRecord::Base
  before_create :create_slug
  
  def to_param
    slug
  end
    
  def create_slug
    self.slug = self.title.parameterize
  end
end

When a post is created, the URL friendly version of the title is stored in the database, in the slug column. We also have to update the finds to find records using the slug column instead of using the ID.

class ProjectsController < ApplicationController
  def show
    @project = Project.find_by_slug!(params[:id])
  end
end

Apply the same tactic to all relevant finds, and you’re good to go.

No controller names

Instead of /users/augustl/projects/a-project, you might prefer this:

/augustl/a-project
/some-user/another-project

Just a heads up: Most people don’t care about how the URLs looks. In my opinion, it only makes sense to do this kind of routing if your users are typing in the URLs by hand, or if the URL is commonly linked to. Github projects is a good example of sensible use of short URLs. If not, I advise you to leave the controller names in there.

Anyhow, here’s how you do it:

map.short_user ":id", :controller => "users", :action => "show"
map.short_project ":user_id/:id", :controller => "projects", :action => "show"

This gives you two very handy named routes.

short_user_path(@user)
short_project_path(@user, @project)

The example above only handles those two show actions. I advise using regular map.resources for create, edit etc. Users will never have to link to the URL that creates a new project, so it’s pointless to style those routes.


Questions or comments?

Feel free to contact me on Twitter, @augustl, or e-mail me at august@augustl.com.