Django Slug Tutorial

In this tutorial, we will add slugs to a Django website. As noted in the official docs for slugfield: "slug" is a newspaper term, a short label for something containing only letters, numbers, underscores, or hyphens. Slugs are generally used in URLs."

To give a concrete example, assume you had a Newspaper website (such as we'll build in this tutorial). For a story titled "Hello World," the URL would be example.com/hello-world assuming the site was called example.com.

Despite their ubiquity, slugs can be challenging to implement the first time. Therefore, we will implement everything from scratch so you can see how the pieces all fit together. If you are already comfortable implementing ListView and DetailView, you can jump to the Slug section.

Set Up

To start, let's navigate into a directory for our code, which can be hosted anywhere on your computer, but an easy-to-find location is the Desktop in a directory called newspaper.

# Windows
$ cd onedrive\desktop\code
$ mkdir newspaper
$ cd newspaper

# macOS
$ cd ~/desktop/code
$ mkdir newspaper
$ cd newspaper

To pay homage to Django's origin in a newspaper, we'll create a basic Newspaper website with Articles. If you need help installing Python, Django, and all the rest (see here for in-depth instructions).

Create and activate a virtual environment on your command line, install Django, create a new project called django_project, set up the initial database via migrate, and then start the local web server with runserver.

# Windows 
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install django~=4.2.0
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py migrate
(.venv) $ python manage.py runserver

# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install django~=4.2.0
(.venv) $ django-admin startproject django_project .
(.venv) $ python3 manage.py migrate
(.venv) $ python3 manage.py runserver

Don't forget to include that period . at the end of the startproject command! It is an optional step that avoids Django creating an additional directory otherwise.

Navigate to http://127.0.0.1:8000/ in your web browser to see the Django welcome page, which confirms everything is configured correctly.

Django welcome page

Articles app

Since this tutorial focuses on slugs, I will give the commands and code to wire up this Articles app. Full explanations can be found in my book Django for Beginners!

Let's start by creating an app called articles. Stop the local server with Control+c and use the startapp command to create this new app.

(.venv) $ python manage.py startapp articles

Then update INSTALLED_APPS within our django_project/settings.py file to notify Django about the app.

# django_project/settings.py
INSTALLED_APPS = [
    ...
    "articles",  # new
]

Article Model

Create the database model with a title and body. We'll also set __str__ and a get_absolute_url, which are Django best practices.

# articles/models.py
from django.db import models
from django.urls import reverse

class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("article_detail", args=[str(self.id)])

Now create a migrations file for this change, then add it to our database via migrate.

(.venv) $ python manage.py makemigrations articles
(.venv) $ python manage.py migrate

Django Admin

The Django admin is a convenient way to play around with models, so we'll use it. But first, create a superuser account.

(.venv) $ python manage.py createsuperuser

Then, update articles/admin.py to display our app within the admin.

# articles/admin.py
from django.contrib import admin

from .models import Article

class ArticleAdmin(admin.ModelAdmin):
    list_display = ("title", "body",)

admin.site.register(Article, ArticleAdmin)

Start the server again with python manage.py runserver and navigate to the admin at http://127.0.0.1:8000/admin. Log in with your superuser account.

Admin Homepage

Click on the "+ Add" next to the Articles section and add an entry.

Admin Article

URLs

In addition to a model, we'll eventually need a URL, view, and template to display an Article page. I like to move to URLs next after models, although the order doesn't matter: we need all four before we can display a single page. The first step is to add the articles app to our project-level django_project/urls.py file.

# django_project/urls.py
from django.contrib import admin
from django.urls import path, include  # new

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("articles.urls")),  # new
]

Next, in your text editor, create the app-level articles/urls.py file. We'll have a ListView to list all articles and a DetailView for individual articles.

Note that we're referencing two views that have yet to be created: ArticleListView and ArticleDetailView. We'll add them in the next section.

# articles/urls.py
from django.urls import path

from .views import ArticleListView, ArticleDetailView

