1: Connecting HTML files with DTL
As of right now we have constructed our web app similarly to a regular website. Let's improve our project by adding additional Django-specific features. We have already used the Django template language (DTL) to render a model in an HTML template, but now let's use it to save ourselves development time as we create new HTML files for different web pages.
Create a new HTML document called header.html
env > mysite > main > templates > main > (New File) header.html
GIF
Let's start off by re-organizing home.html into two separate files. First, we'll create a new file named header.html. Right click on the templates > main folder and click New File to create a file named header.html.
Edit the header.html
env > mysite > main > templates > main > header.html
<!DOCTYPE html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Encore</title>
<!--Bootstrap CSS-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<!-- Optional Javascript -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>
We'll use this file to separate our CDN links and meta tags from the visible HTML elements we display. Copy the contents of home.html and paste into header.html so the files are identical. In the header.html file, delete all code inside the body tag like the example above.
Load in block content
env > mysite > main > templates > main > header.html
<!DOCTYPE html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Encore</title>
<!--Bootstrap CSS-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
{% block content %}
{% endblock %}
<!-- Optional Javascript -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>
Next we will connect the two documents together using the Django template language (DTL). We previously used DTL for creating multiple cards from information provided from our models but it can also be applied to load in HTML code from another document. Add block tags inside the body element. This will automatically link HTML files that we specify in the next step.
Configure the home.html
env > mysite > main > templates > main > home.html
{% extends 'main/header.html' %}
{% block content %}
{% load static %}
<!--Navbar-->
<nav class="navbar navbar-expand-sm navbar-light bg-light">
<a class="navbar-brand" href="/">Encore</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href=" ">Products</a>
</li>
<li class="nav-item">
<a class="nav-link" href=" ">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link" href=" ">Contact</a>
</li>
</ul>
</div>
</nav>
<!--CTA-->
<div class="container py-5">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6 pb-4">
<h1 class="display-4 font-weight-bold">Elevate your listening</h1>
<h5>Everyday headphones that make your favorite artists sound like their performing a never-ending encore.</h5>
<a class="btn btn-primary mt-2" href="/products">FIND YOUR SET</a>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<img class="img-fluid rounded" src="{% static 'img/cta-headphones.jpg' %}" alt="Headphones">
</div>
</div>
</div>
<!--Products-->
<div class="container py-3">
<h2>Products</h2>
<hr>
<br>
<div class="row">
{% for p in product %}
<div class="col-sm-12 col-md-6 col-lg-3 pb-4">
<div class="card h-100" style="border:none">
<img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name}}" style="width: auto; height: 250px; object-fit: scale-down;">
<div class="card-body">
<h5 class="card-title">{{ p.product_name }}</h5>
<p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
<p class="card-text">{{ p.product_description }}</p>
<a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
Go to home.html and delete everything outside of the body element and the optional Javascript code. In other words, only keep the code specific to the homepage. Then add the following tags to the top of the document: {% extends 'main/header.html' %} {% block content %}
and the following to the bottom {% endblock %}
of the file. Make sure home.html looks identical to the example above and save.
Reload the http://127.0.0.1:8000/ page to view the results but the page should actually look the same. We simply reorganized our documents so that we can use the same CDNs on future HTML templates. By "extending" the header.html template at the top of the file, home.html has access to its contents.
Inspect the web page
GIF
Right click on the browser window displaying the homepage and click "Inspect". You can see the two HTML documents render as one page.
2: Creating a separate navbar template
Create a navbar.html file
env > mysite > main > templates > main > (New Folder) includes > (New File) navbar.html
GIF
Just like the CDNs and head element, we do not want to add a navbar in every new HTML file we create. First, create a new folder inside the templates > main folder called includes. Inside the includes folder create a new HTML file called navbar.html.
Place the navbar in the navbar.html file
env > mysite > main > templates > main > includes > navbar.html
<!--Navbar-->
<nav class="navbar navbar-expand-sm navbar-light bg-light">
<a class="navbar-brand" href="/">
Encore
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href=" ">Products</a>
</li>
<li class="nav-item">
<a class="nav-link" href=" ">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link" href=" ">Contact</a>
</li>
</ul>
</div>
</nav>
Cut and paste all of the navbar code that was in home.html into navbar.html. Be sure that the navbar from home.html has been removed and both files are saved.
Connect navbar.html to header.html
env > mysite > main > templates > main > header.html
<!DOCTYPE html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Encore</title>
<!--Bootstrap CSS-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
{% include 'main/includes/navbar.html' %}
{% block content %}
{% endblock %}
<!-- Optional Javascript -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>
Next, let's connect the navbar.html to header.html. We want the navbar to be accessible on all future pages so we will add it to header.html with {% include 'main/includes/navbar.html' %}
instead of using the block content tags.
While we haven't changed the overall appearance of anything, we included some important web app functionality that will save us time as we add new documents. Now the navbar will always appear at the top of our pages and new HTML files will not need a <head> </head>
element.
3: Creating a products page
Now let's create a new template that displays all of the products while extending the header and navbar elements.
Code-it-yourself: Create a products.html file
env > mysite > main > templates > main > (New File) products.html
GIF
Create a new file named products.html in the templates > main folder.
Edit the products.html file
env > mysite > main > templates > main > products.html
{% extends "main/header.html" %}
{% block content %}
<!--Products-->
<div class="container py-5">
<h1 class="font-weight-bold">Products</h1>
<hr>
<br>
<div class="row">
</div>
</div>
{% endblock %}
Open the products.html file and add the code above.
Add a products path to urls.py
env > mysite > main > urls.py
from django.urls import path
from . import views
app_name = "main"
urlpatterns = [
path("", views.homepage, name = "homepage"),
path("products", views.products, name = "products"),
]
Open the main > urls.py file and add a new path for products. Save the file before we move on to the views.py.
Code-it-yourself: Add a products function to views.py
env > mysite > main > views.py
from django.shortcuts import render
from .models import Product
# Create your views here.
def homepage(request):
...
def products(request):
products = Product.objects.all()
return render(request = request, template_name="main/products.html", context = {"products":products})
We have already imported the Product model at the top of the page so we only need to create a simple function that returns a render of products.html with a queryset of all Product objects passed as context.
Update the products.html file
env > mysite > main > templates > main > products.html
{% extends "main/header.html" %}
{% block content %}
<!--Products-->
<div class="container py-5">
<h1 class="font-weight-bold">Products</h1>
<hr>
<br>
<div class="row">
{% for p in products %}
<div class="col-sm-12 col-md-6 col-lg-4 pb-4">
<div class="card h-100" style="border:none">
<img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name }}" style="width: auto; height: 200px; object-fit: scale-down;">
<div class="card-body">
<h5 class="card-title">{{ p.product_name }}</h5>
<p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
<p class="card-text">{{ p.product_description }}</p>
<a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
Open the products.html file again and create a for loop within the row. Add a division element with col-sm-12
col-md-6
col-lg-4
and pb-4
so three products will display per row. Nest a division with a card class attribute in the column and then pass in the product image, name, type, description and url below. We will also add a style attribute object-fit
to these product images and a border:none
to the card.
Add the products link to the navbar.html
env > mysite > main > templates > main > includes > navbar.html
<!--Navbar-->
<nav class="navbar navbar-expand-sm navbar-light bg-light shadow">
<a class="navbar-brand" href="/">
Encore
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/products">Products</a>
</li>
<li class="nav-item">
<a class="nav-link" href=" ">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link" href=" ">Contact</a>
</li>
</ul>
</div>
</nav>
Add the /products
slug to the correct anchor element in navbar.html. Save the file and then refresh the browser window. Click on the navigation link "Products", or the CTA button, and the products.html page will appear with all of the products listed.
GIF
Add the products link to the home.html
env > mysite > main > templates > main > home.html
<!--Products-->
<div class="container py-3">
<h2>Products</h2>
<hr>
<br>
<div class="row">
{% for p in product %}
<div class="col-sm-12 col-md-6 col-lg-3 pb-4">
<div class="card h-100" style="border:none">
<img src="{{ p.product_image.url }}" class="card-img-top" alt="{{ p.product_name}}" style="width: auto; height: 250px; object-fit: scale-down;">
<div class="card-body">
<h5 class="card-title">{{ p.product_name }}</h5>
<p class="card-text text-muted" style="font-size:12px">{{ p.product_type }}</p>
<p class="card-text">{{ p.product_description }}</p>
<a href="{{ p.affiliate_url }}" class="btn btn-warning">Buy now</a>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="container text-right">
<a href="/products">View more</a>
</div>
</div>
Let's also add a link to the products page below the Products section on the homepage. Add a new division element with the class attributes container
and text-right
after the closing tag of the row. Within the new division element, nest an anchor element linking to the products page. Save the changes to the home.html file.

