How to Implement and Manage a Slug in Django for SEO-Friendly URLs

Use Django's SlugField combined with the slugify utility to automatically generate unique, URL-friendly identifiers from content titles, handling collisions by appending numeric suffixes.

A slug in Django is a short label for a piece of content, containing only letters, numbers, hyphens, and underscores, making it ideal for URLs. The django/django repository provides robust built-in tools to handle slug generation, storage, and uniqueness constraints without requiring external dependencies. This guide demonstrates how to implement a complete slug system using the framework's core components.

What Is a Slug in Django?

A slug is a URL-friendly version of a string, typically derived from a title or name. In the context of Django, slugs serve as human-readable, permanent identifiers for database records accessed via URLs. Unlike numeric primary keys, slugs provide semantic meaning to URLs (e.g., /articles/intro-to-django/ vs. /articles/42/), improving both user experience and search engine optimization.

Core Components for Managing Slugs in Django

Django's slug functionality centers on three primary components defined in the core framework.

SlugField: Storing URL-Friendly Identifiers

The SlugField class, defined in django/db/models/fields/__init__.py, extends CharField to provide storage specifically for slug strings. By default, it sets allow_unicode=False, restricting content to ASCII characters, though Unicode support can be enabled.

To enforce uniqueness at the database level, set unique=True in the field definition. Alternatively, use unique_together constraints when slugs must be unique only within a specific context (e.g., unique per author or category).

slugify: Converting Text to URL Format

The slugify utility function, located in django/utils/text.py, transforms arbitrary strings into valid slug format. It performs the following operations:

  • Converts to lowercase
  • Removes non-alphanumeric characters (except spaces)
  • Replaces spaces with hyphens
  • Strips leading/trailing whitespace
  • Optionally handles Unicode transliteration when allow_unicode=False

This function serves as the standard transformation mechanism, ensuring consistency across model methods, views, and admin interfaces.

Signals: Automating Slug Generation

Django's signal framework, defined in django/db/models/signals.py, enables decoupled slug generation logic. The pre_save signal fires immediately before a model instance is saved to the database, making it ideal for populating empty slug fields based on title changes without overriding the model's save() method.

Step-by-Step Implementation: Creating a Unique Slug in Django

Implementing a robust slug system requires handling generation, collision detection, and URL routing.

Step 1: Define the Model with SlugField

Create a model that stores both the source text (e.g., title) and the generated slug:

from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True, blank=True)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return self.title

Setting blank=True allows the admin interface to leave the field empty while the model automatically populates it during save.

Step 2: Implement Automatic Slug Generation

Override the save() method to generate the slug from the title when the instance is first created or when the title changes:

from django.utils.text import slugify

class Article(models.Model):
    # ... fields as defined above ...

    def save(self, *args, **kwargs):
        if not self.slug or self._title_changed():
            self.slug = self._generate_unique_slug()
        super().save(*args, **kwargs)

    def _title_changed(self):
        """Check if title differs from database value."""
        if not self.pk:
            return True
        old_title = Article.objects.filter(pk=self.pk).values_list('title', flat=True).first()
        return old_title != self.title

    def _generate_unique_slug(self):
        """Generate unique slug with collision handling."""
        base_slug = slugify(self.title)
        slug = base_slug
        counter = 1
        
        while Article.objects.filter(slug=slug).exclude(pk=self.pk).exists():
            slug = f"{base_slug}-{counter}"
            counter += 1
            
        return slug

The _generate_unique_slug method implements collision handling by appending incremental numeric suffixes until a unique value is found.

Step 3: Handle Slug Collisions

The collision handling logic in _generate_unique_slug prevents IntegrityError exceptions by checking existing records before assignment. For high-traffic applications, consider wrapping the save operation in a transaction or using database-specific upsert operations to handle race conditions where two processes might generate the same slug simultaneously.

Step 4: Configure URLs to Use Slugs

Define URL patterns that capture the slug parameter using Django's path converter:


# urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('articles/<slug:slug>/', views.article_detail, name='article_detail'),
    path('articles/', views.article_list, name='article_list'),
]

The <slug:slug> pattern validates that the URL segment contains only allowed slug characters (letters, numbers, hyphens, underscores).

Step 5: Retrieve Objects by Slug in Views

Use get_object_or_404 to fetch the article using the slug from the URL:


# views.py

from django.shortcuts import get_object_or_404, render
from .models import Article

def article_detail(request, slug):
    article = get_object_or_404(Article, slug=slug)
    return render(request, 'articles/detail.html', {'article': article})

This approach ensures that requests for non-existent slugs return HTTP 404 responses automatically.

Django Admin Integration for Slug Management

Django's admin interface provides built-in support for slug handling through the prepopulated_fields attribute, defined in django/contrib/admin/options.py.


# admin.py

from django.contrib import admin
from .models import Article

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ('title',)}
    list_display = ('title', 'slug', 'created_at')
    readonly_fields = ('slug',)  # Optional: prevent manual editing after creation

The prepopulated_fields dictionary tells the admin to automatically generate the slug from the title field using JavaScript as the user types. Note that this client-side generation does not handle uniqueness collisions; the model's save() method or signals must still enforce uniqueness constraints.

Alternative Approach: Using pre_save Signals

For applications preferring decoupled logic, implement slug generation using Django's signal framework instead of overriding save().


# signals.py

from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils.text import slugify
from .models import Article

@receiver(pre_save, sender=Article)
def generate_slug_on_save(sender, instance, **kwargs):
    if not instance.slug:
        base = slugify(instance.title)
        slug = base
        counter = 1
        
        while Article.objects.filter(slug=slug).exclude(pk=instance.pk).exists():
            slug = f"{base}-{counter}"
            counter += 1
            
        instance.slug = slug

Register this signal in your application's apps.py ready method or __init__.py to ensure it connects when Django starts:


# apps.py

from django.apps import AppConfig

class ArticlesConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'articles'
    
    def ready(self):
        import articles.signals  # noqa

Summary

  • Use SlugField from django/db/models/fields/__init__.py with unique=True to store URL identifiers at the database level.
  • Leverage slugify from django/utils/text.py to normalize strings into ASCII, lowercase, hyphen-separated formats.
  • Override save() or use pre_save signals from django/db/models/signals.py to automate slug generation and handle collision detection by appending numeric suffixes.
  • Configure URL patterns using the <slug:slug> path converter to validate and capture slug parameters.
  • Integrate with the admin using prepopulated_fields defined in django/contrib/admin/options.py for client-side slug generation during content creation.

Frequently Asked Questions

How do I make a slug unique in Django?

Enforce uniqueness by setting unique=True on your SlugField definition. Additionally, implement collision handling in your model's save() method or a pre_save signal by checking existing records and appending incremental numbers (e.g., my-post-1, my-post-2) until a unique value is found.

What is the difference between SlugField and CharField?

SlugField is a specialized subclass of CharField defined in django/db/models/fields/__init__.py that includes validation ensuring values contain only letters, numbers, hyphens, and underscores. While CharField accepts any string, SlugField specifically rejects spaces and special characters unsuitable for URLs, making it the correct choice for URL path segments.

How does the slugify function handle Unicode characters?

By default, slugify in django/utils/text.py transliterates Unicode characters to their closest ASCII equivalents before converting to lowercase and replacing spaces with hyphens. For example, "Café Münich" becomes "cafe-munich". You can preserve Unicode characters by passing allow_unicode=True, which keeps non-ASCII characters in the resulting slug.

Should I use model save() override or pre_save signals for slug generation?

Override save() when the slug generation logic is tightly coupled to the model and requires access to instance methods or complex state checking. Use pre_save signals when you want decoupled logic that can be easily disabled or when multiple models share identical slug generation patterns. Both approaches reside in the model layer and execute before the database transaction commits, ensuring data integrity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →