Organize Images, CSS, and JavaScript in Flask Asset Bundles
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
Assets
With any website, static assets like CSS, Images, and Javascript files control a large part of how the application looks and behaves. In Yumroad, we have been relying on externally hosted CSS and Javascript files. In the prior chapter, to implement Stripe checkout, we added a raw <script>
tag with a function we wrote to support checkout. Those kinds of scripts and general styling should ideally not live within our templates, but instead in dedicated CSS and JS files.
In this chapter we will organize the static assets for our application into a scalable system and build a landing page for Yumroad.
Flask's default static folder.
Flask comes built in with a way to serve static files by creating a folder called static
within our application and serving all of the files in that folder.
Try it out by creating a static folder within the yumroad
folder and then creating a file called test.txt
. If you save some content there and have the Flask server running, you will be able to see the file being served at localhost:5000/static/test.txt
.
The static folder also works for other types of files, like images. Here we've made a logo for Yumroad and saved it as yumroad/static/yumroad.png
.
In order to use these static files in our templates, we'll need to be able to reference the URLs. Similar to any other route, we can use url_for
to generate the full path of the asset with the filename
argument.
In order to reference our logo in our templates, we would call url_for('static', filename='yumroad.png')
, which would return /static/yumroad.png
. If we had placed yumroad.png
within a subfolder of static
called img
, we would want to change the filename
parameter to be url_for('static', filename='img/yumroad.png')`.
Now that we can serve static files, you might be tempted to write CSS and JS files within the static folder and serve them directly, which would work but would quickly grow untenable as the number of CSS files increased, especially if you want to run your CSS/JS files through a pre-processor.
Flask-Assets & webassets
webassets
is a Python library to manage static assets like CSS, Javascript, SCSS, SASS, allowing us to create bundles, merge files, and minify (reduce in size & compress) them for our eventual production build. Flask-Assets is an extension that integrates webassets
into Flask so that we can easily specify and use asset bundles that we create within our templates.
This install will be more involved than usual since there's some additional configuration steps.
To install this library, you'll want to add flask-assets
to your requirements.txt
, which will also install webassets
. We want to eventually be able to minify CSS and JS files, so we'll also want to install the cssmin
and jsmin
packages
Once you add the three packages to your requirements.txt
file, run pip install -r requirements.txt
.
flask-assets
cssmin
jsmin
Before we integrate the webassets
library into our application, we will first create a Bundle
. A Bundle
is a collection of static resources that are grouped together into an output file. A Bundle
can contain references to externally hosted static files, files within our application, or even other Bundles
.
Once a bundle is made, we should specify a folder for the output file to live.
An asset bundle that would function like our current CSS configuration, would look like this.
from flask_assets import Bundle
common_css = Bundle(
'https://stackpath.bootstrapcdn.com/bootswatch/4.3.1/litera/bootstrap.css',
filters='cssmin',
output='public/css/common.css'
)
If we wanted to add our own custom css file from the static folder, we would add the file path to the bundle. If our custom css file was located at yumroad/static/css/basic.css
, we would add the path relative to the static
folder to the Bundle
.
from flask_assets import Bundle
common_css = Bundle(
'https://stackpath.bootstrapcdn.com/bootswatch/4.3.1/litera/bootstrap.css',
'css/basic.css',
filters='cssmin',
output='public/css/common.css'
)
In a production environment, instead of going to bootstrapcdn.com
, the contents would be stored on our own server and bundled together and served from static/public/css/common.css
with any other CSS files we decide to add to common_css
. This kind of bundling can save HTTP requests for browsers, but more importantly you get control of how you serve your assets.
In a development environment, to save time we can configure webassets
to not bundle together the assets into a single file, and instead include each file individually when used in templates. This helps to make it easier to debug and avoid unnecessary pre-processing. To do that, add ASSET_DEBUG = True
to your DevConfig
and TestConfig
in config.py
.
Adding our own assets
To better organize our Bundle
objects, we'll create a separate file call assets.py
within yumroad
to define the assets we want to used.
Within yumroad/assets.py
, start of a Bundle that only contains a basic reference to the external bootstrap.css
file and has an output of public/css/common.css
from flask_assets import Bundle
common_css = Bundle(
'https://stackpath.bootstrapcdn.com/bootswatch/4.3.1/litera/bootstrap.css',
filters='cssmin',
output='public/css/common.css'
)
Once we've defined the asset in asset.py
, we'll need to tell Flask Assets that we want to be able to use them within templates. First, we will need to initialize Flask-Extensions in extensions.py
.
from flask_assets import Environment
...
assets_env = Environment()
Then in order to setup Flask Assets to see the bundles we've created, we have to import the assets.py
module we created. In addition, we need to load in the asset's we've created. webassets
comes with a built in PythonLoader
that reads the bundle configuration from a Python module (which in our case will be assets.py
). Once we've read the asset configurations, we need to tell Flask-Assets
about each bundle, which is what the following code in yumroad/__init__.py
does.
from webassets.loaders import PythonLoader as PythonAssetsLoader
from yumroad import assets
from yumroad.extensions import (..., assets_env)
def create_app(environment_name='dev'):
...
assets_env.init_app(app)
assets_loader = PythonAssetsLoader(assets)
for name, bundle in assets_loader.load_bundles().items():
assets_env.register(name, bundle)
Next, we want to remove any references to this stylesheet in our templates and replace it with a command to use the assets defined in common_css
.
In yumroad/templates/base_layout.html
, replace our direct link to the stylesheet from
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
to this:
{% assets "common_css" %}
<link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}" />
{% endassets %}
This block tells webassets
to use the common_css
bundle, and for each file that is being output, substitute in the actual path (ASSET_URL
) to that file.
If you reload the page, you should not see anything different. To start customizing this, we can start creating our own static assets.
We are going to have three main types of assets in Yumroad, CSS files, Javascript Files, and images. To organize these we will create a folder for each within the static
folder.
Create folders named img
, css
, js
within the static
folder.
Next, we can start by creating a Javascript and CSS file that will be shared across the application. Within the css
folder, create an empty file called common.css
within the css
folder and within the js
folder, create a Javascript file called common.js
.
For the purposes of testing this out, let's make some changes that will be obvious to notice. Within yumroad/static/css/common.css
, lets set the background color of the page to green.
body {
background-color: green;
}
Within common.js
, add an obnoxious alert
pop up.
window.alert('This is an example of Javascript and CSS')
To load Javascript, we'll need to create a new bundle in yumroad/assets.py
.
common_js = Bundle(
'js/shared.css',
filters='jsmin',
output='public/js/common.js'
)
Then in yumroad/templates/base_layout.html
, we add a new block to render a script
tag with the correct src
attribute within the head
section of the template.
{% assets "common_js" %}
<script type="text/javascript" src="{{ ASSET_URL }}"> </script>
{% endassets %}
Now when you load Yumroad, your page will look like this:
Now that page doesn't look very good, so let's remove our example style and Javascript for now. What we can think about is how to remove the Javascript from the checkout page template and into a dedicated Javascript file.
First, let's define a new Bundle in yumroad/static/assets.py
.
checkout_js = Bundle(
'js/purchase.js',
filters='jsmin'
output='public/js/purchase.js'
)
Then we will create yumroad/static/js/purchase.js
. In the template, we directly template in the checkout_session_id
into the function, but because this purchase.js
is a static file, we don't have access to Jinja templating or even to Python here. Instead what we'll do is to change the function signature to take in the session ID, and have our function call to purchase
pass in the session ID.
In addition, we don't have stripe
defined or loaded yet. Unlike other scripts, we cannot host Stripe.js
ourselves by putting it into a bundle since Stripe requires developers to pull it directly from https://js.stripe.com/v3/
.
What we can do instead is define a dedicated block for templates to be able to into scripts the page
In templates/base_layout.html
add a block called custom_assets
This lesson preview is part of the Fullstack Flask: Build a Complete SaaS App with Flask course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Fullstack Flask: Build a Complete SaaS App with Flask with a single-time purchase.