urlpatterns = [
    path("<int:pk>", ArticleDetailView.as_view(), name="article_detail"),
    path("", ArticleListView.as_view(), name="article_list"),
]

Views

For each view, we specify the related model and appropriate not-yet-created template.

# articles/views.py
from django.views.generic import ListView, DetailView

from .models import Article

class ArticleListView(ListView):
    model = Article
    template_name = "article_list.html"


class ArticleDetailView(DetailView):
    model = Article
    template_name = "article_detail.html"

Templates

Finally, we come to the last step: templates. By default, Django will look for a templates directory within each app. That structure, in our case, would be articles/templates/template.html.

Type Control+c on the command line and create the new templates directory.

(.venv) $ mkdir articles/templates

Then in your text editor, add the two new templates: articles/templates/article_list.html and articles/templates/article_detail.html.

We loop over object_list for our articles list page, provided by ListView. And we add an 'a hrefby using theget_absolute_url` method added to the model.

<!-- article_list.html -->
<h1>Articles</h1>
{% for article in object_list %}
  <ul>
    <li><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></li>
  </ul>
{% endfor %}

The detail view outputs our two fields--title and body--using the object default provided by DetailView. You can, and probably should, rename both object_list in the ListView and object in the DetailView to be more descriptive.

<!-- article_detail.html -->
<div>
  <h2>{{ object.title }</h2>
  <p>{{ object.body }}</p>
</div>

Make sure the server is running--python manage.py runserver--and check out both our pages in your web browser.

The list of all articles is available at http://127.0.0.1:8000/.

ListView

And the detail view for our single article is at http://127.0.0.1:8000/1.

DetailView

Slugs

Finally, we come to slugs. Ultimately, we want our article title to be reflected in the URL. In other words, "A Day in the Life" should have the URL of http://127.0.0.1:8000/a-day-in-the-life.

Only two steps are required: updating our articles/models.py file and articles/urls.py. Ready? Let's go...

In our model, we can add Django's built-in SlugField. But we must also--and this is the part that typically trips people up--update get_absolute_url as well. That's where we pass in the value used in our URL. Currently, it passes in the id for the article an args, so 1 for our first article. We need to change that over to a keyword argument, kwargs, for our slug.

# articles/models.py
from django.db import models
from django.urls import reverse


class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    slug = models.SlugField()  # new

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("article_detail", kwargs={"slug": self.slug})  # new

Let's add a migration file since we've updated our model.

(.venv) > python manage.py makemigrations articles
You are trying to add a non-nullable field 'slug' to article without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

Ack! What is this?! We already have data in our database, our single article, so we can't just willy-nilly add a new field on. Django is helpfully telling us that we either need a one-off default of null or add it ourselves. Hmmm.

Therefore, it is generally good advice to always add new fields with either null=True or a default value.

Let's take the straightforward approach of setting null=True for now. So type 2 on the command line. Then, add this to our slug field.

# articles/models.py
from django.db import models
from django.urls import reverse


class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    slug = models.SlugField(null=True)  # new

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("article_detail", kwargs={"slug": self.slug})

Try to create a migrations file again, and it will work.

(.venv) > python manage.py makemigrations articles

Go ahead and migrate the database to apply the change.

(.venv) > python manage.py migrate

But if you think about it, we've created a null value for our slug. We have to go into the admin to set it properly. Start up the local server, python manage.py runserver, and go to the Article page in the admin. The Slug field is empty.

Empty Slug

Manually add our desired value, a-day-in-the-life and click "Save."

Add Slug

Okay, the last step is to update articles/urls.py so that we display the slug keyword argument in the URL itself. Luckily, that just means swapping out <int:pk> for <slug:slug>.

# articles/urls.py
from django.urls import path

from .views import ArticleListView, ArticleDetailView

urlpatterns = [
    path("<slug:slug>", ArticleDetailView.as_view(), name="article_detail"),  # new
    path("", ArticleListView.as_view(), name="article_list"),
]

And we're done! Go to the list view page at http://127.0.0.1:8000/ and click on the link for our article.

Slug URL

And there it is, with our slug as the URL! Beautiful.

Unique and Null

Moving forward, do we want to allow a null value for a slug? Probably not, as it will break our site! Another consideration is: what happens if there are identical slugs? How will that resolve itself? The short answer is: not well.

Therefore, let's change our slug field so that null is not allowed and unique values are required.

# articles/models.py
from django.db import models
from django.urls import reverse


class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    slug = models.SlugField(null=False, unique=True)  # new

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("article_detail", kwargs={"slug": self.slug})

Then run makemigrations.

(.venv) @ python manage.py makemigrations articles
You are trying to change the nullable field 'slug' on article to non-nullable without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now, and let me handle existing rows with NULL myself (e.g. because you added a RunPython or RunSQL operation to handle NULL values in a previous data migration)
 3) Quit, and let me add a default in models.py
