Add a Blog to Your Django Website

Updated on · 6 min read
Add a Blog to Your Django Website

In the world of web development, Django has become an increasingly popular framework for creating powerful and scalable web applications. One common feature developers often need to integrate into their projects is a blog. Although there are numerous solutions available for creating and hosting blogs, it can be beneficial to have a seamless integration with your existing Django application. In this tutorial, we will explore the process of setting up a blog within your Django website, providing you with complete control over its features and functionality.

This tutorial is intended for web developers who already have a Django website and are looking to add a blog to it. If you don't have an existing Django website, one can be easily created by following the official instructions. We will be using Django 4.1 and assuming that you have a basic understanding of Django's structure and components. By the end of this article, you will have a fully functional, customizable, and easy-to-maintain blog integrated into your Django website.

Setting up

Considering you have Django installed, we need to add a few more packages for the blog.

shell
pip install django-autoslug django-ckeditor
shell
pip install django-autoslug django-ckeditor

We are adding django-autoslug to automatically create post URLs from their titles, and Django CKEditor as a rich text editor for the posts.

Now, we can run the startapp command to scaffold our blog app. This should be executed from the same folder where the manage.py file is located:

shell
python manage.py startapp blog
shell
python manage.py startapp blog

This will create a blog directory with the following structure:

shell
blog/ migrations/ __init__.py __init__.py admin.py apps.py models.py tests.py views.py
shell
blog/ migrations/ __init__.py __init__.py admin.py apps.py models.py tests.py views.py

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

