# martor: a Markdown editor widget for Django forms

> martor is a Django app that replaces a plain textarea with an Ace-based Markdown editor with live preview, image upload, mentions and emoji, and it renders through a configurable Markdown extension list that includes an HTML escaping step. It suits a Django project whose users write long-form content and need a preview.

**agusmakmun/django-markdown-editor** — 🙌 Awesome Django Markdown Editor, supported for Bootstrap & Semantic-UI

- Repository: https://github.com/agusmakmun/django-markdown-editor
- Website: https://django-markdown-editor.readthedocs.io
- Stars: 898 · Forks: 1,279
- Language: JavaScript
- License: GPL-3.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/agusmakmun-django-markdown-editor

## Four steps from a bare Django project to an editor

The installation is the standard Django app shape, and it is short enough to take in one reading. The package comes from PyPI:

```bash
$ pip install martor
```

Then it goes into INSTALLED_APPS, and the documentation is specific that this is done without migrations:

```python
# settings.py
INSTALLED_APPS = [
    ....
    'martor',
]
```

The URLs are then added so that the upload, user search and markdownify endpoints are reachable:

```python
# urls.py
urlpatterns = [
    ...
    path('martor/', include('martor.urls')),
]
```

And the static files are collected into the STATIC_ROOT folder:

```bash
./manage.py collectstatic
```

Those four steps are the whole install, and the requirements behind them are listed as Django 3.2 or newer, Markdown 3.0 or newer, requests 2.12.4 or newer and bleach. The package metadata also lists urllib3, zipp and tzdata. What that list tells you is that the editor is a thin client that renders on the server, and bleach is there because the server, not the browser, decides what HTML comes out.

## The editor is Ace, and the preview is a server round trip

The feature list explains what the user actually experiences. Live Preview sits at the top, and the editor itself is integrated with Ace Editor rather than being a plain textarea with buttons. The preview is not computed in the browser. The configuration exposes a markdownify function and a markdownify URL, defaulting to martor.utils.markdownify and the path /martor/markdownify/, and a timeout setting that controls the delay in milliseconds before the preview updates. The documentation comments show the two ends of that: a timeout of 0 updates the preview instantly, and a timeout of 1000 is the default. The living mode toggle in the enable configs is described as enabling or disabling live updates in the preview, so a project can start with the preview static and turn on the live behaviour once the latency is acceptable. The consequence for the server is that every preview update is a request that runs the Markdown pipeline, which is why the timeout exists and why the markdownify function is configurable, since a project with a faster or slower stack will want a different default.

## The Markdown extension list is where the behaviour and the security live

Martor does not have its own Markdown dialect. It configures Python-Markdown and then adds extensions of its own, and the list in the settings is the real specification of what the editor accepts. The defaults are the standard extensions, extra, nl2br, smarty, fenced_code and sane_lists, followed by the martor ones: urlize, del_ins for strikethrough and emphasis markers, mention for the direct mention syntax, emoji, mdx_video for the embedded and iframe video, escape_html, which the comment says handles the XSS vulnerabilities, and mdx_add_id for the custom id syntax. The escape_html extension is the one to pay attention to. It sits in the same list as the feature extensions, which means it is a line a user can remove, and if it is removed the editor is relying on bleach alone with the allowed tags and attributes you configured. The two lists, ALLOWED_URL_SCHEMES and ALLOWED_HTML_TAGS, plus the allowed attributes list, are therefore the actual boundary of what a user can inject. The scheme list is broad, including file, ftp, sftp, ssh, telnet, vnc and xmpp alongside http and https, and that breadth is a decision you are making when you copy the defaults.

## Image upload, mentions and the endpoints you can switch off

Three features depend on server endpoints, and each endpoint can be disabled by pointing its URL setting at an empty string, which the documentation says completely disables the endpoint. The upload URL defaults to /martor/uploader/, and the user search URL defaults to /martor/search-user/, and setting either to an empty string removes it. That is a good design for a security conscious deployment, because the default install of a content editor includes an image upload endpoint and a user enumeration endpoint, and both are worth turning off if your users do not need them. The image uploader sends images to imgur.com through the API, and to use it you register your application at the Imgur application registration page to get an IMGUR_CLIENT_ID and an IMGUR_API_KEY, which are then set as MARTOR_IMGUR_CLIENT_ID and MARTOR_IMGUR_API_KEY in the settings. The documentation also describes a custom uploader as an alternative, which matters for a project that cannot send content to a third party. The mention feature, with the @ syntax, requires the user to be logged in, and the emoji feature is driven by a base emoji URL that defaults to a GitHub assets path and can also be pointed elsewhere or disabled.

## Theme and asset overrides let you drop Bootstrap or Semantic UI

The editor supports Bootstrap, Semantic-UI and Tailwind CSS, and the theme is a single setting with three named choices:

```python
# Choices are: "semantic", "bootstrap", "tailwind"
MARTOR_THEME = 'bootstrap'
```

The important part for an existing project is the set of alternative asset settings. The example values for MARTOR_ALTERNATIVE_JS_FILE_THEME, MARTOR_ALTERNATIVE_CSS_FILE_THEME and MARTOR_ALTERNATIVE_JQUERY_JS_FILE are semantic-themed and jquery dist paths, and all three settings default to None. The documentation says that if you need your own themed bootstrap or semantic ui dependency, you replace the values with the file in your static files dir. That is the mechanism for using the library in a project that already ships its own version of these frameworks, which is a common situation and one where shipping two copies of jQuery is a reliable way to break other things on the page. There is also a separate MARTOR_ENABLE_ADMIN_CSS setting, described as disabling the admin style when using a custom admin interface such as django-grappelli, and MARTOR_ENABLE_LABEL for the title label, and a toolbar setting listing which buttons appear, from bold and italic through image upload, emoji, direct mention and toggle-maximize.

