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
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
It’s important to note that we’re actually passing in the
book instance here — an
ActiveRecord object, and not the
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:
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
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
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:
- We need a column in our database that will map to the attribute used in generating our url.
- We need to call the
acts_as_urlmethod in our model using the attribute name that we want to use for generating our url.
- We need to override Rails’
to_parammethod (Confused? Hang tight, we’ll get there in a second!)
- We need to
find_byour 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
1 2 3 4 5
Once we run
rake db:migrate and add this attribute, we will want to add the
acts_as_url class method to our
1 2 3
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
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
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
find_by_url method, but we could also use the
find_by method in our controller as well.
1 2 3 4 5
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
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
No worries, we just need to call a method inside of our console:
1 2 3
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:
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_paramon models to get a value for the
idof a model, but you can redefine that method in your models.”
1 2 3 4 5
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:
acts_as_urllibrary expects an
urlattribute on a model, and uses that to generate the path for an object. You need to override Rails’
to_parammethod that, by default, will use the
idof 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_parammethod used to be defined inside of
ActiveRecord::Base, but has since moved to the
ActiveModel::Conversionmodule, which handles default conversions, including
to_partial_path. Read more about how these methods work in the Conversion module documentation.