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>© 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.