Generating a Static Site with Flask and Deploying it to Netlify

Last updated January 26th, 2021

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:

Description Static Site Dynamic Site
Display content to users?
Allow user interaction (forms)?
Client-side files (HTML, CSS)?
Server-side code (Python, etc.)?
Serverless hosting (Netlify, etc.)?
Require web server resources?

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:

  1. Authentication and authorization (Okta)
  2. Payments (Stripe)
  3. Forms (Formspree)
  4. Search (Algolia)
  5. 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:

JAMstack vs. Traditional Web Apps

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:

  1. client-side apps that rely on external services (APIs)
  2. 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:

  1. WordPress
  2. Wix
  3. Squarespace
  4. Bluehost
  5. GoDaddy
  6. Weebly
  7. Netlify CMS
  8. Wagtail

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:

  1. Content written in Markdown or reStructuredText
  2. CLI (command-line interface) tools for generating static content
  3. Themes for quickly developing webpages
  4. 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:

  1. Jinja templates (including template inheritance) for generating HTML code
  2. Blueprints for organizing the project
  3. Development server with hot reloading when changes are made (no need for any complicated compilation step)
  4. 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:

  1. Remove Frozen-Flask
  2. Interface with a database
  3. 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:

Deploy Static Site Using Flask and Netlify Workflow

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:

  1. Application Factory
  2. Blueprints
  3. Jinja Templates
  4. Testing (using pytest)

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:

Jinja Template Processing

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?

  1. Static files found in project/static
  2. Routes found in project/recipes/routes.py

Frozen-Flask will automatically generate all URLs for static files. For example:

  1. static/css/base_style.css
  2. 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:

Development Server Example

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':

Deploy using Netlify - Step 1

You now can select the git hosting solution (GitLab, GitHub, or BitBucket) that you are using to store your git repository on:

Deploy using Netlify - Step 2

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):

Deploy using Netlify - Step 3

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):

Deploy using Netlify - Step 4

If you're interested in the steps to change the default branch in a git repository from master to main, 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:

Deploy using Netlify - Step 5

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:

Deploy using Netlify - Step 6

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':

Deploy using Netlify - Step 7

Update the following fields:

  1. Base directory - '/'
  2. Build command - empty
  3. Publish directory - '/project/build/'
  4. 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:

Deploy using Netlify - Step 8

Click on the 'Trigger deploy' button and select 'Deploy site':

Deploy using Netlify - Step 8

This will trigger the deploy script to run. Navigate back to the 'Site overview' to check the results of the deploy:

Deploy using Netlify - Step 9

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!

Deploy using Netlify - Step 10

You can now view this web application from any device connected to the Internet!

Development Server Example

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:

Deploy Static Site Using Flask and Netlify Workflow

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:

  1. Netlify Functions
  2. Netlify Forms

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.

Patrick Kennedy

Patrick Kennedy

Patrick is a software engineer from the San Francisco Bay Area with experience in C++, Python, and JavaScript. His favorite areas of teaching are Vue and Flask. In his free time, he enjoys spending time with his family and cooking.

Share this tutorial

Featured Course

Developing Web Applications with Python and Flask

This course focuses on teaching the fundamentals of Flask by building and testing a web application using Test-Driven Development (TDD).

Featured Course

Developing Web Applications with Python and Flask

This course focuses on teaching the fundamentals of Flask by building and testing a web application using Test-Driven Development (TDD).