article thumbnail
The django tango
Building Websites with Django: From Setup to Deployment
7 min read
#programming, #python, #django, #webdevelopment, #friday3

Building Websites with Django: From Setup to Deployment


What is Django?

Django is a high-level Python web framework designed to encourage rapid development and clean, pragmatic design. It follows the Model-View-Template (MVT) architectural pattern and comes with batteries included--ORM, admin interface, authentication, and much more.

So Django is the Python world's equivalent to what PHP developers enjoy with Laravel or WordPress--but faster, more secure, and highly scalable.


Setting Up Django

Installation is so similar on both Windows and Linux environments that this article will just show you how on windows.

  1. Install Python

  2. Create a virtual environment

    python -m venv myenv
    myenv\Scripts\activate

    Note: A virtual environment (often created via python -m venv myenv or tools like venv / virtualenv) is an isolated Python environment with its own installation directories for Python packages (i.e. site-packages), independent from the "global" Python installation on your system.

    When you "activate" the virtual environment, commands like pip install ... install packages into that environment, and python resolves to the environment's interpreter (or wrapper) rather than the system-wide one.

    While you do not have to create a virtual environment it is recommended so that dependancies do not conflict.

  3. Install Django

    pip install django
    OR
    python3 -m pip install django
  4. Create your first project

    #change directory to where you want to start
    d:
    mkdir django
    cd d:\django
    #run django-admin to create a project
    django-admin startproject mysite
    #that created the following structure
         mysite/
         ├─ manage.py
         └─ mysite/
            ├─ __init__.py
            ├─ asgi.py
            ├─ settings.py
            ├─ urls.py
            └─ wsgi.py
    #switch to the mysite directory
    cd mysite
    #start the server - this will create a db.sqlite3 database and start the server on port 8000
    python manage.py runserver
    #OR to run on a different port just add the port at the end like this
    python manage.py runserver 8080
    #OR if you want the site to be available to others on your network do the following but note this is just a development server - not intended for production use.
    python manage.py runserver 0.0.0.0:8000
    #Note: to see all options available with runserver do this:
    python manage.py help runserver

Visit http://127.0.0.1:8000/ or http://localhost:8000/ to see your new Django site running!


Building a Blog Website with Django

Django is now installed and running so let's create an blog app inside your project.

Stop the server with Ctrl+C.

Create the Blog App

python manage.py startapp blog

Add blog to INSTALLED_APPS in mysite/settings.py:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",
]

Define the Post Model

Open blog/models.py and add a simple Post model with a slug for clean URLs and a status to allow drafts.

from django.db import models
from django.utils import timezone
from django.urls import reverse

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

    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True, help_text="URL-friendly unique identifier")
    author = models.CharField(max_length=100)
    body = models.TextField()
    created = models.DateTimeField(default=timezone.now, editable=False)
    updated = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=PUBLISHED)

    class Meta:
        ordering = ["-created"]  # newest first

    def __str__(self):
        return self.title

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

Create and apply migrations:

python manage.py makemigrations
python manage.py migrate

Register Post in the Admin

Create a superuser so you can log in:

python manage.py createsuperuser
# Follow prompts for username, email (optional), and password

Register the model in blog/admin.py:

from django.contrib import admin
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ("title", "status", "author", "created", "updated")
    list_filter = ("status", "created", "updated")
    search_fields = ("title", "body", "author")
    prepopulated_fields = {"slug": ("title",)}
    ordering = ("-created",)

Run the server and visit http://127.0.0.1:8000/admin/, then add some posts.

python manage.py runserver

Create URLs for the Blog

Inside the blog app, create a urls.py and define routes:

blog/urls.py

from django.urls import path
from . import views

app_name = "blog"

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

Include the app URLs in the project-level urls (mysite/urls.py):

from django.contrib import admin
from django.urls import path, include

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

Write Views (List + Detail)

Use Django's generic class-based views in blog/views.py:

from django.views.generic import ListView, DetailView
from .models import Post

class PostListView(ListView):
    model = Post
    template_name = "blog/post_list.html"
    context_object_name = "posts"
    paginate_by = 5  # optional pagination

    def get_queryset(self):
        # Show only published posts, newest first (ordering defined in Meta)
        return Post.objects.filter(status=Post.PUBLISHED)

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

Templates (Base, List, Detail)

Create a templates directory and tell Django where to find it. Make a templates folder at the project root (mysite/templates) and update mysite/settings.py:

from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],  # add this
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

Base template templates/base.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}My Django Blog{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'css/styles.css' %}">
  </head>
  <body>
    <header>
      <h1><a href="{% url 'blog:post_list' %}">My Django Blog</a></h1>
      <nav>
        <a href="{% url 'blog:post_list' %}">Home</a>
        <a href="/admin/">Admin</a>
      </nav>
      <hr>
    </header>

    <main>
      {% block content %}{% endblock %}
    </main>

    <footer>
      <hr>
      <p>&copy; {{ now|date:"Y" }} My Django Blog</p>
    </footer>
  </body>
