Our web application will be a maintenance nightmare when all our pages contain the full layout of our site. Luckily for us, Jinja offers a simple solution for this problem called template inheritance.
This post is part of my journey to learn Python. You can find the other parts of this series here.
The problem
If all our templates contain the whole layout of our web site, then a small change means we need to update all templates. Even for small applications this is error prone and takes a lot of time.
A much better approach is to have a base layout with navigation and everything and then only the minimal content needed for a site in the site-specific template. In this case we need only update one file when we need a new entry in the navigation.
The starting template for tasks
As the example for this post I will use this HTML file (tasks.html) that contains all the data to display tasks as I build it up in the last post:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
<!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <script src="https://use.fontawesome.com/***.js"></script> <title>Your Tasks</title> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <a class="navbar-brand" href="#">Task Tracker</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> <a class="nav-link" href="/tasks">Tasks</a> </li> <li class="nav-item"> <a class="nav-link" href="/about">About</a> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> </form> </div> </nav> <div class="container"> <h1>Hello {{ name }}</h1> <p>Your current tasks are: <ul> {% for t in tasks %} <li>[{{ t.id }}] {{ t.description }} {% if t.done %} <i class="fa fa-check" aria-hidden="true"></i> {% else %} <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> {% endif %} </li> {% endfor %} </ul> </p> </div> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> </body> </html> |
Create a base template
To reuse our current HTML, we got to our template folder and copy the tasks.html template to the file base.html:
1 |
$ cp tasks.html base.html |
Jinja has the concept of blocks, where we can specify a part of our HTML template that other pages can override. A block looks like this and needs a name, like footer in this example:
1 2 3 |
{% block footer %} © Copyright 2020 by J. Graber {% endblock %} |
It does not matter if the block is empty or contains data. We can decide later on if the other pages should override what is there or keep the content as it is.
We now need to go through the base.html template and replace all the task specific parts with blocks. I did this and created a block for the page title and one for the main content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
<!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <script src="https://use.fontawesome.com/***.js"></script> <title>{% block title %}Your Tasks {% endblock %}</title> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <a class="navbar-brand" href="#">Task Tracker</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> <a class="nav-link" href="/tasks">Tasks</a> </li> <li class="nav-item"> <a class="nav-link" href="/about">About</a> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> </form> </div> </nav> <div class="container"> {% block main %} {% endblock %} </div> <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> </body> </html> |
Use the base template
We now modify the tasks.html file to use our template. We can have multiple base templates; therefore, we must tell Jinja which one we want to use. The mechanism we use is called template inheritance and we now make tasks.html a child page of our base template (called base.html):
1 |
{% extends "base.html" %} |
For every block we want to override, we write a snipped like this one:
1 |
{% block title %}Your Tasks{% endblock %} |
That is all we need to do. If we repeat this for the main block and get rid of everything else in the tasks.html file, we end up with this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{% extends "base.html" %} {% block title %}Your Tasks{% endblock %} {% block main %} <h1>Hello {{ name }}</h1> <p>Your current tasks are: <ul> {% for t in tasks %} <li>[{{ t.id }}] {{ t.description }} {% if t.done %} <i class="fa fa-check" aria-hidden="true"></i> {% else %} <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> {% endif %} </li> {% endfor %} </ul> </p> {% endblock %} |
What do we gain?
It will take time until the benefits for maintaining our application start to pile up. But as soon as we add another page we can profit from our template. To create an “About” site, we only need these few lines:
1 2 3 4 5 6 7 |
{% extends "base.html" %} {% block title %}About Task Tracker{% endblock %} {% block main %} <h1>About</h1> <p>This is a tiny task tracker to explain Jinja templates and Flask</p> {% endblock %} |
With our base.html template this is all it takes to give us a site with a navigation bar and the whole Bootstrap layout:
Next
Base templates in combination with the basics of Jinja allows us to create nice layouts for our web applications. It is now time to look a bit closer on the routing in Flask to move away from static sites.
1 thought on “Python Friday #35: Shared Layouts in Jinja”