Introduction

A good URL should be easy to read, easy to share, and meaningful for both users and search engines. Compare these two URLs:

/posts/15/

and

/posts/django-slugs-and-clean-urls/

The second one is much clearer. It tells the user what the page is about before even opening it. This is where slugs come in.

In Django, a slug is a short, URL-friendly string, usually based on a title. Slugs are commonly used to create clean and SEO-friendly URLs for blog posts, products, categories, courses, and many other objects.

In this tutorial, you will learn how slugs work in Django, how to create them, how to use them in URLs and views, and how to handle common problems such as duplicate slugs.

What You Will Learn

By the end of this tutorial, you will understand:

Prerequisites

Before starting, you should already know:

1. What Is a Slug?

A slug is a URL-safe text string.

Example:

A slug usually contains:

It should not contain:

Django provides a built-in field for this: SlugField.

2. Why Use Slugs?

Slugs are useful because they make URLs:

For example:

/products/42/

is less clear than:

/products/wireless-keyboard/

3. Adding a Slug Field to a Model

Let us start with a simple blog post model.

models.py

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

Explanation

After adding the field, run:

python manage.py makemigrations
python manage.py migrate

4. Creating Slugs Manually

You can enter slugs manually in Django admin or forms.

Example:

This works, but it is not ideal because users may forget to enter the slug or enter it incorrectly.

That is why automatic slug generation is often better.

5. Generating Slugs Automatically with slugify

Django provides a utility called slugify.

Example

from django.utils.text import slugify

title = "Django Slugs and Clean URLs"
print(slugify(title))

Output:

django-slugs-and-clean-urls

It converts text into a URL-friendly slug.

6. Auto-Generating Slugs in save()

A common approach is to generate the slug automatically when saving the object.

models.py

from django.db import models
from django.utils.text import slugify

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, blank=True)
    content = models.TextField()

    def __str__(self):
        return self.title

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

How it works

7. Using Slugs in URLs

Now that each post has a slug, we can use it in the URL.

urls.py

from django.urls import path
from .views import post_detail

urlpatterns = [
    path('posts/<slug:slug>/', post_detail, name='post_detail'),
]

Explanation

Example URL:

/posts/django-slugs-and-clean-urls/

8. Retrieving Objects by Slug in a Function-Based View

views.py

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

def post_detail(request, slug):
    post = get_object_or_404(Post, slug=slug)
    return render(request, 'blog/post_detail.html', {'post': post})

Explanation

9. Using Slugs with DetailView

Slugs work very well with class-based views too.

views.py

from django.views.generic import DetailView
from .models import Post

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    slug_field = 'slug'
    slug_url_kwarg = 'slug'

urls.py

from django.urls import path
from .views import PostDetailView

urlpatterns = [
    path('posts/<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
]

Explanation

10. Adding Links in Templates

Once your URLs use slugs, links should also use slugs.

Example in post_list.html

{% for post in posts %}
    <h2>
        <a href="{% url 'post_detail' post.slug %}">
            {{ post.title }}
        </a>
    </h2>
{% endfor %}

Now each post link points to a clean URL.

11. The Problem of Duplicate Slugs

Suppose you create two posts with the same title:

Both would generate:

django-tutorial

 

But if slug is unique, the second one will fail.

So you need a way to make slugs unique.

12. Making Slugs Unique

A simple approach is to add a number when the slug already exists.

Example

models.py

from django.db import models
from django.utils.text import slugify

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, blank=True)
    content = models.TextField()

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.slug:
            base_slug = slugify(self.title)
            slug = base_slug
            counter = 1

            while Post.objects.filter(slug=slug).exists():
                slug = f"{base_slug}-{counter}"
                counter += 1

            self.slug = slug

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

Explanation

13. Using get_absolute_url() with Slugs

A very useful Django convention is get_absolute_url().

Example

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

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, blank=True)
    content = models.TextField()

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('post_detail', kwargs={'slug': self.slug})

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

Why it is useful

14. Real Example: Blog Post Workflow

