3

I have a slightly different take on a fairly common problem: SEO-friendly URLs. I have a PagesController, so my URLs currently are like (using restful routing):

/pages/some-content-title

This works just fine, but there is a hierarchical structure to the pages so I need the following:

/some-content-title routes to /pages/some-content-title

I can also get this to happen using:

match '*a', :to => 'errors#routing'

in my routes.rb and trapping it in ErrorsController as:

class ErrorsController < ApplicationController
  def routing
    Rails.logger.debug "routing error caught looking up #{params[:a]}"
    if p = Page.find_by_slug(params[:a])
      redirect_to(:controller => 'pages', :action => 'show', :id => p)
      return
    end
    render :file => "#{Rails.root}/public/404.html", :status => 404, :layout => false
  end
end

My question comes in the desired SEO elimination of the "pages/" part of the URL. What the SEO-dude wants (and here is where an example is key):

/insurance => :controller=>'pages', :id=>'insurance' # but the url in the address bar is /insurance

/insurance/car :controller=>'pages', :category=>'insurance', :id=>'car' # but the url in the address bar is /insurance/car

Is there a generic way for him to get his Google love and for me to keep the routes sane?

Thanks!

Steve Ross
  • 4,134
  • 1
  • 28
  • 40

2 Answers2

5

This is hard to do since you are redefining the parameters based on their presence (or absence) in the path. You could handle the globbed parameters in the controller, but then you don't get the URL that you want, and it requires a redirect.

Rails 3 lets you use a Rack application as an endpoint when creating routes. This (sadly underused) feature has the potential to make routing very flexible. For example:

class SeoDispatcher
  AD_KEY = "action_dispatch.request.path_parameters"

  def self.call(env)
    seopath = env[AD_KEY][:seopath]
    if seopath
      param1, param2 = seopath.split("/") # TODO handle paths with 3+ elements
      if param2.nil?
        env[AD_KEY][:id] = param1
      else
        env[AD_KEY][:category] = param1
        env[AD_KEY][:id] = param2
      end
    end
    PagesController.action(:show).call(env)
    # TODO error handling for invalid paths
  end
end
#

MyApp::Application.routes.draw do
  match '*seopath' => SeoDispatcher
end

will map as follows:

GET '/insurance'     => PagesController#show, :id => 'insurance'
GET '/insurance/car' => PagesController#show, :id => 'car', :category => 'insurance

and will retain the URL in the browser that your SEO dude is asking for.

zetetic
  • 47,184
  • 10
  • 111
  • 119
  • Awesome, had no idea this feature even existed. +1 – Chris Heald Feb 22 '11 at 02:12
  • I'm going to give this a try. Looks very promising! Thanks. – Steve Ross Feb 22 '11 at 18:33
  • I guess I'm missing a piece of this puzzle. Where do you store this file. I stuck it in lib/middleware/seo_dispatcher.rb and did config.middleware.use ::SeoDispatcher in my application.rb but no joy. How do you configure this? – Steve Ross Feb 22 '11 at 18:45
  • I cheated. I just added it to `config/routes.rb`. You could put the class in an initializer, or in `lib/something.rb` and configure the environment to load it from there. FWIW this is *not* middleware, this is an endpoint handler. See the middleware guide: http://guides.rubyonrails.org/rails_on_rack.html and the routing guide: http://guides.rubyonrails.org/routing.html for the difference. The endpoint trick is mentioned in sec. 3.13. – zetetic Feb 22 '11 at 18:52
  • Have you thought about how to test this? Even cuke doesn't load Rack. I'm just a bit stumped beyond unit-testing the code. Thanks! – Steve Ross Feb 23 '11 at 19:50
  • It's a good question. I normally use RSpec, but the routing specs, which I assume use `assert_recognizes` under the hood, come back with "No route matches" for routes handled by the endpoint. RSpecs request specs work though, and I assume that would hold true for Rails' integration tests as well. – zetetic Feb 23 '11 at 21:54
  • Interesting side effect. This eats urls like /javascripts/bundle/default.js. How do I revert to the normal mapping for things like javascripts (or maybe sass files)? – Steve Ross Feb 25 '11 at 18:05
  • Normally those would be served out of /public/javascripts, which seems to work OK. – zetetic Feb 26 '11 at 18:31
  • Here is link to Steve's evolution of this solution with constraints and testing. http://stackoverflow.com/questions/5641786/testing-rack-routing-using-rspec – Agustin Mar 30 '12 at 17:19
0

There is a gem for this called friendly_id. See its github page

Isaac Betesh
  • 2,935
  • 28
  • 37