A Practical Example of Using URL Slugs for your Rails Parameters Instead of :Id
Whelp, it’s time to make a new web app using Rails. Time to go into cruise control. You created your model, let’s say it’s called Items, your controller, and routes. After everything has been set up, you push it online and your users now have to go to a URL that looks like “/items/456” . That doesn’t really feel right. A user could save that link, but they’ll never be able to infer what it is. And imagine if you had to reseed your database, or something. That URL would be useless then. The professional sites you see online don’t do this. That is where URL slugs come into play. A URL slug is the part of a URL that tells us about the specific page we’re on in a easy-to-read form — for example “/items/metal-slug-double-x-game” . And hence, what we are going to go through today. Making it so that our URLs show this easy to read URL slug instead of the ugly item id, while not breaking our app.
Though the concept of URL Slugs really applies to any backend, we are going to be doing this for Rails. Also, the assumption is that you are using Rails backend as an API. If you are going to use Rails entirely for your MVC, the process will be the same, you just need to replace :id with :slug in your view files. Everything else will be the same.
Let’s start with some basic set up of our API. For now, just set up the app the same way you normally would. Make a normal Model, in your routes, set up resources :items, and a basic controller with an index and show methods. We are going to tweak these during the process. After setting up the Model as you wanted, lets refer this to model as Item, create a new migration called slug as a string. We will later go into validations and what to put inside a bit later, but for now, we just need this column set up. So, now that we have everything set up on a basic level, let’s go through everything piece by piece starting out with our model.
For our model, let’s dig into what we want to do. When we create a new Item, we can set up each attribute manually, including the slug if we want (and if you want to, skip to the section about validations& macros). But typically, we want this slug to tell us about the “item”. It should be unique just like our :id is unique and it should be tied to our item itself. We could have our item of Slugterra DVD have a slug of “/golden-carrot-24k”, but it doesn’t make sense. So, our first instinct should be to use the item’s name as the slug. But as you may know about URLs, you can’t use spaces and certain characters in your URL. We need some kind of way to make this slug URL-friendly. So, let’s go into our Item model and create some methods. Before that, let’s add a validation of :name & :slug . Let’s create a method called #create_slug . Rails (specifically ActiveSupport) includes a method called #parameterize that will take a string and basically make it URL friendly. The alternative to this is using regex and changing all the characters, but for the sake of ease, we will stick #parameterize. Now that we have this method, we should create a method called #assign_slug . This method will be very simple, it will simply set self.slug = create_slug . This way that every time we call #create_slug, it will create our URL friendly slug based on the item’s name. The last method we need to make is #update_slug . This method’s responsibility is to simply update slug whenever our item is updated. It would be a bad look if our item’s name didn’t match the item’s slug.
Now that we have these methods, we need to make an after_create macro that will call #update_slug, and a before_update macro that will call #assign_slug. Now anytime a new item is created or if its updated (like the name), the slug will always be there and match. So now we can create and manipulate our items without having to manually add slug every time and make sure it doesn’t get screwed up. Time to go into the controller.
In our “normal” controller methods, like #show and #update, we would find our “item” by doing something like Item.find(params[:id]). But because we want to use URL slugs, we can’t use #find. So, we have to go by #find_by and find by the slug everywhere we are looking for a specific item.
Before we go further, let’s refresh our memory about what :id really and what the point is. When we have a RESTful route, it should follow something like /items/XXXXX . Because “XXXXXX” could be anything, instead of somehow hardcoding every single possibility (which would be insane), we refer to it with a variable name. Convention tells us because we normally had this as the id of “items”, we should refer to it as :id. But truthfully, we could refer this as anything, and as such, anything could be written in “XXXXXXXXX”. Its just that we won’t “communicate” well. As such, in the #find_by method, we could go Item.find_by(slug: params[:id]) and it would technically work as intended. But anyone else (including your future self) may look at :id and assume that you are meant to pass the item id.
So, now that we have the controllers set up, we do want to refer to params[:id] as something more obvious, such as params[:id]. To make that a thing, we have to go into our routes, and make a change there. The solution for this is pretty quick and easy. If you used resources in your routes (such as resources :items), we’re just going to add “, param: :slug” to the route like the picture. If you are doing custom routes, just simply change the :id part to :slug. NOTE: that you can change that variable name to whatever you wanted, but because we are being to whoever sees this in the future, we’re gonna call it :slug. Now that we changed this, we should now go back to the controller, and change params[:id] to params[:slug]
To recap what we’ve done, we’ve made a column in our model called slug. We went into the model and created a few methods. #create_slug that takes the item’s name and turns it into a URL-friendly slug. #assign_slug which set the object.slug = #create_slug. And finally, #update_slug which calls #assign_slug . We then created macros that would call #assign_slug & #update_slug whenever a new object is created or updated. When then went into the controller and changed method where we had to find a specific object, and went by #find_by(slug: params[:slug). And finally, we went into our routes and changed the param variable from :id to :slug. Having done all of this allows our website to now have URLs such as the attached photo.
Now that we have this set up and are done, let’s talk about some optional things you want to consider. For a lot of basic uses, the name of your object should be unique. If you find that the slugs of your “items” are too similar or you want to make them extra unique, the solution for this is simple. Go into the #create_slug in the Model and simply add the item’s id to the string returned. You could end up with something like “/items/white-diamond-ring50” while leaving the name column of the object alone. There are some Pros and Cons of doing it this way, but you’ll find whichever feels better for you. But truthfully, both works. Another thing to consider, is that we made the assumption that the name of the object would be comprised of western characters. #parameterize does a pretty decent job of converting string into something user friendly, but you may find a situation where you are using different characters that may not translate well (an example would be “™Iñtërnâtiônàlizæti™øn™”). In this case, you may have to use regex or an outside gem like Unidecode which may do a better job converting the string to how you want it. Because we were break down the functions into parts, all you have to do is edit #create_slug replace #parameterize with your preferred method.
Now that we have set up our URLs to use URL slugs instead of :ids, we now have a site that looks more professional. Also, because we are using slugs vs numbers, our site just became more SEO-friendly as well as improve our user’s experience. An app worthy of Slurm McKenzie