301 Redirect in a Rails Route

I’ve got 99 problems but a redirect ain’t one

What do you get when you discover you setup a route incorrectly ages ago? Why, a redirect of course! And a blog post with some handy redirect code that you might find useful one day!

The Situation

Our blog posts were routed like so:

  match '/:year/:month/:day/:id' => "posts#show", :as => "blog_post",         :requirements => { :year => /d{4}/, :month => /d{2}/, :day => /d{1,2}/ } 

Which technically works just fine… except that the URL of a post ends up as this:

When I'd really rather have the URL as this:

If I had just setup my routes for the first time today I would simply change it to this (to include 'blog' in the url):

  match '/blog/:year/:month/:day/:id' => "posts#show", :as => "blog_post",         :requirements => { :year => /d{4}/, :month => /d{2}/, :day => /d{1,2}/ } 

But since posts had been routed that way for quite some time, I needed to add a redirect to ensure that older links to posts that were floating around in the ether would still work.

So then I was all like, "Sweet! @merbist mentioned that redirects can be done in the routes in Rails 3! Easy Peasy!"

Thus, I added some code to the routes file that looked like this:

match '/:year/:month/:day/:id', :to => redirect('/blog/:year/:month/:day/:id'), :as => "blog_post",        :requirements => { :year => /d{4}/, :month => /d{2}/, :day => /d{1,2}/ } 

And then Rails was all like, "Oh no she didn't!"

NoMethodError in PostsController#show  undefined method `published_on' for nil:NilClass Rails.root: /Users/renaebair/workspace/intridea/newsite  Application Trace | Framework Trace | Full Trace app/helpers/blogs_helper.rb:23:in post_link' app/controllers/posts_controller.rb:23:inshow' Request  Parameters:  {"requirements"=>{"year"=>/d{4}/, "month"=>/d{2}/, "day"=>/d{1, 2}/}, "year"=>":year", "month"=>":month", "day"=>":day", "id"=>":id"} 

And then I was all like, "Must not have the syntax down. No problem, Google can help!"

And after several dead ends and poor implementations I had to step back.

The Solution

Finally, with my coder ego sulking at subterranean depths, I pulled together the humility to ask for help; I pinged Michael Bleigh, who is a very busy guy but never ignores a coder's plea for help.

In less than 30 seconds he sent me a working solution, which I now present to you:

  match '/:year/:month/:day/:id',          :to => lambda{|env|            Rack::Response.new(['301 Permanently Moved'], 301, {'Location' => "/blog#{env['PATH_INFO']}"}).to_a         }, :as => "old_blog_post", :requirements => { :year => /d{4}/, :month => /d{2}/, :day => /d{1,2}/ } 

As you can see, he embedded a Rack reponse in the route using a lambda. This is important because the route file is only loaded once , right when the app starts up. Putting the redirect code in the lambda ensures that the Rack response will be evaluated anytime that route is matched. When it's matched the Rack response throws a 301, indicating the page has a new home and redirects to the new home. It's important to use a 301 code "Permanently Moved" because otherwise you'll lose a ton of link "juice" (eww). Click here for more HTTP status code definitions.

And there you have it – a juicy snippet of code for all your redirect woes!