Setting up your custom URL shortener (incl. statistics) with CodeIgniter

There are now numerous short URL services (also known as URL shorteners) on the Internet – but what exactly do they do, and why do we actually need them? As the name suggests, these services provide a short URL for any web address. What this means is that you can submit any web address to the service provider and they will give you back a shortened version of it.

This is particularly useful in allowing you to send friends or relatives a short link instead of an extremely long one. Many social media outlets also limit the number of characters per message, and these short URL services enjoy an important role here too as a way of getting around certain limits. The most commonly used short URL service in the world at present is the US-run Bit.ly.

Below I will show you how easily you can build a service like this for yourself using the CodeIgniter framework:

HOW DOES A SHORT URL SERVICE WORK IN PRACTICE?

Virtually all short URL providers have services roughly based on the same principle:

On the provider’s home page there is usually a text field where you can enter your original URL, from which a unique short URL is then generated. Anyone who attempts to visit this short URL is then instantly redirected to the respective destination address – i.e. the address which was entered when generating the URL. In addition to this, many providers also supply statistics about access etc.

CODEIGNITER SETUP

In the following section, I will show you how the CodeIgniter setup works.

Prepare the framework

In order to get started with our short URL service, we first need a foundation on which we can build.

To do so, download the latest version of the framework from the CodeIgniter homepage. This is currently version 3.1.5.

Next, copy all the files, ideally to the subfolder “ci-urlshortener” on the web server (e.g. XAMPP, LAMP, etc.), and open its root directory in your browser, e.g. http://localhost/ci-urlshortener/.

Once all of the requirements for the framework are met, you should then see the CodeIgniter welcome page.

To ensure that the speaking URLs can also be accessed correctly, create a .htaccess file with the following content:

RewriteEngine on

RewriteRule ^(assets)($|/) - [L]
RewriteRule .* index.php

If necessary, an optional environment variable can be specified here to distinguish between a development system (“development”), test system (“testing”) or production system (“production”).

This would look as follows:

SetEnv CI_ENV development

 

Next, open /application/config/autoload.php (the autoloader configuration file) and add the URL and form helper along with the database library. We will need these components later.

$autoload['libraries'] = array('database');
$autoload['helper'] = array('url', 'form');

 

In /application/config/routes.php (the route configuration file), we now need to change the default controller from “welcome” to “home” since we want our URL shortener application to be loaded by default later.

We will also define two additional routes here: one for redirects (as a catch-all) and one for statistics.

$route['default_controller'] = 'home';
$route['404_override'] = '';
$route['translate_uri_dashes'] = FALSE;

$route['(:any)/stats'] = 'home/stats/$1';
$route['(:any)'] = 'home/redirect/$1';

The redirect route (catch-all) is deliberately defined last, otherwise it would intercept all other routes and so routing would not work properly.

CREATE INDEX PAGE AND STORE URLS

Having referred to a “home” controller in the route configuration, we also need to create one. To do so, navigate to /application/controller and create a file named Home.php (case-sensitive!).

In this file we now need to create the basic framework of 3 actions: one for the index page, one for the redirect itself, and one for the statistics.

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Home extends CI_Controller {


	public function __construct()
	{
		parent::__construct();
	}
	
	
	public function index()
	{
		
	}
	
	
	public function redirect( $alias )
	{
		
	}
	
	
	public function stats( $alias )
	{
		
	}
}

Later on, we will load our models in the constructor above to avoid having to do this separately for every single action.

Now we have our controller defined along with the necessary actions.Next we create our database tables as follows:

CREATE TABLE `urls` ( 
	`id` INT NOT NULL AUTO_INCREMENT , 
	`url` VARCHAR(255) NOT NULL , 
	`alias` VARCHAR(100) NOT NULL , 
	`created` DATETIME NOT NULL , 
	PRIMARY KEY (`id`), 
	INDEX (`alias`)
) ENGINE = InnoDB;


CREATE TABLE `statistics` ( 
	`id` INT NOT NULL AUTO_INCREMENT , 
	`url_id` INT NOT NULL , 
	`created` DATETIME NOT NULL , 
	PRIMARY KEY (`id`), 
	INDEX (`url_id`)
) ENGINE = InnoDB;

