Eric / Brooklyn

Visual

Blog Index

Random 5

Stripe and Tap

Multi-tab Websockets

Ruby vs. Ruby

Cookies

Web Accounts

Emoji Bits

Honeypot Bots

Bad Content

JS Data Chutzpah

Sendgrid Ban

Clean URLs

Git

Concerns

Rails UJS

Extending Devise

ENV Variables

See All

Clean User URLs

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