Django Markdown Tutorial

Markdown is a popular text-to-HTML conversion tool for web writers. It is far easier to use than plain old HTML. Many/most static site generators provide a built-in way to write posts using Markdown, however adding it to a Django website--such as a blog--takes an extra step or two. In this tutorial, I'll demonstrate how to quickly add Markdown functionality to any Django website.

This post presumes knowledge of Django and how to build a blog. If you need a detailed overview of the process, check out my book Django for Beginners which walks through building 5 progressively more complex web apps, including a blog!

But for now, let's start on the command line with the usual command to install Django, create a new project called config, setup the initial database using config, and starting the local webserver using the runserver command. I've added steps to install this code on the Desktop, which is convenient for Mac users, but the code directory can live anywhere on your computer.

$ cd ~/Desktop
$ mkdir markdown && cd markdown
$ pipenv install django~=3.1.0
$ pipenv shell
(markdown) $ django-admin startproject config .
(markdown) $ python manage.py migrate
(markdown) $ python manage.py runserver

Head over to http://127.0.0.1:8000 to confirm the Django welcome page is appearing as intended.

Django welcome page

Blog App

Now stop the local server with Control+c and create our basic blog app.

(markdown) $ python manage.py startapp blog

Per usual, we must add the app explicitly to the INSTALLED_APPS configuration within config/settings.py.

# config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog', # new
]

Update the config/urls.py file to include our blog app which will have the URL path of '', the empty string.

# config/urls.py
from django.contrib import admin
from django.urls import path, include # new

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')), # new
]

Models, URLs, Views, Templates

At this point we need four updates within our blog app:

The order in which we make these changes does not matter; we need all of them before the functionality will work. However, in practice, starting with models, then URLS, then views, and finally templates is the approach I usually take.

Start with blog/models.py.

# blog/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()

    def __str__(self):
        return self.title

We must create our blog/urls.py file manually.

(markdown) $ touch blog/urls.py

Then add a single path to blog/urls.py for a ListView to display all posts that imports a view called BlogListView.

# blog/urls.py
from django.urls import path

from .views import BlogListView

urlpatterns = [
    path('', BlogListView.as_view(), name='blog_list'),
]

Here is our blog/views.py file.

# blog/views.py
from django.views.generic import ListView

from .models import Post


class BlogListView(ListView):
    model = Post
    template_name = 'post_list.html'

The template will live within a templates directory in our blog app so let's create that and the file, post_list.html, now.

(markdown) $ mkdir blog/templates
(markdown) $ mkdir blog/templates/post_list.html

The template loops over object_list from ListView and displays both fields in our blog model.

<!-- blog/templates/blog/post_list.html -->
{% for post in object_list %}
<div>
  <h2>{{ post.title }}</h2>
  <p>{{ post.body }}</p>
</div>
<hr>
{% endfor %}

To finish it off, create a migrations file and apply it to our database.

(markdown) $ python manage.py makemigrations blog
(markdown) $ python manage.py migrate

Admin

We're all set but... we need a way to input data into our app! We could configure forms within the website itself for this, however the simpler approach is to use the built-in admin app.

Create a superuser account so we can access the admin.

(markdown) $ python manage.py createsuperuser

Start up the local server again by running python manage.py runserver and login at http://127.0.0.1:8000/admin.

Admin

Apps don't appear in the admin unless we explicitly add them, so do that now by updating blog/admin.py.

# blog/admin.py
from django.contrib import admin
from .models import Post

admin.site.register(Post)

Refresh the admin page now and the Blog app appears along with our Posts model (the admin automatically adds an "s" to models). Click on the "+Add" link and create a post written in Markdown.

Admin Post Model

Click the "Save" button in the lower right and navigate back to our homepage. The text is outputted without any formatting.

Homepage

Markdown

What we want is to automatically convert our Markdown into formatted HTML on the website. One option is to use a third-party package, such as Django MarkdownX, which includes additional features such as live editing, previews, image sizing, and others that are worth exploring.

However, that is overkill for right now and also abstracts away what's happening under the hood. The two dominant Markdown packages are markdown and markdown2. We will use markdown in this example.

First, stop the local server Control+c and install markdown.

(markdown) $ pipenv install markdown==3.2.1

We will create a custom template filter that uses Markdown. Create a templatetags directory within our blog app and then a markdown_extras.py file.

(markdown) $ mkdir blog/templatetags
(markdown) $ touch blog/templatetags/markdown_extras.py

The file itself will import the markdown package and use the fenced code block extension. As noted in the docs, there are a number of features available here so do read the docs.

# blog/templatetags/markdown_extras.py
from django import template
from django.template.defaultfilters import stringfilter

import markdown as md

register = template.Library()


@register.filter()
@stringfilter
def markdown(value):
    return md.markdown(value, extensions=['markdown.extensions.fenced_code'])

Now we load the custom filter into our template so that content written in Markdown will be outputted as HTML for the body field.

<!-- blog/templates/blog/post_list.html -->
{% load markdown_extras %}

{% for post in object_list %}
<div>
  <h2>{{ post.title }}</h2>
  <p>{{ post.body | markdown | safe }}</p>
</div>
<hr>
{% endfor %}

That's it! If you now restart the webserver and view our webpage, the Markdown we wrote in the admin before is now displayed properly.

Homepage

Next Steps

In a production setting there are several additional steps you'd want to take. Use Django forms which come with a number of built-in protections. And also add bleach for an additional layer of sanitation. There is a django-bleach package which is worth a look in this regard.