You have all seen those quirky little URL shortener websites (goo.gl, bitly, tinyurl, etc…) but have you ever wondered why they exist, how they work, or how to make one? In this series, I am going to walk you through the details behind their existence and how they work. Following that, I will show you how I made a URL shortener using PHP, MySQL, and a bit of HTTP magic.
First, what is a URL shortening?
URL shortening is a technique on the World Wide Web in which a Uniform Resource Locator (URL) may be made substantially shorter in length and still direct to the required page. – Wikipedia
And now you ask, But why do we need such a thing? The simple answer is: we don’t, but it does make certain things easier. For example, Twitter has a very limiting message length of 140 character. Similarly, SMS messages sent from your mobile phone have a fixed length of 160 characters. As an example, lets say that you wanted to share this link on twitter: “http://www.engadget.com/2014/04/25/us-judge-search-warrant-overseas/?ncid=rss_truncated“, that link is 87 characters long, leaving only 53 characters for your message. If you were to take that URL and shorten it using one of the many shortening services out there, you’d get something like this: “http://shrtnr.net/p“. Much shorter! At only 19 characters, that leaves 121 characters left for your Twitter message.
In the next entry of this series, I will cover how URL shorteners work.
Continue onto Part 2: The mechanism ↝In the previous entry, we covered the idea behind a URL shortener and why we want them. In this entry, I go over how URL shorteners actually work.
URL shorteners make use of a preexisting functionality of the hyper-text transfer protocol (HTTP). When a browser makes a request to a server, say google.com, your computer is actually doing a lookup of where google.com resides and connects to its server. Once connected, the browser issues a request (usually a HTTP GET, but sometimes a POST). The server then processes the request and generally, replies with HTTP 200 “OK” followed by the content that your requested. You may be familiar with a few of the other possible responses: 404 – not found, or 500 – server error. There are many more errors codes in the protocol. Of particular interest to URL shorteners are HTTP 301 and 307. They are Moved Permanently and Moved Temporarily, respectively.
So what does this have to do with URL shorteners? They rely on the HTTP protocol to do most of the work. Lets explain this process by example.
A user uses shrtnr.net to shorten a url http://www.longassurl.com/somethingbig. The URL shortener gives you this URL http://shrtnr.net/xyz. Now, when your browser sends a GET request for http://shrtnr.net/xyz, the server will do a lookup on its database and reply with a 301 Moved Permanently response followed by the new URL which, in this case, is http://www.longassurl.com/somethingbig. The browser then sends another GET request to http://www.longassurl.com/somethingbig and that server responds with the content that you were looking for. Check out the flowchart below for a graphical representation of the process flow.
In the next entry of this series, I will cover how we actually implement this process using PHP, MySQL and Apache.
Continue onto Part 3: The implementation ↝In the past two entries of this series, I covered why we want URL shorteners and how they work. In this article, I will show you how I made my URL shortener and give you the source code so that you can experiment with one for yourself.
First, a demo. Take a look and feel free to play with my URL shortener website shrtnr.net. My URL shortener is powered by PHP, MySQL, and some fancy Apache directives.
Second, I have to give credit where credit is due:
Last thing before we get into the code.
Now, lets get into the code. I will break the code up into two parts 1) backend and 2) frontend. The backend is responsible for all of the DB interaction, processing user requests, and returning a response to the user. It is entirely written using PHP. The frontend is responsible for giving the user a nice pretty interface for creating short URLs. IT is written primarily in HTML & CSS but also has some javascript in there to make everything work. We make use of the excellent jQuery framework to make the javascript much easier to handle.
The first thing that we have to do is setup the database backend. Every hosting provider has a different method of attaching a database to your account so I will not go over that. Once you have a database, you need to create a table to store the shortened URLs. The code below can be used to create the needed table using MySQL.
CREATE TABLE IF NOT EXISTS `shortenedurls` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`long_url` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime NOT NULL,
`creator` varchar(20) COLLATE utf8_unicode_ci NOT NULL,
`referrals` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
The backend code is broken up in to three different files, each with a different purpose:
<?php
/*
* Authored by Jonathan Mash http://jonmash.ca/
* License: http://creativecommons.org/licenses/by-sa/4.0/
* Original inspiration came from Brian Cray (http://briancray.com/)
*/
// db options
define('DB_NAME', 'dbname');
define('DB_USER', 'dbuser');
define('DB_PASSWORD', 'dbpass');
define('DB_HOST', 'localhost');
define('DB_TABLE', 'shortenedurls');
// base location of script (include trailing slash)
define('BASE_HREF', 'http://' . $_SERVER['HTTP_HOST'] . substr($_SERVER['REQUEST_URI'], 0, strrpos($_SERVER['REQUEST_URI'], '/') + 1));
// change to TRUE to start tracking referrals
define('TRACK', TRUE);
// change the shortened URL allowed characters
define('ALLOWED_CHARS', '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
?>
<?php
/*
* Authored by Jonathan Mash http://jonmash.ca/
* License: http://creativecommons.org/licenses/by-sa/4.0/
* Original inspiration came from Brian Cray (http://briancray.com/)
*/
require('config.php');
$mysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
if ($mysqli->connect_errno) {
echo "Database Error";
exit();
}
if(isset($_POST['url']) && !empty($_POST['url']))
{
$url = $_POST['url'];
$url = trim($url, '!"#$%&\'()*+,-./@:;<=>[\\]^_`{|}~');
if(filter_var($url, FILTER_VALIDATE_URL))
{
$res = $mysqli->query('SELECT id FROM ' . DB_TABLE. ' WHERE long_url="' . $mysqli->real_escape_string($url) . '"');
$already_shortened = $res->fetch_array();
if(!empty($already_shortened[0]))
{
// URL has already been shortened
$shortened_url = getShortenedURLFromID($already_shortened[0]);
}
else
{
// URL not in database, insert
$mysqli->query('LOCK TABLES ' . DB_TABLE . ' WRITE;');
$mysqli->query('INSERT INTO ' . DB_TABLE . ' (long_url, created, creator) VALUES ("' . $mysqli->real_escape_string($url) . '", NOW(), "' . $mysqli->real_escape_string($_SERVER['REMOTE_ADDR']) . '")');
$shortened_url = getShortenedURLFromID($mysqli->insert_id);
$mysqli->query('UNLOCK TABLES');
}
//Return it to the user
echo BASE_HREF . $shortened_url;
}
else
{
echo "Invalid URL: " . $url;
exit();
}
}
//This function converts the base 10 "ID" to the base X "ShortURL". The base is determined by the "ALLOWED_CHARS" array.
function getShortenedURLFromID ($integer, $base = ALLOWED_CHARS)
{
$length = strlen($base);
while($integer > $length - 1)
{
$out = $base[fmod($integer, $length)] . $out;
$integer = floor( $integer / $length );
}
return $base[$integer] . $out;
}
?>
<?php
/*
* Authored by Jonathan Mash http://jonmash.ca/
* License: http://creativecommons.org/licenses/by-sa/4.0/
* Original inspiration came from Brian Cray (http://briancray.com/)
*/
require('config.php');
$mysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
if ($mysqli->connect_errno) {
echo "Database Error";
exit();
}
if(!preg_match('|^[0-9a-zA-Z]{1,6}$|', $_GET['url']))
{
die('That is not a valid short url');
}
//Get the DB ID from the ShortID
$shortened_id = getIDFromShortenedURL($_GET['url']);
$res = $mysqli->query('SELECT long_url FROM ' . DB_TABLE . ' WHERE id="' . $mysqli->real_escape_string($shortened_id) . '"');
$long_url = $res->fetch_array();
$long_url = $long_url[0];
if(empty($long_url) || !isset($long_url) || $long_url == "")
{
//Unknown shortID, redirect to home page.
header('Location: ' . BASE_HREF);
exit;
}
//If tracking is enabled, increment the referral field.
if(TRACK)
{
$mysqli->query('UPDATE ' . DB_TABLE . ' SET referrals=referrals+1 WHERE id="' . $mysqli->real_escape_string($shortened_id) . '"');
}
//Notify the browser of the new location
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $long_url);
exit;
//This function takes the base X shortID and converters it to a base 10 database ID.
//The base X is defined by the number of characters in ALLOWED_CHARS.
function getIDFromShortenedURL ($string, $base = ALLOWED_CHARS)
{
$length = strlen($base);
$size = strlen($string) - 1;
$string = str_split($string);
$out = strpos($base, array_pop($string));
foreach($string as $i => $char)
{
$out += strpos($base, $char) * pow($length, $size - $i);
}
return $out;
}
?>
DirectoryIndex index.php
FileETag none
ServerSignature Off
Options All -Indexes
Options +FollowSymLinks
RewriteEngine On
RewriteBase /
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^(\w+)$ ./redirect.php?url=$1
The last part of our implementation is the frontend. The frontend code is broken into two different files: One file for the CSS styling and another for the HTML and javascript that describes the page layout.
/*
* Authored by Jonathan Mash http://jonmash.ca/
* License: http://creativecommons.org/licenses/by-sa/4.0/
* Original inspiration came from unknown source
*/
*, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }
body, html { font-size: 100%; padding: 0; margin: 0;}
/* Clearfix hack by Nicolas Gallagher: http://nicolasgallagher.com/micro-clearfix-hack/ */
.clearfix:before, .clearfix:after { content: " "; display: table; }
.clearfix:after { clear: both; }
body {
font-family: 'rw','Lato', Calibri, Arial, sans-serif;
color: #f5f5f5;
background: #7B6D3F;
text-align: center;
-webkit-appearance: none;
-webkit-animation: fadeIn 2.0s;
-moz-animation: fadeIn 2.0s;
-o-animation: fadeIn 2.0s;
animation: fadeIn 2.0s;
}
::selection {
background: #ffe327; /* Safari */
}
p{
text-align: center;
line-height: 22px;
font-family: 'Lato', Calibri, Arial, sans-serif;
font-weight: lighter;
}
h1{
font-size: 125px;
font-family: 'rw','Lato', Calibri, Arial, sans-serif;
font-weight: 400;
font-variant: small-caps;
letter-spacing: 2px;
}
h2{
font-size: 35px;
letter-spacing: 1px;
font-family: 'Lato', Calibri, Arial, sans-serif;
font-weight: 200;
font-variant: small-caps;
position: relative;
margin-top: -100px;
}
.ftr{
text-decoration: none;
color: whitesmoke;
padding:5px;
font-family: 'Lato', Calibri, Arial, sans-serif;
font-size: 12.9px;
border-bottom: 1px solid transparent;
opacity: 1;
-webkit-transition: all .2s ease-in-out;
-moz-transition: all .2s ease-in-out;
-ms-transition: all .2s ease-in-out;
-o-transition: all .2s ease-in-out;
}
.ftr:hover{
border-bottom: 1px solid whitesmoke;
color: rgba(245, 245, 245, 0.7);
-webkit-transition: all .2s ease-in-out;
-moz-transition: all .2s ease-in-out;
-ms-transition: all .2s ease-in-out;
-o-transition: all .2s ease-in-out;
}
a:hover{
opacity: .8;
-webkit-transition: all .2s ease-in-out;
-moz-transition: all .2s ease-in-out;
-ms-transition: all .2s ease-in-out;
-o-transition: all .2s ease-in-out;
}
input{
font-family: 'Lato', Calibri, Arial, sans-serif;
border:none;
border: 2px solid #16adc3;
width: 100px;
height: 20px;
color: #f5f5f5;
position: relative;
top: 50px;
border-radius: 0px;
}
input[type=text], textarea {
-webkit-transition: all 0.3s ease-in-out;
-moz-transition: all 0.3s ease-in-out;
-ms-transition: all 0.3s ease-in-out;
-o-transition: all 0.3s ease-in-out;
outline: none;
padding: 3px 0px 3px 3px;
margin: 5px 1px 3px 0px;
border: 1px solid #f5f5f5;
}
input[type=text]:focus, textarea:focus {
padding: 3px 0px 3px 3px;
margin: 5px 1px 3px 0px;
border: 1px solid whitesmoke;
}
input[type=submit], textarea {
-webkit-transition: all 0.005s ease-in-out;
-moz-transition: all 0.005s ease-in-out;
-ms-transition: all 0.005s ease-in-out;
-o-transition: all 0.005s ease-in-out;
outline: none;
padding: 3px 0px 3px 3px;
margin: 5px 1px 3px 0px;
border: 1px solid whitesmoke;
-webkit-appearance: none;
}
input[type=submit]:active {
padding: 3px 0px 3px 3px;
margin: 5px 1px 3px 0px;
border: 1px solid whitesmoke;
}
#afterreturn{
width: 100%;
height: 35px;
background-color: transparent;
max-width: 150px;
color: whitesmoke;
font-size: 14px;
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-ms-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
position: relative;
top: 50px;
text-align: center;
margin-left: auto;
margin-right: auto;
}
#button{
width: 100%;
height: 35px;
background-color: transparent;
max-width: 150px;
color: whitesmoke;
font-size: 14px;
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-ms-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
}
#button:active{
background-color: rgba(245, 245, 245, 0.3);
color: #7B6D3F;
-webkit-transition: all 0s ease-in-out;
-moz-transition: all 0s ease-in-out;
-ms-transition: all 0s ease-in-out;
-o-transition: all 0s ease-in-out;
}
#button:hover{
cursor: pointer;
background-color: rgba(245, 245, 245, 0.3);
}
#lngurl{
text-align: center;
width: 100%;
height: 50px;
background-color: transparent;
font-size: 16px;
max-width: 600px;
min-width: 300px;
}
#lngurl:focus{
background-color: rgba(245, 245, 245, 0.5);
color: #7B6D3F;
}
@-webkit-keyframes fadeIn {
from {opacity: 0; -webkit-transform: translateY(0px);}
to {opacity: 1; -webkit-transform: translateY(0);}
}
@-moz-keyframes fadeIn {
from {opacity: 0; -moz-transform: translateY(0px);}
to {opacity: 1; -moz-transform: translateY(0);}
}
@-o-keyframes fadeIn {
from {opacity: 0; -o-transform: translateY(0px);}
to {opacity: 1; -o-transform: translateY(0);}
}
@keyframes fadeIn {
from {opacity: 0; transform: translateY(0px);}
to {opacity: 1; transform: translateY(0);}
}
@media only screen and (max-width:450px){
#lngurl{
height: 40px;
width: 10px;
margin-left: 5px;
}
h1{
margin-top: -0px;
}
h2{
margin-bottom: -20px;
}
}
.twitter-share-button[style] { vertical-align: text-bottom !important; }
<!DOCTYPE html>
<html>
<title>SHRTNR - Shorten your URL</title>
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<meta name="description" content="Shorten your URL.">
<meta name="keywords" content="URL, shortener, shrtnr">
<meta name="author" content="Jonathan Mash">
<link href='http://fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="css/style.css" />
<body>
<form method="post" action="shrtn.php" id="shrtnr">
<h1>shrtnr</h1>
<h2>shorten your url</h2>
<input type="text" placeholder="http://" name="lngurl" id="lngurl" value="Shorten your URL"> <br />
<br />
<div id="afterreturn" style="display: none; text-align: center;"></div>
<input id="button" type="submit" value="Shrtn" >
</form>
<br /><br /><br /><br />
<a class="ftr" href="https://ca.linkedin.com/in/jonmash/" target="_blank">LinkedIn</a>-<a class="ftr" href="https://plus.google.com/114221103876090917638">Google+</a>-<a class="ftr" target="_blank" href="http://jonmash.ca">JM</a>
<br /><br /><br />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">
$(function () {
$('#shrtnr').submit(function (e) {
if($('#button').attr('disabled') != 'disabled' )
{
$.post( "shrtn.php", { url: $('#lngurl').val() }, function( data ) {
$( "#lngurl" ).val( data );
$('#button').hide();
$( "#lngurl" ).select();
$('#afterreturn').html('<div class="fb-share-button" data-href="'+ data +'" data-width="120" data-type="button"></div> — <a href="https://twitter.com/share" data-text="Check this out: " data-url="'+ data +'" class="twitter-share-button" data-lang="en" data-count="none">Tweet</a>');
$('#afterreturn').show();
FB.XFBML.parse();
twttr.widgets.load();
});
}
$('#button').attr('disabled','disabled');
e.preventDefault();
});
$('#lngurl').click(function (e) {
$('#lngurl').focus();
if($('#lngurl').val() == 'http://')
{
var tmpStr = $('#lngurl').val();
$('#lngurl').val('');
$('#lngurl').val(tmpStr);
}
});
$('#lngurl').blur(function (e) {
if($('#lngurl').val() == '')
{
$('#lngurl').val('Shorten your URL');
}
});
$('#lngurl').focus(function (e) {
if($('#lngurl').val() == 'Shorten your URL')
{
$('#lngurl').val('http://');
var tmpStr = $('#lngurl').val();
$('#lngurl').val('');
$('#lngurl').val(tmpStr);
}
});
});
</script>
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=561012490656421";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
</body>
</html>
That's just about it for now. If I make any significant improvements, I will list them here. You can find the latest version of this software on the SHRTNR GitHub page. If you have any questions, please feel free to contact me using the contact form on this site.
See anything that interested you? Have any questions about my projects? Shoot me an email!