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.0.3
$ 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.
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.apps.BlogConfig', # 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:
-
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.
(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.
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 navigate back to our homepage. The text is outputted without any formatting.
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.
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.