Not logged in (Log in or Sign up)


Taking a rest and making friends

So, what's next on the list?

  • each blog entry will be referenced by a friendly link (rather than its id)

In my last post, I mentioned that I had considered using a single BlogController to handle all requests for my blog... I didn't. First off, the blog consists of Posts and Comments. Now, from a data (or model) point of view these may be the same, but as far as usage goes they get handled a bit differently. Plus, since I ultimately want to have some sort of RESTful API for the blog, and possibly the site in general, I think it makes more sense to divide the work up into two separate controllers (with a nested route).

So, now I've got a PostsController and a CommentsController with the following route(s)...

map.resources :posts do |post|
  post.resources :comments

For anyone who hasn't seen a nested route before, they let you access nice RESTful resources like /posts/1/comments. Now, I don't like the default implementation of some of the nesting, but I'll talk about that in another post (if you just can't wait, you can check out my selective_mapper project on github).

To access a specific post, the generated show, edit, update and delete actions for the PostsController all reference the posts by id... not the friendliest way of linking to a post. Luckily for us, it's really easy change that behaviour.

First off, is the link. We'll add a permalink column to our posts table and we'll fill it in from our post's title (replacing characters where appropriate). There are a number of plugins out there that will take care of this for you (for example, permalink_fu by Rick Olson aka technoweenie).

Just so that we can explore a few more things in this post, I'll look at the basic method of doing this yourself. In the post model, we'll add a before_validation filter to set the permalink attribute from the title.

class Post < ActiveRecord::Base
  before_validation :update_permalink
  def update_permalink
    self.permalink = title if permalink.blank?
    self.permalink = permalink.gsub(/\W/, '_').downcase

This filter is pretty simple... set the link from the title (if not already set), replace anything that is not a word character (alphanumeric or underscore) with an underscore and then slam it all to lower case. We do this before validation so that we can add appropriate validations for the permalink and those validations occur on the cleaned up permalink...

class Post < ActiveRecord::Base
  validates_format_of :permalink, :with => /^\w+$/
  validates_uniqueness_of :permalink

Once we have a unique permalink, Rails makes it really easy to use the link as a parameter instead of the id. All you need to do is override the to_param method of your model class...

class Post < ActiveRecord::Base
  def to_param

... and as Emeril would say, BAM!

Now, as I implemented my Project model later on, I realized that I wanted the same sort of permalink ability for that model. I wanted to convert my project name into a friendly link so that I could reference the projects/warp_core page instead of projects/3. Since I didn't think the world wanted YAPP (yet-another-permalink-plugin), I just added a has_permalink module in my lib directory and included it in my environment.rb...

module ActiveRecord
  class Base
    class << self
      def has_permalink(options = nil)
        options ||= { :permalink => :title }
        link_attr = options.keys.first
        title_attr = options.values.first
        before_validation :update_permalink
        validates_presence_of title_attr, link_attr
        validates_format_of link_attr, :with => /^\w+$/
        validates_uniqueness_of link_attr
        self.class_eval "def to_param;#{link_attr};end"
        self.class_eval "def update_permalink;self.#{link_attr}=#{title_attr} if #{link_attr}.blank?;self.#{link_attr}=#{link_attr}.gsub(/\\W/,'_').downcase;end"

Then, in each of my models I was able to simply call my new has_permalink method.

class Post < ActiveRecord::Base

class Project < ActiveRecord::Base
  has_permalink :link => :name

If I need this functionality again in this project, it's all there and ready. If (or when) I need it in another project, I may move it into a plugin -- more likely, I'll use someone else's plugin since this was just an exercise in how it could be done. One reason I may build my own plugin is that most of the other plugins out there seem to use dashes instead of underscores in permalinks... I use underscores to maintain backward compatibility with my urls for my historic blog posts.

Anyway, friendly links for restful resources without custom routes... simple, eh?


blog comments powered by Disqus