A vanity URL is a great way to get a cleaner URL for your users, and is also a great way to improve SEO. Providing vanity URL’s to your users if they have a public facing page is also a great feature to add to give your users more personalization.
An example of a vanity URL would be linking to a username, instead of a user ID to see a users posts:
https://www.example.com/users/2348 - This is an example of a non-vanity URL
https://www.example.com/users/john-smith - This is a vanity URL and looks much friendlier
There are plenty of articles on how to add vanity URL’s, or slugs, with Rails, with the easiest method to be using a gem like friendly_id.
What happens when you want to make the URL be on your root page, while still having a catch-all route, or use a gem such as high_voltage? It’s still doable, if not a little tricky.
Setting up the initial route
The first thing you’ll want to do is add a route in your
routes.rb to specify the resource, and provide a root level path. This will tell rails that anything to your root level route should try and load the user resource.
# Place at the end of your routes.rb file resources :users, path: ""
This should setup a URL, such as
john-smith is the id passed to the route. Now, whenever somebody visits
example.com/john-smith, it will hit your
UsersController where you use a friendly finder method to find the user details.
What if we have a catch-all route setup further down our routing file to provide better error pages, or we use a gem such as
high_voltage for our static page routing?
users route will kick in first and we’ll never be able to navigate to our static pages or other catch-all routes. This is where Route Constraints come in.
Routing constraints can be a very powerful tool to give you flexibility over your routes. A route constraint is an object that responds to
matches? and tells Rails whether the route matched or not.
We can use a Route Constraint on our
users route, so that if the
:id from the route doesn’t match a username in our system, we return false and Rails moves on to the next route.
Let’s see it in action:
# app/lib/constraints/username_route_constrainer.rb module Constraints class UsernameRouteConstrainer def matches?(request) # Get our username from the route params username = request.params[:id] # Check if this name exists for any users User.where(slug: username).any? end end end
This is a pretty straight forward class. We pull the username out of the request parameters, and then look to see if the user exists in the system.
Now we’ll want to modify our routing configuration to tell routes to use this new route constraint:
constraints(Constraints::UsernameRouteConstrainer.new) do # Same route as before, only within the constraints block resources :users, path: "" end
Note: You might need to restart spring to pick up files in the new
Now anytime we try and load our page, such as
example.com/john-smith, we’ll see Rails make a request to see if
john-smith is a valid slug in our Users table.
There are a few things that we can do to make our route constrainer even better. If we are using
high_voltage for our static pages, we might notice that Rails makes a User lookup each time, even if we are loading a static page. This happens because Rails checks our route, sees that it fails, and then the
high_voltage Engine or catch-all route is then triggered.
Can we try and tell our route constrainer to automatically fail if we’re loading a known static page? Yes we can!
high_voltage gem provides some handy tools, one of which is a list of all of the known static pages that
high_voltage is going to manage. This is available in the
HighVoltage::page_ids method, which returns an array of pages.
All we have to do in our route constrainer is add a check to see if that
/:id param exists in those page ids, and fail if it does. This helps, as Rails will no longer do a needless database lookup, that we know will fail.
Here is what that method might look like:
def is_static_page?(param_id) HighVoltage::page_ids.include?(param_id) end
This method will check the param and see if it is a
high_voltage page id. Let’s see what the full route constraint looks like now:
# app/lib/constraints/username_route_constrainer.rb module Constraints class UsernameRouteConstrainer def matches?(request) # Get our username from the route params username = request.params[:id] return false if param_id.blank? return false if is_static_page?(username) # Check if this name exists for any users User.where(slug: username).any? end private def is_static_page?(param_id) HighVoltage::page_ids.include?(param_id) end end end
Much better. Now we get the benefits of root level vanity URL’s for our users, as well as the ease and flexibility of other catch-all routes further down the routing table. We’ve also added a few short-circuits to our route constraint, to improve performance and remove any unnecessary database look ups.
For full code for this example, see the following repository on GitHub: