Rails Vanity URL's With Route Constraints

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 example.com/john-smith, where 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?

Unfortunately, the 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.

Route Constraints

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 constraints folder.

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.

Performance Improvements

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!

The 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.

Example

For full code for this example, see the following repository on GitHub:

mike1o1/route-constraints-example

comments powered by Disqus