One of the neat things that Dealush allows it’s users to do is to automatically tweet sales when they are listed. A key part of the tweet is a link back to the sale listing on Dealush. If your have heard of twitter you probably know that one of the distinctive features of twitter is that each message can be no more than 140 chars. Now a typical sale listing on Dealush has a URL that looks like this:
Not exactly short now is it? It’s actually 133 chars long! That won’t leave much room in the tweet for a decent description of the sale. The common solution to this problem is to use a link shortening service like Bit.ly or Is.gd, but I wanted people to see “Dealush.com” in the tweet so I decided to roll my own link shortener.
The majority of the solution consists of two parts:
- a model for storing the details of the shortened link (including the user the shortened link belongs to and counter that increments as the link is clicked);
- a controller to accept incoming requests, grab the shortened link data out of the database and redirecting the visitors request to the target URL;
Some niceities of the solution:
- The controller does a 301 redirect, which is the recommended type of redirect for maintaining maximum google juice to the original URL;
- A unique code of is generated for each shortened link, instead of using the id of the shortened link record. This means that we can get more unique combinations than if we just used numbers;
- The link records a count of how many times it has been “un-shortened”;
- The link can be linked to a user, this allows for stats of the link usage for a particular user and other interesting things;
- The controller spawns a new thread to record information to the database, allowing the redirect to happen as quickly as possible;
Some suggested improvements:
- There has not been an attempt to remove ambiguous characters (i.e. 1 l and capital i, or 0 and O etc.) from the unique key generated for the link. This means people might copy the link incorrectly if copying the link by hand;
- The shortened links are found with a case-insensitive search on the unique key. This means that the system can’t take advantage of upper and lower case to increase the number of unique combinations. This may have an effect for people copying the link by hand;
- The system could pre-generate unique keys in advance, avoiding the database penalty when checking that a newly generated key is unique;
- The system could store the shortened URL if the url is to be continually rendered;
- Some implementations might want duplicate links to be generated each time a user requests it.
Anyway, I am pretty happy with solution as is. Dealush doesn’t need a super scalable link shortener as our needs are modest when compared to dedicated link shorteners. Also this solution can be extended as I need and best yet, it was FUN!
The code is below, and is released under a MIT licence.
The model (shortened_link.rb):
class ShortenedLink < ActiveRecord::Base
# this won't please the MVC pedants, but it makes sense
# to access the url writer from the model since the
# model is dealing with the generation of a url
include ActionController::UrlWriter
belongs_to :user # allows the shortened link to be associated with a user
# this is best placed in your env files so you can have localhost for dev
HOST_NAME = 'dealush.com'
UNIQUE_KEY_LENGTH = 5
# it can be useful to easily know how long the shortened link will be
# the start of the link for dealush is http://dealush.com/s/ (21 chars)
# change the number as needed for your site
LENGTH = 21 + UNIQUE_KEY_LENGTH
# generate a shortened link from a url
# link to a user if one specified
def self.generate(orig_url, user=nil)
# don't want to generate the link if it has already been generated
# so check the datastore
uid = user.nil? ? nil : user.id
sl = ShortenedLink.find_by_url_and_user_id(orig_url, uid)
return sl.shortened_url if sl
# generate a unique key for the link
begin
# has about 50 million possible combos
unique_key = self.generate_random_string(UNIQUE_KEY_LENGTH)
end while ShortenedLink.find_by_unique_key unique_key
# create the shortened link, storing it
sl = ShortenedLink.create(:url => orig_url, :unique_key => unique_key, :user => user)
# return the url
return sl.shortened_url
end
# create the shortened url from the unique key
def shortened_url
# use the url writer to generate the url
return link_translate_url(:unique_key => self.unique_key, :host => HOST_NAME)
end
# generate a random string
def self.generate_random_string(size = 6)
# not doing uppercase as url is case insensitive
charset = ('a'..'z').to_a + (0..9).to_a
(0...size).map{ charset.to_a[rand(charset.size)] }.join
end
end
The controller (shortened_links_controller.rb):
class ShortenedLinksController < ApplicationController
# find the real link for the shortened link key and redirect
def translate
sl = ShortenedLink.find_by_unique_key(params[:unique_key])
if sl
# don't want to wait for the increment to happen, make it snappy!
Thread.new do
# this is the place to enhance the metrics captured
# for the system. You could log the request origin
# browser type, ip address etc.
sl.increment!(:use_count)
end
# do a 301 redirect to the destination url
head :moved_permanently, :location => sl.url
else
# if we don't find the shortened link, redirect to the root
head :moved_permanently, :location => root_url
end
end
end
The migration (XXXXXX_create_shortened_links.rb):
class CreateShortenedLinks < ActiveRecord::Migration
def self.up
create_table :shortened_links do |t|
t.integer :user_id # we can link this to a user for interesting things
t.string :url, :null => false # the url this shortened link points to
t.string :unique_key, :null => false # a unique identifier for the link
t.integer :use_count, :null => false, :default => 0 # how many times a link has been accessed
t.timestamps
end
add_index :shortened_links, :unique_key
add_index :shortened_links, :user_id
end
def self.down
remove_index :shortened_links, :unique_key
remove_index :shortened_links, :user_id
drop_table :shortened_links
end
end
routes.rb
map.link_translate "/s/:unique_key", :controller => "shortened_links", :action => "translate"