Generating fancy URLs with Rails

Written on 13 jan 2012 and tagged rails, ruby

There’s a problem with Rails resourceful routes. It is very easy to generate standard resourceful routes; it is equally easy to match incoming fancy URLs. But DRYly generating fancy URLs is a lot harder than suspected.

Matching fancy incoming routes is easy enough:

get '/posts/:year/:month/:id' => 'posts#show'

Our application will now correctly match URLs like /posts/2011/3/1-hello-world, but see what happens when we try to generate a polymorphic route:

link_to(post.title, post)
# => <a href="/posts/1">hello, world</a>

This is not what we want. Of course, we could set :year and :month parameters manually, but that’s not very DRY. We could also try to override the post_url and post_path methods, but this does not cover all use cases.

Hypothetical solution

Ideally, we would like to return a hash of parameters for the to_param method:

class Post
  def to_param
    { year: created_at.year,
      month: created_at.month,
      id: id }

Rails would then have to interpolate these params into the generated URL. Alas, this doesn’t work.

Actual, hacky solution

We can hack around it, tough, by overriding url_for:

class ApplicationController
  def url_for(options = {})
    obj = options[:_positional_args][0]
    if obj === Post
      options[:_positional_keys] = [
  helper_method :url_for

This works fine for the most part – despite being a terrible hack. The helper_method :url_for line does, however, override ActionView’s normal behaviour of generating only paths instead of full URLs. To get around this, we can create a new helper method that uses the controller method:

module ApplicationHelper
  def url_for(options = {})
    return super unless options.is_a? Hash
      options.reverse_merge(only_path: true)

Now all URLs are properly generated:

link_to post.title, post
# => <a href="/2011/2/1-hello,world">Hello, world</a>

A note of warning

The code in these examples is extremely simplified. Being a hack, you need to add some proper checks for argument types and the contents of the :_positional_args and :_positional_keys options. Also, you can probably not depend on this behaviour remaining on subsequent versions of Rails. But it still just might come in useful some time…

You cannot leave comments on my site, but you can always tweet questions or comments at me: @avdgaag.