## Django Admin, multiple fields and the security settings

Two features exist because content is often edited outside the public site. Django Admin is supported, and the MARTOR_ENABLE_ADMIN_CSS setting exists precisely for that, since the admin ships its own styles that clash with the editor theme. The jQuery inclusion toggle in the enable configs is annotated as required for the admin default in Django, which tells you the editor can either bring its own jQuery or use the one the host page has already loaded. Multiple fields are supported, which the feature list points at an old issue for the history of that, and the practical meaning is that one project can have several Markdown fields on a page without the widgets colliding over their element ids. On the security side there is one setting that is not optional in practice. The documentation says to check that CSRF_COOKIE_HTTPONLY is not set, because otherwise csrf will not be sent over ajax calls, and the setting to verify is:

```python
CSRF_COOKIE_HTTPONLY = False
```

That is a real trade-off rather than a free choice. The setting makes the CSRF cookie readable by JavaScript, which is what lets the editor attach the token to its preview and upload requests, and it widens the surface for a cross-site scripting issue to steal a token. The documentation states the requirement plainly and leaves the decision to you.

## Packaging, licensing and what the repository holds

The project is packaged with hatchling, with the version dynamic and the license text given as GPL-3.0, and the classifiers list Django 4.2, 5.0 and 5.1 alongside Python 3.8 to 3.13 with a requires-python of 3.8 or above. The development status is given as Production/Stable, which is a self-assessment rather than an independent one, but the release history supports the claim in the sense that v1.8.2 came on 2026-06-22, v1.8.1 on 2026-04-18 and v1.7.15 on 2025-09-03, and the last push was on 2026-07-20. The repository holds a martor_demo directory with its own manage.py, a Dockerfile and a docker-compose.yaml for running that demo on port 8000, a tests directory, a pytest.ini and a runtests.py, and a pre-commit configuration. The Dockerfile is worth a glance because it shows how the demo is meant to be run in a container: an Alpine base, a non-root appuser, tini as the init process, and a runserver command rather than a production server, which is fine for a demo and would not be for a deployment. The GPL-3.0 terms matter for a Django app in the way they usually do, since a Django app is normally imported rather than linked, and you should take your own advice on whether that composition is a problem for your distribution model.

## Conclusion

martor is a good fit for a Django project where content is written in the browser by people who are not comfortable with raw Markdown, where a live preview matters more than a bare textarea, and where the author can wait for a server round trip on each keystroke. It is the wrong choice for a project that already has a rich text workflow, for a site where editors need collaborative editing, and for anyone unwilling to own the sanitisation settings, because the allowed tags and attributes are yours to configure and the escaping extension is one line in a list. Before adopting it, read the MARTOR_ENABLE_CONFIGS and MARTOR_MARKDOWN_EXTENSIONS blocks closely, since every security-relevant behaviour lives there, and check the CSRF_COOKIE_HTTPONLY requirement, which the documentation says must not be set for the ajax calls to carry a token. The project is GPL-3.0 licensed, the last push was on 2026-07-20, and the most recent release is v1.8.2 from 2026-06-22.

## FAQ

### How do I install martor in a Django project?

Install it with pip install martor, add 'martor' to INSTALLED_APPS without migrations, add path('martor/', include('martor.urls')) to your urls.py, and run ./manage.py collectstatic. The requirements are Django 3.2 or newer, Markdown 3.0 or newer, requests 2.12.4 or newer and bleach.

### How is the live preview generated in martor?

On the server. The markdownify function and URL are configurable, defaulting to martor.utils.markdownify and the path /martor/markdownify/, and MARTOR_MARKDOWNIFY_TIMEOUT controls the delay in milliseconds before the preview updates, with 0 meaning instant and 1000 the documented default. Live updates can be switched off through the living option in MARTOR_ENABLE_CONFIGS.

### How do I disable the image upload and user search endpoints in martor?

Set MARTOR_UPLOAD_URL or MARTOR_SEARCH_USERS_URL to an empty string, which the documentation says completely disables the endpoint. Otherwise they default to /martor/uploader/ and /martor/search-user/. Using the imgur uploader also requires registering your application to get an IMGUR_CLIENT_ID and IMGUR_API_KEY.

### Which HTML is allowed in martor preview output?

You configure it. The ALLOWED_HTML_TAGS, ALLOWED_HTML_ATTRIBUTES and ALLOWED_URL_SCHEMES settings in the project define the boundary, and the martor.extensions.escape_html extension is listed in MARTOR_MARKDOWN_EXTENSIONS with a comment saying it handles the XSS vulnerabilities. Removing that extension from the list leaves bleach and your own tag list as the only protection.

### Does martor support Django Admin and other CSS frameworks?

Yes. Django Admin is supported, and MARTOR_ENABLE_ADMIN_CSS can disable the admin style when you use a custom admin interface. The editor supports Bootstrap, Semantic-UI and Tailwind CSS through MARTOR_THEME, and you can point MARTOR_ALTERNATIVE_JS_FILE_THEME, MARTOR_ALTERNATIVE_CSS_FILE_THEME and MARTOR_ALTERNATIVE_JQUERY_JS_FILE at your own files.

## Sources

- [agusmakmun/django-markdown-editor on GitHub](https://github.com/agusmakmun/django-markdown-editor)
- [License: GPL-3.0](https://github.com/agusmakmun/django-markdown-editor/blob/master/LICENSE)
- [Project website](https://django-markdown-editor.readthedocs.io)
- [README](https://github.com/agusmakmun/django-markdown-editor/blob/master/README.md)
- [Releases](https://github.com/agusmakmun/django-markdown-editor/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/agusmakmun-django-markdown-editor
