Working With Slugs In Django
Introduction
Slugs are used to identify a resource in the url. For example, "/article/an-article-about-slugs/." While the slug component will likely not be used for the retrieval of the resource itself, it plays a significant role is assisting the human resource understanding what the url points to. Additionally, a well-constructed slug may have a positive contribution in search engine optimization (SEO). While a resource could be identified by a numeric key alone, adding a slug to the url will benefit the user and likely the publisher as well when a resource ranks higher with search engines.
Here, I'll share my approach to working with slugs within the context of a Django application.
Set up
For demonstration purposes, I'll use the following model:
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
publication_date = models.DateTimeField(blank=True, null=True)
...
slug = models.SlugField(blank=True, null=True)
I want the slug field to be nullable so I can begin working with an article without having to commit to a slug value. I'll enforce the presence of a slug for published material by overriding the model's clean() method next.
Creating Slugs
Django provides a function for converting a string to a slug: slugify. I'm going to use this function to construct the slug from the article's title, if a slug has not yet been manually provided by user input. This operation will be added to the model's clean() method. But, clean() isn't called by default, so we'll also need to override the model's save method to ensure this functionality is performed.
from django.utils.text import slugify
class Article(models.Model):
...
def save(self, *args, **kwargs):
self.full_clean() # the full_clean() method will call clean()
super().save(*args, **kwargs)
def clean(self):
if self.publication_date and not self.slug:
# we have an article that is being published but a slug has not been provided by the user
# so let's create one using the Django slugify function
self.slug = slugify(self.title)
I want to wait until the last moment to create the slug because I may be adjusting the title during the article's draft stage. I wouldn't want the slug to be constructed based upon an old title version.
Using Slugs
Now that we've established a method for creating slugs for our article posts, we'll use them with our url patterns and views. Within our url patterns, I want both the slug and the primary key to be parameters. For example:
from django.urls import path
from .views import ArticleDetailView
urlpatterns = [
path("article/<int:post_id>/<slug:post_slug>/", ArticleDetailView.as_view(), name="article-detail"),
]
The reason I want to use two separate identifiers that point to the same resource is that I'll use the primary key for the retrieval of the resource while the slug serves the purpose of making the link human readable and aiding in SEO. In the view, this approach would look something like this:
from django.views.generic import DetailView
from .models import Article
class ArticleDetailViews(DetailView):
model = Article
pk_url_slug = "post_id"
In this approach, the slug isn't even considered in the retrieval of the resource. The primary key is entirely relied upon for this.
Bad Slugs
Since our resource is being retrieved via the primary key, it’s possible that a bad slug is passed to the url and the resource is still retrieved without problems. With how the above view is constructed, a bad slug wouldn't affect the lookup at all. It simply isn't being used for that purpose. But we still don't want bad slugs. Let's check to see if the slug passed to the url is correct:
class ArticleDetailView(DetailView):
...
def get_object(self):
obj = super().get_object() # call super() on the method to retrieve the object
slug = self.request.GET.get("post_slug", None) # get the slug passed to the url
if obj.slug != slug:
# bad slug
print("Bad slug!")
return obj
What do we do with this information? First, we could redirect to the correct url. For this example, we'll use Django's redirect shortcut.
# our model needs to have the get_absolute_url() method defined properly
from django.urls import reverse
class Article(models.Model):
...
def get_absolute_url(self):
return reverse("article-detail", kwargs={"post_id": self.pk, "post_slug": self.slug})
# now, redirect to the proper location if a view encounters a bad slug
from django.shortcuts import redirect
class ArticleDetailView(DetailView):
...
# we don't want to perform a redirect from the get_object method where the previous logical test was performed
# the view expects get_object to return an object, we should instead return a redirect from the get method
def get(self, request, *args, **kwargs):
self.object = self.get_object()
# this won't create a redundant lookup because get_object first checks if self.object is already set
# now, lets perform the same logical test as before, but here we're able to redirect if necessary
slug = request.GET.get("post_slug", None)
if slug != self.object.slug:
return redirect(self.object)
return super().get(request, *args, **kwargs)
Using the redirect shortcut, all we need to do is pass the object to the function. The shortcut will invoke the object's get_absolute_url method. Alternatively, we could use HttpResponseRedirect in place of the shortcut. But I like the shortcut, I think it's cleaner, so I'm going to stick with this approach.
A question remains: do we want to build in a redirect? For my purposes, I think it's appropriate but certainly not necessary. Perhaps more importantly, I want to alert an admin to a potentially bad link. Let's log the bad slug to an admin can go in and fix it. Note: make sure you set up logging in your settings.py file.
import logging
from django.views.generic import DetailView
from django.shortcuts import redirect
logger = logging.getLogger(__name__)
class ArticleDetailView(DetailView):
...
def get(self, request, *args, **kwargs):
self.object = self.get_object()
slug = request.GET.get("post_slug", None)
if slug != self.object.slug:
logger.error(
f"A bad slug was encountered: '{slug}.' The expected slug was '{self.object.slug}.' The referring location was
{request.META["HTTP_REFERRER]}."
)
return redirect(self.object)
return super().get(request, *args, **kwargs)
With the above, we'll log basic information to help us track down the issue in addition to redirecting to the correct location. First, we'll log the bad slug that was passed to the url as well as log what the correct slug should be. Then, we'll log where the link came from with META["HTTP_REFERRER"] and hopefully this was an internal source where we can make changes.
Though, it's very possible that the issue is external. If you made changes to a slug, a search engine may have a prior version indexed and stored. This approach will interpret this as a bad slug and log the error. If you have a lot of traffic for the given link from a search engine with an antiquated slug, you might clutter your log files. This situation may require additionally handling that I won’t get into here. Better, avoid changing slugs as much as possible. This is another reason why I don't want to create the slug until the very last moment when it's necessary. Hopefully we can avoid needing to change the slug altogether.
Final Thoughts
Slugs are an important component to any content platform that requires information to be communicated through a url for purposes of resource retrieval and informing users or search engines. Be careful when creating slugs and try to avoid situations where they need to be changed or your relationship with search engines may suffer. Automate as much as you can, like we did with the above function slugify. But also build in the necessary safety measures to make sure we're engaging with good information like the approaches mentioned in the Bad Slugs section. Follow these steps, in addition to other best practices, and you'll be well on your way to using slugs to well within your own project.