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:

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):

Finally,

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

.body_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.
Load Comments