Taking care of old links

Rearranging your site while still supporting those old links floating around the 'net can be a challenge. I tackled it over the weekend (in Rails) and here's what I found.

Sure, you can simply customize public/404.html but that isn't much fun. It won't make use of your layouts, so you will have duplication. And, you won't be able to do interesting things with those unfound url's.

There were two things I wanted to accomplish here:

  • If the url contained .php, redirect to the url without a the file name. Even though I went to a lot of trouble to have clean url's on my old site, somehow I ended up with referrers linking to /foo/index.php. In that case, I want to redirect to /foo.
  • Any other un-found url's get a pretty page in the standard layout.

Start by creating a catch-all route in config/routes.rb. The condition enables it in production mode only, so you still get the routes error page during development.

map.connect '*path', :controller => 'application', :action => 'rescue_404' unless ::ActionController::Base.consider_all_requests_local

Then, write some code in ApplicationController. Mine catches various error states in "public" (production mode) and handles them. Any unknown url's get sent through the handle_error_paths method, where I look for those ugly .php paths and redirect appropriately.

def rescue_404
  rescue_action_in_public CustomNotFoundError.new
end

def rescue_action_in_public(exception)   
  case exception
    when CustomNotFoundError, ::ActionController::UnknownAction then
      handle_error_paths(@request.request_uri)
      render_with_layout 'shared/error404', 'layouts/standard', '404'
    else
      @message = exception
      render_with_layout 'shared/error', 'layouts/standard', '500'
  end
end

def handle_error_paths(path)
  redirect_to_url path.gsub(/^(\/.*?)\/?\w+\.php/, ''), true if path.match /\.php/
end

Now, to test this thing you have two options. Switch to production mode, or mess with development mode. Switching to production mode is simple, but you can't change-and-reload, so making adjustments is annoying. Here's how I made development act like production for our purposes.

Modify config/environmenets/development.rb:

ActionController::Base.consider_all_requests_local = false

Then (re) define one more method in ApplicationController.

def local_request?
  false
end

Now, it won't see your 127.0.0.1 address and give you development error messages. You can probably leave that method, but be sure to change consider_all_requests_local to true when you get back to regular work.

That should do it. This wasn't nearly as easy as I'd have liked it to be but in the end, not too bad. A little bit of patching to the Rails source could make this much better.

If there's an easier way, please share.

  • April 18, 2005
  • Dealing with rails and web

There are 4 comments

  1. 8 days later, Robert Hahn said...

    I tried that; didn't work. I'm using Rails 0.10.1. What are you running?

  2. 8 days later, Ryan said...

    I'm running the latest, (0.12.1 right now)

  3. 66 days later, alex said...

    if I just steal this code for use in my own app, I get:

    uninitialized constant CustomNotFoundError

    so I had to put a simple CustomNotFoundError < Exception class def in my application.rb and it works fantastically

    I also used render_component to call a controller action instead of rendering a template, so I could use all my nice variables from my normal controller method ;)

  4. 113 days later, Marc said...

    With the introduction of Rails 0.13.1, renderwithlayout has been deprecated.

    Now instead of:

    renderwithlayout 'shared/error404', 'layouts/standard', '404'

    it should be:

    render :file => 'shared/error404', :layout => 'standard', :status => 404

    :file by default is relative to the template folder "views". :layout is relative to the "layouts" folder in "views".