Build a production-style blog with Django — the best way to learn Django’s MVT pattern, ORM, and admin panel.

What You’ll Build

  • Public blog with post listing and detail pages
  • Admin panel for managing posts
  • User authentication (register, login, logout)
  • Comments on posts
  • Tag filtering and search

Setup

  python -m venv .venv && source .venv/bin/activate
pip install django pillow
django-admin startproject blogproject .
python manage.py startapp blog
python manage.py startapp accounts
  

Add to INSTALLED_APPS: 'blog', 'accounts'.

Models

  # blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name

class Post(models.Model):
    STATUS_CHOICES = [("draft", "Draft"), ("published", "Published")]

    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, max_length=200)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
    content = models.TextField()
    excerpt = models.CharField(max_length=300, blank=True)
    cover_image = models.ImageField(upload_to="covers/", blank=True)
    tags = models.ManyToManyField(Tag, blank=True, related_name="posts")
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="draft")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("post_detail", kwargs={"slug": self.slug})

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    text = models.TextField(max_length=1000)
    created_at = models.DateTimeField(auto_now_add=True)
    approved = models.BooleanField(default=True)

    class Meta:
        ordering = ["created_at"]

    def __str__(self):
        return f"Comment by {self.author} on {self.post.title}"
  
  python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
  

Admin

  # blog/admin.py
from django.contrib import admin
from .models import Post, Tag, Comment

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "status", "created_at"]
    list_filter = ["status", "tags", "created_at"]
    search_fields = ["title", "content"]
    prepopulated_fields = {"slug": ("title",)}
    raw_id_fields = ["author"]

admin.site.register(Tag)
admin.site.register(Comment)
  

Views

  # blog/views.py
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from .models import Post, Tag, Comment
from .forms import CommentForm

class PostListView(ListView):
    model = Post
    template_name = "blog/post_list.html"
    context_object_name = "posts"
    paginate_by = 10

    def get_queryset(self):
        qs = Post.objects.filter(status="published").select_related("author")
        tag = self.request.GET.get("tag")
        if tag:
            qs = qs.filter(tags__slug=tag)
        q = self.request.GET.get("q")
        if q:
            qs = qs.filter(title__icontains=q)
        return qs

class PostDetailView(DetailView):
    model = Post
    template_name = "blog/post_detail.html"
    context_object_name = "post"

    def get_queryset(self):
        return Post.objects.filter(status="published").prefetch_related("comments__author")

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["comment_form"] = CommentForm()
        return context

class CommentCreateView(LoginRequiredMixin, CreateView):
    model = Comment
    form_class = CommentForm

    def form_valid(self, form):
        form.instance.post = get_object_or_404(Post, slug=self.kwargs["slug"])
        form.instance.author = self.request.user
        return super().form_valid(form)

    def get_success_url(self):
        return self.object.post.get_absolute_url()
  

Templates

  <!-- templates/blog/post_list.html -->
{% extends "base.html" %}
{% block content %}
<h1>Blog</h1>
<form method="get">
  <input type="text" name="q" placeholder="Search..." value="{{ request.GET.q }}">
</form>
{% for post in posts %}
<article>
  <h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
  <p><small>By {{ post.author.username }} · {{ post.created_at|date:"M d, Y" }}</small></p>
  <p>{{ post.excerpt|default:post.content|truncatewords:30 }}</p>
  {% for tag in post.tags.all %}
    <a href="?tag={{ tag.slug }}">#{{ tag.name }}</a>
  {% endfor %}
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
{% include "pagination.html" %}
{% endblock %}
  

URLs

  # blog/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("", views.PostListView.as_view(), name="post_list"),
    path("<slug:slug>/", views.PostDetailView.as_view(), name="post_detail"),
    path("<slug:slug>/comment/", views.CommentCreateView.as_view(), name="add_comment"),
]
  

Authentication

Use Django’s built-in auth views:

  # blogproject/urls.py
from django.contrib.auth import views as auth_views
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("blog.urls")),
    path("accounts/login/", auth_views.LoginView.as_view(), name="login"),
    path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
]
  

Skills Applied

Topic Chapter
Models & ORM Django Models
Views & Templates Django Views
Authentication Django Auth
Admin Django Models
Deployment Django Deployment

Bonus Challenges

  1. Add Markdown support for post content (pip install markdown)
  2. Add RSS feed (django.contrib.syndication)
  3. Add post rating/likes with a many-to-many through model
  4. Add author profile pages
  5. Write tests with Django’s TestCase and Client
  6. Deploy with Docker + Gunicorn

This project covers 80% of what you’ll use in real Django development.