4: Using Django pagination
We currently only have four products to display but what happens when we add more? Eventually the page will become lengthy and require a lot of scrolling. So let's add the built-in Django pagination that will automatically create "previous" and "next" links for additional products.
Add pagination to the products views.py function
env > mysite > main > views.py
...
from django.core.paginator import Paginator #import Paginator
# Create your views here.
...
def products(request):
products = Product.objects.all()
paginator = Paginator(products, 18)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request = request, template_name="main/products.html", context = {"page_obj":page_obj})
Import the Paginator
object at the top of the page then add three new variables to the products function. Assign the Paginator
object to a paginator
variable. The object will have the products
queryset and the number of products per page, 18
, as arguments. Next create a page_number
variable that looks at the request
to get the current page number. Finally, combine all this data into one variable, page_obj
. This is the only variable we need to pass as context.
Add pagination to the products.html
env > mysite > main > templates > main > products.html
{% extends "main/header.html" %}
{% block content %}
<!--Products-->
<div class="container py-5">
<h1 class="font-weight-bold">Products</h1>
<hr>
<br>
<div class="row">
{% for p in page_obj %}
...
{ % endfor %}
</div>
<!--Pagination-->
<div class="container">
<div class="pagination justify-content-center">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">« first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
{% endif %}
</span>
</div>
</div>
<!--end of Pagination-->
</div>
{% endblock %}
Go to the products.html file and replace {% for p in products %}
with {% for p in page_obj %}
. Then add a new comment called Pagination
and the above code after it. Save the file.
Reload the products page in the browser and it should look exactly the same but with the text Page 1 of 1 at the bottom. Because we set the number of products to 18, "next" and a "previous" links will not appear until we pass 18 products.
If you want to see the paginator in action, go back to the views.py file and set the variable paginator to equal Paginator(products, 2)
. Refresh the page and there with now only be 2 products and a next/last link at the bottom of the page. Click either of the links and the pagination will take you to the next page of products. Change the paginator back to 18 when you are done.
