I originally wrote this for Drupal 7, if you are you using Drupal 8 or 9 there are updates here

Recently I've been thinking about how to make URL shortening a little simpler for Drupal sites (well, my blog but this could apply to any site). You see, in addition to seanreiser.com, I own sr7.us which I bought to use as an URL shortener. I took a look at a few of the existing modules and they all seem to work in similar ways, either they take advantage of Drupal's Path module and auto generate a slug that can be used in the short url and the forward happens automagically or they maintain a separate table and do the forward in a hook_init call or via the Redirect Module (which again, works in hook_init). These methods are sound Drupal best practice but when I look at them I see two potential problems:

  1. There are still the potential for URL collisions since a path could defined in both hook_menu and as a URL alias.
  2. You have to wait for Drupal to fully bootstrap in order to decode a short URL and do a redirect.

These aren't horrible constraints but it got me to thinking, "Is there a way to do the shortening algorithmically, so it wouldn't require Drupal to bootstrap and yet be reasonably assured that there would be no collisions".

So I took a look at all the paths in the URL alias table and the menu router table for all the systems I am responsible for and I noticed that every path either have more then one argument (not empty arg(1)) or arg(0) contains a vowel. The only cases where arg(0) didn't contain a vowel was when it was numeric and generally had a format like 2013/01/16/my-article-title-here. When you consider the bias toward English in module development (as well as the sites I work on) this makes some sense. Of course this won't work with Welsh since they have words likee crwth, cwtch or cwm and when Chinese is converted to puny code but for the 70% of the internet that uses English and other European languages this is a useful hack.

The only pages I'm looking to create a short URL for are nodes (individual pieces of content). So, I can use the Node ID as a key to encode and decode the URL.

All that said if we consider that URLs are case sensitive we can use a dataset of "0123456789bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ" (note the lack of a,e,i,o,u,y both lower and upper case) we wind up with a bastardized base 50. At the core of this are 2 generic which convert a decimal to any base, and any base to a decimal:

I copypasta'ed these a while ago from someplace on http://php.net, but the link seems to be dead now. Either way, they were posted for educational purposes, and I'm passing that forward. I just wish I could credit the original author.

You'll notice that the default for these functions is Base 62, containing the entire alpha-numeric universe of characters. To limit this I have 2 wrapper functions:

So I'm sure you've noticed that there's a validate slug function. Basically it's there to ensure that the Slug is valid (contains no vowels) Here's what I'm doing:

Next up the function actually doing the forwarding in a hook_boot call

Now it's not perfect (what in the world is). You can see it in action across the site here. For example this aricle can be found at http://sr7.us/2gM as well as the conical URL of https://seanreiser.com/blog/note/algorithmic-url-shortening-drupal. I need to clean a couple of things up, but it's my intention to post this as a sandbox project shortly.

Share and Enjoy!

7/21/2021 - Updates for Drupal 8 and 9

Since both hook_boot and drupal_goto were deprecated in Drupal 8, I needed to do some rejiggering to get this to work. I could’ve setup an event subscriber, but again the goal is to have the code execute before Drupal fully bootstraps. It’s become more popular to insert code into settings.php so I went that route.

The code is no longer stored in a module. The URL encoding / decoding is stored in /sites/default/shorturl.php (I refactored the code from above).

I then added the following to the top of settings.php

All in all, it works well, ia compatible with my D7 solution and works with D8 and D9.