Result

This is how our site will look like:

This is what kind of directory structure we will have in the end:

.
├── blog
├── db.sqlite3
├── manage.py
├── mysite
├── static
│   └── css
│       └── base.css
└── templates
    ├── base.html
    ├── blog
    │   ├── detail.html
    │   └── index.html
    └── home.html

Mysite project & blog app

Create a new virtual environment and activate it:

mkdir simple-blog-app
cd simple-blog-app
python3 -m venv ~/.virtualenvs/simple-blog-app
source ~/.virtualenvs/simple-blog-app/bin/activate

Install the django package using the pip package manager:

pip install django

Start a new project and create the blog app directory structure:

django-admin startproject mysite .
python manage.py startapp blog

Post model

Edit the blog app models.py file and add a class called Post to it:

from django.db import models
from django.urls import reverse # < here
from django.utils.text import slugify # < here


class Post(models.Model): # < here
    title = models.CharField(max_length=255,
                             default='',
                             blank=True)
    body = models.TextField(default='',
                            blank=True)
    slug = models.SlugField(max_length=255,
                            default='',
                            blank=True, 
                            unique=True)

    def __str__(self):
        return self.title

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

    def get_absolute_url(self):
        return reverse('blog:detail',
                       args=[str(self.slug)])

Views

Edit the blog app views.py file and add the following view functions to it:

from django.shortcuts import render
from blog.models import Post # < here


def index(request, slug=None): # < here
    posts = Post.objects.all()
    return render(request,
                  'blog/index.html',
                  {
                      'posts': posts,
                      'section': 'blog_index'
                  })


def detail(request, slug=None): # < here
    post = Post.objects.get(slug=slug)
    return render(request,
                  'blog/detail.html',
                  {
                      'post': post,
                      'section': 'blog_detail'
                  })


def home(request): # < here
    return render(request,
                  'home.html',
                  {'section': 'home'})

Templates

Create a directory called templates in the site root. Add the following template source files inside it:

.
├── blog
├── db.sqlite3
├── manage.py
├── mysite
└── templates # < here
    ├── base.html # < here
    └── blog # < subdirectory
    │   ├── detail.html # < here
    │   └── index.html # < here
    └── home.html # < here

Add these lines to the templates/base.html file:

<!doctype html>
{% load static %}
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="{% static 'css/base.css' %}">
    <title>MySite</title>
</head>
<body>

<div class="main">

    <div class="header">
        <ul class="menu">
            <li class="menu-li">
                <a class="menu-li-a {% if section == 'home' %} menu-li-a-active{% endif %}" href="{% url 'home' %}">Home</a>
            </li>
            <li class="menu-li">
                <a class="menu-li-a {% if section == 'blog_index' %} menu-li-a-active{% endif %}" href="{% url 'blog:index' %}">Blog</a>
            </li>
        </ul>
    </div>

    <div class="content">

        <h1>{% block title %}Default heading{% endblock %}</h1>

        {% block content %}Default content{% endblock %}

    </div>

</div>

</body>
</html>

Add these lines to the templates/home.html file:

{% extends 'base.html' %}

{% block title %}Home{% endblock %}

{% block content %}Home content{% endblock %}

Add these lines to the templates/blog/detail.html file:

{% extends 'base.html' %}

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

{% block content %}{{ post.body }}{% endblock %}

Add these lines to the templates/blog/index.html file:

{% extends 'base.html' %}

{% block title %}Blog index{% endblock %}

{% block content %}

    {% for post in posts %}
        [ {{ post.pk }} ]
        <a href="{{ post.get_absolute_url }}">
            {{ post }}
        </a>
        <br>
    {% endfor %}

{% endblock %}

Static files

Create static/css directory structure in the site root and add a file called base.css in the css folder:

.
├── blog
├── manage.py
├── mysite
├── static # < here
│   └── css # < here
│       └── base.css # < here
└── templates

Add these lines to the static/css/base.css file:

body {
    font-size: 30px;
}
.menu {
    list-style: none;
    padding-left: 0;
}

.menu-li {
    display: inline-block;
}

.menu-li-a {
    text-decoration: none;
    color: #000;
    margin: 0 0.3em;
}

.menu-li-a-active {
    padding-bottom: 0.3em;
    border-bottom: 4px solid #528DEA;
}

URLs

Create a new file called urls.py in the blog app directory. Add these lines to it:

from django.urls import path

from . import views

app_name = 'blog'

urlpatterns = [
    path('', views.index, name='index'),
    path('<slug:slug>/', views.detail, name='detail'),
]

Edit the mysite/urls.py file and make the following changes:

from django.contrib import admin
from django.urls import path, include # < here

from blog.views import home # < here

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')), # < here
    path('', home, name='home') # < here
]

Settings

Edit the mysite/settings.py file and make the following changes:

INSTALLED_APPS = [
    'blog.apps.BlogConfig', # 1.
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], # 2
        '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',
            ],
        },
    },
]

STATICFILES_DIRS = [ # 3
    os.path.join(BASE_DIR, 'static'),
]
  1. Reference the blog app config class in the INSTALLED_APPS list.
  2. 'APP_DIRS': True makes the engine look for template files inside installed applications. With the DIRS setting we can define a list of additional directories where the engine looks for templates.
  3. By default, Django will find static files stored in a static subdirectory of each app. With the STATICFILES_DIRS setting we can define a list of additional directories where the staticfiles app should look for static files.

Edit the blog app admin.py file and register the Post model:

from django.contrib import admin
from blog.models import Post # < here

admin.site.register(Post) # < here

Run migrations, create a superuser and start the development server:

python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

Visit http://127.0.0.1:8000/admin and create a few blog posts.

You can find mock data in the repository. Use this command to load it: python manage.py loaddata data.json. I used this command to create the json file: python manage.py dumpdata blog > data.json.

The blog app index page should now look like this: