Django Markdown Tutorial

Updated

Table of Contents

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 quickly demonstrate how to 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 commands to create a new virtual environment and install Django.

# Windows
$ cd onedrive\desktop\
$ mkdir django_markdown
$ cd django_markdown
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install django~=5.0

# macOS
$ cd ~/desktop/
$ mkdir markdown && cd markdown
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install django~=5.0

Then create a new project called django_project, run migrate to initialize the database, and runserver to start up the local web server.

(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py migrate
(.venv) $ python manage.py runserver

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

Django welcome page

Blog App

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

(.venv) $ python manage.py startapp blog

We must add the app explicitly to the INSTALLED_APPS configuration within django_project/settings.py.

# django_project/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 django_project/urls.py file to include our blog app which will have the URL path of '', the empty string.

# django_project/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:

  • models.py for the database model
  • urls.py for the URL route
  • views.py for our logic
  • templates/post_list.html for our template

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.

(.venv) $ 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.

(.venv) $ mkdir blog/templates
(.venv) $ touch 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, create a migrations file and apply it to our database.

(.venv) $ python manage.py makemigrations blog
(.venv) $ 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 to access the admin.

(.venv) $ python manage.py createsuperuser

Start up the local server again by running python manage.py runserver and logging in to 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 return to our homepage. The text is outputted without any formatting.

Homepage

Markdown

We want to convert our Markdown into formatted HTML on the website automatically. 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 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.

(.venv) $ python -m pip install markdown==3.5.2

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.

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

The file will import the markdown package and use the fenced code block extension. As noted in the docs, there are several 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 restart the web server 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 with several built-in protections. Also, add bleach for an additional layer of sanitation. There is a django-bleach package, which is worth a look.