Django Markdown Tutorial
Tags: django, python, tutorial, beginners
Published: 2020-02-26 17:14:32

[Markdown](https://daringfireball.net/projects/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](https://djangoforbeginners.com) 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](http://127.0.0.1:8000) to confirm the Django welcome page is appearing as intended.

![Django welcome page](https://dev-to-uploads.s3.amazonaws.com/i/35bwmg6f9et2t6xi5414.png)


## 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`.

```python
# 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.

```python
# 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`.

```python
# 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`.

```python
# 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.

```python
# 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.

```html
<!-- 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](http://127.0.0.1:8000/admin).

![Admin](https://dev-to-uploads.s3.amazonaws.com/i/16zph7pthxs6oqsza6xj.png)

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

```python
# 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](https://dev-to-uploads.s3.amazonaws.com/i/cuvofks8b2wwh3xphe6m.png)

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

![Homepage](https://dev-to-uploads.s3.amazonaws.com/i/a3zwftlhetc0zl3rh1k6.png)


## 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](https://github.com/neutronX/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](https://pypi.org/project/Markdown/) and [markdown2](https://pypi.org/project/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](https://python-markdown.github.io/extensions/fenced_code_blocks/) extension. [As noted in the docs](https://python-markdown.github.io), there are a number of features available here so do read the docs.

```python
# 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.

```html
<!-- 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](https://dev-to-uploads.s3.amazonaws.com/i/7ogn1kbfk4aped0ejy1j.png)

## Next Steps
In a production setting there are several additional steps you'd want to take. Use Django [forms](https://docs.djangoproject.com/en/dev/topics/forms/) which come with a number of built-in protections. And also add [bleach](https://github.com/mozilla/bleach) for an additional layer of sanitation. There is a [django-bleach](https://github.com/marksweb/django-bleach) package which is worth a look in this regard.