This gives us one table for redirects (storing URLs with associated aliases) and one for statistics (logging each time a particular URL is accessed).

Next we will create a model for the URL table. To do so, navigate to /application/models and create a new file named Url_model.php.

We need the following functions here: the ability to add a new URL (with add_url()) and to retrieve a URL by ID (with get_url_by_id()) or alias (with get_url()).

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Url_model extends CI_Model {
	
	
	function add_url( $url )
	{
		// build up data array
		$data = array(
			'url'		=> (string) $url,
			'alias'		=> (string) uniqid(), // creates a random key
			'created'	=> date('Y-m-d H:i:s'),
		);
		 
		// inserts the data into database
		$this->db->insert('urls', $data);
		
		// return this ID of the new inserted record
		return $this->db->insert_id();
	}
	
	
	public function get_url_by_id( $id )
	{
		$this->db->select('*');
		$this->db->from('urls');
		$this->db->where('id', (int) $id);
		
		$result = $this->db->get()->row_object();

		// check if the requested record was found
		if (count($result) > 0) {
			return $result;
		} else {
			return FALSE;
		}
	}
	
	
	public function get_url( $alias )
	{
		$this->db->select('*');
		$this->db->from('urls');
		$this->db->where('alias', (string) $alias);
		
		$result = $this->db->get()->row_object();

		// check if the requested record was found
		if (count($result) > 0) {
			return $result;
		} else {
			return FALSE;
		}
	}


}

The get_url_by_id() and get_url() functions are identical except for retrieving different database fields.

Once we have our primary model for the URLs, we load it as follows in the constructor for our home controller:

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Home extends CI_Controller {


	public function __construct()
	{
		parent::__construct();
		
		// load models
		$this->load->model('Url_model');
	}

Now we can tackle the next step: creating our index page.

To do so, we will define a data array with a few default values, and define logic for what should happen when a POST request arrives – in our case, a new URL should be created here with an alias:

	public function index()
	{
		$data = array(
			'error' => false,
			'show_details' => false,
		);
		$post = $this->input->post(NULL, TRUE);

		// check if request method was 'post' - if yes, then try to create short url
		if($post){
			$url = $post['url'];
			
			// validate url
			if (filter_var($url, FILTER_VALIDATE_URL)) {
				
				$id = $this->Url_model->add_url( $url );
				
				$url_data = $this->Url_model->get_url_by_id( $id );
				
				$data['show_details'] = true;
				$data['url_data'] = $url_data;
				
				
			} else {
				$data['error'] = "Invalid URL!";
			}
			
		}
		
		// load view and assign data array
		$this->load->view('home', $data);
	}

In this example code, there is an additional PHP validation which checks that the URL submitted by the user is in fact a valid URL.

If not, there could be unexpected side-effects during the redirect itself.

Once the new URL has been stored in the database, this record is retrieved from there and passed to our view – after all, we want to show the user what their alias for the submitted URL looks like.

In order to do this, however, we still need the associated home view (based on the Bootstrap framework) in a file named /application/views/home.php:

<?php defined('BASEPATH') OR exit('No direct script access allowed'); ?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<meta name="description" content="">
		<meta name="author" content="">

		<title>CI URL-Shortener</title>

		<!-- Latest compiled and minified CSS -->
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

		<!-- Custom CSS for basic styling -->
		<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/style.css" />
		
		<!-- Optional CSS for individual theming (powered by Bootswatch - https://bootswatch.com/) -->
		<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/bootstrap_flatly.min.css" />

		<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
		<!--[if lt IE 9]>
		  <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
		  <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
		<![endif]-->
		</head>
	<body>

	<nav class="navbar navbar-default navbar-fixed-top">
		<div class="container">
			<div class="navbar-header">
				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
					<span class="sr-only">Toggle navigation</span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
				</button>
				<a class="navbar-brand" href="#">CodeIgniter URL-Shortener</a>
			</div>
			<div id="navbar" class="collapse navbar-collapse">
				<ul class="nav navbar-nav">
					<li class="active"><a href="<?php echo base_url(); ?>">Home</a></li>
					<li><a href="#about">About</a></li>
					<li><a href="#contact">Contact</a></li>
				</ul>
			</div>
		</div>
	</nav>

	<div class="container">

		<div class="intro">
			<h1>My awesome URL shortener</h1>
			<p class="lead">
				Lorem ipsum dolor sit amet, consetetur sadipscing elitr, <br/>
				sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, <br/>
				sed diam voluptua.
			</p>
		</div>
		
		<?php echo form_open(); ?>
			<div class="row">
				<div class="col-lg-8 col-lg-offset-2">
					<div class="input-group">
						<input type="url" name="url" class="form-control" placeholder="Enter your URL here..." autocomplete="off" />
						<span class="input-group-btn">
							<button class="btn btn-default" type="submit">Go!</button>
						</span>
					</div>
					
					<?php if( $error ): ?>
					<p class="text-danger"><?php echo $error; ?></p>
					<?php endif; ?>
				</div>
			</div>
		<?php echo form_close(); ?>
		
		<?php if( $show_details ): ?>
		<div class="well url_data">
			<h2>Your new created Short-URL:</h2>
			
			<div class="input-group">
				<input type="text" value="<?php echo base_url( $url_data->alias ); ?>" class="form-control input-lg" readonly="readonly" />
				<span class="input-group-btn">
					<a href="<?php echo base_url( $url_data->alias ); ?>" class="btn btn-default btn-lg" target="_blank">
						<span class="glyphicon glyphicon-share-alt" aria-hidden="true"></span>
					</a>
				</span>
			</div>
			
			<p>&nbsp;</p>
			<p>You can view statistics for this URL here: <a href="<?php echo base_url( $url_data->alias .'/stats' ); ?>" target="_blank"><?php echo base_url( $url_data->alias .'/stats' ); ?></a></p>
			
		</div>
		<?php endif; ?>

	</div>


	<!-- Bootstrap core JavaScript
	================================================== -->
	<!-- Placed at the end of the document so the pages load faster -->
	<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
	<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
	</body>
	</html>

The linked bootstrap_flatly.min.css file is the free “Flatly” Bootstrap theme from Bootswatch.

This CSS theme is optional and can therefore be left out if you prefer.

In the style.css file, we will define only a few basic styles:

body {
	padding-top: 50px;
}
.intro {
	padding: 40px 15px;
	text-align: center;
}

.intro h1 {
	margin-bottom: 40px;
}

.url_data {
	margin-top: 60px;
	text-align: center;
}

.url_data h2 {
	font-size: 25px;
}

The home view itself merely provides a form to submit a URL, and an area below to display the generated alias.

Now we can test our application for the first time by entering any URL into the form and submitting it.

We should now be able to find this URL in the database along with an alias, which should be displayed to us on the index page underneath the form.

 

CONFIGURE REDIRECT

Because we also want to analyze the requests received by our system instead of simply redirecting to the destination URL, we will now create a model for the statistics table. To do so, navigate to /application/models and create a new file named Statistics_model.php with the following content:

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Statistics_model extends CI_Model {
	
	
	function add_log( $url_id )
	{
		// build up data array
		$data = array(
			'url_id'		=> (int) $url_id,
			'created'	=> date('Y-m-d H:i:s'),
		);
		 
		// inserts the data into database
		$this->db->insert('statistics', $data);
		
		// return this ID of the new inserted record
		return $this->db->insert_id();
	}
	
	
	public function get_logs( $url_id )
	{
		$this->db->select( array('*', 'COUNT(id) AS sum') );
		$this->db->from('statistics');
		$this->db->where('url_id', (int) $url_id);
		$this->db->group_by('DATE_FORMAT(created, "%m-%y-%d")');
		$this->db->order_by('YEAR(created) ASC, MONTH(created) ASC, DAY(created) ASC');
		
		$result = $this->db->get()->result_object();

		// check if the requested record was found
		if (count($result) > 0) {
			return $result;
		} else {
			return FALSE;
		}
	}
	

}

The functions here are limited to creating a new entry for a particular URL (with add_log()), and retrieving all entries for a URL (with get_logs()).

In the latter case, entries are grouped by date and the total number of times the URL was accessed per day is returned as a sum.

Load this model in the home controller constructor in the same way as the URL model:

<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class Home extends CI_Controller {


	public function __construct()
	{
		parent::__construct();
		
		// load models
		$this->load->model('Url_model');
		$this->load->model('Statistics_model');
	}

Having defined an empty “redirect” action earlier, it is now time to give this a function too.

The parameter passed on as $alias is the one that was previously generated when the URL was stored.

This is now used to search the database for the desired entry with the help of the get_url() function we created before.

	public function redirect( $alias )
	{
		$url_data = $this->Url_model->get_url( $alias );
		
		// check if there's an url with this alias
		if(!$url_data){
		
			header("HTTP/1.0 404 Not Found");
			$this->load->view('not_found');
		
		}else{
			
			$this->Statistics_model->add_log( $url_data->id );
			
			header('Location: ' . $url_data->url, true, 302);
			exit();
		}
		
	}

When the appropriate database entry has been found, we can carry out the actual redirect using the location header.

(The “302” at the end indicates the accompanying HTTP status code. Of course, this would usually be a “301” (preferable in production systems from an SEO perspective) – but since redirects with a 301 header are cached by the browser, this would be highly impractical for testing, so in this case it is “302”.)

Before this redirect, however, we still want to our statistics model to store a record of access to the URL.

It is therefore important that there is no other output after the location header (or generally after a header output) – so we will add an exit() afterwards as a precaution.

In the event that no database entry is found for an alias, we simply refer to a “not found” view:

<?php defined('BASEPATH') OR exit('No direct script access allowed'); ?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<meta name="description" content="">
		<meta name="author" content="">

		<title>CI URL-Shortener</title>

		<!-- Latest compiled and minified CSS -->
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

		<!-- Custom CSS for basic styling -->
		<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/style.css" />
		
		<!-- Optional CSS for individual theming (powered by Bootswatch - https://bootswatch.com/) -->
		<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/bootstrap_flatly.min.css" />

		<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
		<!--[if lt IE 9]>
		  <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
		  <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
		<![endif]-->
		</head>
	<body>

	<nav class="navbar navbar-default navbar-fixed-top">
		<div class="container">
			<div class="navbar-header">
				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
					<span class="sr-only">Toggle navigation</span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
				</button>
				<a class="navbar-brand" href="#">CodeIgniter URL-Shortener</a>
			</div>
			<div id="navbar" class="collapse navbar-collapse">
				<ul class="nav navbar-nav">
					<li><a href="<?php echo base_url(); ?>">Home</a></li>
					<li><a href="#about">About</a></li>
					<li><a href="#contact">Contact</a></li>
				</ul>
			</div>
		</div>
	</nav>

	<div class="container">

		<div class="intro">
			<h1>Whoops! Something went wrong...</h1>
			<p class="lead">
				Lorem ipsum dolor sit amet, consetetur sadipscing elitr, <br/>
				sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, <br/>
				sed diam voluptua.
			</p>
		</div>
		

	</div>


	<!-- Bootstrap core JavaScript
	================================================== -->
	<!-- Placed at the end of the document so the pages load faster -->
	<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
	<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
	</body>
	</html>

This view is stored in a file named /application/views/not_found.php. It can be customized, and in our case it has no particular function.

The short URL previously generated in the last test can now be tested – if we have done everything correctly, it will redirect to the destination URL and an access log entry will be stored in the statistics table.

 

SHOW STATISTICS

At first glance, the “stats” action looks like the “redirect” action. This is due to them working in a similar way: both carry out a database search using a transferred alias.

The “redirect” action results in a redirect, and the “stats” action uses this data to retrieve all existing log entries for this URL.

	public function stats( $alias )
	{
		$url_data = $this->Url_model->get_url( $alias );
		
		// check if there's an url with this alias
		if(!$url_data){

			header("HTTP/1.0 404 Not Found");
			$this->load->view('not_found');

		}else{
			
			$logs = $this->Statistics_model->get_logs( $url_data->id );
			
			$data = array(
				'url_data'	=> $url_data,
				'logs'		=> $logs,
			);
			
			$this->load->view('stats', $data);

		}
		
	}

Once again, our “not found” view from above is used in the event that no database entry is found for a particular alias.

The associated view (/application/views/stats.php) would then look as follows:

<?php defined('BASEPATH') OR exit('No direct script access allowed'); ?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<meta name="description" content="">
		<meta name="author" content="">

		<title>CI URL-Shortener</title>

		<!-- Latest compiled and minified CSS -->
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

		<!-- Custom CSS for basic styling -->
		<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/style.css" />
		
		<!-- Optional CSS for individual theming (powered by Bootswatch - https://bootswatch.com/) -->
		<link rel="stylesheet" href="<?php echo base_url(); ?>assets/css/bootstrap_flatly.min.css" />

		<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
		<!--[if lt IE 9]>
		  <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
		  <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
		<![endif]-->
		</head>
	<body>

	<nav class="navbar navbar-default navbar-fixed-top">
		<div class="container">
			<div class="navbar-header">
				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
					<span class="sr-only">Toggle navigation</span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
				</button>
				<a class="navbar-brand" href="#">CodeIgniter URL-Shortener</a>
			</div>
			<div id="navbar" class="collapse navbar-collapse">
				<ul class="nav navbar-nav">
					<li><a href="<?php echo base_url(); ?>">Home</a></li>
					<li><a href="#about">About</a></li>
					<li><a href="#contact">Contact</a></li>
				</ul>
			</div>
		</div>
	</nav>

	<div class="container">

		<div class="intro">
			<h1>Statistics for URL</h1>
			<p class="lead">
				Lorem ipsum dolor sit amet, consetetur sadipscing elitr, <br/>
				sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, <br/>
				sed diam voluptua.
			</p>
		</div>
		
		<div class="well">		
			<h3><?php echo $url_data->url; ?></h3>
			<p class="help-block">Created: <?php echo $url_data->created; ?></p>
		</div>

		
		<?php if($logs): ?>
		
		<table class="table table-striped">
			<thead>
				<tr>
					<th>Date</th>
					<th>Number of clicks</th>
				</tr>
			</thead>
			<tbody>
				<?php foreach($logs as $log): ?>
				<tr>
					<th><?php echo date('Y-m-d', strtotime( $log->created )); ?></th>
					<th><?php echo $log->sum; ?></th>
				</tr>
				<?php endforeach; ?>
			</tbody>
		</table>
		
		<?php else: ?>
		
		<div class="alert alert-warning">
			<p>Unfortunately, there're no statistics available.
		</div>
		
		<?php endif; ?>
				

	</div>


	<!-- Bootstrap core JavaScript
	================================================== -->
	<!-- Placed at the end of the document so the pages load faster -->
	<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
	<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
	</body>
	</html>

We iterate over all existing log entries for it in a foreach loop and show them in table format.

If preferred, this data can also be displayed as a diagram. There are numerous jQuery plugins for this on the Internet.

In the event that there are no log entries (if, for example, the short URL has never been accessed), a note to this effect will be shown.

 

CONCLUSION

Short URL services are very simple in structure. Their use is not always simple, however, and they can also be misused since end users cannot clearly see where they will be redirected.

Although some major providers already have quite good protection against malware and phishing, security remains controversial despite its relatively high standards.

The possible tracking option (limited in this tutorial to logging access) can also be expanded at will – e.g. to include capturing a user’s IP (if possible, it would be advisable for privacy reasons not to store the last part of the IP or to anonymize it), and as a result their country (using GeoIP) or any referrers, etc.

The analysis of such data is especially important in marketing, as this allows advertising campaigns etc. to be targeted more specifically and made more cost-effective. For this reason, these short URL services are of particular interest to businesses. Short URLs of this kind are used in things like newsletters, for example, to find out how often a link was clicked. ;)