models.py

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

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

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('post_detail', kwargs={'slug': self.slug})

    def save(self, *args, **kwargs):
        if not self.slug:
            base_slug = slugify(self.title)
            slug = base_slug
            counter = 1

            while Post.objects.filter(slug=slug).exists():
                slug = f"{base_slug}-{counter}"
                counter += 1

            self.slug = slug

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

views.py

from django.views.generic import ListView, DetailView
from .models import Post

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    slug_field = 'slug'
    slug_url_kwarg = 'slug'

urls.py

from django.urls import path
from .views import PostListView, PostDetailView

urlpatterns = [
    path('posts/', PostListView.as_view(), name='post_list'),
    path('posts/<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
]

post_list.html

<h1>Blog Posts</h1>

{% for post in posts %}
    <h2>
        <a href="{% url 'post_detail' post.slug %}">
            {{ post.title }}
        </a>
    </h2>
{% endfor %}

post_detail.html

<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>

This gives you a complete clean-URL blog structure.

15. Slugs in the Django Admin

If you register the model in admin:

from django.contrib import admin
from .models import Post

admin.site.register(Post)

you can manually edit slugs there.

A more user-friendly admin setup is:

from django.contrib import admin
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'slug']
    prepopulated_fields = {'slug': ('title',)}

What this does

16. Best Practices for Slugs

Keep slugs short and meaningful

Good:

django-class-based-views

Less ideal:

this-is-a-very-long-post-title-about-learning-django-class-based-views-in-2026

Use unique slugs

Avoid conflicts between objects.

Do not change slugs too often

Changing slugs can break old links and hurt SEO.

Use slugs for public content

They are especially useful for blog posts, products, categories, courses, and public profiles.

17. Common Mistakes

Forgetting unique=True

Without it, two objects may get the same slug.

Forgetting blank=True when auto-generating

If the slug is created in save(), forms/admin usually need blank=True.

Not handling duplicates

If two titles are the same, save may fail.

Using spaces or special characters manually

Slugs should remain URL-safe.

Retrieving by id while URL uses slug

Make sure your view and URL match.

Wrong:

post = get_object_or_404(Post, id=slug)

Correct:

post = get_object_or_404(Post, slug=slug)

18. Slugs vs IDs

Sometimes developers use both.

Example:

/posts/15/django-slugs-and-clean-urls/

This combines:

This is common in larger applications, but for beginners, using just the slug is simpler and easier to understand.

19. Mini Project Example: Product Pages

Let us imagine an online store.

models.py

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

class Product(models.Model):
    name = models.CharField(max_length=150)
    slug = models.SlugField(unique=True, blank=True)
    description = models.TextField()
    price = models.DecimalField(max_digits=8, decimal_places=2)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('product_detail', kwargs={'slug': self.slug})

    def save(self, *args, **kwargs):
        if not self.slug:
            base_slug = slugify(self.name)
            slug = base_slug
            counter = 1

            while Product.objects.filter(slug=slug).exists():
                slug = f"{base_slug}-{counter}"
                counter += 1

            self.slug = slug

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

urls.py

path('products/<slug:slug>/', product_detail, name='product_detail')

Now product pages become:

/products/wireless-mouse/
/products/gaming-keyboard/
/products/usb-c-hub/

These are much better than numeric-only URLs.

20. Summary

In this tutorial, you learned that:

 

21. Mini Quiz

1. What is a slug?

A. A database password
B. A URL-friendly string
C. A template tag
D. A file path

2. Which Django field is used for slugs?

A. TextField
B. CharField
C. SlugField
D. URLField

3. Which Django utility converts text into a slug?

A. reverse()
B. slugify()
C. clean_url()
D. pathify()

4. Why is blank=True often used with slug fields?

A. To make the field invisible
B. To allow auto-generation when the form leaves it empty
C. To delete old slugs
D. To shorten URLs

5. Which URL pattern correctly captures a slug?

A. <int:id>
B. <str:name>
C. <slug:slug>
D. <url:path>

22. What Comes Next?

The next ideal tutorial is:

Tutorial: File Uploads in Django
Subject: uploading images and documents, configuring media files, and handling user-uploaded content.