How can you set up Tailwind CSS for your Django app on Heroku? In this article, we’ll see how I did exactly that recently.
I have a side project that uses Tailwind CSS. To get started quickly, I used the version from a Content Delivery Network (CDN) as Tailwind describes in the documentation. This worked fine initially while I got my project started, but the CDN version is huge (around 3MB).
I also deploy my app to Heroku, so I didn’t bother to take the time to set up a customized build of Tailwind. If you follow Tailwind’s more involved instructions, you can reduce the size of your CSS file dramatically. I set all of this up and my CSS file went from the huge CDN size to 29kB.
Here are the high level steps to set up Tailwind for a Django app on Heroku:
- Set up Node.js.
- Install Tailwind.
- Create the Tailwind configuration file and your CSS file.
- Set the build command.
- Build the file locally for development.
- Hook the CSS file into your templates.
- Set up a buildpack for Heroku.
Set Up Node.js
I use a Mac and Homebrew so installing the latest version of Node.js looked like:
$ brew install node
If you’re on a different platform, the way to install Node.js is going to be different. Check out the Node.js docs for install instructions.
Install Tailwind
From within my repo, I ran:
$ npm install tailwindcss
This created a package.json
and package-lock.json
file
in the root of my repository
that will list Tailwind
and its Node.js dependencies.
Installing Tailwind will also create a node_modules
directory.
You definitely want to add this directory
to your .gitignore
file.
Create The Tailwind configuration
Tailwind uses a configuration file for any customizations that you want to make. I created an empty one with:
$ npx tailwindcss init
I moved the newly created tailwind.config.js
to a frontend
directory
to help me keep things tidy.
Before I could get any benefit from doing this work, I had to make sure that Tailwind knew where my templates were. This is critical because Tailwind will use PurgeCSS to eliminate any extra CSS classes that it can’t find in my templates. If you neglect this step, the final version that Tailwind will build will also be huge.
To set the proper purging, I added this block to my Tailwind configuration file:
purge: [
'./templates/**/*.html',
],
In my project,
I keep all of my Django templates
in a templates
directory
at the root
of my repository.
I also used Tailwind with PostCSS
so I needed a postcss.config.js
file
in the root
of my repo as well.
Here’s the content of the file:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
Finally, I needed my actual CSS file.
I added frontend/site.css
with:
@tailwind base;
@tailwind components;
@tailwind utilities;
Set The Build Command
To pull it all together,
we need the build command.
Here’s what I included in package.json
(with added formatting here for display purposes).
{
"scripts": {
"build": "tailwindcss build frontend/site.css
-c frontend/tailwind.config.js -o static/site.css"
},
...
}
The build command instructs Tailwind on:
- The location of the input file
- Which configuration file to use
- Where to put the output file
Build A Local CSS File
Now I can run npm run build
and a generated file named site.css
is stored in my static
directory.
This is another file you’d want to add to your .gitignore
because you don’t want to commit it to source control.
To test that everything was correct, I ran my command using the production configuration and tested it locally. Tailwind showed in the build output a significantly reduced file size.
$ NODE_ENV=production npm run build
If you do this, don’t forget to run it again later
without the production
value
or you’ll be really confused
when your styles don’t appear
as you develop new features
on your local machine!
Hook The CSS File Into Templates
With the Tailwind toolchain in place, I was ready to swap out my CDN version. I went to my base template and replaced the CDN line with:
{% load static %}
...
<link href="{% static "site.css" %}" rel="stylesheet">
By outputting the CSS file
into the static
directory,
Django has no problem loading it
with its standard static files mechanism.
As an added bonus, at deploy time, Django will create a fingerprinted version of the file that includes the hashed filename. This means that my Tailwind CSS file will get the same long term caching benefits that I got from the CDN version, but at a massively smaller size!
Set Up A New Heroku Buildpack
I had to tell Heroku
that I now want to run Node.js
as part of my app deployments.
We can do this by adding a new Heroku buildpack.
It’s important that this come
before the Python buildpack
so that the Django collectstatic
process
will find the generated file.
We can set the order
with the index
option:
$ heroku buildpacks:add --index 1 heroku/nodejs
To tell Heroku
which version of Node.js
to use,
I added an engines
section
to package.json
:
{
"engines": {
"node": "15.x"
},
...
}
Now when I deploy my app
to Heroku,
Tailwind will build the production version CSS file
to serve to my users.
This happens because Heroku defaults
to setting NODE_ENV
to production
.
And that’s how I did it! The pages on my app are much snappier after I made this change. There is way less CSS for the browser to process on page load by many orders of magnitude.
Thanks to the nudge from Will Vincent to get me to get off my lazy rear and finally set up my JavaScript toolchain.
If you have questions or enjoyed this article, please feel free to message me on X at @mblayman or share if you think others might be interested too.