How to Use Django-Markdownx for Your Blog

This is my first crack at using Django-Markdownx, which is a comprehensive Markdown plugin built for Django that has raw editing and a live preview inside of the Django admin. I looked into many other ways to implement markdown (django-markdown, django-markdown2, django-markdown-duex), but the ability to drag & drop image uploads won me over. Implementing it is very straight forward, so let's begin there.

Most of this comes straight from the GitHub instructions at Django Markdownx, so take a look if you need more detail.

First, install django-markdownx:

pip install django-markdownx

Next, add markdownx to your INSTALLED_APPS in settings.py:

INSTALLED_APPS = (
    # [...],
    'markdownx',

And add markdownx URL patterns to your

urls.py:

urlpatterns = [
    [...]
    url(r'^markdownx/', include('markdownx.urls')),
]

Now, MarkdownX has a two static files it uses for the ability to preview the markdown, markdownx.css and markdownx.js, so we need to run "collectstatic" to collect MarkdownX assets to STATIC_ROOT:

python manage.py collectstatic

Now we can start using MarkdownX in our models. In my example (this blog) I used the MarkdownxField for the body of my blog post:

class BlogPost(DateCreateModMixin):
    title = models.CharField(max_length=50)
    body = MarkdownxField()
    background_image = models.ImageField(default='img/header.jpg', upload_to=datetime.now().strftime('backgrounds/%Y/%m/%d'))

# Note: background_image is just for my header background, not MarkdownX

Where DateCreateModMixin is just a mixin I use for any model where I want to keep track of the created and modified date-times:

class DateCreateModMixin(models.Model):
    class Meta:
        abstract = True

    created_date = models.DateTimeField(default=timezone.now)
    mod_date = models.DateTimeField(blank=True, null=True)

Now that we have our model, we can add it to admin:

@admin.register(BlogPost)
class BlogPostAdmin(MarkdownxModelAdmin):
    list_display = ('title', 'created_date', 'mod_date')
    list_filter = ('created_date', 'mod_date')
    search_fields = ('title',)

I'm doing several things here if you aren't that familiar with Django:

  • @admin.register(BlogPost) adds your model to the admin panel

  • class BlogPostAdmin(MarkdownxModelAdmin) allows you to edit what is displayed when you are viewing the entries in admin and it inherits from MarkdownxModelAdmin so we can see the live preview.

The rest of it isn't MarkdownX specific, but let's go over it anyway (I know I hate see extra unexplained code in examples):

  • list_display = ('title', 'created_date', 'mod_date') shows the Title, Created Date, and Modified Date in the list of Blog Posts.

  • list_filter = ('created_date', 'mod_date') allows you to filter by the fields specified.

Finally,

  • search_fields = ('title',) allows you to search the specified field.

The admin list of blog posts now looks like this:

And the blog entry page looks like this:

Since I'm trying to do this in admin, the live preview is below the entry area, so it's not as easy as I'd like. I will be looking into modifying admin to put them side by side or making a separate edit page in the future because it seems to jump around when it resizes. Another issue I've found is code blocks. I thought the three back-ticks (``` ```) only worked for single lines of code, but GPraz mentioned in the comments that it works if formatted correctly. Unfortunately, it's still not working for me, so maybe I need to update my package. Here is an example of the formating:

Incorrectly formatted:

```first line of code
second line of code```

Renders as:

first line of code second line of code

But,

Correctly formatted:

```
first line of code
second line of code
```

Renders as:

first line of code second line of code

But 4 spaces work as expected (except when you tab, the tab is inserted but the cursor doesn't move to that position):

␣ ␣ ␣ ␣ first line of code
␣ ␣ ␣ ␣ second line of code

Renders as:

first line of code    
second line of code

Now, let's add the add the post(s) to views.py:

from .models import BlogPost


def blog_posts(request):
    """Display all blog posts"""

    posts = BlogPost.objects.all().order_by('-created_date')

    return render(request, 'blog.html', {'posts': posts})


def post(request, pk):
    """Display specific blog posts"""

    post_detail = get_object_or_404(BlogPost, pk=pk)

    return render(request, 'post.html', {'post_detail': post_detail})

Note that this still needs pagination, but I'll add that at a later date.

Next, urls.py needs to be updated:

urlpatterns = [
    [...],
    path('blog/<int:pk>/', views.post, name='post_detail'),
    path('blog/', views.blog_posts, name='blog'),
    path('markdownx/', include('markdownx.urls')),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

This adds the paths to our pages and and media files needed for MarkdownX. Don't forget to add MEDIA_ROOT to settings.py like I did:

MEDIA_URL = '/media/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')

MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')

Finally, let's add the fields to the templates (I'm using bootstrap, so ignore the styling):

blog.html

{% extends 'base.html' %}
{% load staticfiles %}

{% block content %}

{% csrf_token %}

    {% for post in posts %}

        <div class="card shadow bg-white">
            <div class="card-body">
                <h1 class="card-title"> <a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
                <p class="card-text"> {{ post.body_summary|striptags }} </p>
                <div> {{ post.created_date }}</div>
            </div>
        </div>

    {% endfor %}

{% endblock %}

post.html

<header class="masthead text-center text-white d-flex" id="mastheadPost" style="background-image: url(/media/{{ post_detail.background_image }})">
    <div class="container my-auto">
        <div class="row">
            <div class="col-lg-10 mx-auto">
                <h1 style="font-family: 'Fira Code', Impact, sans-serif">
                    {{ post_detail.title }}
                </h1>
            </div>
        </div>
    </div>
</header>

<div id="blog">
    <p class="card-text" id="post"> {{ post_detail.formatted_markdown|safe }} </p>
    <div class="text-muted text-right" style="margin-top: 4rem"> {{ post_detail.created_date }}</div>
</div>

You can see above that I added a couple of methods:

.formatted_markdown

  • formats the markdown in the MarkdownxField() into HTML for the template using markdownify

.body_summary

  • grabs the first 300 characters for the summary

So, the updated model in models.py looks like:

class BlogPost(DateCreateModMixin):
    title = models.CharField(max_length=50)
    body = MarkdownxField()
    background_image = models.ImageField(default='img/header.jpg', upload_to=datetime.now().strftime('backgrounds/%Y/%m/%d'))

    def formatted_markdown(self):
        return markdownify(self.body)

    def body_summary(self):
        return markdownify(self.body[:300] + "...")

That should be it! Feel free to ask any questions below!

Aug. 5, 2018, 1:14 p.m.