Words and Code

One writer’s journey from words to code.

Solutions for Slugs of All Sizes: Acts_as_url + To_param

#technicaltuesdays, rails, ruby


Last week was my first week of working from home, which meant two things: spending a lot of time with my computer and limited time with other human beings and, more importantly, debugging things on my own without having anyone nearby to ask for help. The latter of the two actually ended up reaffirming the fact that I actually can debug a decent amount of things on my own if I just power through and am stubborn enough to not give up.

I also learned something interesting about the debugging process: while we’re learning, we often solve the same problem again and again. At least, this was the case for one of the features I was working on which involved using an object’s slug to generate a url. As I started thinking through how to approach solving this, I immediately had the feeling that I had done something similar in another project. Digging through another repository’s source code confirmed my suspicions, and I rediscovered the stringex gem and its multiple libraries, including acts_as_url!

All of this begs the question: why didn’t I remember that this gem existed — or that I had already used it? My guess is that it’s because I neither wrote about it nor understood how it worked until a few days ago. This week, it’s time to rectify that situation and dive into the acts_as_url library and find a solution for all slug problems, once and for all!

The Other Kind of Slug


We all are familiar with the Rails mantra of convention over configuration, aka making the lives of developers easier by eliminating the need for them to make decisions about how to structure their code. Now, this design paradigm is pretty fantastic, particularly when we’re first learning a new framework. But eventually, there comes a time when we need to tweak our code’s conventions just a tiny little bit.

What’s an example of this? Well, take Rails convention of finding an object by it’s id. This standardization pops up all over the place, but the one that we’re particularly concerned with is the generation of a url. By default, Rails applications will build a URL path for the show action of any given controller based on the primary key (aka the id column in our database) of the object that we’re trying to “show”.

Let’s put this in context of our bookstore application. We have a bunch of Book objects, and we want to iterate through their titles and then link to their individual “show” pages. A very basic, not-at-all-fancy template might look something like this:

1
2
3
h2 Books!
- @books.each do |book|
  p = link_to book.title, book_path(book)

It’s important to note that we’re actually passing in the book instance here — an ActiveRecord object, and not the book’s id. Why is this important? Because there’s a method that Rails is using to convert the Book object into a URL in order to generate the correct address for our book_path. That’s why we need to pass it an ActiveRecord object, because the method that’s being called expects an object and returns the id as parameters in the url. What does this look like, exactly? Well, right now our book_path takes a Book instance and creates a path that looks like this: localhost:3000/books/25.

Which is fine! Actually, it’s more than fine: it’s the expected behavior given our Rails mantra of convention over configuration. But, what if we actually want to configure this a little bit more. What if, instead of using the primary key of our Book instances, we wanted to use the title of the book? It would be lovely if we could link someone to a particular book’s page with a more human-readable url (for example, something like awesomebookstore.com/books/the-bell-jar in production).

There’s a solution for this problem, and it’s called slugs. Slugs are a solution for semantic URL generation, which is also sometimes referred to as “RESTful” or “SEO-friendly” URL generation. As our application grows, not only would we want our URLs to be user-friendly, but we probably also will want them to be optimized for search engine results. So, we need to change our application’s configuration to use a slug.

Protip: if anyone ever pop quizzes you about where the term “slug” comes from, you can totally school them with the following interesting fact: a “slug” used to be a shorter name given to a newspaper article while it was in production; during the editing process, the article would be labeled by its slug, which would more specifically indicate the content of the story to the editors and reporters. The more you know, amirite?

There are obviously a lot of ways to approach this, but why reinvent the wheel by writing a bunch of methods that someone else has already written? Let’s make use of someone’s open source work and use the acts_as_url library to get the job done for us!

Don’t Let Slugs Slow You Down


The acts_as_url library is actually part of the stringex gem, which adds some useful extensions to Ruby’s String class. After we add this gem to our Gemfile (gem "stringex") and run bundle install, we can get started doing a quick setup.

The documentation for this library is fairly straightforward, and a quick read-through gives us a good idea of what we need to do in order to make it work properly. The basic implementation of this library is four-fold:

  1. We need a column in our database that will map to the attribute used in generating our url.
  2. We need to call the acts_as_url method in our model using the attribute name that we want to use for generating our url.
  3. We need to override Rails’ to_param method (Confused? Hang tight, we’ll get there in a second!)
  4. We need to find_by our new url attribute inside of our controllers.

