Flask pt II: Templating with Jinja2

You can download the full code for this tutorial at my github under release 1.3

Writing the same thing over and over again sucks. Copy pasting file paths and managing a complicated directory structure sucks even harder. Eventually, half the work of adding a new webpage is tied up in making sure that the navigation bar at the top has links that work. There's got to be a better way!

Oh there is. It's called templates, and if you have ever tried to maintain a pure CSS and HTML site, it will blow your mind.

When I first built my website, I had to hard code a navigation bar for ever single page, and if I decided to change the structure of my web app, well that would mean that I would have to change the navigation bar on every single page again, which I did multiple times.

With templates, you can create reusable components and layouts, so that you only have to write code for things like navbars only once. Flask, as well as Django, use the jinja2 templating engine to accomplish this.

Layouts, Pages, and Partials

In jinja2, templates have a few different components. Some pieces are shared across different webpages, like the head of a html page, while some are unique to a particular page like the actual content of a page. We can think of these elements as falling into three main groups:

For this tutorial, we are just going to be covering layouts and pages.

Structuring the App

Before we go any further, we need to go over how the app will be structured. Flask is opinionated, meaning that certain folders must be named certain things, and certain files must go in certain folders.

We are going to have a static folder where we will keep our static assets like pictures or CSS files, and we are going to have a templates folder where we will keep the html for our website.

Move the index.html and second.html from your archive folder and place it in a new templates folder. Then move your style.css from the style folder and place it in a new static folder. Afterwards your file structure should look something like this:

|----website tutorial
    |---------app.py
    |---------static
    |--------------style.css
    |---------templates
    |--------------index.html
    |--------------second.html
    |---------venv

Extending a Layout

The goal of templating is to cut down on the total amount of code. We are going to accomplish this by taking what is common between index.html and second.html and putting it in a new file called layout.html.

Let's begin by creating layout.html, which will also go in the templates folder. This will be the shared layout that is used by our entire website.

layout.html:

{% block head %}

<head>

    <title>Example</title>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <!-- jQuery library -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <!-- Popper JS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
    <!-- Latest compiled JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

    <!-- Our CSS -->
    <link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}">

</head>
{% endblock %}

<body class="page-style">
    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <ul class="navbar-nav">
            <li class="nav-item">
                <a class="nav-link" href="{{ url_for('index') }}">Home</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{{ url_for('second') }}">Second Page</a>
            </li>
        </ul>
    </nav>
    <div class="container">
        {% block content %}
        {% endblock %}
    </div>
</body>

Then we will edit the html for both the homepage (index.html) and for the second page (second.html).

index.html:

{% extends "layout.html" %}

{% block content %}
<br>
<h2>Create Your Static Website</h2>
<p>Here is the content of your site.</p>
<strong>Using the Grid System</strong>
<div class="row">
    <div class="col m-2 examples"></div>
    <div class="col m-2 examples"></div>
    <div class="col m-2 examples"></div>
</div>
<div class="row">
    <div class="col-2 m-2 examples"></div>
    <div class="col-5 m-2 examples"></div>
    <div class="col-4 m-2 examples"></div>
</div>
{% endblock %}

second.html:

{% extends "layout.html" %}

{% block content %}
<h2>Now you have a second page</h2>
<p>Isn't that nice?</p>
{% endblock %}

Wait, What Exactly is Extension?

Extension is taking the html from our pages (index.html and second.html) and injecting it into the layout (layout.html). As a result, the html from the layout is extended with the html from the pages.

Remember writing cover letters when you were last applying for jobs? Instead of writing a cover letter for each firm, you might write one template and replace the second paragraph with something particular to that firm. Thats all we are doing here. We are injecting unique content for each page into a template, so that we don't have to rewrite the boilerplate again and again. And if we need to change the boilerplate we only have to change it one place.

But how does jinja know where things should be injected? You will notice that layout.html has a section called {% block content %}. Jinja will place the contents of index.html and second.html here!

Let's consider what the html for second.html will actually look like after this whole injection/extension business, when it is rendered in the browser:

<head>

    <title>Example</title>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <!-- jQuery library -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <!-- Popper JS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
    <!-- Latest compiled JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

    <!-- Our CSS -->
    <link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}">

</head>

<body class="page-style">
    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <ul class="navbar-nav">
            <li class="nav-item">
                <a class="nav-link" href="{{ url_for('index') }}">Home</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{{ url_for('second') }}">Second Page</a>
            </li>
        </ul>
    </nav>
    <div class="container">
				<h2>Now you have a second page</h2>
				<p>Isn't that nice?</p>
    </div>
</body>

Again, jinja2 injects the content of second.html between {% block content %} and {% endblock %} on the layout.html page.

Rendering the Templates

Now you may have been faithfully copy pasting all of this into your VS Code but found that when you run flask, you get an error. Well unfortunately, as is too often the case, there is more work to do, but not much more so don't despair.

Next we need to update our app.py script so that it compiles our website. This part is pretty simple.

app.py

from flask import Flask
from flask import render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template('index.html')

@app.route("/secondpage")
def second():
    return render_template('second.html')


if __name__ == "__main__":
    app.run()

We aren't doing too much different here than what we have done before, but I did change some of the function names for the routes.

The big difference is the inclusion of this render_template() function. As you may be able to tell (hopefully you can) by its name, this function renders your templates. Our index() method will now be rendering index.html (by injecting it into layout.html), and our second() method will be doing the same with second.html.

If you app isn't working, make sure that you have the correct file structure. Again, flask is opinionated. Your templates must be in the templates folder, and your css must be in the static folder.

Oh and don't forget to import render_templates at the top of your script!

Referencing CSS and Creating Hyperlinks

Take a look at the links on the navbar section of the layout.html. You will notice that the href/hyperlink isn't to a relative path anymore. Now it reads href="{{ url_for('index') }}" . All this means is that this hyperlink will be pointing at the route for the index method.

Similarly, look at how we load in our custom CSS. The hyperlink is as follows: href="{{url_for('.static', filename='style.css')}}" This is the same concept as before, but this time we are looking in the static folder for a file called style.css.

There is more you can do with jinja2 like variables and for loops. But with just this bit, you have what you need to make a significantly more maintainable website. You now know routing and templating. Next we will cover hosting and port forwarding and enter the dark, dark world of linux.

Prepare to sacrifice your sanity at the alter of command line computing (or not because you have this straight forward guide to help you!)