Skip to main content
Popular Rails Gems:

Pretty urls with FriendlyID

46

Episode 9 · July 7, 2014

Techniques for taking your database models and using attributes to generate pretty urls

Gems ActiveRecord


Transcripts

When it comes to front-end frameworks and integrating JavaScript libraries to your rails application, there's a lot of different ways that you can do it, sometimes you can download the library directly and install it in your application, in the assets folder, or you can use a gem to handle it, and there's many gems to do that, and different methods. So we're going to talk about integrating Bootstrap in your application, which includes a CSS framework, as well as a bunch of JavaScript helpers to do that. Normally, if you were to download bootstrap, you would come over here and click "Download Bootstrap", which this will download a zip file containing the stylesheets and the JavaScript files that you can copy into your applications assets folder. That's one way of doing it, it's ok to do that. But if you really want to integrate this in your application and always depend upon bootstrap, then you don't necessarily need to have bootstrap inside of your application. It's completely third-party, you're not going to be working on bootstrap, and if you do, you should work on bootstrap directly, not hacking it inside of your application. So this is where ruby gems come in. You have ruby gems that wrap bootstrap and allow you to integrate it into your rails application easily. The reason for this is because those gems will handle upgrading to the latest version of bootstrap seamlessly. You change the version, you upgrade the gem, and voila, you're on the latest version of bootstrap, and your application never has to care how bootstrap is designed or works in order to do that, so you completely separate the concern between those two things. Bootstap's code should be separate in your applications code should be independent of that, so this is why we want to use a gem like bootstrap-sass. Now bootstrap sass is a ruby gem that I like to use in my rails applications, the reason why is because it's extremely lightweight, and there's not a lot of code to it, so it loads up the asset So in this episode, we're going to talk about URL design, and what we can do to improve the resources routes that come with rails. So what we've created so far is a bookstore application, we added the bootstrap gem and the better errors gem, and I've taken a few steps to create the scaffold for the books model, and they just have a title and a short description. So if we take a look at this book that I've already added, we go in here, and we take a look at ther URL, and we see /books/1 now this is the default from rails, it's a regular resource route, and the 1 represents that this book is the first record in the database. Now that's great, but if I am trying to access this book from a search engine, the search engines know that the URLs should be designed for your users, and your users aren't going to understand what database id is for this book. So your search engine results are improved slightly if you have the book title in the URL, so how do we go about doing that? So one approach is to add the title of the book right next to the name. So we could have for example: /how-to-win-friends-and-influence-people the reason we don't want to just dump the title in there in lower case is because there are spaces, and we don't generally want spaces in the url, it would be better off having dashes. How do we do that, however? how do we generate this url when we create a link to the book title, and include the database id, because with this url, we will still pull out 1, and that's how it will pull out this book from the database. It will still do the regular Book.find(params[:id]), and it will take this whole thing and convert it only to the number at the beginning. Now let's take a look at how that works in the console.

So if we come in here, and open up the console, we can take a look at the string. So I'm going to paste this in, as a string, and if we convert this to an integer, just like how it was before, we recieve that same numer 1. So the to_i method knows how to start from the beginning, pullout an integer, and then ignore anything else that's not a number, so that's really helpful, and that's actually what rails does internally when you call the find method. You pass it a string, it converts it to an integer. This still works, now how do we create that in a rails application. Well we actually have a string in the database called "How to win friends and influence people". So we have this string, but we need to convert it to the dashed version that's also lower cased. Rails provides a parameterize method that allows you to do this automatically for any string, so if we hit enter, you'll get a deprecation warning, but this is something you can ignore for now, and the result is the correct down case and dashed version of this. So imagine your book has an & in the title, and you call parameterize. An & is actually something important in the url that you don't want to put in there, unless you know exactly what you're doing. So parameterize will filter that out just like you would expect. So what we can do is that we can tell rails: We want to add the database id, a dash, and then a parameterized title of the book. So there's a method inside of the model, that we'll jump into right now that we can modify to make this happen.

app/models/book.rb

class Book < ActiveRecord::Base
    def to_param 
        "#{id}-#{name.parameterize}"
    end 
end 

