Adding a blog to your Django website

clarity89

Alex K.

Posted on July 19, 2019

Adding a blog to your Django website

Nowadays there exist a ton of solutions for easily creating and hosting blogs. However, sometimes you have an already existing website and just want to add a blog to it, without using other tools than the ones already at hand. In this article we'll go through the process of setting up a blog with Django and see how easy and straightforward it is. This post assumes that you already have a Django app/site. If not, one can be easily created by following the official instructions. Django version used is 2.2.

First off, we start by running a handy startapp command, which will scaffold our blog app. This should be run from the same folder where manage.py is:

$ python manage.py startapp blog
Enter fullscreen mode Exit fullscreen mode

This will add a blog directory with the following structure:

blog/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py
Enter fullscreen mode Exit fullscreen mode

The first step would be to add the newly created app to the list of INSTALLED_APPS in the settings.py:

INSTALLED_APPS = [
     # other apps ...
    'mywebsite.blog'
]
Enter fullscreen mode Exit fullscreen mode

Now we can start setting up the blog. We begin by opening models.py and adding a BlogPost model there.

from datetime import datetime

from django.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _

from autoslug.fields import AutoSlugField
from ckeditor.fields import RichTextField

class BlogPost(models.Model):
    title = models.CharField(_('title'), max_length=255)
    slug = AutoSlugField(_('slug'), populate_from='title', unique=True)
    image = models.ImageField(_('image'), blank=True, null=True, upload_to='blog')
    text = RichTextField(_('text'))
    description = models.TextField(_('description'), blank=True, null=True)
    published = models.BooleanField(_('published'), default=False)

    created = models.DateTimeField(_('created'), auto_now_add=True)
    modified = models.DateTimeField(_('modified'), auto_now=True)
    pub_date = models.DateTimeField(_('publish date'), blank=True, null=True)

    class Meta:
        verbose_name = _('blog post')
        verbose_name_plural = _('blog posts')
        ordering = ['pub_date']
Enter fullscreen mode Exit fullscreen mode

Apart from the expected title, image, description and text, we have a few extra fields, which will be used when publishing and navigating to a blog post:

  • slug - is a uniquely identifiable part of the bog post URL. We are using django-autoslug package, which will make preserving the field's uniqueness and auto populating a breeze. In this case we're populating slug from title by using populate_from param.
  • published - to indicate when a post should be visible on the website.
  • pub_date - a date when a post was published, also used to order the posts.

So far so good. However, it would make sense to set the pub_date automatically when a post is set to published. It can be done by extending Django model's save method:

    def save(self, *args, **kwargs):
        """
        Set publish date to the date when post's published status is switched to True, 
        reset the date if post is unpublished
        """
        if self.published and self.pub_date is None:
            self.pub_date = datetime.now()
        elif not self.published and self.pub_date is not None:
            self.pub_date = None
        super().save(*args, **kwargs)
Enter fullscreen mode Exit fullscreen mode

To finish up with the model setup, we are going to add two more utility methods to it: __str__(), which will be used to represent the model throughout the whole app (particularly in the admin), and get_absolute_url(), which will make retrieving the URL to a blog post in the views easier.

 def __str__(self):
     return self.title

 def get_absolute_url(self):
     return reverse('blog:detail', kwargs={'slug': self.slug})
Enter fullscreen mode Exit fullscreen mode

With the model out of the way, we can go on to create the migrations and run them.

# Create migrations
$ python manage.py makemigrations blog

# Apply them
$ python manage.py migrate blog
Enter fullscreen mode Exit fullscreen mode

At this point it would be a good idea to add newly created migrations to version control (I always seem to forget that) and commit/push the progress.

Now that we have a proper and functioning BlogPost model, it's time to add it to Django's admin. Considering how powerful the admin is, enabling a new model there is quite a quick job:

from django.contrib import admin
from .models import BlogPost


@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
    list_display = ('title', 'image', 'text', 'published', 'pub_date')
Enter fullscreen mode Exit fullscreen mode

And that's it for the admin setup! After adding just these few lines of code we can now navigate to http://localhost:8000/admin/blog/ (by default Django runs development sever on port 8000) and start creating blog posts.

The next step, after adding some posts via admin, naturally would be to display them in the views. Our blog is going to have a paginated list view with all the posts in chronological order and a separate detail view for each post. We are going to use Class Based Views since they allow great code reuse and enable writing the most common views with minimal amount of code. We can set them up in the views.py like so:

from django.views.generic import ListView, DetailView

from .models import BlogPost


class BlogPostListView(ListView):
    model = BlogPost
    queryset = BlogPost.objects.filter(published=True)
    template_name = 'blog/list.html'
    context_object_name = 'blog_posts'
    paginate_by = 15  # that is all it takes to add pagination in a Class Based View


class BlogPostDetailView(DetailView):
    model = BlogPost
    queryset = BlogPost.objects.filter(published=True)
    template_name = 'blog/detail.html'
    context_object_name = 'blog_post'
Enter fullscreen mode Exit fullscreen mode

Pretty straightforward. Note that it takes one line of code to enable pagination in the view. That will give us is_paginated and a few other useful context variables to render pagination in templates.

Note that we are applying the same filter to a queryset in two places. This is not a big deal here, however in some cases we would like to abstract this kind of functionality away. In the current case we can add an instance of Manager with custom QuerySet methods to our model.

# models.py
class BlogPostQueryset(models.QuerySet):

    def published(self):
        return self.filter(published=True)

    def draft(self):
        return self.filter(published=False)

class BlogPost(models.Model):
    title = models.CharField(_('title'), max_length=255)
    ...
    objects = BlogPostQueryset.as_manager()
