How to Create a Simple Blog in Django (5.x, Step by Step)

Blog / Django REST Framework · November 3, 2025 · Updated June 9, 2026 · 8 min read
How to Create a Simple Blog in Django (5.x, Step by Step)

Building a simple blog is the classic way to learn Django, and it touches almost every part of the framework: models, the admin, views, URLs, and templates. By the end of this tutorial you will have a working blog where the homepage lists all published posts and each post has its own detail page, all backed by the Django admin for content entry.

Here is exactly what you will build:

  • A Post model with a title, slug, body, author, timestamps, and a published flag.
  • The Django admin wired up so you can create and edit posts in a browser.
  • A post list page and a post detail page — shown as both function-based and class-based views.
  • Clean URLs that use the post slug, plus minimal templates to render everything.
  • A short note on exposing your posts as a REST API with Django REST Framework.

Prerequisites: Python 3.12 or newer, basic familiarity with the command line, and a little Python. This guide targets Django 5.x (the current 5.2 LTS line). No prior Django experience is required.

Step 1: Set up the project and app

Start with an isolated virtual environment so your dependencies stay self-contained, then install Django and create the project and app.

# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

# Install the latest Django 5.x
python -m pip install --upgrade pip
pip install "Django>=5.2,<6.0"

# Create the project and the blog app
django-admin startproject mysite
cd mysite
python manage.py startapp blog

Register the new app in mysite/settings.py by adding it to INSTALLED_APPS:

# mysite/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",  # our app
]

Step 2: Define the Post model

The model is the heart of the blog. We give each post a slug for clean URLs, created/updated timestamps, and an author foreign key. Always reference the user via settings.AUTH_USER_MODEL rather than importing User directly — that keeps your code working even if the project later switches to a custom user model.

# blog/models.py
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify


class Post(models.Model):
    class Status(models.TextChoices):
        DRAFT = "DF", "Draft"
        PUBLISHED = "PB", "Published"

    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250, unique_for_date="published")
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="blog_posts",
    )
    body = models.TextField()
    published = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    status = models.CharField(
        max_length=2,
        choices=Status.choices,
        default=Status.DRAFT,
    )

    class Meta:
        ordering = ["-published"]
        indexes = [models.Index(fields=["-published"])]

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse("blog:post_detail", args=[self.slug])

A few notes on the choices above. TextChoices gives you a readable enum for the draft/published workflow. Auto-populating the slug in save() is convenient for a beginner blog; in the admin you can also use prepopulated_fields (shown below) so editors see the slug fill in as they type. get_absolute_url() returns the canonical URL for a post, which the admin and templates can reuse.

Step 3: Run migrations

Migrations turn your model into a database table. Create them, then apply them along with Django's built-in tables.

python manage.py makemigrations blog
python manage.py migrate

makemigrations generates a migration file describing the Post table, and migrate executes it against the database (SQLite by default, which is perfect for learning).

Step 4: Register the model in the admin

The Django admin gives you a polished interface for creating posts with zero extra code. Register the Post model and customise the list display.

# blog/admin.py
from django.contrib import admin

from .models import Post


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "published", "status"]
    list_filter = ["status", "created", "published", "author"]
    search_fields = ["title", "body"]
    prepopulated_fields = {"slug": ("title",)}
    raw_id_fields = ["author"]
    date_hierarchy = "published"
    ordering = ["status", "published"]

Create a superuser so you can log in, then start the development server:

python manage.py createsuperuser
python manage.py runserver

Visit http://127.0.0.1:8000/admin/, log in, and add a couple of posts (set their status to Published). With content in place, we can build the public pages.

If you want a deeper, production-ready user setup before going further, see our guide on creating a custom user model in Django — it pairs naturally with the AUTH_USER_MODEL reference used above.

Step 5: Write the views

We need two pages: a list of published posts and a single post detail. Django lets you write these as plain functions or as reusable generic class-based views. Here are both styles so you can see the trade-offs.

Function-based views

# blog/views.py
from django.shortcuts import get_object_or_404, render

from .models import Post


def post_list(request):
    posts = Post.objects.filter(status=Post.Status.PUBLISHED)
    return render(request, "blog/post/list.html", {"posts": posts})


def post_detail(request, slug):
    post = get_object_or_404(
        Post,
        slug=slug,
        status=Post.Status.PUBLISHED,
    )
    return render(request, "blog/post/detail.html", {"post": post})

Class-based views

The same two pages using Django's generic ListView and DetailView. These remove boilerplate — Django handles fetching the object, pagination, and 404s for you.

# blog/views.py (class-based alternative)
from django.views.generic import DetailView, ListView

from .models import Post


class PostListView(ListView):
    queryset = Post.objects.filter(status=Post.Status.PUBLISHED)
    context_object_name = "posts"
    paginate_by = 5
    template_name = "blog/post/list.html"


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

    def get_queryset(self):
        return super().get_queryset().filter(status=Post.Status.PUBLISHED)

Use whichever fits the task: function-based views are explicit and easy to follow, while class-based views shine when you want built-in behaviour like pagination and minimal repetition. If you are weighing them up, our walkthrough on migrating from function-based views to class-based views goes deeper into when each pays off.

Step 6: Configure URLs

Give the app its own urls.py with a namespace, then include it in the project's root URL configuration. The detail route uses a slug path converter so URLs look like /blog/my-first-post/.

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

from . import views

app_name = "blog"

urlpatterns = [
    # Function-based views:
    path("", views.post_list, name="post_list"),
    path("<slug:slug>/", views.post_detail, name="post_detail"),
    # Class-based equivalents (swap in if you prefer):
    # path("", views.PostListView.as_view(), name="post_list"),
    # path("<slug:slug>/", views.PostDetailView.as_view(), name="post_detail"),
]
# mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls", namespace="blog")),
]

Step 7: Create the templates

Django looks for app templates under each app's templates/ directory. Create blog/templates/blog/base.html plus the two page templates. The default APP_DIRS setting (already on in a fresh project) means no extra TEMPLATES configuration is needed.

The base template:

{# blog/templates/blog/base.html #}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>{% block title %}My Blog{% endblock %}</title>
</head>
<body>
    <h1><a href="{% url 'blog:post_list' %}">My Blog</a></h1>
    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>

The list page, which links each post to its detail page using get_absolute_url:

{# blog/templates/blog/post/list.html #}
{% extends "blog/base.html" %}

{% block title %}All posts{% endblock %}

{% block content %}
  <h2>Latest posts</h2>
  {% for post in posts %}
    <article>
      <h3><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
      <p>By {{ post.author }} on {{ post.published|date:"M d, Y" }}</p>
      <p>{{ post.body|truncatewords:30 }}</p>
    </article>
  {% empty %}
    <p>No posts yet.</p>
  {% endfor %}
{% endblock %}

And the detail page:

{# blog/templates/blog/post/detail.html #}
{% extends "blog/base.html" %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
  <article>
    <h2>{{ post.title }}</h2>
    <p>By {{ post.author }} on {{ post.published|date:"M d, Y" }}</p>
    {{ post.body|linebreaks }}
  </article>
  <p><a href="{% url 'blog:post_list' %}">&larr; Back to all posts</a></p>
{% endblock %}

Restart the server with python manage.py runserver and open http://127.0.0.1:8000/blog/. You should see your published posts, each linking to its own detail page. That is a complete, working Django blog.

Step 8 (optional): Expose the blog as a REST API

Because a blog's data model maps so cleanly onto resources, it is a great first REST API. With Django REST Framework you can serve your posts as JSON in just a few lines. Install it with pip install djangorestframework and add "rest_framework" to INSTALLED_APPS, then add a serializer and a viewset.

# blog/serializers.py
from rest_framework import serializers

from .models import Post


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "title", "slug", "author", "body", "published", "status"]


# blog/api.py
from rest_framework import viewsets

from .models import Post
from .serializers import PostSerializer


class PostViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Post.objects.filter(status=Post.Status.PUBLISHED)
    serializer_class = PostSerializer
    lookup_field = "slug"
# mysite/urls.py (add the API routes)
from rest_framework.routers import DefaultRouter

from blog.api import PostViewSet

router = DefaultRouter()
router.register(r"posts", PostViewSet, basename="post")

urlpatterns += [
    path("api/", include(router.urls)),
]

Your posts are now available at http://127.0.0.1:8000/api/posts/. From here you can add authentication, write access, filtering, and pagination. For a fuller treatment, follow our guide on building a RESTful web service in Django with DRF, or start with the introduction to API development with Django REST Framework.

Where to go next

With the core in place, common next steps are letting readers comment, adding a form so authors can publish from the front end, full-text search, tags, and pagination on the list view. Each of these reuses the same model–view–template loop you just built, so the learning curve stays gentle.

If you are building something more ambitious than a learning project, MicroPyramid has shipped production Django applications for startups and enterprises for 12+ years; our Django development services cover architecture, REST APIs, and long-term maintenance.

Frequently Asked Questions

What do I need to build a blog in Django?

You need Python 3.12 or newer and Django 5.x (pip install "Django>=5.2,<6.0"). Beyond that, the framework ships with everything else you need — an ORM for the database, an admin interface, a templating engine, and a development server — so a simple blog needs no extra third-party packages.

Should I use function-based or class-based views for a blog?

Both work. Function-based views are explicit and easy to read, which makes them great for learning. Class-based generic views like ListView and DetailView remove boilerplate and give you pagination and object lookup for free, so they scale better as the project grows. This tutorial shows both so you can compare them on the same two pages.

Why reference settings.AUTH_USER_MODEL instead of importing User?

Using settings.AUTH_USER_MODEL in your ForeignKey keeps the model decoupled from a specific user class. If you later swap in a custom user model — which is recommended for any serious project — your blog keeps working without a painful migration. Importing django.contrib.auth.models.User directly hard-codes that dependency.

How do I add a REST API to my Django blog?

Install Django REST Framework (pip install djangorestframework), add "rest_framework" to INSTALLED_APPS, then create a ModelSerializer for the Post model and register a viewset with a router. That exposes your posts as JSON endpoints under /api/, ready for a SPA or mobile client.

How do I let visitors create or comment on posts?

For author-facing creation, add a ModelForm for Post and a view that saves it; for reader comments, create a Comment model with a ForeignKey to Post and a small comment form. Both build on the same model–view–template pattern. Our Django forms basics guide walks through validating and saving form data.

Is this tutorial current for Django 5.x?

Yes. The code targets the Django 5.2 LTS line on Python 3.12+, using current idioms such as TextChoices, path() URL converters, the @admin.register decorator, and app-directory template loading. The 5.2 LTS release is supported into 2028, so this structure will stay valid for new projects.

Share this article