= Routing: Native Ruby Rewriting
After a long delay, Ruby on Rails finally supports native Ruby rewriting. This
book discusses the advantages to be gained in using Rail's new Routing feature,
how you can do so, and contains a brief description of the underlying
implementation.
== An Introduction to Routes
=== What and Why
This patch implements *native Ruby rewriting* for Rails, thus shifting the
responsibility of URL parsing from the webserver to Rails itself. This has been
a requested feature for two primary reasons.
# Not all webservers support rewriting. By moving this code to the framework
Rails is able to function "out of the box" on almost all webservers.
# A rewriting implementation in Rails can also be used to generate custom URLs
by linking it to the standard URL helpers such as url_for, link_to, and
redirect_to.
=== Diving In
Routes have been designed to be fully customizable by the average Ruby
programmer. You'll no longer have to be a htaccess wizard to have pretty URLs
for your site.
Routes are defined in config/routes.rb. This file is a typical ruby
source file which contains a document similar to
ActionController::Routing.draw do |map| map.connect
':controller/:action/:id' end
Instead of wading through the tedious and obvious, let's dive right in.
h3. Example 1 -- Setting A Default Controller
Let's say you're one of those cool kids with a blog, and you'd like
http://www.your-cool-domain.com/ to show your recent posts.
h4. Case 1 -- BlogController#index
Lets say your BlogController's index action is already setup to show the list
of recent posts. To specify the default controller, we add :controller =>
'blog' to the default route:
ActionController::Routing.draw do |map| map.connect
':controller/:action/:id', :controller => 'blog' end
As an additional perk, Routes will opt for the shortest path possible, so
url_for :controller => 'blog', :action => 'index' will generate a url
such as http://www.your-cool-domain.com/.
h4. Case 2 -- BlogController#recent
Lets suppose that for whatever reason, your BlogController displays the recent
posts with another action. We need to add a new Route that matches an empty
path and points to BlogController's recent action.
ActionController::Routing.draw do |map| map.connect '', :controller
=> 'blog', :action => 'recent' map.connect ':controller/:action/:id' end
Notice that we put our new route before the default Rails route. Routes are
recognized and generated in the order they are defined. By placing our custom
route before the default Rails route, we can be assured that url_for
:controller => 'blog', :action => 'recent' will generate a URL with an
empty path, such as http://www.your-cool-domain.com/.
Note that if public/index.html exists, it will be served and Rails
will not see the request. Be sure to delete the Congratulations page that ships
with Rails.
h3. Example 2 -- Setting up date-based URLs
Suppose you want to add a feature to your blog that let's your visitors view
posts by date. You'd like to allow them to browse by year, month, and day, and
you already have an action which uses query or post parameters named year,
month, and action.
The ideal URL for this looks like
http://www.your-cool-domain.com/2005/02/14, with the month and day
components being optional. Because you're a sane person, you decide that the
date order should be YYYY/MM/DD, rather than some arcane order such as
MM/DD/YYYY.
Once again, we will want to add our custom route before the default Rails
route. A first try at this might yield map.connect
':year/:month/:day', :controller => 'blog', :action => 'by_date'
Although this will successfully match
http://www.your-cool-domain.com/date/2005/02/14, it will fail to do so
for tt>http://www.your-cool-domain.com/date/2005/02 -- we need to mark the
:month and :day components as optional. To do so, we add
:month => nil to our route: map.connect
'date/:year/:month/:day', :controller => 'blog', :action => 'by_date', :month
=> nil, :day => nil
Now our custom route will recognize URLs such as
http://www.your-cool-domain.com/date/2005/02 or even
http://www.your-cool-domain.com/date/2005. On the flip side, our
custom route will also be used automatically when URLs are generated with the
standard forms helpers such as link_to and url_for.
Now we run into another problem -- our new route is too general, and it will
catch all three-component URLs -- such as posts/show/10. To combat
this, we must tell place some requirements on our path components. Requirements
are placed using a :requirements sub-hash like so:
map.connect 'date/:year/:month/:day', :controller => 'blog',
:action => 'by_date', :month => nil, :day => nil, :requirements => {:year =>
/\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/}
h3. Example 3 -- Posts by Category
Lets move back to your BlogController. Suppose each of your posts belong to a
category, and that we would like URLs to show items based on category,
such as http://www.your-cool-domain.com/posts/rails. Lets assume that
you also use the category "all" to mean show all categories. A quick route for
this would be:
map.connect 'posts/:category', :controller => 'blog', :action =>
'posts'
The problem with this route is that your link_to statements will all
have to include :category => 'all'. It would be nicer if our templates
could contain <%= link_to 'Posts', :controller => 'blog', :action =>
'posts' %>
We can achieve this by giving the route a default value for :category:
map.connect 'posts/:category', :controller => 'blog', :action =>
'posts', :category => 'all'
An added bonus is that the URL generated for the above link_to will
omit the extraneous 'all' component, and be displayed as
http://www.your-cool-domain.com/posts.