plinky - A bit like Bitly

I recently got frustrated with Bitly as a url shorter and decided to write my own simple alternative.

The result is a very small (and prety dumb) Python web app that will allow short url to be redirected to other (longer) urls, and will capture some stats about how often those links are being used.

That web app is called plinky, and you can find the source code on GitHub.com:

https://github.com/martinpeck/plinky

The Problem with Bitly

On Bitly you can set up custom domains. These allow you to replace the bit.ly part of any short link with your own short domain name.

Each short link on Bitly is made up of the domain plus a hash. The hash is autogenerated by Bitly when you ask them to shorten the link.

For example, in the short link…

http://bit.ly/1X5IvtX

… (a link to this blog) the domain is bit.ly and the text 1X5IvtX is the hash.

If you want, you can then customize your short links to make the hash easier to read/type.

For example, I’ve customized the previous short link to be:

http://bit.ly/martinpeck

So, with a custom domain, you are essentially replacing the bit.ly domain with your own.

However, even with a custom domains, all hashes must be globally unique across the whole of Bitly. This means that if someone has already used the hash martinpeck for a short url. That sounds fair enough, but this rule is enforced across custom domains too. So, regardless of whether the domain would make the short url look unique, the hash must be globally unique across all of Bitly and all of the custom domains.

The result is that you end up spending a lot of time trying to find useful hashes and, in many cases, the hashes get very long which defeats the purpose of using a link shortener.

The other problem with Bitly is that you can’t ever change the final location of a short url. Once created, you can never edit the short url to direct users to a different resource. This is a real problem if you’ve printed the short url on posters or leaflets, or if the destination url changes for reasons you can’t control. I’m sure there are very good reasons why changing the redirects for short urls is a bad idea but, combined with the difficulty in finding unique hashes in the first place it’s sometimes desirable to update one.

The Solution - Write Your Own

My solution to this was to stop using Bitly, to say to myself “it can’t be that hard to redirect web requests”, and to write my own.

Now…I’ve not written anything nearly as complicated or feature rich as Bitly. I’ve written the stupidest thing that does what I need:

  1. accept an HTTP request
  2. look up the destination
  3. respond with an HTTP 301 redirect to the final destination.
  4. track all such requests
  5. respond with a default redirect if the lookup fails

Plinky is written in Python, and uses the flask micro-framework. I picked flask because I’d not really played much with flask. I’ve used Sinatra (with Ruby), but hadn’t tried flask. Plinky is hardly the most complicated example of a flask app, but I certainly found it simple to set up and get working.

At the heart of Plinky is a YAML file that contains a list of hashes with the intended destinations. If you look at the example YAML file in Github you’ll see it looks like this…

# default, if other shorturls can't be found
default: https://github.com/martinpeck/plinky

# some github short urls for testing
me: https://github.com/martinpeck
issues: https://github.com/issues/assigned
stars: https://github.com/stars

The default hash is used any time that a hash lookup fails. The other name/value pairs are short url hashes and their redirect destinations.

Plinky just sits there looking up hashes and issuing HTTP 301 redirects. If you look at the code you can see that there’s not much to it really.

plinky.py is the main entry point for HTTP requests. It sets up a handful of routes (some of which are a work in progress) but the main one is this…

@app.route("/<path:shorturl>", methods=['GET'])
def redirect_to_short_url(shorturl=""):
  tracking.track_redirect(shorturl)
  return redirect(shortcuts.lookup_shorturl(shorturl), 301)

This route tracks the redirect event using some code in tracking.py and then performs the HTTP 301 redirect using some code in shortcuts.py

Plinky uses Segment to track how many times a given hash is used for redirect. This is optional, and controlled by the existance of an environemnt variable holding the key that Segment requires.

Segment is a service that takes events and then hands them off to 1 or more other analytics services. So, you can send a single event to Segment and have Segment issue an analytics event to Google Analytics, MixPanel or a whole host of other analytics endpoints. This makes it super easy to add and replace the services you use for analytics.

And that’s about it. If you deploy Plinky to Heroku it’ll sit there and preform redirects for you.

Another Use for Plinky

At the place I work we recently deprecated one of our domains and wanted to set up redirects to the new domain and new URL structure. Plinky was perfect for this.

Plinky just needs name/value pairs in the configured YAML file, and it really doesn’t care in the name is more complex than a few characters. So, if you set up the short urls like this…

old/url/structure: https://www.some-new-domain.com/new/url/structure
old/url/structure/example1.html: https://www.some-new-domain.com/new/url/structure/example-one
old/url/structure/example2.html: https://www.some-new-domain.com/new/url/structure/example-two

… and then configure a Plinky instance, via DNS, to sit on your old domain, then it can sit there and redirect your users to the new domain and new url strucutre, and if you’re using the tracking feature you can see whether this traffic slowly migrates over to your new domain.

Again, I’m sure there are better ways to do this, but this one got me and my team out of a hole and worked really well.

TODO

There are some things I want to improve with Plinky. For example, I want to add some stats and info pages to the service so that it’s easier to see how the short urls are set up. If you have any suggestions, or see any problems, please log an issue in the GitHub Issues database for this project.

}