A "slug" is the part of a URL which identifies a page using human-readable keywords, usually based on the title of the page. For example, the full URL of this post is:

https://keyerror.com/blog/automatically-generating-unique-slugs-in-django

Here, the slug part is automatically-generating-unique-slugs-in-django. The slugs make the URL more user-friendly and let visitors know what to expect when they click a link. In addition, search engines often rank pages higher if the search term is in the URL.

Django provides a SlugField model field to make this easier for you. Here's an example of it in a "blog" app's models.py:

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField(blank=True)

    slug = models.SlugField(unique=True)

    @models.permalink
    def get_absolute_url(self):
        return 'blog:post', (self.slug,)

Note that we've set unique=True for our slug field — in this project we will be looking up posts by their slug, so we need to ensure they are unique. Here's what our application's views.py might look like to do this:

from django.shortcuts import get_object_or_404, render

from .models import Post

def post(request, slug):
    post = get_object_or_404(Post, slug=slug)

    return render(request, 'blog/post.html', {
        'post': post,
    })

Initial implementation

So where should these slugs come from? Our first choice might be to simply ask the user to supply a slug for us. Here is our forms.py:

from django import forms

from .models import Post

class AddForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = (
            'title',
            'slug',
            'content',
        )

... and a simple views.py to match:

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import user_passes_test

from .forms import AddForm
from .models import Post

@user_passes_test(lambda x: x.is_superuser)
def add(request):
    if request.method == 'POST':
        form = AddForm(request.POST)

        if form.is_valid():
            post = form.save()
            return redirect(post)
    else:
        form = AddForm()

    return render(request, 'blog/add.html', {
        'form': form,
    })

We then just provide a regular slug HTML form field alongside the content and title fields. Django will automatically validate that the slug is unique before saving.

Generating the slug ourselves

Asking users to write their own slugs isn't the best user experience. How about we generate the slug automatically on the backend based on the post's title? Luckily, we can use Django's django.utils.text.slugify method to do the heavy lifting for us. Here's our updated forms.py:

from django import forms
from django.utils.text import slugify

from .models import Post

class AddForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = (
            'title',
            'content',
        )

    def save(self):
        instance = super(AddForm, self).save(commit=False)
        instance.slug = slugify(instance.title)
        instance.save()

        return instance

Note how we pass in commit=False so we have control over when the Post instance is actually saved. You can read more about the Form.save() method in the Django documentation.

Also note how we do this in in our forms.py rather than our views.py - this makes our form more re-usable and hides this implementation detail from our view.

(Another approach, including the one taken by the Django admin, is to populate and POST back a slug field using Javascript. We might look at this approach in another post.)

Update: In the above examples (and the ones to follow) we are planning on using this Form definition for instance creation in order to keep the code concise and on-topic. However, it is often possible and desirable to re-use forms for editing as well. If you wish to do this, you will have to short-circuit the slug generation or we would generate a fresh slug on every edit. One possible method would be to:

def save(self):
    if self.instance.pk:
        return super(AddForm, self).save()

    instance = super(AddForm, self).save(commit=False)
    # [etc]

Ensuring uniqueness

Our previous implementation will result in an error if the slug already exists, but we can work around this by appending -1, -2, -3, etc. to the slug until it becomes unique.

This solution uses itertools.count which avoids having to maintain a counter ourselves:

import itertools

from django import forms
from django.utils.text import slugify

from .models import Post

class AddForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = (
            'title',
            'content',
        )

    def save(self):
        instance = super(AddForm, self).save(commit=False)

        instance.slug = orig = slugify(instance.title)

        for x in itertools.count(1):
            if not Post.objects.filter(slug=instance.slug).exists():
                break
            instance.slug = '%s-%d' % (orig, x)

        instance.save()

        return instance

Truncating to size

The last remaining problem is one of length. By default, Django's SlugField has a max_length of 50 characters — if we try and save a longer slug, we will get a traceback. Ironically, this more actually more likely to happen as we might be appending characters to the slug to ensure it's unique.

Note that we can't simply truncate the entire candidate slug after appending -1, -2, etc. — we need to truncate the original slug, ensuring we truncate more and more as the appended suffix gets longer (eg. -10, -100 or even -1000):

import itertools

from django import forms
from django.utils.text import slugify

from .models import Post

class AddForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = (
            'title',
            'content',
        )

    def save(self):
        instance = super(AddForm, self).save(commit=False)

        max_length = Post._meta.get_field('slug').max_length
        instance.slug = orig = slugify(instance.title)[:max_length]

        for x in itertools.count(1):
            if not Post.objects.filter(slug=instance.slug).exists():
                break

            # Truncate the original slug dynamically. Minus 1 for the hyphen.
            instance.slug = "%s-%d" % (orig[:max_length - len(str(x)) - 1], x)

        instance.save()

        return instance

Summary

I hope that's given you some actionable advice on how to automatically generate unique slugs for your models, as well as given you some insight into how you can use forms to cleanly add features to your project.

Please let us know if you have any questions or if you have suggestions for future posts!


comments powered by Disqus



Learn how to speed up your Django site — get your FREE 14-day course

Become an expert in high-performance web applications.