02/15/2012

Dynamic routing using SEO urls

Hello there, is it me you're looking for? I can see it in your eyes, I can see it in your smile.

Recently we had to develop a SEO optimized url structure for a rails project where a url would have to include slugs on multiple levels of the suburls. As an example a product should be accessible by accessing it through its category (/sedan/camry should open the /cars/42 page). Also some items should be accessible by just having their slug in the url (/bob should open the /users/42). We decided to do this using the awesome rack-rewrite gem rewriting the url before it hits the rails app and creating our own wrapper for the functionality we needed.

The basic idea was to not have a bunch of implementation details in the config/application.rb where the gem should be mounted as a middleware. We also wanted an easily expandable framework if the need for more custom urls arises. So we built the rack-rewrite-dynamic gem.

Example Usage

The gem assumes you are using the friendly_id gem on a central Slug model which has a polymorphic association to the models included in the SEO urls. This is the only way to ensure there are no conflicts in the urls.

class Slug < ActiveRecord::Base
  extend FriendlyId
  friendly_id :content, use: [:slugged, :history]
  belongs_to :sluggable, :polymorphic => true
end

To setup the gem add gem 'rack-rewrite-dynamic' to your gemfile and bundle.

Show pages

Now to route to the car example do the following in your config/application.rb.

config.middleware.insert_after "ActiveRecord::QueryCache", 'Rack::Rewrite' do |base|
  rewriter = 'Rack::Rewrite::Dynamic::Rewrites'.constantize.new do
    rewrite url_parts: [{'Category' => 'slug', 'Car' => 'slug'}]
  end
  rewriter.apply_rewrites(base)
end

From now on if you have a Category and a Car models attached to the slug model via the polymorphic association you can use the /category_slug/car_slug urls to access the detail page of the car. This is defined by the url_part configuration option. It includes an array of hashes where each of the key/value pairs is a part of url that is being routes and the last one is the one that the show page is being rendered for.

To route multiple url types, just add a hash to the array:

[
    {'Category' => 'slug', 'Car' => 'slug'},
    {'Category' => 'slug', 'IceCream' => 'slug'}
]

Filters

Another requirement is to have nicely formatted URLs for index pages with filters. The ugly /cars?category_ids[]=42&color_ids[]=42 should look like /red-sedan-cars. To accomplish this we created a different rewrite class that handles this scenario.

config.middleware.insert_after "ActiveRecord::QueryCache", 'Rack::Rewrite' do |base|
  rewriter = 'Rack::Rewrite::Dynamic::Rewrites'.constantize.new do
    rewrite_filter separator: '-', target: 'cars', suffix: 'cars'
  end
  rewriter.apply_rewrites(base)
end

The separator option defines the separator character. Target and suffix are the same. The suffix option is used for matching the SEO url suffix and the target option for generating the target url.

Custom rewrites

The basic idea of rack-rewrite-dynamic is to be easily extensible to meet needs we could not anticipate. You can use one of our own rewriters as a template to create your own. The only requirement is to have a perform instance method that receives a match object of the url and a rack_env object containing the request information from the rack environment. To bring in some functionality that should be usefull include our base module.

class MyRewriter
  include Rack::Rewrite::Dynamic::Base
  def perform(match, rack_env)
    # some awesome rewriting
  end
end

You can then pass them in as an argument to the rewrite method.

config.middleware.insert_after "ActiveRecord::QueryCache", 'Rack::Rewrite' do |base|
  rewriter = 'Rack::Rewrite::Dynamic::Rewrites'.constantize.new do
    rewrite {option: 'value'}, MyRewriter
  end
  rewriter.apply_rewrites(base)
end

Conclusion

These are the features of the rack-rewrite-dynamic gem, so go check it out on the github.

This wonderful post was written by Michal Oláh