# How CKEditor 5 is Configured for Rich Text Editing in Yappuccino

> Discover how Yappuccino configures CKEditor 5 for rich text editing using Django settings, model fields, form widgets, and a custom upload endpoint supporting local and AWS S3 storage.

- Repository: [Ja'farbek Yusupov/yappuccino](https://github.com/jafarbekyusupov/yappuccino)
- Tags: internals
- Published: 2026-03-04

---

**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 layer** ([`blogpost/settings.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blogpost/settings.py)): Defines global editor configurations, storage backends, and upload paths
- **Model layer** ([`blog/models.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/models.py)): Persists HTML content using `CKEditor5Field`
- **Form layer** ([`blog/forms.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/forms.py)): Renders the editor widget via `CKEditor5Widget`
- **Upload layer** ([`blog/ckeditor_views.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/ckeditor_views.py), [`blog/patch_ckeditor.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/patch_ckeditor.py)): Handles image uploads with storage abstraction
- **Permission layer** ([`blog/ckeditor_upload_permissions.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/ckeditor_upload_permissions.py)): Controls upload access rights

## Settings Configuration

The core CKEditor 5 configuration resides in [`blogpost/settings.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blogpost/settings.py), where the `CKEDITOR_5_CONFIGS` dictionary defines multiple editor profiles for different use cases.

### Global Configuration

```python

# 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

```python

# 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

```python

# 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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/ckeditor_views.py) handles multipart file uploads with validation:

```python

# 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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/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:

```python

# 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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blog/ckeditor_views.py) handles file validation, unique naming, and storage backend abstraction, while [`patch_ckeditor.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/patch_ckeditor.py) ensures consistency across all upload entry points
- **Extensible security**: The permission decorator pattern in [`blog/ckeditor_upload_permissions.py`](https://github.com/jafarbekyusupov/yappuccino/blob/main/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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/blogpost/settings.py) under `CKEDITOR_5_CONFIGS`, then reference it in your model field:

```python

# 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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/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`](https://github.com/jafarbekyusupov/yappuccino/blob/main/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.