How CKEditor 5 is Configured for Rich Text Editing in Yappuccino

Yappuccino integrates CKEditor 5 through django-ckeditor-5 by layering configuration across Django settings, model fields, form widgets, and a custom upload endpoint that supports both local filesystem and AWS S3 storage backends.

The Yappuccino blog platform implements a production-ready rich text editing solution using CKEditor 5 within its Django architecture. This article examines the multi-layered configuration spanning settings, models, forms, and custom upload handlers that enable seamless content authoring with image support.

Architecture Overview

The CKEditor 5 integration in jafarbekyusupov/yappuccino follows a five-layer architecture that separates concerns between configuration, data persistence, UI rendering, file handling, and security:

Settings Configuration

The core CKEditor 5 configuration resides in blogpost/settings.py, where the CKEDITOR_5_CONFIGS dictionary defines multiple editor profiles for different use cases.

Global Configuration


# blogpost/settings.py

CKEDITOR_5_FILE_STORAGE = DEFAULT_FILE_STORAGE  # S3 in production, FileSystemStorage locally

CKEDITOR_5_UPLOAD_PATH = "uploads/"

CKEDITOR_RESTRICT_BY_USER = False      # Uploads are not isolated per-user

CKEDITOR_BROWSE_SHOW_DIRS = True

CKEDITOR_5_CONFIGS = {
    'default': {
        'toolbar': [
            'heading', '|', 'bold', 'italic', 'link',
            'bulletedList', 'numberedList', 'blockQuote',
            'imageUpload',
        ],
        'uploadUrl': '/ckeditor5/upload/',
        'image': {
            'toolbar': [
                'imageTextAlternative', '|',
                'imageStyle:alignLeft', 'imageStyle:full',
                'imageStyle:alignRight'
            ],
            'styles': ['full', 'alignLeft', 'alignRight'],
            'upload': {'types': ['jpeg', 'png', 'gif', 'jpg']},
        },
    },
    'basic': {
        'toolbar': ['bold', 'italic', 'link', 'bulletedList', 'numberedList', 'imageUpload'],
        'uploadUrl': '/ckeditor5/upload/',
    },
}

Storage Backend Selection

The configuration dynamically switches storage backends based on the DEBUG environment variable. In production, DEFAULT_FILE_STORAGE points to 'storages.backends.s3boto3.S3Boto3Storage', writing files to s3://<bucket>/uploads/. In development, the system falls back to Django's FileSystemStorage, storing uploads under <project>/media/uploads/.

Database Models

Rich text content is persisted using CKEditor5Field from the django-ckeditor-5 package. The field declaration references a specific configuration name from CKEDITOR_5_CONFIGS, allowing different models to use different toolbar configurations.

Post and SimplePost Implementation


# blog/models.py

from django_ckeditor_5.fields import CKEditor5Field

class Post(models.Model):
    # … additional fields …

    content = CKEditor5Field('Content', config_name='default')
    # …

class SimplePost(models.Model):
    # … additional fields …

    content = CKEditor5Field('Content', config_name='basic')
    # …

The Post model uses the full-featured default configuration with headings and image alignment tools, while SimplePost employs the streamlined basic configuration for lighter editing requirements. The stored value is standard HTML generated by the CKEditor 5 JavaScript runtime.

Form Integration

Forms instantiate the editor interface using CKEditor5Widget, which loads the required JavaScript bundles and maps the image upload button to the custom endpoint.

Widget Configuration


# blog/forms.py

from django_ckeditor_5.widgets import CKEditor5Widget

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content']
        widgets = {
            'content': CKEditor5Widget(
                attrs={'placeholder': 'Write your article...'},
                config_name='default',
            ),
        }

class SimplePostForm(forms.ModelForm):
    class Meta:
        model = SimplePost
        fields = ['title', 'content']
        widgets = {
            'content': CKEditor5Widget(config_name='basic')
        }

