Published
So you want to deploy a traditional Django application, but you also want Typescript, Tailwind, and all the good that modern frontend offers?
Show me the code! Here's a link to the
GitHub repository of html-webpack-plugin-django
that also contains all the code from this post in the example
folder.
Introduction
In this article I will explain how I've set up the latest Django project I've deployed at RIPE NCC to work with the modern frontend toolchain. If you want to check it out, it's a small app called Hosted DNS.
While other web frameworks are developing close integrations with Webpack, for example Laravel Mix or Rails Webpacker, I think Django is lagging behind.
On the other hand, I don't think such abstractions are even necessary as Webpack can be easily configured to play nicely with Django.
In this post, I explain how you can set up Django to benefit from modern frontend tooling, without compromising the conventional structure of your project.
I'm going to build a Django app with a Webpack bundle served from
the usual static folder managed with
django.contrib.staticfiles
.
First things first: why do you even need this? Nothing wrong with jQuery, but maybe you want to write your scripts with Typescript; maybe you want to use Tailwind instead of Bootstrap and you need to build your stylesheet; maybe you need to import Web Components distributed through npm; maybe you want to use Material Design UI components which require Sass; or, as in my case, all of the above.
Your Django app
Let's start with a basic Django app. If you're not feeling at home at this stage, you may want to take a look at Django's Get Started page.
.
├── mysite/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── requirements.txt
├── myapp
│ ├── __init__.py
│ ├── apps.py
│ ├── migrations/
│ ├── models.py
│ ├── static/
│ ├── templates/
│ ├── urls.py
│ └── views.py
└── templates/
└── base.html
At this point, you could throw all your scripts and stylesheets
in ./myapp/static
and in base.html
manually add <script>
and <link>
tags and be done with it.
However, if you want to easily use Typescript, Tailwind, Sass and more, read on to the next section to learn how to set it up.
The frontend toolchain
First things first, you'll want to install node.js on your machine.
I greatly prefer yarn
to npm
, but that's another story. In
any case, you can install yarn
with
npm install --global yarn
.
Then, in the root of your project, run this command:
yarn init -2
# if you prefer the `node_modules` approach, run this instead
yarn init
You should now have these files in your project's root:
.yarn/
: a directory that will store all the frontend dependencies you will add.yarnrc.yml
: a file to configure yarn's internal settingspackage.json
: a file to list your frontend dependencies and build scriptsyarn.lock
: a lockfile to store which version of each dependency was installed
Installing Webpack and the necessary dependencies
At this point, you'll want to install these dependencies:
yarn add webpack webpack-cli \
html-webpack-plugin html-webpack-plugin-django
webpack
: webpack is an open-source JavaScript module bundler. It can also transform front-end assets like HTML, CSS, and images if the corresponding loaders are includedhtml-webpack-plugin
: this plugin will generate an HTML5 file for you that includes all your webpack bundles usingscript
tagshtml-webpack-plugin-django
: a plugin I wrote to transform the output ofhtml-webpack-plugin
to use Django's{% static %}
tags
Configuring Webpack
At this stage, running webpack build
will throw a lot of errors
because it's not configured yet.
Let's start by creating an empty webpack.config.js
file. This
is simply a JavaScript file that should export an object
containing the configuration for our build.
// webpack.config.js
module.exports = () => {
return {};
};
Entry point
Your bundle needs an entry point: the point or points where to start the application bundling process. If an array is passed then all items will be processed. Learn more about this in the official Webpack documentation.
Let's stick to convention and name this file index.js
. We'll
place it in ./myapp/static/index.js
because that's where a
Django developer would expect to find this, and we're trying to
integrate Webpack into Django in the least surprising way
possible.
At this point, we can configure Webpack so it knows that's our entry point.
// webpack.config.js
module.exports = () => {
return {
entry: {
index: "./myapp/static/index.js",
},
};
};
Output
The output is the location where Webpack will save the final
bundle it produces. We'll configure Webpack to save the bundle in
./myapp/static/dist
so we don't have to tweak Django to serve
these static files.
// webpack.config.js
const path = require("path");
module.exports = () => {
return {
entry: {
index: "./myapp/static/index.js",
},
output: {
path: path.resolve("myapp/static/dist"),
filename: "[name].[contenthash].js",
// corresponds to the STATIC_URL setting in Django
publicPath: "/static/",
},
};
};
There is one thing I'd like to point out in this configuration entry:
output.filename
: this setting tells Webpack to process our bundle's entry point and output it with a filename made of these two parameters[name]
: corresponds to the original filename[contenthash]
: a hash based on the content of the bundle
For example, in the case of the entry point index.js
, the
output could be something like index.744a73e75a45758334f9.js
.
Plugins
Now the most important part: configuring html-webpack-plugin
.
The following configuration will output all your webpack bundles
using script
or link
tags to a webpack.html
file in the
templates
directory.
You can then easily include this file in your base.html
.
Alternatively, you can
write your own template,
base_template.html
, and let html-webpack-plugin
take over and
output a full base.html
.
e.g.
<!-- base.html -->
<!DOCTYPE html>
<html lang="en-US">
<head>
{% include "webpack.html" %}
</head>
...
</html>
// webpack.config.js
const path = require("path");
const templateContent = ({ htmlWebpackPlugin }) =>
`{% load static %}
${htmlWebpackPlugin.tags.headTags}
${htmlWebpackPlugin.tags.bodyTags}`;
module.exports = () => {
return {
entry: {
index: "./myapp/static/index.js",
},
output: {
path: path.resolve("myapp/static/dist"),
filename: "[name].[contenthash].js",
// corresponds to the STATIC_URL setting in Django
publicPath: "/static/",
},
plugins: [
new HtmlWebpackPlugin({
appMountId: "index",
filename: path.resolve(__dirname, "templates", "webpack.html"),
inject: false,
templateContent: templateContent,
scriptLoading: "defer",
}),
new HtmlWebpackPluginDjango({ bundlePath: "dist" }),
],
};
};
Building the bundle
All is configured now, you can build your bundle. In your
package.json
add the following script, then run yarn build
.
// package.json
{
"scripts": {
"build": "webpack --mode production"
},
"dependencies": {
"webpack": "*",
"webpack-cli": "*",
"html-webpack-plugin": "*",
"html-webpack-plugin-django": "*"
}
}
Now you'll see that ./myapp/static/dist
contains a few files
and that they are all imported in templates/webpack.html
, which
now contains something like this:
{% load static %}
<script
defer
src="{% static 'dist/js/runtime.XXX.js' %}"
></script>
<script defer src="{% static 'dist/js/index.XXX.js' %}"></script>
Conclusion
If you kept reading until here, thank you first and foremost.
You should now have a conventional Django app with a Webpack
integration that doesn't sacrifice anything: by using the
{% static %}
tag, your project doesn't require any adjustments
to run locally or to be deployed.
From here the sky is the limit: you can now easily start using
Typescript with
ts-loader
or process
your CSS with PostCSS using
postcss-loader
for example. The basic set up implemented above is just a
starting point.
Here's once again a link to a GitHub repository that contains all the code from this post.