Let’s take it step by step. First, we’ll write a migration that will add a slug column to our books database. This is going to be the column that will map to a slug attribute on our Book objects:

1
2
3
4
5
class AddSlugColumnToBooks < ActiveRecord::Migration
  def change
    add_column :books, :slug, :string
  end
end

Once we run rake db:migrate and add this attribute, we will want to add the acts_as_url class method to our Book model.

1
2
3
class Book < ActiveRecord::Base
  acts_as_url :title, url_attribute: :slug
end

The default behavior of this method expects that we have a column and attribute called url on our object. Since we aren’t using the default attribute name, we need to specify the name of the attribute that we’re using to store the generated url string. Thankfully, acts_as_url takes a bunch of options, including url_attribute, which is what we’re using here. There are some other useful options worth checking out in the documentation, including scope, limit, truncate_words, and blacklist.

Next, we’ll need to override Rail’s to_param method in order to actually use our generated url attribute. Basically, we’ll just want to write our own to_param method and return our slug attribute from inside of it.

1
2
3
4
5
6
7
class Book < ActiveRecord::Base
  acts_as_url :title, url_attribute: :slug

  def to_param
    slug
  end
end

And finally, we need to make sure that we’re finding our object using the appropriate attribute from within the context of our controller. The documentation suggests we use the find_by_url method, but we could also use the find_by method in our controller as well.

1
2
3
4
5
class BooksController < ApplicationController
  def show
    @book = Book.find_by(slug: params[:id]).decorate
  end
end

Nothing changes about how our controller works and we can still do all the fancy things we were doing before, like use a decorator (Remember those?). The only thing that happens now is that our original book_path helper will now use the book instance we passed it to generate a url with a slug instead of the primary key!

Success! We’ve done it! Actually, we’ve almost done it. One tiny little thing that I always forget is all of the books that already exist in our database. What about them? They all have a slug attribute, sure, and a slug column – but there’s a slight problem: the column is empty! So we can’t find_by the slug attribute for those books, can we? In fact, if we try to call to_param on any of our preexisting Book instances right now, all we’ll get is nil!

No worries, we just need to call a method inside of our console:

1
2
3
 rails c
Loading development environment (Rails 4.1.4)
irb(main):001:0> Book.initialize_urls

Now all of the Book instances that had empty slug attributes have ben initialized, and we’re good to go! Right? Wrong. Because I haven’t explained the whole to_param situation yet, and I promised that I would get to it. Now’s the time to figure out the magic behind that!

Rails Non-Sluggish Solution: to_param

The Rails solution to generating params for an object’s url path comes from its elegant to_param method. By default, this method just calls to_s on a Plain Old Ruby Object, and converts it to an instance of Ruby’s String class. However, there are plenty of places where Rails itself overrides this method (which explains why we also have to do it in the context of our own controller)!

In fact, the Rails documentation even explains when and how to go about redefining the implementation of this method:

“Notably, the Rails routing system calls to_param on models to get a value for the :id placeholder. ActiveRecord::Base#to_param returns the id of a model, but you can redefine that method in your models.”

1
2
3
4
5
class User
  def to_param
    "#{id}-#{name.parameterize}"
  end
end

Of course, we could have easily redefined this in the context of each of our models, but using the acts_as_url library reduces the amount of duplicated code that we need in each of our models, and is pretty sophisticated in that it allows us to use different attributes across different models to generate our url path.

Interestingly, the source code for Rails’ to_param method reveals some elegant checks as well. This method first checks whether the object as been persisted to the database, and then returns a string representing the object’s key. We can actually see how the to_key method is being called from inside of to_param, and how the default return value of an unpersisted object’s param will be nil. This is the magic that goes on under the hood when we were trying to find the slug attributes for all of those Book instances before we called initialize_urls on them!

So, even though slugs have a reputation of being slow, now we know how to speed through this problem with an elegant and quick solution! I can’t say the same for this poor guy, though:


tl;dr?

  • The acts_as_url library expects an url attribute on a model, and uses that to generate the path for an object. You need to override Rails’ to_param method that, by default, will use the id of an object to generate its path.
  • This awesome gist by Jeff Casimir is the best write-up on slugs and Rails’ url generation out there. Give it a read!
  • The original to_param method used to be defined inside of ActiveRecord::Base, but has since moved to the ActiveModel::Conversion module, which handles default conversions, including to_model, to_key, and to_partial_path. Read more about how these methods work in the Conversion module documentation.