Implementing my Website in Flask - Part 3
This is part 3 in a series about implementing a personal blog in Python. In this post, I'd like to cover the basics of serving a website with Python, particularly its really lightweight Flask framework, which I've decided to use for this project.
If you're interested, other parts of this series can be found below:
- Part 1: Introduction and Project Outline
- Part 2: Setting Up Static Resources and Styles
- Part 3: Flask and Jinja
- Part 4: Managing Posts and Highlighting Code Blurbs
- Part 5: Deployment to Netlify
- Part 6: RSS Feed
As always, you can find the source of this blog on Github.
Introduction
In my search for the most well supported and well liked Python web frameworks, there were a few that always topped the chart. In the top three most popular on every tutorial I found were always Flask and Django. Flask is marketed as a fast, lightweight "micro" framework that provides just enough to be productive and then "gets out of your way." Django is marketed as "fully loaded" with features in order to handle data-driven websites.
I decided to go with Flask because I knew my website would be simple. I didn't even expect to have a database, so Django seemed like overkill. I think I made the right decision, even if it was just so I wasn't overwhelmed learning all of the features of Django.
The Basic Flask App
I was surprised at how little code is actually needed in order to get a simple app working:
1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World!'
if __name__ == '__main__':
app.run()
Then you can run it from the command line like this:
$ python hello.py
* Running on http://localhost:5000/
... and that's it!
In this case, app
is the web application instance, created as an instantiation
of Flask. Line 4 tells the app that any requests to relative URL "/" should be served the string
Hello World!
. Of course, this wouldn't include any style information or
anything, but Flask does include a default 404 page if any other URLs are requested.
Lines 8 and 9 just tell Flask to run the development server if the script is called directly (opposed to being imported as a module).
Flask Templates
If you'd like to serve real HTML instead of simple strings, Flask provides a couple options, the
most straightforward being the render_template
method. Most of my URLs are
handled this way. The basic usage looks like this:
1
2
3
4
5
6
7
8
9
10
import flask
app = flask.Flask(__name__)
@app.route('/')
def index():
context = { 'content': 'Hello World!' }
return flask.render_template('index.html', context)
if __name__ == '__main__':
app.run()
The first argument in render_template
is the template Flask will use to render
the content of the page (more on that in a second). The second variable is the 'context', or a
dict of all the variables that should be available to the template.
Of course, there's no real benefit to throwing Python into the mix if a website is completely static and unchanging. In that case, its possible to just define the HTML and CSS to be standalone. But if the website is based on data that can change regularly, the HTML will need to be changed on the fly between requests based on the latest data. That's where Python comes in.
To dynamically render HTML, Flask is dependent on the Jinja template engine and its handy HTML templates.
Jinja templates look really similar to HTML, just with some new Python-like syntax thrown in
where content needs to be dynamically rendered. Here's an example for the hypothetical
Hello World
website from earlier:
1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang='en'>
<head>
<title>Hello World!</title>
</head>
<body>
<h1>{{ content }}</h1>
</body>
</html>
The string between the h1
tags on line 7 is actually referring to the
content
key of the context
dict that was passed as the
second argument to render_template
in the previous example. Whatever value is
stored for the content
key (in this case, 'Hello World!') will be substituted
between the h1
tags.
Jinja also provides syntax for conditionals and control flow. For example, if a block of HTML
should be conditionally included, the {% if %}
syntax can be used. An example
from this website would be the Next/Prev page navigation links at the bottom of the index page.
The code for the Next link looks like this:
1
2
3
4
5
{% if next_page is none %}
<span>Next</span>
{% else %}
<a href='/page/{{ next_page }}/'>Next</a>
{% endif %}
This example first checks if there's a valid next page of posts. If that's the case, the Next
button is rendered as a link. If not, the Next button still looks the same, but it's rendered as
a span
so the user can't select it.
Jinja also provides more complex expressions to use in conditionals. For the complete list, see the documentation.
Along with conditionals, Jinja also provides loops. They use a very similar syntax to Python
loops. Just like conditionals, they are specified within {% %}
blocks. An
example from this blog is again from the index page, where I show the latest blog posts:
1
2
3
4
5
6
{% for post in posts %}
<article>
{{ post }}
</article>
<hr>
{% endfor %}
Jinja then repeats the pattern for each post that the Flask application provides to the template,
wrapping each post in an article
tag and adding a horizontal line underneath.
This can be really helpful when maintaining repetitive pages. Instead of having to copy/paste
multiple times, I can just specify the pattern once and Jinja repeats it for each iteration of
the loop.
Another great feature of Jinja templates are their ability to extend other templates. For example, each one of my pages includes roughly the same information in the HTML head, the navigation area, and the footer. It would be a real pain to need to maintain them separately for each page. Instead, I can add these features once to a template called base.html:
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
<!DOCTYPE html>
<html lang='en'>
<head>
<title>{{ title }}</title>
</head>
<body>
<nav>
<h1>{{ blog-title }}</h1>
<p>{{ blog-caption }}</p>
<div>
<h3><a href='/about'>About Me</a></h3>
<h3><a href='/archive'>Archive</a>
</div>
</nav>
<main>
{% block content %}
{% endblock %}
</main>
<footer>
<p><strong>{{ blog-title }}</strong></p>
<p>Copyright © 2021 All Rights Reserved</p>
<br/>
</footer>
</body>
</html>
Then any template can 'extend' this base template. The key here is the two lines on 16 and 17.
This notifies Jinja that the content
block has not been defined yet, and it should look for
other templates which define content
. Note that content
is not
a reserved word or anything. {% block example %}
would work exactly the same,
as long as another template actually defined what was in the example
block.
To define the content
block, create another template called posts.html:
1
2
3
4
5
6
7
8
9
10
{% extends '_base.html' %}
{% block content %}
<h2>Latest Posts</h2>
{% for post in posts %}
<article>
{{ post }}
</article>
<hr>
{% endfor %}
{% endblock %}
Now, the contents of each post will be substituted for {% block content %}
in
the rendered HTML any time render_template
is called for posts.html.
One last Jinja templates feature I found myself using a lot was the {% include %}
tag. This substitutes the contents of the included template in place of the tag. This allows
larger templates to be a composition of smaller, more modular ones. I found this particularly
useful when adding the social media buttons to the header and footer. Since both use the same
code, it made sense to just factor it out into its own template. Then it can be included into
the footer like this:
1
2
3
4
5
6
7
8
9
<footer>
<p><strong>{{ blog-title }}</strong></p>
<p>Copyright © 2021 All Rights Reserved</p>
<br/>
<p><strong>Want to get in touch?</strong></p>
<div>
{% include 'socialmedia.html' %}
</div>
</footer>
(Note the {% include %}
on line 7)
How I Chose My Templates
Each of my page types are defined as a template, based off of the base template. For example,
the 404 page, about page, archive page, etc all are their own templates. Then, any features
that I found myself using in multiple places are defined as their own template to be
{% include %}
'ed. The actual blog posts are a special case and also designed
to be {% include %}
'ed.
Here's a picture describing the relationships between templates. Note the arrows pointing to
the left are inheritance ({% extends %}
). The arrows pointing to the right are
composition ({% include %}
).
Flask Design Patterns I Implemented
While the basic Flask app at the beginning of this post is impressive at how little code is actually needed, it isn't scalable, even for something as simple as a blog.
I knew that the application would need multiple Python modules, so I followed what's called
the application factory design pattern. This made it so that
I wouldn't have to rely on access to a global variable in order to access my Flask instance.
Instead, I could use the flask.current_app
property to access my Flask
instance.
This pattern also provides access to the flask
command line tool. I could now
run my app with the following commands:
$ export FLASK_APP=src
$ export FLASK_ENV=development
$ flask run
I could also add further CLI tools for my app which could be accessed like flask run
.
For example, the 'build' tool (which I'll cover in a later blog post) can be defined with the following code:
1
2
self.app = flask.Flask(__name__)
self.app.cli.add_command(cli.build)
...and then accessed from the command line like this:
$ export FLASK_APP=src
$ export FLASK_ENV=development
$ flask build
My application factory didn't turn out exactly as the pattern specifies in the Flask
documentation. I wanted to keep my application as object-oriented as possible, so my
__init__.py
looks like this, instead of containing the actual creation of the
flask instance:
1
2
3
4
5
def create_app():
""" Create and configure blog app """
config = Settings.instance()
blog = Blog(config)
return blog.app
Then, my flask instance is created within the Blog class as Blog.app
. Note that
the Settings.instance()
above is another module in my application which defines
configurations like relative routes, the name of the blog, etc.
The Blog
constructor needs to look like this to provide create_app
with the configured Flask instance:
1
2
3
4
5
6
7
8
9
class Blog:
def __init__(self, settings):
self.app = flask.Flask(__name__)
self.app.cli.add_command(cli.build)
@self.app.route('/')
def index():
...
Note that I elected to define all the routes within the Blog
constructor. This
is similar to the application factory pattern shown in the Flask
tutorial, just with all of the Flask-specific stuff wrapped in my Blog
class.
Unlike the simple Flask app at the beginning of this post, there are several URLs which need to be dynamically generated, like those associated with blog posts. In those cases, instead of using hard-coded routes, I used Flask's dynamic URL processing feature. These are specified in angle brackets, like the routes of the two methods below:
1
2
3
4
5
6
7
8
9
10
@self.app.route('/')
@self.app.route('/page/<int:page>/')
def index(page=1):
""" Creates the index page with latest blog posts """
pass
@self.app.route('/post/<name>/')
def blog_post(name=None):
""" Creates a blog post page """
pass
This feature is really useful. In the case of the first method, it passes the value stored in
page
as the first parameter to the method. If a page is not specified (like in
the default route /
), the page will default to page 1. The int
specifier before page
tells Flask to only accept integer values here. If a
string is passed instead, Flask will render the 404 page.
In the second method on line 7, the name
parameter can be any string since
there is no specifier in front. It is worth mentioning that Flask escapes these strings and
does its best to ensure that there are no characters which may cause a security problem.
In both examples, once Flask checks that the URL matches the template URL, it's up to my
application to verify whether page
is a valid page and name
is
a valid blog post.
Since my blog is pretty simple, I didn't use many other advanced Flask design patterns that are mentioned in the documentation. One thing that I regret not implementing, though, is Blueprints. These provide a very modular approach that would have been easier to test and scale.
Instead, I just stuck everything in my Blog class. Initially, this worked great. But as it (unexpectedly) grew, it became a little unwieldy. One thing that I noticed as well was that many of my routes were really similar and ended up sharing data, while others were completely distinct. These lines of shared data would have provided a great boundary to break certain features into new blueprints.
Oh well. If I find myself adding a new feature any time soon, I might roll in blueprints along with it...
That covers everything that I wanted to cover in Flask! Next post, I'd like to discuss what went into actually building the context that Flask renders with each template.