Click here to Skip to main content
Click here to Skip to main content

Permalinks for Fun and Profit

By , 4 Sep 2008
Rate this:
Please Sign up or sign in to vote.

Introduction

When building the ASP.NET web site for CALM [did you notice the permalink?], I wanted an easy way of knowing which referring links were driving traffic to the website and which ones were not. I also wanted a simple way to redirect incoming traffic based on the context of origin, and to track different marketing campaigns (from articles, emails, blogs, Google AdWords, etc.). Permalinks solve all three problems.

Contents

Background

The formal notion of permalinks has been around since roughly 2000 (see Wikipedia). However, anyone who has ever reorganized a website, renamed pages, changed the landing page, or clicked on a link that was gone has probably imagined such a thing.

"Permalink" is short for "permanent link". The term originated as a way of referencing a web page or blog post that had been archived or moved. Permalinks are often combined with URL-rewriting, so that the browser presents human-readable (and search-engine-friendly) URLs while the back-end still tracks links using a database.

Implementation

The simplest implementation of permalinks is simply an ID number passed in the URL’s query string. The main page uses the ID to decide which internal page to display.

Permalinks for Fun

Permalinks allow you to change the landing page without changing the website, simply by changing the destination entry in the database for the link ID. This allows you to reorganize your website as needed without the risk of killing external links.

Permalinks for Profit

Permalinks allow you to track which referring links are getting to your "goal pages", whether for demo downloads or orders, or even Easter Eggs. For example, the main page can remember the incoming link ID (in the session, for example), and the goal page can retrieve it and update a counter to track the hits.

Permalinks Demo Code

The demo application includes an SQL script to create and populate a small database, and an ASP.NET website to demonstrate the use and tracking of permalinks. The SQL script creates the Campaign and CampaignLink tables, and populates them with some sample data. The website has a few content pages and "goal" pages in it, plus an "external referral" page to simulate external links and demonstrate how permalinks can be used to track referrals very specifically. A statistics page is also included to show the hit and goal statistics for each Campaign and CampaignLink.

Using the Code

To run the demo, open Microsoft SQL Server Management Studio using a login that has rights to create databases, tables, and Stored Procedures. Then, open the script create-permalinks-database.sql and execute it. Refresh the Databases list, and you should see the PermaLinks database.

Then, open the PermaLinkDemo.sln file in Visual Studio (VS2005, one master page), change the connect-string in the web.config file, if necessary, and build and run the website. The default page is external.aspx, which simulates external links using the CampaignLinks table in the database. Click on an external link to see which page it takes you to. Click on the "goal" buttons to increase the counters. Click the Back link to go back to the external-links page. Repeat for a while to drive the counts up. Then, click on the Show PermaLink Stats link to see the results.

Points of Interest

