The Great Slug War
Introduction
For quite awhile, my goto choice for permalink / slug / seo-friendly url's in Rails has been friendly_id. If you haven't seen it it, I highly suggest checking it out - it's a self-proclaimed "Swiss Army Bulldozer" of slug and the like, making it simple to support pretty much any common slug variation out of the box.
For me though, one of the biggest things that originally drew me to friendly_id (and, correct me if I'm wrong, one that no other major / well known Rails / AR slug plugin offered out of the box) was transparent support for slug history out of the box. If you're not familiar with friendly_id, this means that if you want to sync slugs (as makes sense - you don't want, for example, a posts slug reflecting an old, outdated title - you want it to refer to the newest version) but at the same time don't want to break existing URLs (e.g. the old slug should be a 301 redirect to the new one), friendly_id offered a simple option to record slug history - meaning that it would track prior versions of the slugs.
By default, the way this was implemented that when you wanted to add slugs to a bunch of models, you didn't even have to add a slug column to the table - you just generated a single slug table and let friendly_id handle the rest. The idea of being able to record this history seemed like a kick ass idea to me (and to be honest, I was surprised that no other plugins seemed to do it) and so I used friendly_id for most of my projects with great success.
Then, along came Rails 3. friendly_id was quite quick to support the rails 3 betas out of the box (which is awesome to see in a reasonably popular rails plugin) but the public release still suffered a few problems. Namely, the way it overrides find to support all the awesome features it does meant that in Rails 3 you had to either monkey patch a bunch of different classes / find some neater way to integrate into the find lifecycle.
With this in mind, I set out to write a set of patches to support friendly_id on Rails 3 (namely, the public release didn't support find on named scopes or relations - quite possibly one of the biggest / most handy changes of Rails 3) which meant in an application where scopes are heavily used for finders, you're pretty much shit out of luck. My patches went some of the way to fix support but still left a lot to desire.
I set out mentally to find a better way to solve it (and am still doing so) for friendly_id, but in the mean time a bug (possibly directly related to said patches) in an application I was writing forced me to get rid of friendly_id and to write something simpler of my own.
Initially I scrambled together (the bug was a site breaker) to write a single, ~50 or so line so-simple-it-hurts permalink library, using stringex for the url conversions. It worked but it wasn't quite what I wanted. So, With the fact I'd been reading the Jason Wander series of books, I set out to write my own, AR3.0+ only slug plugin that was lighter weight than friendly_id but still supported some of the best ideas (e.g. slug history). Also, most importantly to me, I decided early on it should never override the find method, instead using another alternative name (more on that later).
Introducing Pseudocephalopod
(Pardon the long name, I'm always open to shorter / nicer suggestions - although I hope it should be reasonably memorable if anything).
Pseudocephalopod was written from scratch to support several core ideas:
- It should be tested (as any plugin should be / all the good ones e.g. friendly_id are)
- It should make the most of relations in Rails 3.0, at the expensive of no backwards compatibility
- It should support (and default to using) slug history, but with the option to turn it off.
- It should let you easily cache slug looks in memcache or the like.
- It should give you the option that if the source column is blank, it may generate a uuid in it's place (more on this in a minute)
- It should use Stringex if available.
Number six was a big one for me (even though implementation wise it's such a very tiny one). For quite a while I've been a huge fan of Russel Norris' awesome Stringex library. If you've never used it, it provides a simple slug plugin (acts_as_url) which I'd tried but it was a little too bare bones for myself. What it does do perfectly though was provide a String#to_url method that does a reasonably nice / simple conversion of a string to a url safe string, much like String#parameterize in rails. In my opinion, it does it better than most other alternatives and if you're wondering why, I suggest just reading the project's (linked above) README.
Along the same lines, I felt strong about number 5 but had been yet to see it implemented. I liked the idea of not exposing the id of a record to users in the url, instead opting to show a uuid instead. Importantly though, since this is very opinionated I provided the option to turn it off.
Lastly, I liked the idea of using the naming scheme "find_using_slug" (vs. overriding find, or using from_slug or similar) after being inspired by part of authlogic - it's similar enough to find_by_slug that it's memorable but the distinction of using implies that there is (as is rightfully so) more going on behind the scenes than just a simple lookup.
Using Pseudocephalopod
So, if you're intrigued by now, I suggest actually taking a look - the code is simple enough to peruse (at the moment it's a little over 300 lines of code seperated into a few mixins and one or two classes). More importantly, it's as simple as running the following if you want to contribute back:
git clone git://github.com/Sutto/pseudocephalopod.git
cd pseudocephalopod
bundle install
bundle exec rake
# ... hack on the code here ...
bundle exec rake
Bundler really does make it easier to manage dependencies when righting something like a gem plugin. If you wish to use it on a rails app, it's as simple as doing the following:
-
Add a dependency to your Gemfile
echo "gem 'pseudocephalopod', '>= 0.2.1'" >> Gemfile -
Generate the slug history table
rails generate pseudocephalopod:slugs -
Generate cached slug fields for each model
rails generate pseudocephalopod:slug_migration User -
Run your migrations
rake db:migrate -
Edit your files (e.g. user.rb) and add an is_sluggable call
class User is_sluggable :name end -
Change your find calls to find_using_slug (find_using_slug! if you want to raise ActiveRecord::RecordNotFound; Also note it will work even if you pass in an id)
class UsersController < ApplicationController def show @user = User.find_using_slug!(params[:id]) end end
And you're done.
It provides a bit more than that but that's enough to get anyone started. If you want to check it out, click here to view it on GitHub.