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.
By the end of this tutorial, you will understand:
Before starting, you should already know:
DetailViewA slug is a URL-safe text string.
Example:
Introduction to Django Formsintroduction-to-django-formsA slug usually contains:
It should not contain:
Django provides a built-in field for this: SlugField.
Slugs are useful because they make URLs:
For example:
/products/42/is less clear than:
/products/wireless-keyboard/Let us start with a simple blog post model.
models.pyfrom 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.titletitle stores the post titleslug stores the clean URL versionunique=True ensures no two posts have the same slugAfter adding the field, run:
python manage.py makemigrations
python manage.py migrateYou can enter slugs manually in Django admin or forms.
Example:
Django Models Guidedjango-models-guideThis 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.
slugifyDjango provides a utility called slugify.
from django.utils.text import slugify
title = "Django Slugs and Clean URLs"
print(slugify(title))Output:
django-slugs-and-clean-urlsIt converts text into a URL-friendly slug.
save()A common approach is to generate the slug automatically when saving the object.
models.pyfrom 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)blank=True allows the form/admin to leave it emptyNow that each post has a slug, we can use it in the URL.
urls.pyfrom django.urls import path
from .views import post_detail
urlpatterns = [
path('posts/<slug:slug>/', post_detail, name='post_detail'),
]<slug:slug> means Django expects a slug in the URLslugExample URL:
/posts/django-slugs-and-clean-urls/views.pyfrom 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})get_object_or_404() searches for a matching postDetailViewSlugs work very well with class-based views too.
views.pyfrom 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.pyfrom django.urls import path
from .views import PostDetailView
urlpatterns = [
path('posts/<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
]slug_field = 'slug' tells Django which model field to useslug_url_kwarg = 'slug' tells Django which URL parameter contains the slugOnce your URLs use slugs, links should also use slugs.
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.
Suppose you create two posts with the same title:
Django TutorialDjango TutorialBoth would generate:
django-tutorial
But if slug is unique, the second one will fail.
So you need a way to make slugs unique.
A simple approach is to add a number when the slug already exists.
django-tutorialdjango-tutorial-1django-tutorial-2models.pyfrom 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)-1, -2, etc.get_absolute_url() with SlugsA very useful Django convention is get_absolute_url().
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)CreateView and UpdateView can redirect automaticallypost.get_absolute_url()models.pyfrom 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.pyfrom 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.pyfrom 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.
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',)}Good:
django-class-based-viewsLess ideal:
this-is-a-very-long-post-title-about-learning-django-class-based-views-in-2026Avoid conflicts between objects.
Changing slugs can break old links and hurt SEO.
They are especially useful for blog posts, products, categories, courses, and public profiles.
unique=TrueWithout it, two objects may get the same slug.
blank=True when auto-generatingIf the slug is created in save(), forms/admin usually need blank=True.
If two titles are the same, save may fail.
Slugs should remain URL-safe.
id while URL uses slugMake sure your view and URL match.
Wrong:
post = get_object_or_404(Post, id=slug)Correct:
post = get_object_or_404(Post, slug=slug)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.
Let us imagine an online store.
models.pyfrom 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.pypath('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.
In this tutorial, you learned that:
SlugField and slugifysave()get_absolute_url() works very well with slugs
A. A database password
B. A URL-friendly string
C. A template tag
D. A file path
A. TextField
B. CharField
C. SlugField
D. URLField
A. reverse()
B. slugify()
C. clean_url()
D. pathify()
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
A. <int:id>
B. <str:name>
C. <slug:slug>
D. <url:path>
The next ideal tutorial is:
Tutorial: File Uploads in Django
Subject: uploading images and documents, configuring media files, and handling user-uploaded content.