Слаги в URL в Django: короткая инструкция для новичка
Как заменить числовой id в адресе на читаемый слаг вроде /blog/kak-nastroit-slug/ — от поля в модели до маршрута и шаблона. Займёт около 20 минут.

К концу этой инструкции у вас будет страница, которая открывается по адресу вида /blog/kak-nastroit-slug/ вместо /blog/42/. Мы добавим поле SlugField в модель, научим его заполняться автоматически из заголовка, пропишем маршрут и подтянем объект во вьюхе. Всё на Django 5.x, но для 4.2 LTS шаги те же. По времени — около 20 минут, если проект уже развёрнут.
Что нужно иметь до начала
- Установленный Python 3.10+ и Django 5.x (проверьте:
python -m django --version). - Готовый проект и хотя бы одно приложение — в примере оно называется
blog. - Модель, у которой уже есть текстовое поле для заголовка (у нас
title). - Доступ к консоли, где вы запускаете
manage.py.
Если модель ещё пустая — ничего страшного, весь нужный код ниже.
Что такое слаг
Слаг (slug) — короткая строка из строчных латинских букв, цифр и дефисов, которая используется в URL вместо числового идентификатора. Например, из заголовка «Как настроить слаг» получается kak-nastroit-slug. Слаг делает адрес читаемым и чуть лучше воспринимается поисковиками, чем голый id.
Шаг 1. Добавляем поле в модель
Откройте blog/models.py и добавьте SlugField. Параметр unique=True нужен, чтобы два разных объекта не получили одинаковый адрес.
# blog/models.py — Django 5.x
from django.db import models
from django.utils.text import slugify
class Post(models.Model):
title = models.CharField("Заголовок", max_length=200)
slug = models.SlugField("Слаг", max_length=200, unique=True, blank=True)
body = models.TextField("Текст", blank=True)
def save(self, *args, **kwargs):
# Если слаг не задан вручную — генерируем из заголовка
if not self.slug:
self.slug = slugify(self.title, allow_unicode=False)
super().save(*args, **kwargs)
def __str__(self):
return self.title
Здесь slugify из django.utils.text сам переводит строку в нижний регистр и заменяет пробелы дефисами. С allow_unicode=False кириллица будет отброшена, и из «Привет мир» получится пустая строка — про это в разделе про ошибки.
Проверка: файл сохраняется без синтаксических ошибок, редактор не подчёркивает импорты.
Шаг 2. Создаём и применяем миграцию
В консоли из корня проекта:
python manage.py makemigrations blog
python manage.py migrate
Проверка: после первой команды в консоли появится строка вида Add field slug to post, после второй — Applying blog.0002... OK.
Если в таблице уже есть записи, Django спросит, чем заполнить slug у существующих строк. Про это ниже — сначала проще потренироваться на пустой таблице.
Шаг 3. Прописываем маршрут со слагом
Откройте blog/urls.py (создайте файл, если его нет) и добавьте путь с конвертером slug. Он ловит только допустимые символы слага — буквы, цифры, дефис, подчёркивание.
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("blog/<slug:slug>/", views.post_detail, name="post_detail"),
]
Подключите этот файл в корневом config/urls.py:
# config/urls.py
from django.urls import path, include
urlpatterns = [
path("", include("blog.urls")),
]
Проверка: python manage.py check завершается без ошибок.
Шаг 4. Достаём объект во вьюхе по слагу
В blog/views.py ищем запись не по pk, а по полю slug. get_object_or_404 сам вернёт страницу 404, если такого слага нет.
# blog/views.py
from django.shortcuts import get_object_or_404, render
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})
Проверка: имя параметра во вьюхе (slug) совпадает с именем в маршруте — иначе будет ошибка про неожиданный аргумент.
Шаг 5. Ссылаемся на страницу из шаблона
Чтобы не собирать адрес руками, добавьте метод get_absolute_url в модель и используйте тег {% url %}.
# дополнение к blog/models.py
from django.urls import reverse
# внутри класса Post:
def get_absolute_url(self):
return reverse("blog:post_detail", kwargs={"slug": self.slug})
В шаблоне списка постов:
<!-- blog/templates/blog/post_list.html -->
{% for post in posts %}
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
{% endfor %}
Проверка: запустите python manage.py runserver, создайте пост в админке, кликните по ссылке — в адресной строке появится /blog/ваш-слаг/, и откроется нужная страница.
Слаг должен быть неизменным после публикации. Меняете его — старый адрес отдаёт 404, ломаются внешние ссылки и закладки. Если менять всё же нужно, настраивайте редирект со старого слага на новый.
Если не получилось
NoReverseMatch: Reverse for 'post_detail' not found— не совпадает пространство имён. Проверьте, что вurls.pyзаданapp_name = "blog", а в шаблоне вы пишетеblog:post_detail, а не простоpost_detail.- Страница отдаёт 404 при верном адресе — слаг в базе не совпадает с тем, что в URL. Зайдите в админку и посмотрите точное значение поля
slugу объекта. IntegrityError: UNIQUE constraint failed— два поста дали одинаковый слаг. Так бывает при одинаковых заголовках. Решение — в разделе ниже.- Слаг получился пустым — заголовок был на кириллице, а
slugifyсallow_unicode=Falseотбросил все символы. Либо задайте слаг руками в админке, либо см. следующий раздел. post_detail() got an unexpected keyword argument— имя параметра вpath()не совпадает с именем аргумента функции. Оба должны называтьсяslug.
Кириллица и повторяющиеся слаги
Два частых случая, на которые новичок натыкается сразу же после первого рабочего примера.
Русские заголовки. Если вам нужны слаги вроде privet-mir из русского текста, чистый slugify не транслитерирует — он просто выкидывает нелатинские символы. Для транслитерации ставят отдельную библиотеку и прогоняют заголовок через неё перед slugify. Альтернатива — разрешить кириллицу в самом слаге через slugify(self.title, allow_unicode=True); тогда в URL будут русские буквы в закодированном виде.
Одинаковые заголовки. Чтобы unique=True не ронял сохранение, добавляйте суффикс, если слаг занят:
# внутри save() модели Post
if not self.slug:
base = slugify(self.title, allow_unicode=False)
candidate = base
counter = 2
# Ищем свободный слаг: base, base-2, base-3 ...
while Post.objects.filter(slug=candidate).exists():
candidate = f"{base}-{counter}"
counter += 1
self.slug = candidate
Этот вариант делает лишние запросы к базе при сохранении, но для блога с ручным добавлением постов это несущественно.
Что дальше
Базовый слаг работает. Дальше стоит посмотреть в сторону автозаполнения поля в админке: prepopulated_fields = {"slug": ("title",)} в вашем ModelAdmin заполнит слаг из заголовка прямо в форме через JavaScript, ещё до сохранения. Уникальность в паре с датой (unique_for_date) и редиректы со старых слагов — отдельные темы, которые здесь не разбираем. Точный список параметров SlugField для вашей версии сверяйте в официальной документации Django.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Django SlugField — официальная документация, django.utils.text.slugify — документация
Частые вопросы
А если у меня Django 4.2, а не 5.x?
Что делать, если посты уже есть в базе, а поля slug у них нет?
Можно ли оставить в URL и id, и слаг одновременно?
Почему у меня в адресе появились русские буквы в странном виде вроде %D0%BF?
Не сработало: страница открывается по id, а не по слагу. В чём дело?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.