Sept 2020
User Pages in Rails
When you create an account on a website, it’s nice when you receive a small piece of virtual real estate in return.
And it’s even nicer when it’s a clean URL.
Some examples include Github (github.com/username
), or Twitter (twitter.com/username
), or Instagram (instagram.com/username
).
A non-clean URL will contain some subpage modifier, like
Youtube (youtube.com/user/username
) or LinkedIn (linkedin.com/in/username
).
Setting up non-clean URLs for a web application is pretty simple, whereas clean URLs can be a bit tricky.
This post will talk about clean URLs.
Complication
So why are clean URLs tricky? It’s because we’ll have other pages on the top-level namespace that we don’t want interpreted as user pages.
For example — we have a page like bounce.so/about, but about
isn’t a user, and we don’t want the application to attempt to load a user page. And as a website grows, you can imagine that there will be a wide range of URLs that need to be carved out from the user space.
User Route
When we go to bounce.so/eric, we want to see Eric’s profile page.
Our relevant route looks like:
resources :users, param: :username, path: '', only: [:show]
or
get "/:username", to: "users#show", as: 'user'
These are effectively the same, but the first route gives us flexibility should we decide to add additional actions for this resource.
For route # 1, the param: :username
allows us to reference params[:username]
in our controllers (as opposed to the default params[:id]
).
Also, the path: ''
is necessary so we have clean urls. With resources :users
, Rails will precede these routes with user
.
So at this point, GET
requests to bounce.so/eric
route us to the show
action of the UsersController
.
Reserved URLs
As it currently stands, if we had an bounce.so/about
page, the router would try to find a user about
. Not good.
So we need to override this behavior for certain reserved routes.
For /about
, we have:
get '/about', to: 'home#about'
Which says, for this specific route, go to this controller action.
But, we aren’t clear yet. The crucial part of getting this to work is the ordering of the routes.
Routes that live at at the top of our routes.rb
override the routes at the bottom.
Rails.application.routes.draw do
get '/about', to: 'home#about'
get '/faq', to: 'home#faq'
resources :users, param: :username, path: '', only: [:show]
end
So we can be confident that bounce.so/about
routes us to home#about
instead of users#show
.
Reserved Usernames
So far, we can rest assured that our About
and FAQ
pages will go to the right places, but we should probably make sure that no user can create the username about
or faq
, because that would be confusing.
class User < ApplicationRecord
validates :username,
exclusion: { in: excluded_usernames }
def self.excluded_usernames
%w[
about
site
... lots of reserved usernames ...
faq
login
signup
]
end
end
Our exclusion validation runs our excluded_usernames
class method, which returns an array of reserved usernames. This will prevent any user from selecting the username about
among others.
It’s important to keep this list updated for any usernames you want reserved.
Another Complication
When we’re creating links to user pages from elsewhere on the application, typically we’d say something like:
<%= link_to @user.username, user_path(@user) %>
or, a shorter version:
<%= link_to @user.username, @user %>
Ordinarily, these link_tos
would generate something that looks like:
<a href="/3">eric</a>
This is because the default behavior is for an ActiveRecord object to use its id
when creating a URL param
.
If we wanted, we could override this behavior by feeding in @user.username
into our route helper — user_path(@user.username)
.
However, this isn’t recommended, because if our route changes in the future, we’d have to find and change a bunch of link_tos
. So best practice is to pass the full object into a route helper, not an attribute of the object.
Since we’re passing in the full object, we’ll need to specify that we want the helper to grab the username
and not the id
.
In our User
model:
# user.rb
def to_param
username
end
Adding this method just overrides the default behavior for creating URL params from route helpers.
Now, with our route helper user_path(@user)
, we’ll get a link that looks something like:
<a href="/eric">eric</a>
Which is what we need!
Conclusion
That’s all for now.