The widget automatically injects the CKEditor 5 CSS and JavaScript resources into the template when {{ form.media }} is rendered.

Image Upload Handling

Yappuccino overrides the default upload behavior to ensure consistent storage backend usage and logging across both explicit views and internal library calls.

Custom Upload Endpoint

The ckeditor_upload view in blog/ckeditor_views.py handles multipart file uploads with validation:


# blog/ckeditor_views.py

@csrf_exempt
@login_required
def ckeditor_upload(request):
    # 1. Verify POST method and file presence

    # 2. Enforce file size < 5 MB and MIME type whitelist

    # 3. Generate unique filename using UUID + original extension

    # 4. Save via default_storage.save() to respect configured backend (S3 or local)

    # 5. Return JSON response: {"url": "<file_url>", "uploaded": "1", "fileName": "<filename>"}

This implementation logs the upload path and active storage backend, facilitating debugging when switching between local development and S3 production environments.

Monkey-Patching for Consistency

To ensure all upload paths respect the custom storage configuration, blog/patch_ckeditor.py replaces the upstream django_ckeditor_5.views.upload_file function with the same logic used in the custom view. This guarantees that any internal library calls or future package updates maintain the desired permission and storage handling.

Security and Permissions

Upload security is managed through a decorator-based permission system located in blog/ckeditor_upload_permissions.py.

Current Permission Model

The user_has_upload_permission decorator currently acts as a pass-through, allowing any authenticated user to upload images. However, the structure is designed for extension:


# blog/ckeditor_upload_permissions.py

def user_has_upload_permission(view_func):
    """
    Placeholder for upload permission logic.
    Extend to enforce staff-only uploads or user-specific quotas.
    """
    return view_func

The @login_required decorator on the ckeditor_upload view ensures only authenticated sessions can access the endpoint, while the whitelist in CKEDITOR_5_CONFIGS restricts acceptable file types to jpeg, png, gif, and jpg.

Summary

  • Configuration hierarchy: CKEditor 5 behavior is controlled through CKEDITOR_5_CONFIGS in blogpost/settings.py, supporting multiple toolbar profiles (default, basic, or custom)
  • Storage abstraction: The CKEDITOR_5_FILE_STORAGE setting enables seamless switching between local filesystem storage and AWS S3 without code changes
  • Model integration: CKEditor5Field stores HTML content in database models, with config_name parameter selecting the appropriate editor profile
  • Custom upload pipeline: The ckeditor_upload view in blog/ckeditor_views.py handles file validation, unique naming, and storage backend abstraction, while patch_ckeditor.py ensures consistency across all upload entry points
  • Extensible security: The permission decorator pattern in blog/ckeditor_upload_permissions.py provides a hook for implementing role-based upload restrictions

Frequently Asked Questions

How do I add a custom toolbar configuration for specific models?

Define a new configuration dictionary in blogpost/settings.py under CKEDITOR_5_CONFIGS, then reference it in your model field:


# settings.py

CKEDITOR_5_CONFIGS['custom'] = {
    'toolbar': ['heading', 'bold', 'italic', 'link'],
    'uploadUrl': '/ckeditor5/upload/',
}

# models.py

content = CKEditor5Field('Content', config_name='custom')

Can I restrict image uploads to staff users only?

Yes. Modify the user_has_upload_permission decorator in blog/ckeditor_upload_permissions.py to check for request.user.is_staff or specific group membership, then apply it to the ckeditor_upload view in blog/ckeditor_views.py.

Where are uploaded images stored in production versus development?

In production (DEBUG=False), images are stored in the AWS S3 bucket configured in DEFAULT_FILE_STORAGE under the uploads/ path. In development, files are saved to the local filesystem at <project_root>/media/uploads/ due to the fallback to FileSystemStorage.

How does the system handle filename collisions during upload?

The ckeditor_upload view in blog/ckeditor_views.py generates unique filenames by combining a UUID with the original file extension, preventing collisions regardless of how many users upload files with identical names.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →