URL в Django: path, reverse, статика и медиа
Как устроены URL в Django: urls.py и include, path и конвертеры, reverse и тег url, отправка POST из формы, STATIC_URL и MEDIA_URL, разбор NoReverseMatch и 404.
Содержание · 11 разделов
- Как Django выбирает view по URL
- urls.py проекта и приложений
- path() и конвертеры
- Встроенные конвертеры
- Свой конвертер
- re_path: регулярные выражения
- Порядок маршрутов
- Имена маршрутов и пространства имён
- reverse, reverse_lazy и тег url: обратное построение URL
- reverse() во view
- reverse_lazy() в атрибутах классов
- Тег url в шаблоне
- get_absolute_url: канонический адрес объекта
- POST-запрос на URL: форма, action и CSRF
- Статика: STATIC_URL, STATICFILES_DIRS и STATIC_ROOT
- Статика в разработке и в продакшне
- Медиа: MEDIA_URL и MEDIA_ROOT
- Раздача медиа в режиме разработки
- Статика и медиа: в чём разница
- Частые ошибки с URL в Django
- NoReverseMatch
- 404 на статику или медиа
- RuntimeError: You called this URL via POST, but the URL doesn’t end in a slash
- 403 CSRF verification failed
- Страница отдаёт 404, хотя маршрут есть
- Частые вопросы
- Как посмотреть все URL проекта?
- Как получить текущий URL и имя маршрута?
- Как передать в URL GET-параметры?
- Как настроить редирект со старого адреса на новый?
- Как сделать свою страницу 404?
- Как поменять адрес админки?
- Итог
URL в Django задаются в файле urls.py: список urlpatterns сопоставляет адрес с view через функцию path(), а каждому маршруту дают имя name. По имени адрес строят обратно — функцией reverse() в Python и тегом {% url %} в шаблоне, поэтому ссылки не приходится писать руками. Статические файлы доступны по префиксу STATIC_URL, загруженные пользователями — по MEDIA_URL, и в режиме разработки медиа подключают к маршрутам через static().
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.post_list, name='post_list'),
path('<slug:slug>/', views.post_detail, name='post_detail'),
]
from django.urls import reverse
reverse('blog:post_detail', kwargs={'slug': 'hello'}) # → '/blog/hello/'
Ниже разберём маршруты проекта и приложений, конвертеры, пространства имён, отправку POST-формы на нужный URL, настройку статики и медиа и типичные ошибки. Примеры проверены на Django 6.1 в виртуальном окружении; отличия от 5.2 LTS отмечены в тексте.
Как Django выбирает view по URL
Когда приходит запрос, Django берёт корневой модуль маршрутов из настройки ROOT_URLCONF (после startproject это config.urls) и перебирает urlpatterns сверху вниз. Первый совпавший шаблон вызывает свой view, остальные уже не проверяются. Если ничего не совпало, Django отвечает 404, а при DEBUG = True показывает список маршрутов, которые он перебрал.
При сопоставлении учитывается только путь:
- домен и строка запроса не участвуют: для
https://example.com/blog/?page=2Django сравнивает с шаблонами строкуblog/, аpageчитают во view изrequest.GET; - HTTP-метод тоже не учитывается: GET и POST на один адрес попадают в один и тот же view, и различать их нужно внутри view;
- ведущий слэш в шаблонах не пишут:
path('blog/', ...), неpath('/blog/', ...). Иначеmanage.py checkвыдаст предупреждениеurls.W002.
Весь путь запроса от сервера через middleware до view описан в руководстве по Django для начинающих.
urls.py проекта и приложений
В корневом urls.py держат только подключения: админку и маршруты приложений через include(). Маршруты конкретного раздела живут в urls.py своего приложения — этот файл startapp не создаёт, его добавляют вручную (подробно — в статье «Как создать приложение в Django»).
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
]
include() отрезает совпавшую часть адреса и передаёт остаток во вложенный модуль. Запрос /blog/hello/ сначала совпадает с префиксом blog/, затем строка hello/ сравнивается с шаблонами из blog/urls.py. Префикс задаётся в одном месте: поменяете 'blog/' на 'articles/' — изменятся все адреса приложения.
path() и конвертеры
Сигнатура функции — path(route, view, kwargs=None, name=None). В route части адреса в угловых скобках захватываются и передаются во view именованными аргументами: шаблон <int:pk> превратит /blog/42/ в вызов post_detail(request, pk=42).
Встроенные конвертеры
| Конвертер | Что совпадает | Тип во view | Пример |
|---|---|---|---|
str | любая непустая строка без / | str | <str:username> |
int | ноль или положительное целое | int | <int:pk> |
slug | латинские буквы, цифры, дефис и подчёркивание | str | <slug:slug> |
uuid | UUID в нижнем регистре с дефисами | uuid.UUID | <uuid:token> |
path | непустая строка, включая / | str | <path:file_path> |
Запись без конвертера, например <username>, равна <str:username>. Если значение не подходит под конвертер, маршрут просто не совпадает: /blog/abc/ не попадёт в <int:pk>/, и Django проверит следующие шаблоны.
Важно: конвертер
slugне принимает кириллицу. Для адреса вида/blog/привет/нуженstrили свой конвертер.
Свой конвертер
Когда встроенных не хватает, пишут класс с атрибутом regex и методами to_python() и to_url(). Конвертер года из четырёх цифр:
# blog/converters.py
class YearConverter:
regex = r'[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return f'{value:04d}'
# blog/urls.py
from django.urls import path, register_converter
from . import converters, views
register_converter(converters.YearConverter, 'yyyy')
urlpatterns = [
path('archive/<yyyy:year>/', views.archive, name='archive'),
]
/blog/archive/2026/ передаст во view year=2026 типа int, а /blog/archive/26/ вернёт 404. Метод to_url() работает при построении ссылок: reverse('blog:archive', args=[7]) даст /blog/archive/0007/. Если to_python() или to_url() выбрасывает ValueError, Django считает, что маршрут не совпал.
Начиная с Django 6.0 register_converter() не даёт переопределить уже зарегистрированное имя: попытка зарегистрировать свой класс как int завершится ошибкой ValueError: Converter 'int' is already registered.
re_path: регулярные выражения
Для сложных шаблонов есть re_path() — он принимает регулярное выражение с именованными группами:
from django.urls import re_path
urlpatterns = [
re_path(
r'^archive/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$',
views.archive_month,
name='archive_month',
),
]
Группы из re_path() всегда приходят во view строками — '2026' и '05', без преобразования в числа. Если задачу решает path() со своим конвертером, выбирайте его: шаблон короче, а значение приходит нужного типа.
Порядок маршрутов
Общие шаблоны ставьте ниже конкретных. В таком списке адрес /blog/subscribe/ никогда не дойдёт до своего view: его раньше перехватит <slug:slug>/, потому что subscribe — допустимый slug.
urlpatterns = [
path('<slug:slug>/', views.post_detail, name='post_detail'),
path('subscribe/', views.subscribe, name='subscribe'), # недостижим
]
Правильный порядок — сначала subscribe/, затем <slug:slug>/. То же касается «ловушек» вида <path:page>/: поставленные выше static(), они перехватят запросы к /media/.
Имена маршрутов и пространства имён
Аргумент name даёт маршруту имя, по которому строят ссылки. В разных приложениях часто встречаются одинаковые имена — index, detail, create, — поэтому маршруты приложения помещают в пространство имён (namespace) атрибутом app_name в его urls.py:
# blog/urls.py
app_name = 'blog'
urlpatterns = [
path('', views.post_list, name='post_list'),
path('subscribe/', views.subscribe, name='subscribe'),
path('archive/<yyyy:year>/', views.archive, name='archive'),
path('<slug:slug>/', views.post_detail, name='post_detail'),
]
Теперь к маршрутам обращаются как 'blog:post_list' и 'blog:post_detail'. Без префикса reverse('post_list') вызовет NoReverseMatch.
Аргумент namespace в include('blog.urls', namespace='news') задаёт имя экземпляра: так одно приложение подключают дважды под разными префиксами. Без app_name во вложенном модуле Django откажется запускаться с ошибкой ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported.
reverse, reverse_lazy и тег url: обратное построение URL
Обратное построение (reverse) — получение адреса по имени маршрута и аргументам. Оно защищает от битых ссылок: адрес меняют в urls.py, а ссылки во view и шаблонах обновляются сами.
reverse() во view
from django.urls import reverse
reverse('blog:post_list') # → '/blog/'
reverse('blog:post_detail', args=['hello']) # → '/blog/hello/'
reverse('blog:post_detail', kwargs={'slug': 'hello'}) # → '/blog/hello/'
reverse('blog:post_list', query={'page': 2, 'q': 'django url'}, fragment='top')
# → '/blog/?page=2&q=django+url#top'
args и kwargs вместе передавать нельзя. Параметры query и fragment появились в Django 5.2; в более старых версиях строку запроса склеивают вручную через urllib.parse.urlencode().
Для перенаправления удобнее redirect(): он принимает имя маршрута с аргументами и сам вызывает reverse().
from django.shortcuts import redirect
return redirect('blog:post_detail', slug=post.slug)
Полный адрес с протоколом и доменом — для писем, API, ссылок в мессенджерах — получают из запроса: request.build_absolute_uri(reverse('blog:post_detail', args=[post.slug])).
reverse_lazy() в атрибутах классов
reverse() нельзя вызывать при импорте модуля — в атрибутах class-based views, аргументах декораторов, настройках. В этот момент urls.py ещё не загружен, и Django падает с ошибкой:
django.core.exceptions.ImproperlyConfigured: The included URLconf 'config.urls' does not appear to have any patterns in it. If you see the 'urlpatterns' variable with valid patterns in the file then the issue is probably caused by a circular import.
Для таких мест есть reverse_lazy(): он вычисляет адрес при первом использовании.
from django.urls import reverse_lazy
from django.views.generic import CreateView
from .models import Post
class PostCreate(CreateView):
model = Post
fields = ['title', 'slug']
success_url = reverse_lazy('blog:post_list')
Тег url в шаблоне
В шаблонах ссылки строят тегом {% url %}. Имя маршрута пишут в кавычках, аргументы — через пробел позиционно или по имени:
<a href="{% url 'blog:post_list' %}">Все посты</a>
<a href="{% url 'blog:post_detail' post.slug %}">{{ post.title }}</a>
<a href="{% url 'blog:post_detail' slug=post.slug %}">{{ post.title }}</a>
{% url 'blog:archive' 2026 as archive_url %}
<a href="{{ archive_url }}">Архив за 2026 год</a>
Форма as archive_url сохраняет адрес в переменную и не выбрасывает ошибку, если маршрут не найден, — переменная остаётся пустой. Остальные теги шаблонов разобраны в статье «Шаблоны Django».
get_absolute_url: канонический адрес объекта
Модели, у которых есть своя страница, получают метод get_absolute_url(). Он хранит правило построения адреса в одном месте:
# blog/models.py
from django.db import models
from django.urls import reverse
class Post(models.Model):
title = models.CharField('Заголовок', max_length=200)
slug = models.SlugField(unique=True)
cover = models.ImageField('Обложка', upload_to='covers/', blank=True)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', kwargs={'slug': self.slug})
Метод используют сам Django и сторонние пакеты:
- в шаблоне:
<a href="{{ post.get_absolute_url }}">— метод вызывается без скобок; redirect(post)перенаправляет наpost.get_absolute_url();CreateViewиUpdateViewбезsuccess_urlпосле сохранения ведут на страницу объекта;- в админке на странице объекта появляется кнопка «Смотреть на сайте».
Поля моделей и Meta подробно описаны в статье «Модели Django».
POST-запрос на URL: форма, action и CSRF
Отправить POST на нужный адрес — значит указать его в атрибуте action формы. Адрес строят тегом {% url %}, а внутрь формы добавляют {% csrf_token %}:
<form method="post" action="{% url 'blog:subscribe' %}">
{% csrf_token %}
<input type="email" name="email" required>
<button type="submit">Подписаться</button>
</form>
Браузер получит такой HTML:
<form method="post" action="/blog/subscribe/">
<input type="hidden" name="csrfmiddlewaretoken" value="..." />
<input type="email" name="email" required />
<button type="submit">Подписаться</button>
</form>
View, который принимает только POST:
# blog/views.py
from django.shortcuts import redirect
from django.views.decorators.http import require_POST
@require_POST
def subscribe(request):
email = request.POST.get('email', '')
# ...сохранить подписку...
return redirect('blog:post_list')
Что здесь важно:
- Метод проверяет view. Маршрут один для всех методов. Декоратор
@require_POSTотвечает405 Method Not Allowedна GET; если форма и обработчик живут в одном view, ветвятся поrequest.method == 'POST'. - Пустой
actionили отсутствие атрибута отправляет форму на текущий адрес страницы. Это обычный вариант, когда один view и показывает форму, и обрабатывает её. - Относительный
actionвидаaction="subscribe/"браузер склеит с адресом текущей страницы: со страницы/blog/form/форма уйдёт на/blog/form/subscribe/. Пишите адрес через{% url %}. - Без
{% csrf_token %}Django отклонит запрос с403 Forbiddenи сообщениемCSRF verification failed. Request aborted.Защиту от CSRF (подделки межсайтовых запросов) выполняетCsrfViewMiddleware. - После успешного POST делайте
redirect(). Если вернуть HTML сразу, обновление страницы в браузере отправит форму повторно. - Слэш в конце адреса обязателен, если маршрут объявлен со слэшем, — иначе POST упадёт, подробности в разделе об ошибках ниже.
Для загрузки файлов у формы должен быть атрибут enctype="multipart/form-data", а во view файлы приходят в request.FILES. Классы Form и ModelForm, валидацию и вывод ошибок разбирает статья «Формы в Django».
Статика: STATIC_URL, STATICFILES_DIRS и STATIC_ROOT
Статические файлы — CSS, JavaScript, шрифты и картинки оформления, которые вы кладёте в проект сами. За них отвечает приложение django.contrib.staticfiles (подключено в новом проекте) и три настройки:
# config/settings.py
STATIC_URL = 'static/' # префикс адреса: /static/css/site.css
STATICFILES_DIRS = [BASE_DIR / 'static'] # общие исходники проекта
STATIC_ROOT = BASE_DIR / 'staticfiles' # сюда collectstatic собирает всё для продакшна
STATIC_URLесть вsettings.pyсразу послеstartproject. Значение без ведущего слэша ('static/') Django дополнит до/static/, а при развёртывании в подпапку — префиксом этой подпапки.- Файлы приложения кладут в
blog/static/blog/— их Django находит сам. Второй уровеньblog/нужен, чтобы одинаковые имена файлов из разных приложений не перекрывали друг друга. STATICFILES_DIRSнужен для общих файлов вне приложений, например папкиstatic/в корне проекта.STATIC_ROOTдолжен отличаться от папок изSTATICFILES_DIRS, иначеmanage.py checkвыдаст ошибкуstaticfiles.E002.
В шаблоне адрес строит тег static:
{% load static %}
<link rel="stylesheet" href="{% static 'css/site.css' %}">
<link rel="stylesheet" href="/static/css/site.css" />
Статика в разработке и в продакшне
При DEBUG = True команда runserver раздаёт статику сама — дополнительные маршруты в urls.py для неё не нужны. Найти, откуда Django берёт конкретный файл, помогает findstatic:
python manage.py findstatic css/site.css
Found 'css/site.css' here:
C:\projects\mysite\static\css\site.css
При DEBUG = False Django перестаёт отдавать статику, и адреса /static/... возвращают 404 — так задумано. В продакшне выполняют collectstatic, который копирует файлы всех приложений и STATICFILES_DIRS в STATIC_ROOT, а раздаёт эту папку Nginx или пакет WhiteNoise:
python manage.py collectstatic --noinput
Флаг runserver --insecure заставляет сервер разработки раздавать статику и при DEBUG = False. Он годится, чтобы проверить страницу ошибки 404 локально, но не для боевого сервера. Настройка Nginx для /static/ описана в статье «Деплой Django: Gunicorn и Nginx».
Медиа: MEDIA_URL и MEDIA_ROOT
Медиафайлы — то, что загружают пользователи и администраторы: аватары, обложки, документы. Их адресом и папкой управляют две настройки:
# config/settings.py
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
Поле FileField или ImageField сохраняет файл в MEDIA_ROOT с учётом upload_to, а в базе хранит только относительный путь. Для ImageField нужен пакет Pillow (python -m pip install pillow). Адрес файла даёт атрибут .url:
post.cover.name # → 'covers/cat.jpg'
post.cover.url # → '/media/covers/cat.jpg'
post.cover.path # → полный путь на диске внутри MEDIA_ROOT
{% if post.cover %}
<img src="{{ post.cover.url }}" alt="{{ post.title }}">
{% endif %}
Проверка {% if post.cover %} обязательна для необязательного поля: у пустого файла обращение к .url вызывает ValueError.
Раздача медиа в режиме разработки
Медиа runserver автоматически не раздаёт. В разработке маршрут добавляют функцией static() в корневой urls.py:
# config/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
static() работает только при DEBUG = True и только с локальным префиксом вида /media/: при выключенном DEBUG или префиксе с доменом (https://cdn.example.com/...) функция возвращает пустой список маршрутов. Проверка if settings.DEBUG подчёркивает это для читателя кода. В продакшне медиа раздаёт веб-сервер или внешнее хранилище, например S3.
Статика и медиа: в чём разница
| Параметр | Статика | Медиа |
|---|---|---|
| Что это | CSS, JS, шрифты, картинки оформления | Файлы, загруженные через сайт или админку |
| Кто добавляет | Разработчик, вместе с кодом | Пользователи во время работы сайта |
| Префикс адреса | STATIC_URL | MEDIA_URL |
| Папка | STATIC_ROOT (после collectstatic) | MEDIA_ROOT |
| В шаблоне | тег static | .url поля модели |
| Раздача в DEBUG | runserver сам | маршрут static() в urls.py |
| В Git | исходники да, STATIC_ROOT — нет | нет |
MEDIA_URL и STATIC_URL должны различаться: при одинаковых значениях collectstatic остановится с ImproperlyConfigured: The MEDIA_URL and STATIC_URL settings must have different values. Папки MEDIA_ROOT и STATIC_ROOT тоже держите раздельно: collectstatic --clear очищает STATIC_ROOT целиком.
Частые ошибки с URL в Django
NoReverseMatch
NoReverseMatch: Reverse for 'post_detail' with arguments '('',)' not found. 1 pattern(s) tried: ['blog/(?P<slug>[-a-zA-Z0-9_]+)/\\Z']
Django не смог построить адрес по имени и аргументам. Сообщение подсказывает причину:
'post_list' is not a valid view function or pattern name— опечатка в имени, маршрут не подключён черезinclude()или у приложения естьapp_name, а вы обращаетесь без префиксаblog:;with no arguments not found— маршрут требует аргумент, а его не передали;with arguments '('',)'— аргумент пустой: переменной нет в контексте шаблона или у объекта не заполнено поле, какpost.slugвыше;- аргумент передан, но не подходит под конвертер: кириллица для
slug, буквы дляint; Could not parse the remainder: ':post_list'— имя в{% url %}написано без кавычек.
404 на статику или медиа
Проверьте по порядку:
DEBUG = True? При выключенном режиме отладкиrunserverне отдаёт ни статику, ни медиа.- В шаблоне есть
{% load static %}, а путь передан через тегstatic, без жёстко прописанногоhref="static/css/site.css". Относительный адрес без начального слэша браузер склеит с текущей страницей, и на/blog/hello/он превратится в/blog/hello/static/css/site.css. - Файл находится командой
findstatic. Если нет — папка не указана вSTATICFILES_DIRSили приложение не добавлено вINSTALLED_APPS. - Для медиа в корневом
urls.pyподключёнstatic(settings.MEDIA_URL, ...), а выше нет «ловушки» вроде<path:page>/, которая перехватывает/media/.... - В продакшне выполнен
collectstatic, аlocation /static/в Nginx указывает наSTATIC_ROOT.
RuntimeError: You called this URL via POST, but the URL doesn’t end in a slash
RuntimeError: You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining POST data. Change your form to point to 127.0.0.1:8000/blog/hello/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.
Настройка APPEND_SLASH включена по умолчанию: если адрес без слэша не совпал ни с одним маршрутом, а со слэшем совпал, CommonMiddleware отвечает редиректом 301 на адрес со слэшем. Для GET это незаметно. POST при редиректе превращается в GET и теряет данные формы, поэтому при DEBUG = True Django останавливается с этой ошибкой, а при DEBUG = False молча перенаправляет — и данные пропадают.
Исправление — отправлять форму на адрес со слэшем, то есть строить action через {% url %}. Отключать APPEND_SLASH не нужно. Как устроены CommonMiddleware и другие слои обработки запроса — в статье «Django Middleware».
403 CSRF verification failed
В POST-форме нет {% csrf_token %}, или страницу отрисовали без request — например, через render_to_string() без этого аргумента. Используйте render(request, ...). Для запросов из JavaScript передавайте токен в заголовке X-CSRFToken.
Страница отдаёт 404, хотя маршрут есть
Обычно виноват порядок: маршрут выше перехватил адрес (пример с subscribe/ и <slug:slug>/). Другие причины — не совпал слэш в конце, urls.py приложения не подключён через include() или префикс в include() записан без завершающего слэша: path('blog', include(...)) вместе с path('post/', ...) даст адрес /blogpost/.
Частые вопросы
Как посмотреть все URL проекта?
Встроенной команды для этого нет. При DEBUG = True список маршрутов показывает страница 404. Полный список с именами и view выводит команда python manage.py show_urls из пакета django-extensions.
Как получить текущий URL и имя маршрута?
Во view: request.path — путь без строки запроса (/blog/where/abc/), request.get_full_path() — вместе с ней (/blog/where/abc/?page=3). Имя маршрута хранит request.resolver_match.url_name, полное имя с пространством имён — request.resolver_match.view_name (blog:where). В шаблоне те же значения доступны через request, например для подсветки активного пункта меню.
Как передать в URL GET-параметры?
GET-параметры не описывают в path(). Во view их читают из request.GET.get('page'), а ссылку с ними строят reverse(..., query={'page': 2}) в Django 5.2 и новее или дописывают вручную в шаблоне: {% url 'blog:post_list' %}?page=2.
Как настроить редирект со старого адреса на новый?
Для постоянного переезда подойдёт RedirectView прямо в urls.py — он отдаст 301:
from django.views.generic import RedirectView
urlpatterns = [
path('news/', RedirectView.as_view(pattern_name='blog:post_list', permanent=True)),
]
Внутри view используйте redirect('blog:post_list', permanent=True).
Как сделать свою страницу 404?
Положите шаблон 404.html в общую папку шаблонов — Django использует его при DEBUG = False. Если нужна логика, объявите в корневом urls.py переменную handler404 = 'blog.views.page_not_found' с view вида page_not_found(request, exception), который возвращает ответ со статусом 404.
Как поменять адрес админки?
Замените префикс в корневом urls.py: path('control-panel/', admin.site.urls). Ссылки внутри админки строятся через reverse(), поэтому обновятся сами. Зачем это делать и какие ещё меры защищают вход в админку — в статье «Защита админки Django».
Итог
Маршруты описывают в urls.py приложений и подключают в корневой urls.py через include(). Для параметров берите path() с конвертерами, для сложных шаблонов — свой конвертер или re_path(), а конкретные маршруты ставьте выше общих. Каждому маршруту давайте name, приложению — app_name, и стройте адреса через reverse(), reverse_lazy(), {% url %} и get_absolute_url(). POST-формы отправляйте на адрес из {% url %} со слэшем в конце и с {% csrf_token %}. Статику подключайте тегом static с настройками STATIC_URL и STATIC_ROOT, загрузки — через MEDIA_URL, MEDIA_ROOT и static() в режиме разработки. Подробный справочник по маршрутизации — в документации Django.
- #Django
- #urls.py
- #reverse
- #Статика
- #Медиафайлы