Generating slugs automatically in Django without packages - Two easy and solid approaches

Slugs in Django
Image: Slugs in Django (License: CC-BY-SA Marcelo Canina)

Slug generation management

Published:
Last modified:
Tags: django, slugs, urls

Overview

One important task when developing a Django website is to have pretty URLs, i.e.: human readable and SEO friendly URLs.

Options

We will explore two approaches to have clean URLs, from having the typical Django's URLs with the object/pk scheme, like /article/12345 to have one of these:

  1. pk and slugs: object/pk-slug like /article/12345-my-example-title where we add the object's slug after the primary key.

  2. unique slug: generate a unique slugs without showing the primary key like /article/my-example-title

Example app

The following app consisting of an Article model with a title will help to show how to use the pk-slug URL.

Example blog/views.py:

from django.views.generic.detail import DetailView

from blog.models import Article

class ArticleDetailView(DetailView):

    model = Article

Example blog/models.py:

from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=100)

Example djangoslugs/urls.py:

from django.urls import path

from blog.views import ArticleDetailView

urlpatterns = [
   path('blog/<int:pk>/', ArticleDetailView.as_view(), name='article-detail'),
]

Github Repo

There is sample app showing each case at: https://github.com/marcanuy/django-slugs-example-app Star using the above scheme.

It has models and views for both approaches.

First approach: PK and Slug

Pros

This approach increases Web Application security by avoiding an authorization attack: Insecure Direct Object References (IDOR)

This can be prevented by using both, an object primary key and the slug, as the Simple Object Mixin query_pk_and_slug docs states:

Note

Many apps dedicated to manage slugs comes with a max_length restriction for its size and they truncate the string according to that value.

We are gonna use the same length as the slugified field.

1. Use SlugField

There is a special model Field type in Django for slugs: SlugField.

Create a field named slug with type: SlugField.

in blog/models.py:

from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.text import slugify

class ArticlePkAndSlug(models.Model):
    title = models.CharField(
        max_length=settings.BLOG_TITLE_MAX_LENGTH
    )
    slug = models.SlugField(
        default='',
        editable=False,
        max_length=settings.BLOG_TITLE_MAX_LENGTH,
    )

    def get_absolute_url(self):
        kwargs = {
            'pk': self.id,
            'slug': self.slug
        }
        return reverse('article-pk-slug-detail', kwargs=kwargs)

    def save(self, *args, **kwargs):
        value = self.title
        self.slug = slugify(value, allow_unicode=True)
        super().save(*args, **kwargs)

URLs in djangoslugs/urls.py:

    ...
    path('blog/<int:pk>-<str:slug>/', ArticleDetailView.as_view() , name='article-detail')

And in the view: blog/views.py

class Article(DetailView):
    model = Article
    query_pk_and_slug = True

Before saving the instance, we convert the title to a slug with the slugify Django command, that basically replaces spaces by hyphens.

As SlugField inherits from CharField, it comes with the attribute max_length which handles the maximum length of the string it contains at database level.

If we use a SlugField without specifying its max_length attribute, it gets the value of 50 by default, which can lead to problems when we generate the string from a bigger max_length field.

So the trick is to make them both use the same length.

Second approach: unique slugs

In this case title and slug don't need to have the same max_length, but this brings two issues when generating the slug:

  1. Truncate the slug to respect max_length
  2. Control the slug uniqueness by adding a suffix if another slug with the same string exists1
  3. Avoid generating a new slug if it already has one when saving an existing instance

1. Truncate the slug

Before generating the slug, get the max_length value max_length = self._meta.get_field('slug').max_length and truncate at that position slug = slugify(self.title)[:max_length].

2. Ensure uniqueness

For each slug candidate we make sure it is unique by testing against the database until we have a non existing one.

import itertools

...

slug_candidate = slug_original = slugify(self.title)

for i in itertools.count(1):
	if not Article.objects.filter(slug=slug_candidate).exists():
		break
	slug_candidate = '{}-{}'.format(slug_original, i)

3. Avoid regenerating slug

To avoid generating the slug each time you save an existing model, detect if it is an update of the model on each call of model's save() method.

def _generate_slug(self):
	...

def save(self, *args, **kwargs):
	if not self.pk:
		self._generate_slug()

	super().save(*args, **kwargs)

All together

In save method:

import itertools
...

class ArticleUniqueSlug(Article):

    def _generate_slug(self):
        max_length = self._meta.get_field('slug').max_length
        value = self.title
        slug_candidate = slug_original = slugify(value, allow_unicode=True)
        for i in itertools.count(1):
            if not ArticleUniqueSlug.objects.filter(slug=slug_candidate).exists():
                break
            slug_candidate = '{}-{}'.format(slug_original, i)

        self.slug = slug_candidate

    def save(self, *args, **kwargs):
        if not self.pk:
            self._generate_slug()

        super().save(*args, **kwargs)

In view blog/views.py:

class Article(DetailView):
    model = ArticlePkAndSlug
    query_pk_and_slug = False

Looking like:

]

References


  1. Slug Uniqueness Code based in https://keyerror.com/blog/automatically-generating-unique-slugs-in-django .

Uruguay
Marcelo Canina
I'm Marcelo Canina, a developer from Uruguay. I build websites and web-based applications from the ground up and share what I learn here.
comments powered by Disqus


Guide to handle slugs for Django models focusing in just using Django's SlugField and avoiding third party apps/packages. Two approaches to solve it.

Clutter-free software concepts.
Translations English Espa簽ol