Outbound link tracking is becoming popular from an SEO (Search Engine Optimization) standpoint. You can see this because a variety of statistics providers are now offering it. MyBlogLog, AddFreeStats, RiteCounter, BlogFlux and Google Analytics (with a special scriptlet) are just a few.
Why would you want to track outbound links? The exit link can tell you where your visitors go after leaving your website or blog, and it tells you which links on your site are most likely to take your visitors away. You could describe it as "outbound traffic". But it also tells you what on your site or blog is most interesting to the visitor, and that can be very valuable in building content and choosing keywords.
I decided to build a simple outbound link tracker script to experiment with, and so the first step was to search around for some javascript that could automatically enable every page to capture outbound link clicks. I found several, but the one I found most easy to customize is called "Clicky". Unfortunately, Clicky needed to be upgraded to support the Firefox browser (which I did) and it also needed to be souped up to provide automatic database inserts. The result is a nice script that can be included in your MasterPages which points a dynamicaly generated <img.. tag at an ASHX handler I whipped up to log the outbound clicks. In my particular scenario, I am more interested in counting "most popular" outbound clicks than in logging every single click, so my insert stored proc does an "insert or update" and increments the count column. This makes it very easy to have a report page that will give me a list of outbound clicks that can be ranked by popularity.
First, let's take a look at my customized Clicky.js include client script, and then I will show the ASHX handler codebehind:
var clicky_link_url_arg = "&link=";
var clicky_text_url_arg = "text=";
var clicky_stamp_url_arg = "&stamp=";
var clicky_image_load_pause_msecs = 500;
function clicky_getDomain(url) {
var i = url.indexOf("://");
if (i == -1) {
return "";
}
url = url.substring(i + 3, url.length);
i = url.indexOf("/");
if (i != -1) {
url = url.substring(0, i);
}
return url;
}
function clicky_handleError() {
return true;
}
function clicky_report_onclick(ev) {
var oldonerror = window.onerror;
window.onerror = clicky_handleError;
clicky_report_onclick_inner(ev);
window.onerror = oldonerror;
if (old_onclick) {
old_onclick(e);
}
}
function clicky_report_onclick_inner(ev) {
var text;
var url;
var target;
if(ev) {
target = ev.target;
}
else {
target = window.event.srcElement;
}
for (var i=0; target && i<=20; i++) {
if(target.href) {
break;
}
target = target.parentNode;
}
if (target && target.href) {
url = target.href;
if (target.innerHTML) {
text = target.innerHTML;
}
else if (target.innerText) {
text = target.innerText;
}
else if(target.text) {
text = target.text;
}
}
if (!url || typeof(url) != 'string' || url == "") {
return;
}
if (!text || typeof(text) != 'string' || text == "") {
text = "[no text found]";
}
var currentDomain = clicky_getDomain("" + window.location);
var currentBaseDomain = currentDomain;
if (currentBaseDomain.indexOf("www.") == 0) {
currentBaseDomain = currentBaseDomain.substring(3, currentBaseDomain.length);
}
var expressionStr = "(\\w+[.])*(" + currentBaseDomain + ")$";
var regex = new RegExp(expressionStr, "i");
if (text.length > 200) {
text = text.substring(0, 200) + "...";
}
if (!clicky_getDomain(url).match(regex)) {
var timeStamp = new Date();
var reportUrl = "http://" + currentDomain + "/ClickLog/Pix.ashx?" + clicky_text_url_arg + escape(text) + clicky_link_url_arg + escape(url) + clicky_stamp_url_arg + timeStamp.valueOf();
//alert(reportUrl);
var image = new Image();
image.src = reportUrl;
var now = new Date();
var stopTime = now.getTime() + clicky_image_load_pause_msecs;
while(now.getTime() < stopTime) {
now = new Date();
}
}
}
try{
window.captureEvents(Event.CLICK);
window.onclick = clicky_report_onclick
}
catch(e) {}
var old_onclick = document.body.onclick;
document.body.onclick = clicky_report_onclick;Sorry about the try /catch at the bottom, I just got real frustrated trying to wire up a document onclick handler for FireFox and took a shortcut! You want to include the above script at the very bottom of your page, and I'd recommend using the defer attribute to ensure that this script isn't processed until everything else in the page is loaded. So, a simplified script tag would look like so: ...
<SCRIPT src="clicky.js" defer="defer"></SCRIPT>
If you take a look at the script, you can see that it constructs a dynamic Image object that points to my ASHX Handler:
using System;
using System.Web;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace ClickLog
{
public class Pix : IHttpHandler
{
public bool IsReusable
{ get { return true; } }
public void ProcessRequest(HttpContext ctx)
{
string ip= String.Empty;
string referer=String.Empty;
string link = String.Empty;
string text = String.Empty;
if(ctx.Request.ServerVariables["REMOTE_ADDR"]!=null)
ip=ctx.Request.ServerVariables["REMOTE_ADDR"];
if(ctx.Request.ServerVariables["HTTP_REFERER"]!=null)
referer=ctx.Request.ServerVariables["HTTP_REFERER"];
if(ctx.Request.QueryString["link"]!=null)
link=ctx.Request.QueryString["link"];
if(ctx.Request.QueryString["text"]!=null)
text =ctx.Request.QueryString["text"];
/*
dbo.InsertClickLog
@Link varchar(1000),
@Text varchar(200),
@Referer varchar(1000),
@RemoteIp varchar(100)
*/
string connectionString = ConfigurationManager.ConnectionStrings["clicklog"].ConnectionString;
SqlConnection cnn = new SqlConnection(connectionString);
SqlCommand oCmd = new SqlCommand("dbo.InsertClickLog",cnn);
oCmd.Connection = cnn;
try
{
cnn.Open();
oCmd.CommandType=CommandType.StoredProcedure;
oCmd.Parameters.Add(new SqlParameter("@Link",SqlDbType.VarChar,1000));
oCmd.Parameters.Add(new SqlParameter("@Text",SqlDbType.VarChar,200));
oCmd.Parameters.Add(new SqlParameter("@Referer",SqlDbType.VarChar,1000));
oCmd.Parameters.Add(new SqlParameter("@RemoteIp",SqlDbType.VarChar,100));
oCmd.Parameters["@Link"].Value = link;
oCmd.Parameters["@Text"].Value = text;
oCmd.Parameters["@Referer"].Value = referer;
oCmd.Parameters["@RemoteIp"].Value = ip ;
oCmd.ExecuteNonQuery();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
}
finally
{
cnn.Close();
oCmd.Dispose();
}
}
}
}
That is pretty much all you need! In the downloadable solution below, I've included a SQL script that will create the ClickLog table and the dbo.InsertClickLog stored proc you need. I've also included a version of my "db.aspx" script-only database viewing page that allows you to dynamically view your results.

Sample display of results
There is a test page with a link to "yahoo" as well as a link to the report page. To set this up, all you need to do is unzip, create a virtual directory, run the SQL Script against your favorite test database, and modify the connection string in the web.config to match your environment. Comments, suggestions, or improvements are always welcome!
Download the Visual Studio 2005 solution that accompanies this article.
Hello
Peter,
One
question;
I don’t
know if you experienced this behavior with your Outbound Link Hit Tracking With
ASP.NET.
When I click
on links in Firefox 2.0 I get double count for each click link.
But when
tested in IE 6.0 for example, it count’s them ok.
I tried it
with your ‘testpage.htm’
Do you know
what could be the cause of it ?
Thanks,
Goran
Grgic.
Ok ..
I think I figured it out fixes the issue with FF2
I tested old_onclick event and it works fine with FF1.5, FF2, IE6 and IE7
Doing coment on js block:
// try{
// window.captureEvents(Event.CLICK);
// window.onclick = clicky_report_onclick
// }
//catch(e) {}