Skip to main content

Django Unfold Markdown Widget

Markdown editor widget for Django Unfold admin using EasyMDE.

Screenshots

Editor with Preview

Markdown Editor

Fullscreen Mode

Fullscreen Mode

Editor inside tab and Dark theme

Editor tab and dark theme

Features

  • Plain text editor with monospace font
  • Side-by-side preview and fullscreen modes
  • Dark/light theme integration with Unfold
  • Material Symbols icons matching Unfold design
  • Toolbar: bold, italic, strikethrough, headings, lists, links, images, tables, horizontal rules
  • Image upload straight from the toolbar (optional, off by default)
  • Works inside tabs — Unfold fieldset tabs, django-modeltranslation, jQuery UI
  • Works inside inlines, including rows added with “Add another”
  • No autosave (saves on form submit)
  • Responsive design

Installation

pip install django-unfold-markdown

Configuration

Add to your INSTALLED_APPS:

# settings.py
INSTALLED_APPS = [
    "unfold",
    "unfold_markdown",  # Add this
    # ...
]

Usage

For all TextField fields

# admin.py
from django.db import models
from unfold.admin import ModelAdmin
from unfold_markdown.widgets import MarkdownWidget


@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    formfield_overrides = {models.TextField: {"widget": MarkdownWidget}}

For specific fields

from django import forms
from unfold_markdown.widgets import MarkdownWidget


class ArticleForm(forms.ModelForm):
    content = forms.CharField(widget=MarkdownWidget())

    class Meta:
        model = Article
        fields = "__all__"


@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    form = ArticleForm

Image Upload

The image toolbar button prompts for a URL by default. Point the widget at an upload endpoint and it opens a file picker instead, inserting the Markdown once the upload finishes.

Using the bundled endpoint

Add the URLs, then name them in settings:

# urls.py
from django.urls import include, path

urlpatterns = [
    # ...
    path("unfold-markdown/", include("unfold_markdown.urls")),
]
# settings.py
UNFOLD_MARKDOWN = {
    "image_upload_url": "/unfold-markdown/image-upload/",
}

The bundled view rejects anonymous and non-staff users, requires CSRF, stores the file under Django's default storage with a random UUID filename, and responds with {"url": "..."}.

Settings

Key Default Meaning
image_upload_url None Upload endpoint. Unset means "prompt for a URL".
image_upload_dir "markdown-images" Directory within your storage backend.
image_max_size 10485760 (10 MB) Rejected above this size.
image_allowed_extensions png jpg jpeg gif webp bmp ico avif Permitted extensions.

On SVG: it is deliberately absent from the defaults. An SVG served from your own domain can execute scripts, so uploading one is a stored-XSS vector when staff accounts are not fully trusted. Add it only if you serve media from a separate domain or sanitise on the way in:

UNFOLD_MARKDOWN = {
    "image_upload_url": "/unfold-markdown/image-upload/",
    "image_allowed_extensions": ["png", "jpg", "jpeg", "webp", "svg"],
}

Note that the extension is the only check performed — the view does not decode the image. If you need stronger guarantees, point image_upload_url at your own view.

Using your own endpoint

Any URL accepting a POST with a file field in multipart/form-data and returning JSON with a url key will do:

UNFOLD_MARKDOWN = {"image_upload_url": "/api/my-upload/"}

The editor sends the CSRF token as X-CSRFToken and includes same-origin cookies. On a non-2xx response it reads {"error": "..."} from the body and shows it to the user.

Per-widget override

class ArticleForm(forms.ModelForm):
    content = forms.CharField(widget=MarkdownWidget(upload_url="/api/my-upload/"))

Storage and Rendering

The widget stores pure Markdown text in your database. To render Markdown as HTML, use a Python library:

# Using markdown
from markdown import markdown

html = markdown(article.content)

# Using mistune
import mistune

html = mistune.html(article.content)

Requirements

Requirement
Python 3.10 – 3.14
Django 4.2 or newer
django-unfold 0.70 or newer (0.99+ on Python 3.12+)

django-unfold dropped Python 3.11 and older in 0.99, so this package asks for a newer floor only on interpreters that can actually install it. Bundled EasyMDE: 2.21.0.

Local development

The repo ships a throwaway Django project under demo/ so you can click through the editor yourself. It uses uv.

uv sync --all-groups
uv run demo/manage.py migrate && uv run demo/manage.py seed
uv run demo/manage.py runserver

Then open http://127.0.0.1:8000/admin/ and log in as admin / admin. The seeded "Sample article" is set up to exercise the awkward cases:

  • Body — a plain editor with sample Markdown covering every toolbar button.
  • Summary / Notes — two Unfold fieldset tabs. Notes starts hidden, so it shows the lazy initialisation path; a CodeMirror built inside a hidden container would otherwise render zero-height.
  • Sections — a stacked inline. Press Add another to confirm new rows get a working editor and that the hidden __prefix__ template row is left alone.
  • Image upload is enabled in the demo settings, so the toolbar shows an upload button. Files land in demo/media/.

Tests

uv run pytest
uv run ruff check . && uv run ruff format --check .

The suite runs against the installed Django/Unfold. To check an older floor:

uv run --python 3.10 --isolated --with pytest --with pytest-django pytest

License

MIT License

Repository

https://github.com/sergei-vasilev-dev/django-unfold-markdown

Credits

Metadata

Release files for django-unfold-markdown 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-unfold-markdown 0.2.0
File Size Uploaded
django_unfold_markdown-0.2.0.tar.gz 127.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-unfold-markdown 0.2.0
File Interpreter ABI Platform
django_unfold_markdown-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 252.7 kB

Release files / django_unfold_markdown-0.2.0.tar.gz

Download URL django_unfold_markdown-0.2.0.tar.gz
Size 127.3 kB
Tags Source
SHA-256 checksum
How to use checksums
86bacb2acc97ca3c7f239913e80c0a9b5abac04a1ef45d950e842ecdac24f5ed
BLAKE2b-256 checksum
How to use checksums
abbda759c84abcd302b747b0c2eb6e5be130031f645925a848ce9944debd1ec2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / django_unfold_markdown-0.2.0-py3-none-any.whl

Download URL django_unfold_markdown-0.2.0-py3-none-any.whl
Size 125.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6b48744218e239aca222f07dcdd0203da21b4696000eecc1b3030c15c7d4acb2
BLAKE2b-256 checksum
How to use checksums
257a65ec1a591a3787f199bf5604d3df330aac83bfdf3c966bae7ee9f9277274
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page