WYSIWYG-редактор в админке Django: разбор на живом примере
Поле TextField в админке Django — это голый textarea без форматирования. Разбираем, как подключить редактор, где он ломается и чем платить за каждый вариант.

Модель с полем models.TextField() в админке Django рендерится как обычный <textarea>. Контент-редактор вводит текст новости или описание товара, а на сайте получает сплошную простыню без абзацев, заголовков и ссылок. Задача звучит просто: дать в админке привычный редактор с кнопками жирного, списков и вставки изображений. На практике за этой простотой прячется выбор между тремя разными подходами, и каждый из них чем-то расплачивается — безопасностью, весом страницы или гибкостью.
Разбираем на живом примере: блог с моделью Article, поле body. Версии — Django 5.x, Python 3.11+. Покажу, чем подключение редактора отличается от «просто поставить пакет», и где это выстреливает в проде.
Почему нельзя просто отдать HTML из редактора на сайт
Любой WYSIWYG-редактор хранит результат как HTML-строку. Django по умолчанию экранирует переменные в шаблоне, поэтому наивный вывод {{ article.body }} покажет не текст, а сам HTML с тегами <p> на экране. Разработчик добавляет {{ article.body|safe }} — и вот здесь появляется цена вопроса.
Фильтр safe отключает экранирование целиком. Если редактор позволяет вставлять произвольный HTML, а пользователь админки — не только вы, то любой <script> в теле статьи выполнится у каждого посетителя сайта. Это классическая stored XSS. Поэтому выбор редактора — это в первую очередь выбор модели доверия к тому, кто пишет контент, и стратегии очистки HTML на выходе.
Редактор в админке решает задачу удобства ввода. Безопасность вывода — отдельная задача, и ни один популярный редактор не закрывает её за вас автоматически.
Три подхода
1. Textarea с Markdown
Самый дешёвый вариант: поле остаётся обычным textarea, редактор набирает Markdown, а на выводе вы конвертируете его в HTML через библиотеку вроде markdown с последующей санитизацией через bleach. В админке — никакого JavaScript, никаких зависимостей от фронтенда.
Плюс: минимум магии, контент хранится в человекочитаемом виде, XSS перекрывается на этапе конвертации, если разрешить только безопасный набор тегов. Минус: редактор должен знать Markdown, а контент-менеджеру без технического бэкграунда это неудобно.
2. Готовый пакет с WYSIWYG
Пакеты вроде django-ckeditor-5 (обёртка над CKEditor 5) или django-tinymce подключают полноценный визуальный редактор к конкретному полю модели. Ставите пакет, регистрируете статику, меняете виджет поля — и в админке появляется панель с кнопками.
Плюс: привычный для редактора интерфейс, загрузка изображений из коробки. Минус: тянет за собой заметный объём JavaScript и CSS, требует настройки хранения загруженных файлов и — главное — по умолчанию хранит и отдаёт произвольный HTML.
3. Кастомный виджет поверх готового редактора
Когда нужен нестандартный набор кнопок или интеграция с вашим хранилищем медиа, вы описываете свой forms.Widget, подключаете нужную библиотеку редактора через Media и полностью контролируете конфигурацию. Дороже по времени, но не зависите от того, как автор пакета решил за вас вопросы настройки.
| Критерий | Markdown + bleach | Готовый пакет | Кастомный виджет |
|---|---|---|---|
| Время на внедрение | Полчаса | Час | Полдня и больше |
| Удобство для нетехнического автора | Низкое | Высокое | Высокое |
| Вес страницы админки | Не меняется | Плюс сотни КБ JS/CSS | Плюс сотни КБ JS/CSS |
| Риск XSS без доп. настройки | Низкий | Высокий | Высокий |
| Контроль над конфигурацией | Полный | В рамках пакета | Полный |
Как подключить готовый пакет
Разберём вариант с CKEditor 5 через django-ckeditor-5 — один из поддерживаемых на момент написания пакетов. Точные имена настроек и актуальность версии сверяйте с README пакета на PyPI, они менялись между релизами.
Установка:
pip install django-ckeditor-5Добавьте приложение в INSTALLED_APPS в settings.py:
# settings.py
INSTALLED_APPS = [
# ... остальные приложения
"django_ckeditor_5",
]
# Куда складывать загруженные из редактора файлы
CKEDITOR_5_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
# Конфигурация панели инструментов.
# Набор toolbar-элементов сверяйте с документацией CKEditor 5:
# имена кнопок задаёт сам CKEditor, а не пакет.
CKEDITOR_5_CONFIGS = {
"default": {
"toolbar": [
"heading", "|", "bold", "italic", "link",
"bulletedList", "numberedList", "|", "imageUpload",
],
},
}Подключите служебные URL пакета в корневом urls.py:
# urls.py
from django.urls import path, include
urlpatterns = [
# ... остальные маршруты
path("ckeditor5/", include("django_ckeditor_5.urls")),
]Теперь замените тип поля в модели. Пакет предоставляет CKEditor5Field — используйте его вместо TextField:
# models.py
from django.db import models
from django_ckeditor_5.fields import CKEditor5Field
class Article(models.Model):
title = models.CharField(max_length=200)
# config_name ссылается на ключ в CKEDITOR_5_CONFIGS
body = CKEditor5Field("Текст", config_name="default")
def __str__(self):
return self.titleСоздайте и примените миграцию:
python manage.py makemigrations
python manage.py migrateСоберите статику, если проект работает не в режиме разработки:
python manage.py collectstaticОткройте админку на странице редактирования статьи. Вместо серого прямоугольника textarea должна появиться панель с кнопками форматирования. Это и есть проверка, что подключение сработало.
Обязательный шаг: санитизация на выводе
Редактор подключён, но статья на сайте всё ещё небезопасна. Нельзя отдавать body|safe напрямую, если в админке работает больше одного человека или есть хоть какая-то форма пользовательского ввода. Прогоняйте HTML через whitelist-санитайзер перед сохранением или перед выводом.
Библиотека bleach на момент написания в режиме поддержки — проверьте её актуальный статус и рассмотрите активные альтернативы вроде nh3 (обёртка над Rust-библиотекой ammonia). Схематично идея одинаковая: разрешаем только безопасный набор тегов и атрибутов, всё остальное вырезаем.
# Пример на nh3 — санитизация перед выводом.
# Точный API сверяйте с документацией nh3 нужной версии.
import nh3
ALLOWED = {"p", "a", "strong", "em", "ul", "ol", "li", "h2", "h3", "img"}
def clean_html(raw_html: str) -> str:
# Оставляем только теги из белого списка, скрипты и события вырезаются
return nh3.clean(raw_html, tags=ALLOWED)Где вызывать очистку — вопрос отдельный. Чистить на выводе просто, но нагружает каждый рендер; чистить в save() модели — один раз, но грязная запись, попавшая мимо, останется грязной. Инвалидацию уже сохранённого контента здесь не рассматриваем — это отдельная тема.
Что ломается
- Статика не отдаётся в проде. Забыли
collectstaticили неверно настроенSTATIC_ROOT— редактор не подгрузит JS и покажет обычный textarea или пустое место. Проверьте, что файлы пакета попали в собранную статику. - Загрузка изображений возвращает 404 или 403. Не подключены URL пакета в
urls.pyили не настроенMEDIA_URL/MEDIA_ROOTи раздача медиа веб-сервером. В деве Django раздаёт медиа сам только при явной настройке, в проде — задача Nginx. - CSRF-ошибка при загрузке файла. Запрос от редактора должен нести CSRF-токен. Если админка за прокси и заголовки теряются — проверьте
CSRF_TRUSTED_ORIGINS. - HTML на сайте показывается как текст с тегами. Забыли вывести через безопасный рендер — но тут нельзя просто дописать
|safe, сначала санитизация (см. выше). - Конфликт версий JS в админке. Если в проекте несколько редакторов или кастомные скрипты в админке — их статика может пересекаться. Смотрите порядок подключения через атрибут
Media.
Когда что выбирать
Если контент пишут разработчики или технически грамотные редакторы, а на первом месте — безопасность и лёгкость админки, берите Markdown + санитайзер. Если статьи ведут менеджеры без технического бэкграунда и нужны привычные кнопки — готовый пакет, но обязательно с очисткой HTML на выходе. Кастомный виджет оправдан, только когда стандартная конфигурация пакета вам мешает: свой набор кнопок, интеграция с внешним медиахранилищем, брендированная панель.
Что делать дальше
После подключения редактора настройте раздачу MEDIA_ROOT через Nginx и ограничьте типы и размер загружаемых файлов — иначе редактор станет каналом для заливки чего угодно на сервер. Проверьте, что санитизация реально срабатывает: сохраните статью с <script>alert(1)</script> в теле и убедитесь, что на сайте скрипт не выполняется. И следите за размером бандла редактора в проде — на медленных соединениях страница редактирования грузится ощутимо дольше обычной формы.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Django: Form and field validation / widgets (официальная документация), django-ckeditor-5 на PyPI
Частые вопросы
А если у меня старая версия Django, например 4.2?
Редактор подключился, но на сайте вместо текста видны теги. Что делать?
Насколько это опасно с точки зрения безопасности?
Можно ли обойтись без сторонних пакетов вообще?
Загрузка картинок из редактора не работает, что проверить?
Редактор сильно замедляет админку. Это нормально?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.