Recently, I was working on a Rails app where I wanted to make some pretty URLs for it. I have a route where you can view a Dog resource GET /dog/1
. Having an ID in the URL is not what I want. People could increment the ID to potentially see other dogs. But more importantly, it’s not personal! I wanted the URL to show your dog name instead, like /dog/albie
or /dog/st-george-bernard-the-3rd
. I needed a way to make pretty kebab case URLs.
Pretty kebab case URLs
To get a URL that’s not the default id
, I added the to_param
method to my model. Where slug
was a column on my Model:
def to_param
slug
end
Since I wanted the slug to be auto-generated, I had to transform the name
column to kebab case (see below). Much to my surprise, there wasn’t a Ruby / Rails kebabcase
method:
class Dog < ApplicationRecord
before_create ->(dog) { dog.slug = dog.name.kebabcase } # .kebabcase doesn't exist
def to_param
slug
end
end
Looking at the docs, I found .underscore
and .dasherize
. Unfortunately, they only work for camel case names:
"DogName".underscore.dasherize
# => "dog-name"
But who names their dog in camel case? Maybe people do, but I’m assuming most people use spaces. Unfortunately, .underscore
only works on camel case strings.
"Dog Name".underscore.dasherize
# => "dog name"
Searching for a Gem
If Ruby and Rails didn’t have a kebab case method. I’m sure there’s a gem out there that does. In comes Strings::Case to the rescue:
strings.kebabcase("PostgreSQL adapter")
# => "postgre-sql-adapter"
I could’ve called it a day here, but looking at the gem, it has so many other methods that I wouldn’t use. Honestly, it seems like overkill for my tiny side project. It’s also another library I would have to maintain and make security updates to. In the end, I decided to roll a custom kebab case method.
Rolling a custom kebab case method
Before jumping into a solution, I wanted some inspiration from Strings::Case.
Under the hood, the gem scans through a string, building words determined by whether there is a specific delimiter or if there’s an uppercase. “DogName” would be two words “Dog” and “Name” since there’s an upper case in the middle. And “Dog Name” would be two words, since there’s a space in the middle. These words get put into an array, and after the string has been scanned, a .join
method is called with a separator. In kebab case, the separator would be -
. I’m oversimplifying this approach. If you’re curious, about the source code, you can view it here.
For the gem, this process makes total sense. It handles many use cases as well as includes multiple string manipulation functions. Since I only wanted kebab case, I decided to go with a regex approach:
dog.name.downcase.gsub(/[^\w\d\s]/, "").gsub(/\s/u, "-")
Let’s break down what this is doing:
.downcase
- A built-in function that makes the string lowercase..gsub(/[^\w\d\s]/, "")
- Regex that matches anything that’s not a character, digit or space, i.e. any special characters and removes it..gsub(/\s/u, "-")
- Regex that matches a space and replaces with a dash
"St. George Bernard the 3rd".downcase.gsub(/[^\w\d\s]/, "").gsub(/\s/u, "-")
# => st-george-bernard-the-3rd
The biggest limitation with my approach is the regex. I make plenty of assumptions with what I think names should be, and it’s focused primarily on English. But what if someone uses a character language like Japanese?
"餅".downcase.gsub(/[^\w\d\s]/, "").gsub(/\s/u, "-")
# => ""
If I wanted to make this more robust, I should handle non-English characters properly. If I can, I could transliterate it. Like é
becomes e
. If it’s in a character language, I could leave it because non-English characters can be used in URLs.
However, for a small side project, what I have is enough. And I didn’t need to pull in a new gem to achieve it.
Using Ruby 3.2.0
and Rails 7.1.3.3
at time of writing