Handling Vanity URLs & Legacy Routes in Rails 3.2

Handling Vanity URLs is not a new problem. Website Owners want their URLs to be simple or catchy or elegant. For marketing purposes, they may need to be short(ish) and possibly temporary. Imagine a URL like


which in April points to /parties/april, then May points to nothing (May’s a dull month) and then in June it points to /parties/june. We need something that acts sort of like Rack::Rewrite but that looks into an easily editable chunk of data (read: database) for the list of paths and redirects.

If you comb the web (as I did) looking for Vanity URL solutions, most of them solve the issue for pretty URLs for a given Model/Controller pair. They solve the issue that numeric IDs are not informative, but words with hyphens can be more informative. If that is the problem you’re hoping to solve here, read no further. Go straight to FriendlyID (or similar).

If, instead, you’re trying to build a simple redirect/route-fallback system that allows for root level temporary Vanity URLs that can be managed within the running application (not by a static configuration file), then you’ve come to the right place.

First, we need a place to store the paths and redirects. Let’s start by adding a simple VanityUrl model

class VanityUrl < ActiveRecord::Base
  attr_accessible :active, :path, :url
  validates :url, link: true, presence: true
  validates :path, presence: true, uniqueness: true
  class << self
    def active
      where(active: true)

    def routes
      Hash[VanityUrl.active.map{|v| [v.path, v.url]}]

path is the path we will be watching for

url is the url we’ll redirect to, if we hit that path

active is a boolean (in case we want to easily turn the redirect off or on).

Looking ahead a bit, we’ve added an #active scoping method and #routes which returns all active VanityUrls. You’ll see where this is used shortly.

Exercise for the reader

Build controller and views to show/list/edit these guys in your application.

Now we want these to hit, only if other routes have not been matched. So at the end of our config/routes.rb file, we add a new catch-most route.

# in config/routes.rb

MyApp::Application.routes.draw do
  # your apps routes are defined here
  # then way down at the end... 
  # as the last defined route

  # this routes to the Vanity controller which handles the actual redirect
  # format:false allows us to recognize paths like '/this/old/page.htm
  match "/*vanity", to: 'vanity#routing', constraints: RoutingConstraints::VanityUrls.new, format: false

This last line is catch-most because it will only match if the constraint defined in RoutingConstraints::VanityUrls is hit. And with format: false is telling the router to ignore the any extension (as the comment says – so we can match page.htm if need be).

The RoutingConstraints::VanityUrls object needs to have a #matches? method (more in the Rails Routing Docs). So we can add the following:

# in app/lib/routing_constraints.rb
module RoutingConstraints
  class VanityUrls
    def matches?(request)
      routes = VanityUrl.routes
      vanity_url = request.params['vanity']
      if routes.has_key? vanity_url
          return true
        rescue URI::InvalidURIError
          # vanity url is no good
          return false

Notice here, we’re using the VanityUrl::routes method. Now, as routes are processed by Rails, *IF* they don’t match any of our normal routes *AND* they match one of our VanityURLs by path, *THEN* the router will pass them off to the VanityController#routing method which we told it to do in the routes.rb file.

Let’s add that method:

class VanityController
  def routing
    vanity_routes = VanityUrl.routes
    vanity_url = params['vanity']
    if vanity_routes.has_key? vanity_url
      redirect_to URI.parse(vanity_routes[vanity_url]).to_s and return
    raise "Unable to find requested VanityUrl for #{params}"

Notice, this is not the VanityUrlController which you’ve written to handle VanityUrls in the database. We make a new controller which will only have this one method to handle the rerouting of requests.

That’s basically it. If you’re app gets a lot of traffic, you’ll probably want to cache the VanityUrl collection (since it should be read-often and write-occasionally). Without a cache, every request is doing a full table retrieval of the VanityUrl table.

Initially, we added this feature to support true VanityUrls for a client e.g. http://example.com/party, http://example.com/temporary_coupon etc. And for this purpose, it works great. But it also saved our butts in a way we hadn’t forseen. The application we built was replacing an existing site almost entirely. When we actually launched, we starting getting piles of 404’s. The bits of the old site that were still up were asking for resources, images, stylesheets etc that were not duplicated in the new site. With a handful of VanityUrls, we were able to point those misdirected requests to the old assets (or wherever they needed to go) without modifying code or adding Rack::Rewrite directives. It made launch day a relative breeze.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s