How to Create Human Readable URLs in Django That Don't Break With Content Changes

Slugs and IDs for speedy pretty URLs, a la The Onion

You’ll notice (for the moment, at least), that the URL of this blog post is made up of a pretty easy to read components. There’s the domain, the /learn/ route, and then the post’s slug, the lowercase title with dashes. It tells the server to use the blog URL configuration, and then to look up a blog post whose slug matches the following text. You could use something like blog.asp?post=12 (if you were running ASP… ahem) and the script you’re running there will pull up the blog post with row ID

  1. It works, but it’s ugly and not terribly user friendly. Hence the ‘beautiful’ URLs you see here, courtesy of Django.

Why would you want to change this then? How about performance gains?

When The Onion launched their Django-based refresh a few weeks ago, I noticed something funky with the article URLs. Instead of the usual slug, like this:

the new URLs looked like this:,17176/

It looks like they had added the ID of the article to the slug. I wasn’t sure why until I started messing around with both parameters and realized that the slug wasn’t used at all to retrieve the articles. You could change the URL from this:,17176/

to this:,17176/

and still get the original article about the coke addled chimp.

The article ID is what Django uses to retrieve the article. The slug is there for human eyes, but it’s just window dressing. It makes sense to you if you see the address bar, it makes it easier to access the page through your location bar history, and it’s got more search result mojo (or so the SEO mavens keep telling us). It would be faster, however, for the database to use the primary key for the article. Since you’re probably not going to want to use the article slug as your primary key, that leaves you with something like a UUID or the row ID. But the whole point of beautiful URLs is that we get away from this ugly article=17176 business.

The Django answer is to use both. Set up your URL to take a slug and an integer as parameters, but in your view use only the integer primary key to access the article (or whatever object it is you want). The object’s get_absolute_url object inserts both slug and primary key into the URL and ouila, fast, readable URLs.

url(r'^(?P<slug>[-\w\d]+),(?P<id>\d+)/$', view=myviews.article, name='article'),

def my_view(request, slug, id):
        article = get_object_or_404(pk=id)
        # everything else in your view

class Article(models.Model):
  def get_absolute_url(self):
        return ('article', (), {
            'slug': self.slug,

You might be better served using generic views, and this doesn’t add in the redirect in The Onion’s views which rewrite the proper slug if something else is there, but it’s a basic way of getting the job done. I switched a view and URL configuration from a slug based to lookup to this primary key lookup and saw a 13% query speed improvement (via the debug toolbar). It’s hardly scientific, but for sites looking for a little performance help it’s a pretty simple improvement to implement.