Select an option:

Select 2 since we can manually handle the existing row ourselves and already have. Then migrate the database.

(.venv) $ python manage.py migrate

PrePopulated Fields

Manually adding a slug field each time quickly becomes tedious. So we can use a prepopulated_field in the admin to automate the process for us.

Update articles/admin.py as follows:

# articles/admin.py
from django.contrib import admin

from .models import Article

class ArticleAdmin(admin.ModelAdmin):
    list_display = ("title", "body",)
    prepopulated_fields = {"slug": ("title",)}  # new

admin.site.register(Article, ArticleAdmin)

Now head over to the admin and add a new article. You'll note that as you type in the Title field, the Slug field is automatically populated. Pretty neat!

Signals, Lifecycle Hooks, Save, and Forms/Serializers

In the real world, it's unlikely to simply provide admin access to a user. You could, but at scale, it's definitely not a good idea. And even on a small scale, most non-technical users will find a web interface more appealing.

How do you auto-populate the slug field when creating a new Article? It turns out Django has a built-in tool for this called slugify!

But how to use slugify? In practice, it's common to see this done with a Signal. But I would argue--as would Django Fellow Carlton Gibson--that this is not a good use of signals because we know both the sender and receiver here. There's no mystery. We discuss the topic's proper use of signals at length in our Django Chat Podcast episode.

An alternative to signals is to use a lifecycle hook via something like the django-lifecycle package. Lifecycle hooks are an alternative to Signals that provide similar functionality with less indirection.

Another common way to implement this is by overriding the Article model's save method. This approach also "works" but is not the best solution. Here is one way to do that.

# articles/models.py
from django.db import models
from django.template.defaultfilters import slugify  # new
from django.urls import reverse

class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    slug = models.SlugField(null=False, unique=True)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("article_detail", kwargs={"slug": self.slug})

    def save(self, *args, **kwargs):  # new
        if not self.slug:
            self.slug = slugify(self.title)
        return super().save(*args, **kwargs)

The best solution is to create the slug in the form itself by overriding the form's clean method so that cleaned_data has the slug, or JavaScript can be used to auto-populate the field as is done in the Django admin itself. If you're using an API, the same approach can be applied to the serializer.

This solution does not rely on a custom signal and handles the slug before it touches the database. In the words of Carlton Gibson, who suggested this approach, it is a win-win-win.

Words of Caution

A quick word of caution on using slugs in a large website. Despite requiring an Article to be unique, naming conflicts using slugs is almost inevitable. However, slugs do seem to have good SEO properties. A good compromise is to combine a slug with a UUID or a username. The ultimate URL would therefore be {{ uuid }}/{{ slug }} or {{ username }}/{{ slug }}. If you look at Github, for example, they use the pattern of username + slug for each repo, which is why my Django for Beginners source code is located at https://github.com/wsvincent/djangoforbeginners. While you could also use an id + a slug, using IDs is often a security concern and should be avoided for production websites.

Join My Newsletter

Subscribe to get the latest tutorials/writings by email.

    No spam. Unsubscribe at any time.