The techniques used in the website and Stored Procedures are straightforward, but a few may be of interest to some:

  • The external links page (external.aspx) is generated using a SqlDataReader and a StringBuilder to populate a Literal control with a table of links.
  • protected void Page_Load(object sender, EventArgs e)
    {
        //we want to generate a full URL, 
        //e.g. http://localhost/PermaLinks/?linkid=###
        //this is probably not the best way to do this,
        //but it works ;-)
        string baseUrl = this.Request.Url.AbsoluteUri;
        baseUrl = baseUrl.Replace("/external.aspx",
            "/?linkid=");
        //generate a table of links using a data reader;
        //we could have used a repeater control instead
        SqlConnection conn = null;
        SqlCommand cmd = null;
        SqlDataReader rdr = null;
        StringBuilder sb = new StringBuilder();
        try
        {
            conn = new SqlConnection(
                ConfigurationManager.ConnectionStrings["db"].ConnectionString);
            cmd = new SqlCommand("select CampaignLinkId, 
                CampaignLinkDescription from dbo.CampaignLink", conn);
            conn.Open();
            rdr = cmd.ExecuteReader(CommandBehavior.Default);
            sb.Append("<table border='0' align='center' width='90%'>");
            while (rdr.Read())
            {
                //generate a row in the table for each external link
                sb.Append("<tr><td>>a href='");
                sb.Append(baseUrl);
                sb.Append(rdr.GetInt32(0));     //link id
                sb.Append("'>");
                sb.Append(rdr.GetString(1));    //campaign link description
                sb.Append("</a></td></tr>");
            }
            sb.Append("</table>");
            this.Literal1.Text = sb.ToString();
        }
        catch(Exception ex)
        {
            this.Literal1.Text = "ERROR: " + ex.Message;
        }
        finally
        {
            if (rdr != null && !rdr.IsClosed)
            {
                rdr.Close();
                rdr.Dispose();
                rdr = null;
            }
            if (conn != null && conn.State != ConnectionState.Closed)
            {
                conn.Close();
                conn.Dispose();
                conn = null;
            }
        }
    }
  • The Default.aspx page captures the link ID, stores it in the Session, and uses a Stored Procedure to retrieve the target URL for redirection.
  • protected void Page_Load(object sender, EventArgs e)
    {
        int linkId = 0; //default to first permalink if no linkid found
        if (!int.TryParse(this.Request.Params["linkid"], out linkId))
        {
            linkId = 1; //default to first link
        }
        //find page to redirect to
        string url = getUrl(linkId);
        this.Session["linkid"] = linkId;
        this.Response.Redirect(url);
    }
    protected string getUrl(int id)
    {
        SqlConnection conn;
        SqlCommand cmd;
        string url = "hireme.aspx";
        try
        {
            conn = new SqlConnection(
             ConfigurationManager.ConnectionStrings["db"].ConnectionString);
            cmd = new SqlCommand("dbo.uspGetLinkUrl " 
                + id.ToString(), conn);
            conn.Open();
            object result = cmd.ExecuteScalar();
            if (result != null && result != DBNull.Value)
            {
                url = Convert.ToString(result);
            }
        }
        catch
        {
            //just send to default page
        }
        return url;
    }
  • The incoming link ID is retrieved from the Session by the goal-button handlers to update the counters using Stored Procedures.
  • protected void Button1_Click(object sender, EventArgs e)
    {
        //update buy counter
        updateBuyCounter();
    }
    private void updateBuyCounter()
    {
        SqlConnection conn = null;
        SqlCommand cmd = null;
        string linkid = Convert.ToString(this.Session["linkid"]);
        if (linkid == null || linkid.Length <= 0) { return; }
        try
        {
            conn = new SqlConnection(
                ConfigurationManager.ConnectionStrings["db"].ConnectionString);
            cmd = new SqlCommand("exec dbo.uspUpdateBuyCount " 
                + linkid, conn);
            conn.Open();
            cmd.ExecuteNonQuery();
        }
        catch
        {
            //in a real system, only catch SQL errors
        }
        finally
        {
            if (conn != null && conn.State != ConnectionState.Closed)
            {
                conn.Close();
                conn.Dispose();
                conn = null;
            }
        }
    }
  • The statistics page is generated using three DataGridView controls, a SqlDataReader, and a Stored Procedure that returns three result sets.
  • protected void Page_Load(object sender, EventArgs e)
    {
        SqlConnection conn = null;
        SqlCommand cmd = null;
        SqlDataReader rdr = null;
        try
        {
            conn = new SqlConnection(
                ConfigurationManager.ConnectionStrings["db"].ConnectionString);
            cmd = new SqlCommand("dbo.uspCampaignStats", conn);
            cmd.CommandType = CommandType.StoredProcedure;
            conn.Open();
            rdr = cmd.ExecuteReader(CommandBehavior.Default);
            GridViewCampaign.DataSource = rdr;
            GridViewCampaign.DataBind();
            rdr.NextResult();
            GridViewCampaignLink.DataSource = rdr;
            GridViewCampaignLink.DataBind();
            rdr.NextResult();
            GridViewTotals.DataSource = rdr;
            GridViewTotals.DataBind();
        }
        catch
        {
            //in a real system, only catch SQL errors
        }
        finally
        {
            if (rdr != null && !rdr.IsClosed)
            {
                rdr.Close();
                rdr.Dispose();
                rdr = null;
            }
            if (conn != null && conn.State != ConnectionState.Closed)
            {
                conn.Close();
                conn.Dispose();
                conn = null;
            }
        }
    }

Conclusions

Permalinks are an easy way to prevent "link rot", track referrals, and monitor traffic. Much more detailed information is available in a website’s logs, and there are several products that mine these logs and produce detailed reports complete with graphs, charts, paths, etc. Such tools are not always available or convenient; however, a simple statistics page can be formatted to be readable on a SmartPhone – and thus accessible anytime, anywhere.

Future Enhancements

The basic permalinks implementation could be expanded to track visitor paths, using the session ID. Permalinks can also be implemented using an HttpModule, but the basic functionality remains the same. Permalinks can also be combined with URL rewriting to create more user-friendly links, effectively hiding the link IDs from the browser or search engine. Finally, a full-blown permalink solution would obviously include administration pages to maintain Campaigns and CampaignLinks.

History

  • 2008-09-04
    • Initial article published.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Steven A. Lowe
Founder Innovator LLC
United States United States

Steven is the founder and CEO of the consulting and custom software development company Innovator LLC (nov8r.com), which helps companies identify problems that can be transformed into opportunities and strategic advantages by providing innovative solutions for complex problems.

Monitor your .NET applications with CALM, the Common Application Lightweight Monitor.Customizable application monitoring for .NET written by developers, for developers. CALM provides continuous application monitoring and automatically traps and reports unhandled exceptions in .NET applications of all types.

Personal blog at stevenalowe.com

Steven is a consultant, software architect, developer, innovator, and inventor with several decades of experience in many different languages and platforms across dozens of industries.

Follow on   Twitter   Google+

Comments and Discussions

 
GeneralApologies for the &amp;quot; in the source code listings... PinmemberSteven A. Lowe11-Sep-08 10:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 5 Sep 2008
Article Copyright 2008 by Steven A. Lowe
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid