

Generating a Static Site with Flask and Deploying it to Netlify
source link: https://testdriven.io/blog/static-site-flask-and-netlify/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

This tutorial looks at how to leverage the JAMstack with Python and Flask. You'll learn how to generate a static site with Flask, via Frozen-Flask, and deploy it to Netlify. We'll also look at how to test the static site with pytest.
This tutorial assumes that you have prior experience with Flask. If you're interested in learning more about Flask, check out my course on how to build, test, and deploy a Flask application: Developing Web Applications with Python and Flask.
The website created in this tutorial can be found at: https://www.kennedyrecipes.com
Static vs Dynamic Websites#
Static websites are intended to provide information by displaying the same content for every user. Dynamic websites, meanwhile, provide different content and are intended to be functional by enabling user interaction.
Here's a summary of the differences:
The 'Serverless hosting' category is intended to show that static sites can easily be deployed using serverless solutions (i.e., Netlify, Cloudflare, GitHub Pages, etc.). Dynamic sites can also be hosted using serverless solutions (i.e., AWS Lambda), but it's a much more complex process.
Static websites display the same fixed content for every user that accesses the website. Typically, static websites are written with HTML, CSS, and JavaScript.
Dynamic websites, on the other hand, can display different content to each user and they provide user interaction (login/logout, create and modify items in the database, etc.). Dynamic websites are much more complex than static websites as they require server-side resources and application code to handle requests. The application code needs to be maintained (crash fixes, security updates, language upgrades, etc.) as well.
Why develop a static website?
If you're creating a website that is intended to provide information, a static site is a great option. Static websites are a lot easier to create and maintain than dynamic websites, as long as you understand their limitations.
JAMstack
JAMstack is a web architecture that focuses on two key concepts: pre-rendering content and decoupling services.
- JAM - JavaScript, APIs, and Markup
- stack - layers of technology
Pre-rendering content means the front-end content (HTML, CSS, JavaScript, and other static files) is built into static sites. The advantage of this process is that the static content can be served quickly to web browsers from a CDN (Content Delivery Network).
We'll be using Netlify to deploy the static site in this tutorial. Netlify is able to serve sites lightning quick thanks to an extensive CDN.
Decoupling services means leveraging the incredible set of APIs that provide services and products. APIs are available for:
- Authentication and authorization (Okta)
- Payments (Stripe)
- Forms (Formspree)
- Search (Algolia)
- Comments (Disqus)
For more API services, check out the Awesome Static Website Services.
When compared to traditional web apps, JAMstack apps have a reduced set of layers:
Source: JAMstack.org
A key reason for using JAMstack (as opposed to the traditional approach) is to go as "serverless" as possible, by relying on hosting solutions (Netlify) and external services (APIs).
JAMstack is a great architecture choice for building:
- client-side apps that rely on external services (APIs)
- static sites for providing information to users
Traditional web apps are a great approach when building database-driven apps that focus on server-side apps.
Alternatives
Content Management System (CMS) Solutions
CMS solutions are used to manage and deploy websites. WordPress is the most popular CMS tool, with a lot of products being developed using WordPress.
There are lots of sophisticated options for building a website these days, including:
Most of these options allow websites to be created without writing any code. These options are a great choice for quickly developing a website for displaying content, such as a blog.
I'm currently using WordPress to generate my personal blog site: https://www.patricksoftwareblog.com.
Lektor is a popular CMS solution written in Python, though it has a lot of features of a static site gnerator as well.
Lektor was created by Armin Ronacher, who is also the creator of Flask!
Static Site Generators
Static site generators create static files (HTML, CSS, and JavaScript) for publishing a website by parsing content created in a markdown language (typically Markdown or reStructuredText).
Jekyll, Eleventy, Gatsby, and Hugo are the most popular static site generators.
There are a number of Python-based options as well.
Pelican is one of the most popular static site generators written in Python. It has some powerful features:
- Content written in Markdown or reStructuredText
- CLI (command-line interface) tools for generating static content
- Themes for quickly developing webpages
- Publication in multiple languages
Flask with Frozen-Flask (which we'll be using in this tutorial) can also be considered a static site generator. The advantage of this approach is being able to leverage an existing development process with Flask to develop a static site. Additionally, the ability to test your static site when using Flask with Frozen-Flask is a big advantage, as testing is frequently ignored when developing static sites.
The approach in this tutorial is not a "pure" static site generator, as the content is being created in HTML files. To make this approach a "pure" static site generator, you can utilize Flask-FlatPages to be able to create the content in Markdown.
If you're more familiar with Django, Django-Distill is a static site generator for Django apps.
Why Use Flask for Static Sites?#
If you're already comfortable with developing apps using Python and Flask, then you can continue to use the same tools and workflow for developing a static site. There's no need to learn any new tools or languages, in other words.
With Flask, you can continue to use the following tools and processes:
- Jinja templates (including template inheritance) for generating HTML code
- Blueprints for organizing the project
- Development server with hot reloading when changes are made (no need for any complicated compilation step)
- Testing using pytest
What's more, if you decide in the future to expand your site into a full web app that requires a backend database, since you started with Flask, you won't need to re-write your app. You'll just need to:
- Remove Frozen-Flask
- Interface with a database
- Deploy to Heroku (or a similar hosting solution)
Workflow#
The following diagram illustrates the typical workflow for developing a static site with Flask and deploying it to Netlify:
Let's dive into the details of this workflow...
Flask Project#
While this Flask project will generate static files, it still uses the best practices for a Flask app:
Additionally, the Frozen-Flask package is used for generating the static content.
The source code for the project created in this tutorial can be found on GitLab at: Flask Recipe App.
Project Structure
The folder structure for the project is typical for a Flask project:
├── project │ ├── build # Static files are created here by Frozen-Flask! │ ├── blog # Blueprint for blog posts │ │ └── templates # Templates specific to the blog blueprint │ ├── recipes # Blueprint for recipes │ │ └── templates # Templates specific to the recipes blueprint │ ├── static │ │ ├── css # CSS files for styling the pages │ │ └── img # Images displayed in recipes and blog posts │ └── templates # Base templates ├── tests │ └── functional # Test files └── venv
The key folder to highlight is the "project/build" folder, which will be generated by the Frozen-Flask package with the static files.
To get started, pull down the source code from this GitLab repository:
$ git clone [email protected]:patkennedy79/flask-recipe-app.git
Create a new virtual environment:
$ cd flask-recipe-app $ python3 -m venv venv
Activate the virtual environment:
$ source venv/bin/activate
Install the Python packages specified in requirements.txt:
(venv) $ pip install -r requirements.txt
Recipe Routes
The routes to the recipes displayed on the site are defined in the recipes
blueprint.
Blueprints allow you to cleanly organize the source code of your Flask project into distinct components. Each blueprint should encapsulate a significant piece of functionality in your application.
For example, the breakfast recipes are defined using the following variables and view functions in project/recipes/routes.py:
from . import recipes_blueprint from flask import render_template, abort breakfast_recipes_names = ['pancakes', 'acai_bowl', 'honey_bran_muffins', 'breakfast_scramble', 'pumpkin_donuts', 'waffles', 'omelette'] @recipes_blueprint.route('/breakfast/') def breakfast_recipes(): return render_template('recipes/breakfast.html') @recipes_blueprint.route('/breakfast/<recipe_name>/') def breakfast_recipe(recipe_name): if recipe_name not in breakfast_recipes_names: abort(404) return render_template(f'recipes/{recipe_name}.html')
The breakfast_recipes()
view function renders the template for displaying all of the breakfast recipes.
The breakfast_recipe(recipe_name)
view function renders a specified breakfast recipe. If an invalid recipe title is specified, then a 404 (Not Found) error is returned.
This same set of view functions is used for each of the recipe types:
- Breakfast
- Dinner
- Side Dishes
- Dessert
- Smoothies
- Baked Goods
Jinja Templates
Flask comes packaged with the Jinja templating engine out-of-the-box, which we'll use to generate our HTML files.
A template file contains variables and/or expressions, which get replaced with values when a template is rendered:
Template inheritance allows template files to inherit other templates. You can create a base template that defines the layout of the website. Since child templates will use this layout, they can just focus on the content.
The base template is defined in project/templates/base.html:
<!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"> <title>Flask Recipe App</title> <!-- Local CSS file for styling the application--> <link rel="stylesheet" href="{{ url_for('static', filename='css/base_style.css') }}"> <!-- Additional Styling --> {% block styling %} {% endblock %} </head> <body> <header> <h1>Kennedy Family Recipes</h1> <nav> <ul> <li class="nav__item"><a href="{{ url_for('recipes.recipes') }}" class="nav__link">Recipes</a></li> <li class="nav__item"><a href="{{ url_for('blog.blog') }}" class="nav__link">Blog</a></li> <li class="nav__item"><a href="{{ url_for('blog.about') }}" class="nav__link">About</a></li> </ul> </nav> </header> <main class="content"> <!-- child template --> {% block content %} {% endblock %} </main> <footer> <p>Created by Patrick Kennedy (2021)</p> </footer> </body> </html>
The base template defines the navigation bar (<header>
tag) and the footer (<footer>
tag). The content to be displayed is specified in the <main>
tag, but this content is expected to be filled in by the child template.
For example, the template for displaying the list of breakfast recipes (defined in project/recipes/templates/recipes/breakfast.html) expands on the base template to display all the breakfast recipes:
{% extends "base.html" %} {% block content %} <div class="recipe-container"> <div class="card"> <a href="{{ url_for('recipes.breakfast_recipe', recipe_name='pancakes') }}"> <img src="{{ url_for('static', filename='img/pancakes.jpg') }}" alt="Pancakes" class="card__image" /> <div class="card__body"> <h2>Pancakes</h2> <p class="recipe-badge dairy-free-badge">Dairy-Free</p> <p class="recipe-badge soy-free-badge">Soy-Free</p> </div> </a> </div> <div class="card"> <a href="{{ url_for('recipes.breakfast_recipe', recipe_name='honey_bran_muffins') }}"> <img src="{{ url_for('static', filename='img/honey_bran_muffins.jpg') }}" alt="Honey Bran Muffins" class="card__image" /> <div class="card__body"> <h2>Honey Bran Muffins</h2> <p class="recipe-badge dairy-free-badge">Dairy-Free</p> <p class="recipe-badge soy-free-badge">Soy-Free</p> </div> </a> </div> ... </div> {% endblock %}
Testing
pytest is a test framework for Python used to write, organize, and run test cases. After setting up your basic test structure, pytest makes it easy to write tests and provides a lot of flexibility for running the tests.
The test files are specified in the tests/functional/ directory. For example, the tests for the breakfast recipes are specified in tests/functional/test_recipes.py:
""" This file (test_recipes.py) contains the functional tests for the `recipes` blueprint. """ from project.recipes.routes import breakfast_recipes_names def test_get_breakfast_recipes(test_client): """ GIVEN a Flask application configured for testing WHEN the '/breakfast/' page is requested (GET) THEN check the response is valid """ recipes = [b'Pancakes', b'Honey Bran Muffins', b'Acai Bowl', b'Breakfast Scramble', b'Pumpkin Donuts', b'Waffles', b'Omelette'] response = test_client.get('/breakfast/') assert response.status_code == 200 for recipe in recipes: assert recipe in response.data def test_get_individual_breakfast_recipes(test_client): """ GIVEN a Flask application configured for testing WHEN the '/breakfast/<recipe_name>' page is requested (GET) THEN check the response is valid """ for recipe_name in breakfast_recipes_names: response = test_client.get(f'/breakfast/{recipe_name}/') assert response.status_code == 200 assert str.encode(recipe_name) in response.data
These are high-level checks to make sure the expected pages render properly.
Each of these test functions is using the test_client
fixture defined in tests/conftest.py:
import pytest from project import create_app @pytest.fixture(scope='module') def test_client(): flask_app = create_app() # Create a test client using the Flask application configured for testing with flask_app.test_client() as testing_client: yield testing_client # this is where the testing happens!
The tests should be run from the top-level directory:
(venv)$ python -m pytest
They should pass:
================================ test session starts ================================= platform darwin -- Python 3.9.0, pytest-6.2.1, py-1.10.0, pluggy-0.13.1 rootdir: flask-recipe-app collected 18 items tests/functional/test_blog.py .... [22%] tests/functional/test_recipes.py .............. [100%] ================================ 18 passed in 0.33s ==================================
Frozen-Flask#
Up to this point, the Flask app looks like a typical web app. This is where Frozen-Flask comes into play. We can generate static files and URLs automatically.
Think back to the project thus far. What do we need to create URLs for?
- Static files found in project/static
- Routes found in project/recipes/routes.py
Frozen-Flask will automatically generate all URLs for static files. For example:
static/css/base_style.css
static/img/acai_bowl.jpg
Take note of project/recipes/routes.py:
@recipes_blueprint.route('/') def recipes(): return render_template('recipes/recipes.html') @recipes_blueprint.route('/breakfast/') def breakfast_recipes(): return render_template('recipes/breakfast.html') @recipes_blueprint.route('/breakfast/<recipe_name>/') def breakfast_recipe(recipe_name): if recipe_name not in breakfast_recipe_names: abort(404) return render_template(f'recipes/{recipe_name}.html')
Frozen-Flask will automatically generate URLs for GET routes without variable parts in the URL. In our case, it will generate URLs for /
and /breakfast/
.
How about '/breakfast/<recipe_name>/
? Any links from url_for()
will also be found, which should cover everything.
For example, the breakfast.html template contains
url_for()
calls for each of the breakfast recipes.
Development
During the development process, we want to be able to test out the routes on our local computer, just like with any Flask app.
The app.py file is used for running the Flask development server:
from flask_frozen import Freezer from project import create_app # Call the application factory function to construct a Flask application # instance using the development configuration app = create_app() # Create an instance of Freezer for generating the static files from # the Flask application routes ('/', '/breakfast', etc.) freezer = Freezer(app) if __name__ == '__main__': # Run the development server that generates the static files # using Frozen-Flask freezer.run(debug=True)
This file starts by calling the application factory function, which creates the Flask app. Next, an instance of Freezer
(from Frozen-Flask) is created. Finally, the development server from Frozen-Flask is run:
Starting the development server is just like any other Flask app:
(venv)$ export FLASK_APP=app.py (venv)$ export FLASK_ENV=development (venv)$ python -m flask run
Now you can navigate to http://localhost:5000:
Build Script
Once you're ready to deploy the static files, you need to build the files in order to generate the static files with your application's content. The build.py script in the top-level folder runs Freezer-Flask to generate the static files:
from flask_frozen import Freezer from project import create_app # Call the application factory function to construct a Flask application # instance using the development configuration app = create_app() # Create an instance of Freezer for generating the static files from # the Flask application routes ('/', '/breakfast', etc.) freezer = Freezer(app) if __name__ == '__main__': # Generate the static files using Frozen-Flask freezer.freeze()
Run the script:
(venv)$ python build.py
This script generates all the static files based on the routes in the Flask app and writes them to the "project/build" folder:
(venv)$ tree -L 3 project/build project/build ├── breakfast │ ├── acai_bowl │ │ └── index.html │ ├── breakfast_scramble │ │ └── index.html │ ├── index.html │ └── waffles │ └── index.html ├── index.html └── static ├── css │ ├── base_style.css │ └── recipe_style.css └── img
This folder is what we'll deploy to Netlify!
Deploy to Netlify#
What is Netlify?
Netlify is a service that simplifies hosting front-end web applications.
Netlify offers free hosting of front-end web applications and they also provide the ability to purchase a custom domain name for your application. What I especially like about their service is that they provide HTTPS with all their hosting solutions.
Before we jump into these steps, you need to make sure to create an account on Netlify. Go to https://www.netlify.com and click on 'Sign up' to create a new account (it's free and no credit card is required).
Some alternative to Netlify are: Cloudflare, GitHub Pages, are GitLab Pages.
Configure
After you have logged in to Netlify, go to your account page and click on 'New site from Git':
You now can select the git hosting solution (GitLab, GitHub, or BitBucket) that you are using to store your git repository on:
Next, you'll be asked to select the git repository that you'd like to host (if you haven't connected Netlify to your git hosting service before, there will be an additional step to allow Netlify to access your git repositories):
Now you can select who the owner of the project is and the branch from which you want to deploy the builds from (I'm selecting the 'main' branch as this is my stable branch for doing builds from for this project):
If you're interested in the steps to change the default branch in a git repository from
master
tomain
, refer to this very helpful blog post: Rename your Git default branch from master to main (with GitLab screenshots).
Scrolling down further, you now have the option to select the command to run to build your application and where the code should be deployed from:
This is where these steps get tricky... update the 'Build command' and 'Publish directory' fields based on the screenshot, but we will need to fix them in a few steps.
Netlify expects that your app is following the JAMstack approach, so they want to build the front-end code for you on their servers.
With this configuration set, click on the 'Deploy site' button.
It will take some time (< 1 minute) for Netlify to attempt to deploy the site:
The location that Netlify deploys the project to will be different for each user (in my case, it's deployed to https://vigorous-blackwell-9400a0.netlify.com/.
Once the deployment fails, you'll need to change the build settings by clicking on 'Site Settings' and then 'Build & deploy':
Update the following fields:
- Base directory - '/'
- Build command - empty
- Publish directory - '/project/build/'
- Builds: Active
'Save' the changes.
Changing the build settings does not cause the deployment script to re-run, so we need to manually deploy to check the build settings. Navigate to the 'Deploys' tab:
Click on the 'Trigger deploy' button and select 'Deploy site':
This will trigger the deploy script to run. Navigate back to the 'Site overview' to check the results of the deploy:
You should see a preview of the site and confirmation that the site was deployed.
Click on the link (such as https://vigorous-blackwell-9400a0.netlify.com/) to view the site!
You can now view this web application from any device connected to the Internet!
Workflow
As with a lot of processes, the configuration steps are a bit complex, but now Netlify will notice any new commits on the 'main' branch of your repository (on GitLab, GitHub, or BitBucket) and re-deploy your website using the new commit. Remember: Netlify will publish the files in the "project/build" folder, so make sure to always build the static site before committing changes:
(venv)$ python build.py
To review, the following diagram illustrates the typical workflow for developing a static site using Flask and deploying it to Netlify:
Additional Features of Netlify
Netlify also provides a number of tools for adding server-like functionality to an application, without actually needing a server-side application. For example:
Conclusion#
This tutorial shows how to create a Flask application and then use Frozen-Flask to generate the static files for all the specified routes. The static files are then published to Netlify to deploy the static site.
If you're familiar with developing Flask applications, then this process is a great way to develop static sites that can be easily deployed to the web.
If you're interested in learning more about Flask, check out my course on how to build, test, and deploy a Flask application: Developing Web Applications with Python and Flask.
Recommend
-
240
netlify/netlify-cms master
-
28
Netlify is one of the best places to deploy an application or a website today. There is no need to manage a server, NGINX, certificates, or scaling due to high traffic. While it is...
-
10
Deploying Scala.js to the Web with Netlify Scala has long been my language of choice, but while I was aware of Scala.js, the process to get from Scala code to something that people could access in a web browser was a bit intimidating...
-
11
Adding HTTP headers to a Netlify static site 27 Jun 2019 Recently I have been using the Mozilla Observatory to do some security hunting on a community site I work on (http...
-
14
Add Comments to a Static Site with Netlify Functions and the GitHub API Comment systems are one of the easiest ways to solicit feedback from your readers and to encourage the kinds of civil and respectful discussions for whic...
-
9
Using Netlify API Authentication for Static Site BuildsMaking it easier to work with APIs where APIs are least likely to be easy to use.By Den Delimarsky in Engineering
-
6
Introduction to Deploying Angular Micro Frontends with NetlifyThis series of articles will aim to showcase the process of scaffolding and deploying a Micro Frontend Architecture using Nx and
-
5
Deploying to Netlify on Release Tags with Github Actions Jan 21, 2023 I recently had to set up a workflow where tagging a release on Github would trigger a deploy...
-
2
My motivation to write this article A few weeks ago I built a Next.js application and deployed it on Netlify. I enjoyed working with Next.js but a few issues came up after the deploy on Ne...
-
7
Moving to a static site with Gatsby + Netlify + ContentfulFlexport has been
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK