Previously on Foodiegram: In Part 2 we created the posts app, defined a Post model, wrote basic views to list and display recipes and wired up URL patterns.  But if you point your browser at the homepage now, you’ll see a blank page.  This instalment is all about making those recipes visible by adding templates and some minimal styling.

Django templates are HTML files with placeholders.  We’ll build a base template, a post list template and a detail template.  We’ll also add some Bootstrap styling to make the app look decent without spending hours on CSS.

Setting up static directories

Inside the posts app, create a static/posts/ folder with subfolders for CSS, JavaScript and images.  The original tutorial illustrates the structure as follows:

posts/
└── static/
    └── posts/
        ├── css/
        ├── images/
        └── js/

Create posts/static/posts/css/style.css and add some minimal styling.  The Foodiegram example uses a light background, bold navbar brand and card shadows.  Feel free to customise your own colours – here’s a starting point:

body {
    font-family: sans-serif;
    background-color: #f8f9fa;
}

.navbar-brand {
    font-weight: bold;
    color: #28a745 !important;
}

.card {
    transition: transform 0.2s;
    border: none;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.card:hover {
    transform: translateY(-5px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}

.post-image {
    height: 200px;
    object-fit: cover;
}

.post-detail-image {
    max-height: 400px;
    object-fit: cover;
}

Base template

In posts/templates/posts/ create base.html.  This file contains the HTML skeleton and loads Bootstrap via a CDN, then our custom CSS.  Foodiegram’s base template includes a responsive navigation bar and a footer.  Here is a trimmed version:

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Foodiegram{% endblock %}</title>
    <!-- Bootstrap CSS -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
    <!-- Custom CSS -->
    <link href="{% static 'posts/css/style.css' %}" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
        <div class="container">
            <a class="navbar-brand" href="{% url 'post_list' %}">Foodiegram</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'post_list' %}">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
    <main class="container my-5">
        {% block content %}{% endblock %}
    </main>
    <footer class="bg-dark text-white text-center py-3 mt-5">
        <p>&copy; 2016 Foodiegram.  Share your favourite recipes with the world!</p>
    </footer>
    <!-- Bootstrap JS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Post list template

Create posts/templates/posts/post_list.html.  This file extends base.html and loops over the posts context variable provided by post_list().  Foodiegram’s example displays recipe cards with images, truncated descriptions and meta information:

{% extends 'posts/base.html' %}
{% load static %}

{% block title %}Home – Foodiegram{% endblock %}

{% block content %}
<div class="row">
    <div class="col-12">
        <h1 class="text-center mb-5">Delicious Recipes</h1>
        {% if posts %}
            <div class="row">
                {% for post in posts %}
                <div class="col-md-6 col-lg-4 mb-4">
                    <div class="card h-100">
                        <a href="{% url 'post_detail' post.slug %}">
                            <img src="{{ post.image.url }}" class="card-img-top post-image" alt="{{ post.title }}">
                        </a>
                        <div class="card-body d-flex flex-column">
                            <h5 class="card-title">
                                <a href="{% url 'post_detail' post.slug %}" class="text-decoration-none">{{ post.title }}</a>
                            </h5>
                            <p class="card-text">{{ post.description|truncatewords:20 }}</p>
                            <div class="mt-auto">
                                <small class="text-muted">By {{ post.author.username }} • {{ post.pub_date|date:"M d, Y" }}</small>
                            </div>
                        </div>
                        <div class="card-footer bg-transparent">
                            <small class="text-muted">{{ post.views }} views • {{ post.likes }} likes</small>
                        </div>
                    </div>
                </div>
                {% endfor %}
            </div>
        {% else %}
            <div class="text-center">
                <h3>No posts available</h3>
                <p>Be the first to share a delicious recipe!</p>
            </div>
        {% endif %}
    </div>
</div>
{% endblock %}

Post detail template

Finally, create posts/templates/posts/post_detail.html.  This template displays a single post with full description, ingredients and counters for views and likes:

{% extends 'posts/base.html' %}

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

{% block content %}
<div class="row justify-content-center">
    <div class="col-lg-8">
        <div class="card">
            <img src="{{ post.image.url }}" class="card-img-top post-detail-image" alt="{{ post.title }}">
            <div class="card-body">
                <h1 class="card-title">{{ post.title }}</h1>
                <p class="text-muted mb-3">By {{ post.author.username }} • Published {{ post.pub_date|date:"F d, Y" }}</p>
                <div class="mb-4">
                    <h4>Description</h4>
                    <p class="lead">{{ post.description }}</p>
                </div>
                <div class="mb-4">
                    <h4>Ingredients</h4>
                    <div class="ingredients">{{ post.ingredients|linebreaks }}</div>
                </div>
            </div>
            <div class="card-footer bg-transparent">
                <div class="row text-center">
                    <div class="col-6"><strong>{{ post.views }}</strong><br><small class="text-muted">Views</small></div>
                    <div class="col-6"><strong>{{ post.likes }}</strong><br><small class="text-muted">Likes</small></div>
                </div>
            </div>
        </div>
        <div class="text-center mt-4">
            <a href="{% url 'post_list' %}" class="btn btn-outline-primary">← Back to all recipes</a>
        </div>
    </div>
</div>
{% endblock %}

With these three templates in place, restart the server and you should see a styled homepage listing your posts.  Of course there are no posts yet, but after we create some sample recipes in the admin interface they’ll appear here.