Django Admin with Kria Lite WYSIWYG Editor
Learn how to integrate Kria Lite WYSIWYG editor into your Django admin and forms. Replace Django's default textarea with a modern, lightweight rich text editor.
📋 Prerequisites
- ✓ Django 3.2+ (works with Django 4.x and 5.x)
- ✓ Python 3.8+
- ✓ Basic understanding of Django models and admin
Installation
First, download Kria Lite and add it to your Django project's static files:
# Create a directory for the editor in your static files
mkdir -p static/js/kria-lite
mkdir -p static/css/kria-lite
# Download from CDN or npm
curl -o static/js/kria-lite/kria.editor.min.js \
https://cdn.jsdelivr.net/npm/kria-lite/dist/kria.editor.min.js
curl -o static/css/kria-lite/kria.editor.css \
https://cdn.jsdelivr.net/npm/kria-lite/dist/kria.editor.css
Alternatively, you can use the CDN directly in your templates (shown in later steps).
Static Files Setup
Ensure your Django settings are configured for static files:
# Static files configuration
STATIC_URL = '/static/'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Media files for image uploads
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
Create Custom Widget
Create a custom Django widget that renders Kria Lite:
from django import forms
from django.utils.safestring import mark_safe
class KriaEditorWidget(forms.Textarea):
"""
A Django widget that renders Kria Lite WYSIWYG editor.
"""
def __init__(self, attrs=None, config=None):
default_attrs = {
'class': 'kria-editor',
'rows': '10',
}
if attrs:
default_attrs.update(attrs)
self.config = config or {}
super().__init__(attrs=default_attrs)
class Media:
css = {
'all': (
'https://cdn.jsdelivr.net/npm/kria-lite/dist/kria.editor.css',
)
}
js = (
'https://cdn.jsdelivr.net/npm/kria-lite/dist/kria.editor.min.js',
)
def render(self, name, value, attrs=None, renderer=None):
textarea = super().render(name, value, attrs, renderer)
# Build config JSON
config = {
'height': self.config.get('height', '400px'),
'placeholder': self.config.get('placeholder', 'Start writing...'),
}
# Add image upload URL if configured
if 'imageUploadUrl' in self.config:
config['imageUploadUrl'] = self.config['imageUploadUrl']
import json
config_json = json.dumps(config)
widget_id = attrs.get('id', name)
init_script = f'''
<script>
(function() {{
function initKria() {{
if (typeof WYSIWYG !== 'undefined') {{
WYSIWYG.init('#{widget_id}', {config_json});
}} else {{
setTimeout(initKria, 100);
}}
}}
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', initKria);
}} else {{
initKria();
}}
}})();
</script>
'''
return mark_safe(textarea + init_script)
Admin Integration
Use the widget in your Django admin:
from django.contrib import admin
from django import forms
from .models import BlogPost
from .widgets import KriaEditorWidget
class BlogPostAdminForm(forms.ModelForm):
class Meta:
model = BlogPost
fields = '__all__'
widgets = {
'content': KriaEditorWidget(
config={
'height': '500px',
'placeholder': 'Write your blog post...',
'imageUploadUrl': '/api/upload-image/',
}
),
}
@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
form = BlogPostAdminForm
list_display = ['title', 'created_at', 'is_published']
search_fields = ['title', 'content']
Or override formfield_overrides for all TextFields:
@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
formfield_overrides = {
models.TextField: {'widget': KriaEditorWidget()},
}
Custom Model Field (Optional)
Create a custom model field for automatic widget assignment:
from django.db import models
from .widgets import KriaEditorWidget
class RichTextField(models.TextField):
"""
A TextField that uses Kria Lite WYSIWYG editor in admin.
"""
def __init__(self, *args, editor_config=None, **kwargs):
self.editor_config = editor_config or {}
super().__init__(*args, **kwargs)
def formfield(self, **kwargs):
kwargs['widget'] = KriaEditorWidget(config=self.editor_config)
return super().formfield(**kwargs)
Use in your models:
from django.db import models
from .fields import RichTextField
class BlogPost(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
# Uses Kria Lite automatically in admin
content = RichTextField(
editor_config={
'height': '500px',
'imageUploadUrl': '/api/upload-image/',
}
)
excerpt = RichTextField(
blank=True,
editor_config={
'height': '200px',
'placeholder': 'Short description...',
}
)
created_at = models.DateTimeField(auto_now_add=True)
is_published = models.BooleanField(default=False)
def __str__(self):
return self.title
Image Upload View
Create a view to handle image uploads from the editor:
import os
import uuid
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.contrib.admin.views.decorators import staff_member_required
from django.core.files.storage import default_storage
from django.conf import settings
@staff_member_required
@require_POST
def upload_image(request):
"""
Handle image uploads from Kria Lite editor.
Only accessible by staff members.
"""
if 'file' not in request.FILES:
return JsonResponse({
'error': 'No file provided'
}, status=400)
uploaded_file = request.FILES['file']
# Validate file type
allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
if uploaded_file.content_type not in allowed_types:
return JsonResponse({
'error': 'Invalid file type. Allowed: JPEG, PNG, GIF, WebP'
}, status=400)
# Validate file size (max 5MB)
max_size = 5 * 1024 * 1024
if uploaded_file.size > max_size:
return JsonResponse({
'error': 'File too large. Maximum size: 5MB'
}, status=400)
# Generate unique filename
ext = os.path.splitext(uploaded_file.name)[1].lower()
filename = f'editor/{uuid.uuid4().hex}{ext}'
# Save file
saved_path = default_storage.save(filename, uploaded_file)
file_url = default_storage.url(saved_path)
return JsonResponse({
'url': file_url,
'filename': os.path.basename(saved_path)
})
Add the URL pattern:
from django.urls import path
from . import views
urlpatterns = [
# ... other URLs
path('api/upload-image/', views.upload_image, name='upload_image'),
]
Frontend Display
Display the rich text content safely in your templates:
<!-- blog_detail.html -->
{% extends "base.html" %}
{% block content %}
<article class="blog-post">
<h1>{{ post.title }}</h1>
<div class="post-meta">
<time datetime="{{ post.created_at|date:'c' }}">
{{ post.created_at|date:"F j, Y" }}
</time>
</div>
<!-- Render HTML content safely -->
<div class="post-content prose lg:prose-xl">
{{ post.content|safe }}
</div>
</article>
{% endblock %}
⚠️ Note: Using |safe on user-generated content requires
proper sanitization. See the Security section below.
Security Best Practices
While Kria Lite has built-in XSS protection, always sanitize HTML on the server side:
# Install bleach for HTML sanitization
pip install bleach
import bleach
from django.db import models
class BlogPost(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
# Allowed HTML tags for rich text
ALLOWED_TAGS = [
'p', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'strong', 'em', 'u', 's', 'mark',
'ul', 'ol', 'li',
'a', 'img',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'blockquote', 'pre', 'code',
]
ALLOWED_ATTRS = {
'a': ['href', 'title', 'target', 'rel'],
'img': ['src', 'alt', 'width', 'height'],
'*': ['class'],
}
def save(self, *args, **kwargs):
# Sanitize HTML before saving
self.content = bleach.clean(
self.content,
tags=self.ALLOWED_TAGS,
attributes=self.ALLOWED_ATTRS,
strip=True
)
super().save(*args, **kwargs)
def get_safe_content(self):
"""Get sanitized content for display."""
return bleach.clean(
self.content,
tags=self.ALLOWED_TAGS,
attributes=self.ALLOWED_ATTRS
)
✅ Security Checklist
- • Sanitize HTML on save (server-side)
- • Restrict image uploads to authenticated staff
- • Validate file types and sizes
- • Use CSRF protection on upload endpoints
- • Serve uploaded files from a separate domain if possible
🎉 Congratulations!
You now have Kria Lite WYSIWYG editor integrated into your Django admin! Your editors can now create rich content with images, tables, and formatting.