This allows us to take the book, and when you pass it into a link_to. You're probably familiar with the link_to it's a very common method of linking to different urls, you pass in the book object, and this takes a look at the method to_param that ActiveRecord::Base provides, so what we do when we add this method, we're overriding ActiveRecord::Base normal to_param method, so we're going to define it how we want to, and this will be great. So this will automatically handle everything for you which is awesome. If we take this and we add the id and interpolate that as well as the book title and parameterize that, then we can save this, and this link_to part, this part will call to_param, and it will generate this to put inside the url, and we can go back to the homepage, and if we click on it, we get the new url. So we've automatically changed this to pretty urls without doing really any work aside from adding one method which is really cool. But there are some drawbacks about this, a user still has to remember the database id, and that is not necessarily a problem for your application. So for example, if you are Amazon, and you have this book but you have it on hardback paperback, kindle, audiobook and so on, it's ok to keep the database id in the url, because you have a lot of complexity here, there's essentially duplicates of the same book, and that's ok. You want to know how your application works and think about this. But what if you want to take this and get rid of the database id in the url? what if you want to find by the parameterize version of this title? Well, this is something we would call a slug. So this is a converted version of the name attribute of the book that we convert to a parameterezed verion, and we want to look up the database id based upon this. So our database actually needs to add a slug field, and before the record gets saved, we could convert it to this format. So let's go take a look at how to do that. Let's add a slug attribute to the book model in a migration

rails generate migration AddSlugToBooks slug

rake db:migrate

We will now save the slug every single time that the book saves. In our book model, we want to update the slug every single time the name changes. That's something we need to do every time before we save it.

app/models/book.rb

class Book < ActiveRecord::Base
    before_save :update_slug

    def update_slug 
        self.slug = name.parameterize 
    end 

    def to_param 
        "#{id}-#{name.parameterize}"
    end 
end 

self.slug = name.parameterize is different from slug = name.parameterize because here inside of the book, this line is going to create a local variable called slug, and it's not going to get saved when you write this to the database, so you want to make sure you run self.slug when you're assigning attributes inside of the model. If we save this and we go back to our application, we can update the book, and then this will save the slug, and we can see that if we run rails c and we grab our first book out of the database, you can see the slug attribute is correctly set to the parameterized version. Now this is great, but we have to go do two more changes before we can go any further. First one is that we need the the to_param method instead of the custom string that we built, we just want it to be the slug. So if we come back to our homepage, and we look at the link_to "How to win friends and influence people". In the bottom of Chrome we can find a little grey hover, and it says "how-to-win-friends-and-influence-people" when we click on it, the url is correct, however, we got an ActiveRecord not found, because it looked for a book with the id "how-to-win-friends-and-influence-people", but that's not right. The slug is this value, that's not the id. So we need to go update the controller to handle this. Part of the reason why you want to consider all these things is because before, with this method of to_param we never had to update the controller, and that's really awesome. So you get the benefits of doing a little bit better urls that are more friendly to your users, and you don't even need to update your controller. So if we go into the controller, and we jump down to our set_book method, where it crashed. You can see here the line 67, this is part of the benefit of better errors, you get to see exactly what went wrong, and you can actually interact with it right here. So we know that the params id is correct, but it worked out fine, it's looking for the right thing. So when we come in here, and go into the book.find we want to find by slug, rather than find where it looks by default for the id. So if we refresh, now our page works. So this is really cool. However, if we edit this, and we give it number 2, and we update the book, there's actually the number 2 in the url, so this is kind of a problem, because if we remove this, that book, at the same location no longer exists. So we need to be a little bit careful there and pay attention if the url changes, is that ok? Is it ok that google can no longer index that page, maybe it is, maybe it's not. You need to pay attention to that and know exactly what you're doing here. If I delete that and update the book again, that is correct. So we can't just delete the number 2 out of the url and get back to the book it was before, we have to actually update the book and that changes the url. So you have no history as what the old urls used to be and what they are now. It get's complex really quickly, I'm sure you've noticed, you start out with one little change to put words in the url, well that influences a little bit of other code, and then you get to this point and you're like: Oh no, we have lost history, and we've lost all these things, it looks prettier and users can remember it but it's actually more complicated. So what we're going to do in this episode now is take a look at friendly_id.

friendly_id is the "Swiss Army Bulldozer" of slugging and permalinks, and urls like these are also called permalinks pretty often. So this is a gem that allows you to ignore all of the complexities of this, I wanted to make sure that you understood what we're actually getting into before we dive into this. This topic is actually pretty complex and an important part of designing every web application. So friendly_id is very very nice. It does exactly what we were talking about before, it allows you to say /states/washington rather than the id number. That is more of a middle ground hack that isn't ideal for your users, and the goal really for building good software is that it's ideal for your users. So there's a lot of information here, I recommend reading through all of it. For example, if you have duplicates, they provide you different methods so that you can say: This url rather than adding "-2" at the end, you can add a city or a street and a city, and so on. So you can really differentiate that a lot better. However this is also like default, they can do UUIDs and whatever, so lot's to learn about this gem, and I'm just getting into the basics which is under this "Rails QuickStart". So as usual, don't grab the Gemline from your README's in your gems, rather go to rubygems, grab the gemfile line and put it in your Gemfile. So we'll do that right now. Save that, hop into our terminal bundle install.