</html>

List template templates/blog/post_list.html:

{% extends "base.html" %}
{% load static %}
{% block title %}Blog -- Posts{% endblock %}
{% block content %}
  {% for post in posts %}
    <article>
      <h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
      <p><small>By {{ post.author }} • {{ post.created|date:"M j, Y" }}</small></p>
      <p>{{ post.body|truncatechars:200 }}</p>
      <p><a href="{{ post.get_absolute_url }}">Read more →</a></p>
      <hr>
    </article>
  {% empty %}
    <p>No posts yet.</p>
  {% endfor %}

  {# Pagination controls #}
  {% if is_paginated %}
    <nav aria-label="Pagination">
      {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">← Newer</a>
      {% endif %}
      <span>Page {{ page_obj.number }} of {{ paginator.num_pages }}</span>
      {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">Older →</a>
      {% endif %}
    </nav>
  {% endif %}
{% endblock %}

Detail template templates/blog/post_detail.html:

{% extends "base.html" %}
{% block title %}{{ post.title }} -- Blog{% endblock %}
{% block content %}
  <article>
    <h1>{{ post.title }}</h1>
    <p><small>By {{ post.author }} • {{ post.created|date:"M j, Y" }}</small></p>
    <div>{{ post.body|linebreaks }}</div>
  </article>
  <p><a href="{% url 'blog:post_list' %}">← Back to all posts</a></p>
{% endblock %}

Static Files (CSS)

Enable static files in mysite/settings.py (Django already includes django.contrib.staticfiles in INSTALLED_APPS).

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

Create static/css/styles.css:

body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; max-width: 780px; margin: 2rem auto; padding: 0 1rem; }
header h1 a { text-decoration: none; }
nav a { margin-right: .75rem; }
article h2 { margin-bottom: .25rem; }
hr { border: none; border-top: 1px solid #ddd; margin: 1.5rem 0; }

Collectstatic is not needed for local dev, but for production you'll run python manage.py collectstatic.


Make It Work: Create Posts


Optional: Add Comments

If you want a simple comment section, add this model to blog/models.py:

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    name = models.CharField(max_length=80)
    body = models.TextField()
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["created"]

    def __str__(self):
        return f"Comment by {self.name}"

Migrate and register in admin:

python manage.py makemigrations
python manage.py migrate

blog/admin.py:

from .models import Post, Comment

class CommentInline(admin.TabularInline):
    model = Comment
    extra = 0

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

To show comments in the detail page, update templates/blog/post_detail.html:

{% extends "base.html" %}
{% block title %}{{ post.title }} -- Blog{% endblock %}
{% block content %}
  <article>
    <h1>{{ post.title }}</h1>
    <p><small>By {{ post.author }} • {{ post.created|date:"M j, Y" }}</small></p>
    <div>{{ post.body|linebreaks }}</div>
  </article>

  <section>
    <h2>Comments</h2>
    {% if post.comments.all %}
      <ul>
        {% for c in post.comments.all %}
          <li>
            <p><strong>{{ c.name }}</strong> -- <small>{{ c.created|date:"M j, Y, g:i a" }}</small></p>
            <p>{{ c.body|linebreaks }}</p>
          </li>
        {% empty %}
          <li>No comments yet.</li>
        {% endfor %}
      </ul>
    {% endif %}
  </section>

  <p><a href="{% url 'blog:post_list' %}">← Back to all posts</a></p>
{% endblock %}

For a full comment form (create via POST), you would add a ModelForm, a CreateView/view function, and CSRF-protected form template. The inline admin shown here is the fastest route to a working demo.


Optional: Friendly 404 & 500 Pages

Create templates/404.html and templates/500.html to customize error pages (shown when DEBUG = False).

<!-- templates/404.html -->
{% extends "base.html" %}
{% block title %}Page Not Found{% endblock %}
{% block content %}
  <h1>Sorry, we couldn't find that.</h1>
  <p><a href="{% url 'blog:post_list' %}">Back home</a></p>
{% endblock %}

Run & Test

python manage.py runserver

Open:

Smoke test checklist:


Next Steps (Nice to Have)


Final Project Structure (Minimal Blog)

mysite/
├─ manage.py
├─ mysite/
│  ├─ __init__.py
│  ├─ asgi.py
│  ├─ settings.py
│  ├─ urls.py
│  └─ wsgi.py
├─ blog/
│  ├─ __init__.py
│  ├─ admin.py
│  ├─ apps.py
│  ├─ migrations/
│  │  └─ 0001_initial.py
│  ├─ models.py
│  ├─ tests.py
│  ├─ urls.py
│  └─ views.py
├─ templates/
│  ├─ base.html
│  └─ blog/
│     ├─ post_detail.html
│     └─ post_list.html
└─ static/
   └─ css/
      └─ styles.css

You're Done 🎉

You now have a working Django blog you can extend as you like. Add posts in the admin and enjoy your new site!