python
INSTALLED_APPS = [ # other apps ... 'mysite.blog' ]
python
INSTALLED_APPS = [ # other apps ... 'mysite.blog' ]

Adding models

Now we can start setting up the blog. We begin by adding a BlogPost model in the models.py file.

python
from django.db import models from django.utils.translation import gettext_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']
python
from django.db import models from django.utils.translation import gettext_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']

In addition to 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 - a uniquely identifiable part of the blog post URL. We are using the django-autoslug package, which makes it easy to preserve the field's uniqueness and auto-populating it. In this case, we're populating the slug from the title using the populate_from parameter.
  • published - to indicate when a post should be visible on the website.
  • pub_date - the 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. This can be done by extending the Django model's save method:

python
from datetime import datetime def save(self, *args, **kwargs): """ Set publish date to the date when the post's published status is switched to True, reset the date if the 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)
python
from datetime import datetime def save(self, *args, **kwargs): """ Set publish date to the date when the post's published status is switched to True, reset the date if the 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)

To complete the model setup, we will add two more utility methods to it: __str__(), which will be used to represent the model throughout the entire app (particularly in the admin), and get_absolute_url(), which will simplify retrieving the URL to a blog post in the views.

python
from django.urls import reverse def __str__(self): return self.title def get_absolute_url(self): return reverse('blog:detail', kwargs={'slug': self.slug})
python
from django.urls import reverse def __str__(self): return self.title def get_absolute_url(self): return reverse('blog:detail', kwargs={'slug': self.slug})

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

shell
python manage.py makemigrations blog python manage.py migrate blog
shell
python manage.py makemigrations blog python manage.py migrate blog

If you encounter an error while creating the migrations: "django.core.exceptions.ImproperlyConfigured: Cannot import 'blog'.", ensure that the app name in the blog/apps.py file is correct. The apps.py should look like this (replace mysite with the name of your site):

python
from django.apps import AppConfig class Blog2Config(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'mysite.blog'
python
from django.apps import AppConfig class Blog2Config(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'mysite.blog'

At this point, it would be a good idea to add the newly created migrations to version control and commit/push your progress.

Adding the model to the Django admin

Now that we have a properly functioning BlogPost model, it's time to add it to Django's admin. Considering the power of Django's admin, enabling a new model there is a relatively quick task:

python
from django.contrib import admin from .models import BlogPost @admin.register(BlogPost) class BlogPostAdmin(admin.ModelAdmin): list_display = ('title', 'image', 'text', 'published', 'pub_date')
python
from django.contrib import admin from .models import BlogPost @admin.register(BlogPost) class BlogPostAdmin(admin.ModelAdmin): list_display = ('title', 'image', 'text', 'published', 'pub_date')

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 the development server on port 8000) and start creating blog posts.

Displaying the posts in the views

The next step, after adding some posts via the admin, would naturally be to display them in the views. Our blog will 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 a minimal amount of code. We can set them up in the views.py like so:

python
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'
python
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'

Quite straightforward. It takes only one line of code to enable pagination in the view. This provides us with is_paginated and several other useful context variables to render pagination in templates.

Note that we are applying the same filter to a queryset in two places. While this is not a significant issue here, in some cases, we might want to abstract this kind of functionality away. In the current situation, we can add an instance of Manager with custom QuerySet methods to our model.

python
# 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()
python
# 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()

Now, in the views, we can use BlogPost.objects.published() to get all the published blog posts. At this point, we have already implemented the main bulk of functionality, and it only required a few lines of code across several files.

Setting up the templates

Finally, we can set up the list and detail templates. Following the Django convention, we will place them in mysite/blog/templates/blog/. First, we'll extend the main base template and also add a reusable navbar template, setting "blog" as the active tab:

python
{% extends "base.html" %}{% load i18n staticfiles %} {% block header %} {% include "includes/navbar.html" with active="blog" %} {% endblock %}
python
{% extends "base.html" %}{% load i18n staticfiles %} {% block header %} {% include "includes/navbar.html" with active="blog" %} {% endblock %}

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

html
{% load i18n staticfiles %} <nav class="navbar"> <div class="navbar__header"> <div class="navbar--left"> <a class="{% if active == 'home' %}active{% endif %} no-underline" href="/" >{% trans "Home" %}</a > </div> </div> <div class="navbar__inner"> <ul class="navbar--right"> <li class="navbar__tab"> <a class="{% if active == 'portfolio' %}active{% endif %} no-underline" href="{% url 'portfolio:list' %}" > {% trans "Portfolio" %} </a> </li> <li class="navbar__tab"> <a class="{% if active == 'blog' %}active{% endif %} no-underline" href="{% url 'blog:list' %}" > {% trans "Blog" %} </a> </li> <li class="navbar__tab"> <a class="{% if active == 'about' %}active{% endif %} no-underline" href="{% url 'about' %}" > {% trans "About" %} </a> </li> </ul> </div> </nav>
html
{% load i18n staticfiles %} <nav class="navbar"> <div class="navbar__header"> <div class="navbar--left"> <a class="{% if active == 'home' %}active{% endif %} no-underline" href="/" >{% trans "Home" %}</a > </div> </div> <div class="navbar__inner"> <ul class="navbar--right"> <li class="navbar__tab"> <a class="{% if active == 'portfolio' %}active{% endif %} no-underline" href="{% url 'portfolio:list' %}" > {% trans "Portfolio" %} </a> </li> <li class="navbar__tab"> <a class="{% if active == 'blog' %}active{% endif %} no-underline" href="{% url 'blog:list' %}" > {% trans "Blog" %} </a> </li> <li class="navbar__tab"> <a class="{% if active == 'about' %}active{% endif %} no-underline" href="{% url 'about' %}" > {% trans "About" %} </a> </li> </ul> </div> </nav>

Since the HTML structure and its styling are not the main focus of this post, we'll discuss them only briefly. A simple list of posts can be set up with the following HTML:

html
<!-- Assuming you have an overridable 'content' block in your 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 %}
html
<!-- Assuming you have an overridable 'content' block in your 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 %}

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 the post's id in the template.
  • As mentioned earlier, enabling pagination in the views exposes a bunch of useful context variables in the template, namely is_paginated, used to check if pagination has to be rendered, and page_obj, containing information about page numbering.

Blog URLs

Speaking of URLs, if we try to navigate to our blog posts list page, we are going to encounter a NoReverseMatch error since we haven't set up the URLs for the blog. To fix that, let's add a urls.py to our blog app (at the 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.

python
from django.urls import path from .views import BlogPostDetailView, BlogPostListView urlpatterns = [ path('', BlogPostListView.as_view(), name='list'), path('<slug>', BlogPostDetailView.as_view(), name='detail'), ]
python
from django.urls import path from .views import BlogPostDetailView, BlogPostListView urlpatterns = [ path('', BlogPostListView.as_view(), name='list'), path('<slug>', BlogPostDetailView.as_view(), name='detail'), ]

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

python
from django.urls import include, path urlpatterns = [ # .... other urls path('blog/', include(('clarityv2.blog.urls', 'blog'), namespace='blog')), ]
python
from django.urls import include, path urlpatterns = [ # .... other urls path('blog/', include(('clarityv2.blog.urls', 'blog'), namespace='blog')), ]

Blog post detail view

And with that, we can navigate to localhost:8000/blog and see the list of blog posts 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:

html
{% 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 %}
html
{% 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 %}

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

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

Conclusion

In this blog post, we have successfully demonstrated how to create a simple, yet functional, blog for an existing Django website. By leveraging Django's built-in features and a few additional packages, we were able to implement CRUD functionality, create views, and set up templates with ease. While this basic blog serves as a great starting point, there are endless possibilities for customization and enhancements to better suit your specific needs.

I hope this tutorial has provided you with valuable insights into Django's capabilities for blog creation and inspired you to further explore its features. As you continue to develop your blog, don't hesitate to dive deeper into Django's extensive documentation and community resources.

References and resources