Создание sitemap для Django-сайта — задача, которую многие разработчики решают через сторонние пакеты, не подозревая, что фреймворк содержит готовый модуль django.contrib.sitemaps. Он поддерживает генерацию как стандартных карт сайта, так и индексных файлов для проектов с миллионами страниц. В этой статье рассмотрим полный цикл: от подключения встроенного механизма до настройки кэширования, добавления изображений и автоматической отправки через IndexNow.
Встроенный механизм django.contrib.sitemaps
В Django 5.2 модуль django.contrib.sitemaps поставляется «из коробки». Он преобразует объекты QuerySet в XML-формат, соответствующий протоколу Sitemap. Вам не нужно писать собственные представления или вручную формировать разметку.
На одном из проектов с интернет-магазином на 50 000 товаров мы изначально генерировали sitemap через кастомный скрипт. При каждом изменении каталога приходилось вручную пересобирать файл. Переход на django.contrib.sitemaps сократил время настройки до 15 минут и исключил расхождения между реальными страницами и картой сайта.
Подключение модуля
Для активации sitemap-фреймворка добавьте две строки:
- В файле
settings.pyподключите приложение вINSTALLED_APPS:
INSTALLED_APPS = [
# ...
'django.contrib.sites',
'django.contrib.sitemaps',
# ...
]
Приложение django.contrib.sites также необходимо — оно предоставляет объект Site, который используется для формирования абсолютных URL.
- В корневом
urls.pyукажите маршруты для карты сайта.
from django.contrib.sitemaps.views import sitemap
from myapp.sitemaps import ArticleSitemap
sitemaps = {
'articles': ArticleSitemap,
}
urlpatterns = [
# ... ваши URL ...
path('sitemap.xml', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
]
После этого по адресу /sitemap.xml будет отдаваться действительный XML-документ. Если в проекте несколько секций (статьи, категории, статические страницы), вы можете передать словарь с несколькими классами. Django автоматически сгруппирует их или создаст индексный файл, если записей больше лимита.
Класс Sitemap
Базовый класс Sitemap определяет методы, которые вы переопределяете под свою модель. Рассмотрим основные атрибуты и методы.
| Атрибут / метод | Описание |
|---|---|
changefreq |
Строка с частотой обновления: "always", "hourly", "daily", "weekly", "monthly", "yearly", "never". Может быть задана как атрибут класса или переопределена методом changefreq(obj) для динамического управления. |
priority |
Приоритет от 0.0 до 1.0. По умолчанию 0.5. Аналогично можно задать статически или через метод. |
items() |
Возвращает QuerySet или список объектов, для которых будут сгенерированы записи в sitemap. Это основной метод — здесь важно правильно оптимизировать запрос к базе данных. |
location(obj) |
Возвращает абсолютный путь для объекта. По умолчанию вызывает obj.get_absolute_url(). Переопределите, если URL формируется нестандартно. |
lastmod(obj) |
Возвращает дату последнего изменения объекта. Поисковые системы используют это поле для принятия решения о повторной индексации. |
Пример для модели Article:
from django.contrib.sitemaps import Sitemap
from .models import Article
class ArticleSitemap(Sitemap):
changefreq = "weekly"
priority = 0.8
def items(self):
return Article.objects.filter(is_published=True)
def lastmod(self, obj):
return obj.updated_at
Метод lastmod критичен для новостных и часто обновляемых проектов. Если модель не имеет поля с датой правки, но вы используете поле DateTimeField с auto_now=True, то для QuerySet из нескольких тысяч записей лучше добавить индексацию, чтобы сортировка по дате не приводила к полному сканированию таблицы.
GenericSitemap
Когда структура всех типовых страниц одинакова (например, новости, блоги, страницы с простой моделью), можно не создавать отдельный класс для каждой модели, а использовать готовый GenericSitemap.
from django.contrib.sitemaps import GenericSitemap
from .models import Category
info_dict = {
'queryset': Category.objects.all(),
'date_field': 'updated_at',
}
sitemaps = {
'categories': GenericSitemap(info_dict, priority=0.6),
}
GenericSitemap принимает словарь с обязательным ключом queryset и опциональными date_field (соответствует lastmod) и priority. Он подходит для простых случаев. Однако как только требуется кастомная логика location или динамический priority, удобнее наследоваться от Sitemap.
Sitemap для разных моделей
Реальный проект редко состоит из одной модели. У вас могут быть статьи, категории, товары, страницы брендов. Каждый тип контента требует отдельной секции в sitemap. Django решает это через отдельные классы для каждой модели.
Например, для интернет-магазина можно организовать три класса:
# sitemaps.py
from django.contrib.sitemaps import Sitemap
from catalog.models import Product, Category, Brand
class ProductSitemap(Sitemap):
changefreq = "daily"
priority = 0.9
def items(self):
return Product.objects.filter(available=True).select_related('category').only('slug', 'updated_at')
def lastmod(self, obj):
return obj.updated_at
class CategorySitemap(Sitemap):
changefreq = "weekly"
priority = 0.7
def items(self):
return Category.objects.all().only('slug')
class BrandSitemap(Sitemap):
changefreq = "monthly"
priority = 0.6
def items(self):
return Brand.objects.filter(active=True).only('slug')
def location(self, obj):
# предполагаем, что URL бренда строится вручную
return f'/brands/{obj.slug}/'
Затем все классы регистрируются в словаре sitemaps:
sitemaps = {
'products': ProductSitemap,
'categories': CategorySitemap,
'brands': BrandSitemap,
}
При обращении к /sitemap.xml Django автоматически проверяет общее количество записей. Если оно превышает заданный лимит (SITEMAP_LIMIT), генерируется индексный sitemap, состоящий из ссылок на отдельные файлы секций. В противном случае все записи объединяются в один XML. Такое поведение избавляет от ручного дробления карты.
Важно оптимизировать метод items(). Запросы без select_related или prefetch_related могут порождать проблему N+1, когда для каждого объекта выполняется дополнительный запрос для получения связанных данных (например, URL категории). В примере выше мы использовали select_related('category') для товаров, чтобы заранее загрузить категорию одним JOIN. Если вам нужны только определённые поля, добавьте only() — это уменьшит объём передаваемых данных.
Карта сайта-индекс для крупных проектов
Стандартный протокол Sitemap ограничивает один файл 50 000 URL и размером 50 МБ. Django соблюдает эти ограничения через настройку SITEMAP_LIMIT (по умолчанию 50 000). Когда общее количество записей во всех классах превышает лимит, фреймворк автоматически создаёт индексный файл.
Индексный sitemap — это XML-документ, содержащий ссылки на несколько файлов карты. В каждом таком файле находится до 50 000 записей из одной или нескольких секций. Если в секции «товары» 120 000 записей, она будет разбита на три файла: sitemap-products.xml?p=1, ?p=2, ?p=3.
Проверка лимита происходит на уровне представления sitemap. Для принудительного уменьшения размера файла можно задать меньшее значение в settings.py:
# settings.py
SITEMAP_LIMIT = 10_000
На одном проекте с каталогом 200 000 товаров мы снизили лимит до 10 000. Это позволило генерировать файлы быстрее и уменьшило нагрузку на сервер при единовременной индексации. Однако количество файлов возросло до 20, что потребовало более аккуратной настройки кэширования.
При использовании индексного sitemap важно, чтобы все входящие в него файлы были доступны по стабильным URL. Django делает это автоматически: URL каждого подфайла содержит параметр ?p=номер_страницы. Поисковые системы корректно обрабатывают такие адреса.
Если вы используете CDN или статическое размещение, можно переопределить шаблон sitemap_index.xml и формировать имена файлов без GET-параметров. Однако для большинства проектов поведение по умолчанию удовлетворительно.
Кэширование sitemap
Генерация карты сайта на каждый запрос, особенно для крупных моделей, создаёт лишнюю нагрузку на базу данных. Решение — кэшировать ответ с инвалидацией при изменении данных.
Самый простой способ — обернуть представление декоратором cache_page:
from django.views.decorators.cache import cache_page
from django.contrib.sitemaps.views import sitemap
from .sitemaps import sitemaps
urlpatterns = [
path('sitemap.xml', cache_page(60 * 60)(sitemap),
{'sitemaps': sitemaps}),
]
Этот код кэширует ответ на один час (3600 секунд). Минус — при публикации новой статьи обновлённая карта будет недоступна в течение часа, пока кэш не истечёт. Для динамичных сайтов лучше использовать инвалидацию через сигналы.
Более гибкий подход:
- В настройках подключите кэш, например Redis.
- Создайте сигнал
post_saveиpost_deleteдля моделей, участвующих в sitemap. - В обработчике сигнала очищайте соответствующий ключ кэша.
# signals.py
from django.core.cache import cache
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import Article, Product
@receiver([post_save, post_delete], sender=Article)
@receiver([post_save, post_delete], sender=Product)
def invalidate_sitemap_cache(sender, **kwargs):
# ключ генерируется автоматически системой кэширования
# для точной инвалидации используйте cache.clear() или определённый ключ
cache.clear()
# Если используете шаблонный кэш или низкоуровневый API,
# можно удалять только нужный ключ, например:
# cache.delete('sitemap_cache_key')
Минус полной очистки кэша (cache.clear()) — сбрасываются все кэшированные данные проекта. В высоконагруженных системах это недопустимо. Вместо этого можно кэшировать результат работы каждого Sitemap класса отдельно. Для этого переопределите метод get_urls() и поместите результат в кэш с уникальным ключом, зависящим от модели и количества объектов. Тогда инвалидация по сигналу будет удалять только нужный кэш-ключ.
На практике для проекта с 15 000 товаров мы реализовали кэширование на уровне Nginx: sitemap-файлы периодически генерировались фоновой задачей и сохранялись в директорию, отдаваемую как статика. Django вообще не участвовал в обработке запросов к карте сайта. Это радикально снизило задержку. Но такое решение требует ручного управления файлами и не подходит для часто обновляемых данных.
Дополнительные возможности
Добавление изображений в sitemap
Для интернет-магазинов и фотогалерей полезно сообщать поисковикам о картинках. Django позволяет расширить стандартный XML через переопределение шаблона или собственный метод get_urls(). Официальная документация рекомендует создать подкласс и добавить метод images(), но на практике проще подключить пакет django.contrib.sitemaps и дополнить шаблон.
Можно использовать пакет django-sitemap-image (неофициальный, но стабильный). Или написать собственную реализацию:
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
class ProductImageSitemap(Sitemap):
def items(self):
return Product.objects.filter(available=True)
def location(self, obj):
return obj.get_absolute_url()
def _images(self, obj):
# возвращает список словарей с ключами 'location', 'title', 'caption'
images = []
for image in obj.images.all():
images.append({
'location': image.url,
'title': image.alt_text,
'caption': image.alt_text,
})
return images
Затем в шаблоне sitemap.xml (по умолчанию находится в django/contrib/sitemaps/templates/) нужно вывести тег <image:image>. Для этого создайте кастомный шаблон и укажите его в URL-маршруте через параметр template_name:
path('sitemap.xml', sitemap, {
'sitemaps': sitemaps,
'template_name': 'custom_sitemap.xml'
}, name='sitemap'),
В шаблоне перебираем список изображений для каждого URL. Такой подход добавляет вес файлу, но улучшает индексацию медиаконтента. Google и Яндекс поддерживают расширение Image Sitemap.
Мультиязычные sitemap
Для сайтов с несколькими языковыми версиями нужно указывать альтернативные ссылки (hreflang). Django не предоставляет встроенного решения, но его легко реализовать через переопределение шаблона.
Допустим, каждая статья имеет переводы, связанные через общий translation_id. В методе items() вы возвращаете основной объект, а в кастомном шаблоне получаете все переводы и генерируете XML вида:
<url>
<loc>https://example.com/en/article/123/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/article/123/" />
<xhtml:link rel="alternate" hreflang="ru" href="https://example.com/ru/article/123/" />
<lastmod>2026-03-15</lastmod>
</url>
По состоянию на 2026 год оба поисковика (Google и Яндекс) корректно обрабатывают sitemap с hreflang-аннотациями. Для крупных порталов с десятками языков лучше генерировать отдельные sitemap-файлы на каждый язык, чтобы избежать раздувания XML.
Оптимизация items() с select_related и only()
Метод items() выполняется при каждом запросе к карте (если кэш не используется). Неправильно составленный запрос может привести к сотням лишних обращений к базе. Основные правила:
- Используйте
select_relatedдля ForeignKey. - Используйте
prefetch_relatedдля ManyToMany или обратных связей, если они нужны в шаблоне. - Метод
only()ограничивает набор полей. Не включайте тяжелые текстовые поля, если они не участвуют в формировании URL или lastmod.
Пример для модели Article с автором и категорией:
def items(self):
return Article.objects.filter(is_published=True)\
.select_related('category', 'author')\
.only('slug', 'updated_at', 'category__slug', 'author__username')
Обратите внимание: only() заставляет Django создавать отложенные объекты, и при обращении к не указанным полям последуют дополнительные запросы. Убедитесь, что в шаблоне или дальнейшей логике не используются поля, отсутствующие в only().
Мониторинг и отправка sitemap поисковикам
Сгенерированную карту сайта необходимо регулярно отправлять в панели вебмастеров. Стандартный способ — добавить в robots.txt директиву Sitemap: https://example.com/sitemap.xml. Это позволяет поисковым роботам автоматически находить карту.
Для активного оповещения используйте вызов ping-URL после каждого обновления значимых страниц. Django позволяет автоматизировать это через сигналы и Celery:
# tasks.py (Celery)
from celery import shared_task
import requests
from django.urls import reverse
from django.conf import settings
@shared_task
def notify_search_engines():
sitemap_url = f'https://{settings.SITE_DOMAIN}/sitemap.xml'
engines = [
f'https://www.google.com/ping?sitemap={sitemap_url}',
f'https://www.bing.com/ping?sitemap={sitemap_url}',
# Яндекс принимает через IndexNow
]
for url in engines[:2]:
requests.get(url)
@shared_task
def submit_indexnow(url):
# Используем IndexNow API
api_url = 'https://api.indexnow.org/indexnow'
data = {
"host": settings.SITE_DOMAIN,
"key": settings.INDEXNOW_KEY,
"urlList": [url],
}
requests.post(api_url, json=data)
Сигнал post_save модели вызывает задачу Celery с новым или изменённым URL. Так поисковая система узнает о странице ещё до запланированного обхода. Этот подход особенно ценен для новостных сайтов и интернет-магазинов, где скорость индексации напрямую влияет на трафик.
Протокол IndexNow поддерживается Яндексом, Bing, Naver и рядом других систем с 2022 года. Google в 2025 году начал экспериментальную поддержку. Отправка уведомлений через IndexNow сокращает задержку между публикацией и появлением в поиске в среднем до нескольких минут.
Для удобной настройки IndexNow на Django-сайте можно воспользоваться специализированными сервисами. Один из таких — Index-Now.ru. Он предоставляет готовые ключи, дашборд статистики отправок и интеграцию через API. Вам не нужно вручную разбираться с лимитами или хранить ключи: сервис выступает прокси и гарантирует доставку уведомлений в поддерживаемые поисковые системы.
Частые вопросы
Нужно ли обновлять sitemap при изменении только мета-тегов, например Title или Description?
Формально sitemap не содержит эти поля, поэтому поисковик не узнает об изменениях из карты. Однако Google рекомендует отправлять уведомление через IndexNow и для незначительных правок, так как это сигнал к переобходу. На практике мы отправляем запрос в IndexNow при любом сохранении модели, если страница входит в sitemap. Это не нагружает систему, а положительный эффект заметен на новостных ресурсах.
Можно ли использовать django.contrib.sitemaps без django.contrib.sites?
Технически нет. Приложение sites необходимо для формирования абсолютных URL. Если вы не хотите управлять объектами Site, можно настроить SITE_ID = 1 и разрешить домен через переменную окружения в шаблоне, но полностью исключить приложение не получится.
Как исключить определённые URL из sitemap, например страницы с noindex?
В методе items() просто отфильтруйте нужные записи. Например, добавьте в модель булево поле noindex и исключите такие объекты: Article.objects.filter(is_published=True, noindex=False). Либо переопределите метод get_urls() и пропустите генерацию для отдельных URL.
Как проверить, что sitemap работает корректно и валиден?
Используйте Google Search Console: в разделе «Файлы Sitemap» добавьте URL вашей карты и дождитесь проверки. Для локального тестирования можно применить библиотеки lxml или встроенный валидатор XML. Также полезно проверить на сервере: запустите ./manage.py shell, импортируйте Sitemap-класс и выполните list(MySitemap().get_urls()), чтобы увидеть список сгенерированных записей и обнаружить ошибки в URL или данных.
Влияет ли размер sitemap на производительность сервера?
Да, при большом количестве записей и отсутствии кэширования. Каждый запрос к sitemap без кэша вызывает выполнение метода items(), что может создавать нагрузку на БД. Кроме того, генерация большого XML (более 20 000 строк) потребляет память. Рекомендуется либо агрессивное кэширование, либо предварительная генерация файлов статики с периодическим обновлением. Так мы поступили на проекте с 500 000 товаров: кроны раз в час генерировали набор XML-файлов, отдаваемых Nginx. Django не участвовал в ответах, что убрало всякую нагрузку.
Сервис Index-Now.ru дополнительно помогает сократить нагрузку на сервер, связанную с постоянными проверками поисковыми роботами. Благодаря мгновенным уведомлениям роботы приходят только за действительно изменившимися страницами, а не переиндексируют весь сайт.