Configuring Customized Routing Behavior

Problem

You need precise control over how Rails maps URLs into controllers actions. By default, a request to calls the show action of the Blog controller with an id of 5 (i.e., :controller/:action/:id, which you can see in the last map.connect line in config/routes.rb). You want Rails to route URLs constructed from date information directly to articles. But requests the 2005 action of the Blog controller, which makes little sense. How do you map URLs with dates into meaningful controllers and actions?

Solution

Add the following as the first rule in config/routes.rb:

ActionController::Routing::Routes.draw do |map|
 map.connect 'blog/:year/:month/:day', 
 :controller => 'blog', 
 :action => 'display_by_date',
 :month => nil, 
 :day => nil,
 :requirements => { :year => /\d{4}/, 
 :day => /\d{1,2}/, 
 :month => /\d{1,2}/ }
 map.connect ':controller/service.wsdl', :action => 'wsdl'
 map.connect ':controller/:action/:id'
end

With display_by_date defined in the Blog controller:

app/controllers/BlogController.rb:

class BlogController < ApplicationController
 def display_by_date
 year = params[:year]
 month = params[:month]
 day = params[:day]
 day ='0'+day if day && day.size == 1
 @day = day
 if ( year && month && day )
 render(:template => "blog/#{year}/#{month}/#{day}")
 elsif ( year )
 render(:template => "blog/#{year}/list")
 end
 end end

Discussion

The solution routes a request to directly to the display_by_date method of the BlogController. The display_by_date method receives the following parameter hash:

params = { :year => 2005, 
 :day => 6, 
 :month => 11 }

When presented with these values, display_by_date retrieves the blog entry from November 6, 2005. This method has some additional display functionality as well, which we'll get to in a moment.

Here's how our map.connect rule works:

The first argument of map.connect is a pattern that describes the URL path that we're looking for this rule to match. In this case, when we see a URL path of the form /blog/2005/6/11, we create a hash with :year => 2005, :month => 6, and :day => 11. (All this really matches is the /blog///; the stuff between the last three slashes is added to the hash.) This does nothing to guarantee that the stuff between the slashes has anything to do with an actual date; it just matches the pattern and adds key/value pairs to the hash.

The initial argument does not add :controller or :action keys to our hash. Without a controller specified, Rails produces a routing error. If we specify the Blog controller but no action, Rails assumes an action of index or throws an error if no index method is defined. So we've added :controller => 'blog' and :action => 'display_by_date' to explicitly tell Rails to use the display_by_date method of the Blog controller.

The next two arguments in our rule, :month => nil and :day => nil, set a default of nil to the :month and :day keys of the hash. Keys with nil values won't get included in the params hash passed to display_by_date. Using nil values lets you specify the year but omit the month and day components of the URL path. display_by_date interprets the lack of month and day variables as a special request to display all blog entries for the specified year.

The last argument assigns a subhash to the :requirements key. This subhash contains specifics about what we're willing to accept as a valid date. We use it to provide regular expressions that tell us whether we're actually looking at a year, month, and a daythe value assigned to year must match /\d(4)/ (i.e., a string of four digits)and so on.

See Also