Enter fullscreen mode Exit fullscreen mode

Now in the views we can use BlogPost.objects.published() to get all the published blog posts. At this point we already got the main bulk of functionality out of the way and it only took a few lines of code in a bunch of files.

Finally we can setup the list and detail templates. Per Django convention we will put them into mywebsite/blog/templates/blog/. Firstly we'll extend the main base template and also will add a reusable navbar template, setting "blog" as an active tab:

{% extends "base.html" %}{% load i18n staticfiles %}

{% block header %}
    {% include "includes/navbar.html" with active="blog" %}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

For reference, navbar.html could look something like this:

{% load i18n staticfiles %}
<nav class="navbar">
    <div class="navbar__header">
        <div class="navbar--left">
            <a class="no-underline {% if active == 'home' %}active{% endif %}" href="/">{% trans "Home" %}</a>
        </div>
    </div>

    <div class="navbar__inner">
        <ul class="navbar--right">
            <li class="navbar__tab">
                <a class="no-underline {% if active == 'portfolio' %}active{% endif %}"
                   href="{% url 'portfolio:list' %}">
                    {% trans "Portfolio" %}
                </a>
            </li>
            <li class="navbar__tab">
                <a class="no-underline {% if active == 'blog' %}active{% endif %}"
                   href="{% url 'blog:list' %}">
                    {% trans "Blog" %}
                </a>
            </li>
            <li class="navbar__tab">
                <a class="no-underline {% if active == 'about' %}active{% endif %}" href="{% url 'about' %}">
                    {% trans "About" %}
                </a>
            </li>
        </ul>
    </div>
</nav>
Enter fullscreen mode Exit fullscreen mode

Since the HTML structure and its styling is not the main purpose of this post, we'll discuss those only in brief. A simple list of posts can be setup with this HTML:

<!-- Assuming you have overridable 'content' block in you master.html, otherwise leave this out-->
{% block content %}
    <div class="blog__container">
        <h3 class="blog__header">{% trans "Latest posts" %}</h3>
        <ul class="blog__list">
            {% for post in  blog_posts %}
                <li class="blog__item">
                    <div class="blog__inner">
                        {% if post.image %}
                            <div class="blog__image" style="background-image: url({{ post.image.url }})"></div>
                        {% endif %}
                        <div class="blog__info">
                            <a href="{{ post.get_absolute_url }}" class="blog__title">{{ post.title }}</a>
                            <p class="blog__description">{{ post.description }}</p>
                            <p class="blog__footer">{{ post.pub_date|date }}</p>
                        </div>
                    </div>
                </li>
            {% endfor %}
        </ul>
    </div>
     {% if is_paginated %}
        <div class="blog__pagination">
            {% if page_obj.has_previous %}
                <a href="{% url 'blog:list' %}?page={{ page_obj.previous_page_number }}"><<</a>
            {% endif %}
                <span class="page-current">
                    {% blocktrans with num=page_obj.number num_pages=page_obj.paginator.num_pages %}
                        Page {{ num }} of {{ num_pages }}
                    {% endblocktrans %}
                </span>
            {% if page_obj.has_next %}
                <a href="{% url 'blog:list' %}?page={{ page_obj.next_page_number }}">>></a>
            {% endif %}
        </div>
    {% endif %}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

A few things worth noting here:

  • background-image is used instead of <img/> because it's easier to make responsive in most cases.
  • We're using get_absolute_url, added to the model, in the post's title href to easily redirect to a post's detail view without needing to explicitly pass post's id in the template.
  • As mentioned before, enabling pagination in the views exposes a bunch of useful context variables in template, namely is_paginated, used to check if pagination has to be rendered and page_obj, containing the information about page numbering.

Talking about URLs, if we try to navigate to our blog posts list page, we are going to receive a NoReverseMatch error since we haven't setup the URLs for the blog. To fix that, let's add urls.py to our blog app (same level as models.py and views.py) and enable the necessary routes. Note that Django starting from version 2 introduced a new path function for declaring URLs.

from django.urls import path

from .views import BlogPostDetailView, BlogPostListView

urlpatterns = [
    path('', BlogPostListView.as_view(), name='list'),
    path('<slug>', BlogPostDetailView.as_view(), name='detail'),
]
Enter fullscreen mode Exit fullscreen mode

After that we need to go to the root urls.py and add the blog URLs there:

from django.urls import include, path

urlpatterns = [
    # .... other urls
    path('blog/', include(('clarityv2.blog.urls', 'blog'), namespace='blog')),
]
Enter fullscreen mode Exit fullscreen mode

And with that we can navigate to localhost:8000/blog and see the list of blogs we created. Clicking on the post title should redirect us to the post detail page, however since we have not set it up, we see a blank page. To fix this let's add some simple HTML to display a post:

{% extends "base.html" %}{% load i18n staticfiles %}

{% block header %}
    {% include "includes/navbar.html" with active="blog" %}
{% endblock %}

{% block content %}
    <article class="blog__container">
        <section class="blog__header">
            <h3 class="blog__title blog__title--large">{{ blog_post.title }}</h3>
            <p class="blog__footer">
                {{ blog_post.pub_date|date }}
            </p>
        </section>
        <section class="blog__text">{{ blog_post.text|safe }}</section>
    </article>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Here we use safe template filter to escape HTML markdown from the editor.

That's about it! Now we have a fully functional, albeit simple, personal blog with all necessary CRUD functionality. There are several ways it can be styled and extended, but that is left as an exercise for the reader :)

💖 💪 🙅 🚩
clarity89
Alex K.

Posted on July 19, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related