How to create vanity URLs in Rails with Devise

Coverlist originally was built to be a place where readers could recommend books, but along the way it veered into being a place for authors to showcase their books and also showcase the blurbs other authors were writing about their books. These authors would need to be able to easily promote their own profile page, and one of the most important features I would need to add to the app would be vanity URLs.

Nobody shares coverlist.com/users/dani, but they can easily share coverlist.com/dani.

I read a couple explainers – it’s fairly easy to get a descriptive URL based on the object name, there’s a gem for vanity URLs that can help you and a wonderful, albeit based on an outdated version or Rails, long description from Scribd on how to build the functionality of top-level Rails social profile URLs.

It turns out, the process was easier than the information I found had let on. 

First, if you’re building user profiles with /username, or top-level URLs, each user needs to have an attribute that is unique and lowercase and without punctuation. I’m using Devise and users sign in with their email and password, but they also are required to make a “name” which must be unique. 

In my user.rb:

validates :name, :uniqueness => { :case_sensitive => false }, format: { with: /A[a-zA-Z0-9]+Z/, message: “No spaces or special characters, please!” }

before_validation :downcase_name

If someone registers with “Dani,” I don’t want someone to come and register with “dani,” because browsers will read it as the same. 

After that, you want Rails to serve the User show view when it sees coverlist.com/dani. Right now it’s responding to coverlist.com/user/25. First, you need it to look for your “name” attribute, not the user ID.

Also in user.rb:

def to_param
  self.name
  name
end

Then, in users_controller.rb:

def show
  # comment out # user profiles by id
  # comment out # @user = User.find(params[:id])

  # user profiles by user name
  @user = User.find_by_name!(params[:id])

  respond_to do |format|
  format.html # show.html.erb
  format.xml { render :xml => @user }
end
end

Now, your coverlist.com/users/dani should be working. But you want it to be top-level.

Go into config/routes.rb.

Mine reads:

devise_for :users, :controllers => { :registrations => “registrations”, :passwords => “passwords” }
resources :users
match ’/:id’ => ‘users#show’, :as => :user

The order is important because the routes.rb file reads things in order. So, if you have a controller that matches a user’s name (as I have a controller called books, so coverlist.com/books will list all books), Rails knows to show the controller action, not the user profile.

That said, you don’t want users to register with a name that you do or plan to create a controller with. To prevent that, you can add this line, in user.rb, to the end of your validates name, listing any possible conflicts:

, :exclusion => %w(about blog application books likes pages passwords profiles recommendations registrations users manuscripts stories jobs plans account admin signin signout signup help new popular shop tour)

That should be it. Now you have pretty, shareable user profile URLs!