First we need to run the friendly_id generator, and this is going to create a table for us to keep track of the friendly_id slug or whatever else that they have and an config/initializers/friendly_id.rb and you can look through all these, it looks pretty good. If you want to do something complex, you can absolutely jump in here. They also have reserved_words, so this means that you cannot have a book with the name of "login" or "logout" or "admin", which is really good because sometimes people try to attack your website, and they're like: Ok, if you're going to have twitter.com/my username what if my username is admin, or login, logout, then that means that you could effectively break twitters website if they have not thought about that where their users could no longer logout because they would be taken to your profile, which would be really interesting, but you need to think about that. So we're going to run this migration

rake db:migrate

then, we're going to look at the next step, which is to generate a scaffold for the user model. We don't need to do this, they didn't present this very clearly but they're basically saying: add a slug to whichever model that you want, and we've already done that with our previous example, they do encourage you to put an index on the slug, which I very much agree. So let's go back and make this change. So first we want to rollback twice, because first we're going to get rid of the friendly_id slugs, and then we want to rollback again to remove the slug from the books model. So when we jump into our db/migrate/add_slug_to_books, rather than have it generated it like we had before, we also want to add in this index so that the database can query this column very quickly. So this index allow you to query the column faster than you normally would, and it's very important when you're doing queries, you want to add index.

rake db:migrate

Generally, you don't want to generate your migrations like we just did, however this is in development, we've never pushed these migrations to the server so it's ok to do that, it will be cleaner later, our code will be much cleaner. But the important part is that we've never pushed this to any other computer, it's never been on GitHub, you don't want to edit migrations after that. It's ok to do it now, but don't do it later on. So then we've run our migrations and we just take the extend FriendlyId and put that in out book model, which is very nice. We can paste that in, and what this does basically, is it adds essentially the before_save and update_slug method that we had before, which is very cool, as well as the params. So you can just put that in and if we take a look at this, the only other change they recommend, is in your controller you call .friendly.find, which means that the other change that we made in the contoller before should be .friendly.find.

I'm going to restart the rails server to make sure that we have our friendly gem available, and we'll go back to the homepage. That is still incorrect, so let's update the book, and see that it updates this slug, which it did, that's great. This happens because the record already existed before we added friendly_id. So now, you can take a look at that, you can automatically have all of those features that we built by hand by adding .friendly and

extend FriendlyId
FriendlyId :name, use: :slugged

as well as the migration into oyur application. And this also provides you a bunch of other things, like history, that we talked about before, when you update the name of the book and the url is broken. You can save that in the database, and it will look up the record as well. So you can dive into this, and you can see the history.rb file. This is the code that actually allows you to do that, it creates slugs based upon different parameters, and it has different queries and whatever else. So this, as long as you define this properly, which it looks like you can say use => :history, this allows you to have the history in your app.

So that's friendly_id, there's a lot to it. This guy has done a fantastic job with it. You can use this if you like, you can also build it from scratch, but it really depends on what you're trying to accomplish with your application and your urls. Definitely click on to it, because it's one of the most important pieces of a good application. Look at GitHub, there is not a user id anywhere or a database id anywhere the url. And we can remember that, and that's good. Think about those things and pay attention to them when you're designing your application.s, and that's basically it. It has a handful of helpers but in order to evaluate why this gem is one that I want to use, I'm going to walk you through that, so I've taken a while to read through this gem to figure out if it's something worth using. Because bootstrap doesn't have a lot of ruby logic, it just simply needs to serve up the CSS and JavaScript files. We want a gem that simply does that so if we dive into this gem, we can go into the lib directory and actually take a look at the rails integration here, and see how it does that. So normally we would just dump in the CSS and that would be ok. But because it's in a gem, it need a little bit of ruby code around it to properly integrate it. So we can dive in here into bootstrap-sass.rb file and see that ok this is going to simply load the gem properly, it has a handful of helpers of the paths, there's a couple other helper methods and really not a lot going on, and that's all that's in this file, so there's very little logic going on in here. If we dive into the gem more itself, we see that there's a rails engine to integrate it, they made som sass helper functions, and the ruby gem version, so there's really not a whole lot in here. If we want to dive into it and double check, you can look that ok there's some more path helpers, and that's about it. So these are really good signs that this gem is very lightweight and does exactly what we want from it, because if we were to integrate bootstrap ourselves, we would just copy files in and we'd be off to the races, so some people write gems that add a lot of overhead and things get to be slow and confusing, ant then when there's a bug it's hard to fix. So looking at this one, the code for itself is very simple, they have some rake tasks and templates and tests, but if we look in the vendor folder, you can see that inside of here, it's just the bootstrap files in SASS format, so this is good, it's in SASS which means that we can write, we can change the variables and then change the colors of bootstrap and tweak the settings, so if you want to make it a 16 column grid, you can do that easily. So, in here, if you want to learn how any of bootstrap works, you can dive right in and you know exactly what files contain which thing, so if you want to go into the mixers or the modals or anything like that, you can just simply dive in, and this gem is very transparent, so there's nothing very confusing or hidden away, and it's very simple, which I like. If I'm going to use a gem, I will not use a gem unless I trust that it's straightforward and that I would be able to go fix a bug myself if I came across one. So you want to really trust the gems that you use and not use them arbitrarily because: Oh, I wanted a great bootstrap, I guess I should find a gem for it. DO NOT DO THAT, because that is not a good way to handle things.

