On this page
article
Project: Django Blog
Build a complete Django blog — models, views, templates, admin, user auth, comments, and deployment.
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
- Add Markdown support for post content (
pip install markdown) - Add RSS feed (
django.contrib.syndication) - Add post rating/likes with a many-to-many through model
- Add author profile pages
- Write tests with Django’s
TestCaseandClient - Deploy with Docker + Gunicorn
This project covers 80% of what you’ll use in real Django development.