Url Canonicalization in Rails
In one of my last posts I showed how I was able to create completely custom urls for SEO, but there is an issue that sometimes comes up when creating custom urls or when migrating urls, etc.
Here is a simple way to ensure that urls that are being requested are valid. Google and Yahoo! (and others) crawl your sites links and can on occasion come across an incorrect ink from someone else’s site that may be old or mistyped. There are some stiff penalties associated with having two different urls pointing to the same page. There may also be a need to retire certain urls or to change the way they are formated.
Here is an example, the URL:http://domain.com/d-123456-mountain_viewering
Should be redirected to:
http://domain.com/d-123456-mountain_view
Here is the simple solution:
I created a module that looked like the following in the lib directory and included it into the ActionController class.
include ActiveRecord
module MY
module URL
def page_code_object_map
{
'd' => Destination, 'p' => Photo
}
end
def execute_url_post_process
canonicalize if params[:canonicalize]
end
def canonicalize
whole_url = request.request_uri().split('?')[0].split('#')[0]
url_pieces = current_url.split('-')
page_type = url_pieces[0].gsub(/\//, '')
type_id = url_pieces[1]
begin
object = page_code_object_map[page_type].find(type_id)
canonical_url = send "custom_#{page_type}_path", object, params
rescue RecordNotFound => e
render :file => File.join(RAILS_ROOT, 'public', '404.html'), :status => 404
return
end
if canonical_url and canonical_url != whole_url
headers['Status'] = '301 Moved Permanently'
redirect_to("#{http_base}#{canonical_url}", :status => 301)
end
end
end
end
ActionController::Base.send :include, MY::URL
ActionView::Base.send :include, MY::URL
In the route below, notice that I am passing a parameter named :canonicalize with the value of true. This parameter is passed through to the controller as a request parameter and can be accessed in the params hash.
map.d '/d-:destination_global_id-:name*other_params', :controller => 'destinations', :action => 'show', :canonicalize => true, :destination_global_id => /\d{1,20}/, :name => /[^-]+/
How does this all work you say? Simple. In your application controller (controllers/application.rb) you need to include something like this:
before_filter :execute_url_post_process
This will start the checking process by calling the execute_url_post_process() method defined above in my module. If the route that matches passes the :cononicalize parameter, the conanicalize() method will get the current url and certain important pieces. Then depending on the object that is mapped to the page code (d) it will reconstruct the url of the destination object that should match the existing url. If it matches then were golden, if it doesn’t then we redirect to the new/correct url ensuring that we do not loose page rank or be counted as spam (duplicate content).
There are many things that you can do within this code. Some of them include managing authorization, hiding pages, etc.
I hope you enjoyed this tip. If you have any suggestions, please post them, I am sure some genius will have something to add. :)
Really Customized Urls for SEO in Rails
I needed to build urls that were packed with keywords for SEO. I needed to make sure that the url more fully described the contents of the page.
This default rails url does not cut it.
/destinations/12345
This does cut it.
/d-12345-mountain_view
So here is the hack that I did to get the desired affect. (Suggestions or insults on my approach are welcomed!)
First, I added this code into a plugin that I was using for our custom routes stuff. You can probably add this to the environment.rb file or better yet to a a file within lib and just make sure that you require the file from within environment.rb. I really needed to add the ’-’ as a delimiter.
This is step is important because by default rails uses slashes (/) as a dilimeter for parts of the url, but by adding a dash (-) to the array things work the way they should.
module ActionController
module Routing
SEPARATORS = %w( / ; . , ? -)
end
end
Then I added a named route (config/routes.rb) that looked something like this:
map.d '/d-:destination_global_id-:name*other_params', :controller => 'destinations', :action => 'show', :canonicalize => true, :destination_global_id => /\d{1,20}/, :name => /[^-]+/
Now we can create helper methods that take all of these wonderful parameters.
def custom_d_path(destination, params={})
d_path(
destination.global_id,
string_for_url(destination.name)
) + (params.size > 0 ? create_other_parameters(params) : "")
end
The method string_for_url() just replaced spaces with underscored and removed illegal characters.
The create_other_parameters() appended parameters in a subtle way that ensured that Google and Yahoo! wouldn’t get prejudice about dynamic pages with parameters. (This is another topic for another time.)
In short, now we can simply call custom_d_path(destination) from any view (or controller if we included the helper in both ActionView and ActionController classes).
I realize that there may be a better way to do this to make it simpler to code, but this is a simple example of a way to solve this problem.
Now for a couple of caveats:
- For those who have OCRD (obsesive compulsive REST disorder) the urls may not suite your style. I use them for the read only pages of a site.
- You may not need to go to this extreme to keyword pack your urls… there are many other approaches that may be more robust and easier to implement.
Hopefully this example helps someone. :)
Capistrano 2, Multistage, and Mongrel Clusters
Make sure you have ruby, rails, capistrano, capistrano-ext, mongrel, palmtree and mongrel_cluster gems installed on your system.
To generate your cluster files you can run the following commands with your specific details of course. ;)
~ $ cd rails_app ~/rails_app $ mongrel_rails cluster::configure -e development -c "/home/www/apps/web2.0app.com" -p 8000 -N 4 -C config/mongrel_development.yaml ~/rails_app $ mongrel_rails cluster::configure -e staging -c "/home/www/apps/web2.0app.com" -p 8010 -N 4 -C config/mongrel_staging.yaml ~/rails_app $ mongrel_rails cluster::configure -e production -c "/home/www/apps/web2.0app.com" -p 8020 -N 4 -C config/mongrel_production.yaml
Now all you need to do is configure Capistrano config/deploy.rb file to make use of these cluster files and the Multistage extension for Capistrano. If you don't how to setup Multistage, you can do a search for it or read my other post in it.
require 'palmtree/recipes/mongrel_cluster'
set :stages, %w(staging production development)
set :default_stage, "development"
require 'capistrano/ext/multistage'
...
set(:mongrel_conf) { "#{current_path}/config/mongrel_cluster.yml" }
...
deploy.task :after_update_code, :roles => [:web] do
desc "Copying the right mongrel cluster config for the current stage environment."
run "cp -f #{release_path}/config/mongrel_#{stage}.yml #{release_path}/config/mongrel_cluster.yml"
end
In order for the correct mongrel cluster to start you will need to have a recipe in your deploy that ensures that the correct cluster file is renamed to the config/mongrel_cluster.yml and then it is used by Capistranos' mongrel:cluster:restart task.
Admittedly there are many more things that need to be done before and after this little tip, but I thought that this may help someone who may need to bridge their understanding of how easy it is to solve the Mongrel cluster on different stages problem.
Active Merchent Support for the Verifi Payment Gateway
Found any good Rails resources... well, here are 74 great ones
Oh, yeah! before I forget, here is the link to Richs article.
74 Quality Ruby on Rails Resources and Tutorials
With all of the resources out there on Rails, its nice to have a quality list.
Rails Meetup is Picking Up
I have been attending the Silicon Valley Ruby on Rails Meetup since the first meeting a year ago this next May (or so.) I am surprised at the number of people that show up and their purpose in coming. it started out with just a few computer geeks who love Ruby and Rails and now I think half of the attendees tonight were VCs, companies looking for Rails engineers, or people looking for partners for their next big idea for a web app.
I think its great but I have to admit it must be a sign of the times.
Ruby on Rails is here to stay and even companies like Spock (spock.com) are building large apps with it with 9M$ in VC.
now all I have to do is figure out a way to use Rails more since I don’t get the pleasure currently at my day job. :)
Ruby on Rails Camp at IBM
Installing RMagick on OSX
I am working on a little app (link coming soon) with a friend of mine in an effort to practice my rails and now Rmagick skills since my day job doesn't allow me the opportunity.
One of the things that I am building is an logo generator so I need to have an image manipulator/generator of some sort. I have used ImageMagick on many projects in the past so I looked forward to spitting out the classy logos uswing Rmagick.
Like most open-souce installs on OSX and Linux there were some issues that came up along the way.
I first ran the following command on my OSX terminal but got a couple of errors.
# sudo gem install RMagick
...
Can't find Magick-config or GraphicsMagick-config program.
...
I fixed this error by installing the imagemagick-dev version as opposed to imagemagick.
Then when I tried it again I received this error:
...
Can't install RMagick. Can't find libMagick or one of the dependent libraries
...
I resolved this error by searching google and finding this thread so I told fink (one of my osx package managers) that I wanted it to build imagemagick from source with the following command:
# fink --no-use-binary-dist install imagemagick-dev
After I rebuilt ImageMagick form source and inclused all of the dependent libraries i was able to successfully run the following command with no problems:
# sudo gem install RMagick
It worked! Yeah!
Now I will get back to the Rmagick docs. :)