So if you want to go to rubygems and download and install this gem in your application, I always recommend grabbing this gemfile line so that you have this version, and most importantly, the version specifier here then make sure that when you run bundle update it will update this version and not include any major version changes.

Now that we've spent some time looking into the bootstrap gem and determining why we want to use it, let's go add the bootstrap gem into one of our applications. I'm going to create a new application, and we're going to install a nav bar from bootstrap, and we're going to go take a look at that right now. If we go over to the components section, we come down to the navbar, we're going to put this inside of a rails application that I'm just going to create from scratch, so we'll go though the whole installation process and then integrate this navbar. So let's hop over to the terminal and create a new rails application.

rails new bookstore

Before we do anything, let's set up a page, so that we have a homepage to look at and we can tell if it's going to be using bootstrap or not. So normally you know there's no CSS, so we'll be able to tell pretty easily weather or not it's working.

rails generate scaffold book name:string description:text

rake db:migrate

config/routes.rb

root to: "books#index"

rails server

We see that we get the regular scaffold styles that come with a rails scaffold. First I think we should delete that so we don't have it to conflict with the bootstrap stylesheets, so we can remove the app/asset/stylesheets/scaffold.css.scss and now if we go back to Chrome, we can refresh and see that all the styles are gone, and this is just the basic stuff that Chrome uses. So if we come over here to the rubygems bootstrap sass tab and we grab the gemfile line, we'll go back to our application and open put the gemfile, and add it at the bottom. Come back to the terminal and run bundle install, and then we'll restart our rails server, refresh the page, and nothing changed. So the gem needs to be installed, but it also needs to be loaded, so if we go back to our terminal, we're going to use the move command to rename our application css stylesheet to one that uses SCSS formatting so that we will import this bootstrap sass gem. So all we have to do is say mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss, and if we go back to our editor and we open that at the bottom here we can use Sass to import the bootstrap stylesheet file which is provided from the bootstraps sass gem that we just installed. So now if we go back to Chrome and refresh, now we have bootstrap's font, bootstrap's color and styles, so this is good, we're almost there. Now we want to integrate this navbar code, so I'm going to copy this, and you'll be able to edit it later, but for now we'll just put in the example. So we want this navbar at the top here on every single page, which means that we want to put this inside of our application layout file. And instead of putting the code directly in this file, it's going to make this file quite a bit bigger than we want, because if you looked at that before, there was a whole lot of lines of code just for the single navbar, so we're going to do is we're going to put this in a partial so that the navbar can be shared between multiple layouts if we happen to decide that for checkout we're going to have a different layout, and it will be a little bit different. So to prepare for that, we're going to render a partial, and we're going to call it <%= render partial: "shared/navbar" %>, and this is going to look in the app/views/shared/_navbar.html.erb

mkdir app/views/shared

touch app/views/shared/_navbar.html.erb

And here we can paste in the navbar, and now, that will render on every single page that the application layout is rendered on, so if we come back to Chrome and refresh the page. Voila, now we have our nav bar in every page so if we go to add a new book, we still got the navbar and so on. So we have access to all of bootstrap's components and other so you can do, you know icons, and you can do dropdowns and anything that you see on here, you can take and then integrate it into your application, and all of it's available pretty much immediately. So if you want to use JavaScript for example, you'll do the same thing that we did with the application CSS, although, if we go into the README, we can see that you can use the asset pipeline require to handle it rather than sass to import. So going down to the JavaScript here, you can load all of the bootstrap JavaScript by saying require bootstrap, and we'll copy tha and put in out application.js file and we'll do it right after turbolinks. And now if we come back here, I haven't refreshed the page, so you can see that our dropdowns are working which that is because they need JavaScript, so if we refresh the page and click it now, it has loaded the JavaScript through the asset pipeline, and that is as simple as it is to evaluate and integrate a bootstrap gem into your application.

Transcript written by Miguel

Loading...

Subscribe to the newsletter

Join 18,000+ developers who get early access to new screencasts, articles, guides, updates, and more.

By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

More of a social being? We're also on Twitter and YouTube.