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.
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 modelurls.py
for the URL routeviews.py
for our logictemplates/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.
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.
Click the "Save" button in the lower right and return to our homepage. The text is outputted without any formatting.
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.
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.