Hi, I'm Matthias

I am a founding partner of Feinheit AG and Die Bruchpiloten AG. Find me on GitHub, Mastodon, Bluesky, LinkedIn or by email.

2018-04-10

Our approach to configuring Django, Webpack and ManifestStaticFilesStorage

I spent some time finding out how Django and webpack should be configured so that they work well together both in development and in production. We are now using this setup on dozens of websites, so now is a good time to write down the approach.

Requirements for development were:

  • Hot module reloading (especially of CSS, but also of JavaScript code).
  • Ability to use SCSS, ES6 and ES7.
  • PostCSS support (although I currently only use the autoprefixer, nothing else).

Requirements for production:

  • Filenames depending on the content of assets, so that we can add far future expiry headers without worrying about cache busting, and not only for webpack-generated assets but also for everything else, so ManifestStaticFilesStorage is a must.
  • Separate delivery of CSS and JavaScript code – we want to add CSS to the <head>, and JavaScript at the end of the <body>.
  • Working webpack code splitting is a plus if possible.

There are a few problems that have to be solved:

  1. Django has to know about asset URLs generated by webpack.
  2. Some assets are used both in HTML and in CSS/JavaScript code, e.g. a logo image. The obvious way to be able to do both things is to add the logo image twice, but that’s not nice.

The best way I found for solving 1. is django-webpack-loader. webpack (respectively webpack-bundle-tracker) generates a JSON file when compiling assets, which can be read by the Django site to determine the publicPath of webpack bundles.

The following webpack.config.js snippet helps solve 2. It is by no means complete – uninteresting parts and development settings have been removed.

/* global __dirname, process */
var path = require('path')
var webpack = require('webpack')
var BundleTracker = require('webpack-bundle-tracker')

module.exports = {
  context: path.join(__dirname, 'app', 'static', 'app'),
  entry: {
    main: './main.js',
  },
  output: {
    path: path.resolve('./static/app/'),
    publicPath: '/static/app/',
    filename: '[name]-[chunkhash].js',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {},  // babel-preset-env etc...
          },
        ],
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: 'css-loader',
        }),
      },
      {
        test: /\.(png|woff|woff2|svg|eot|ttf|gif|jpe?g)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 1000,
              // ManifestStaticFilesStorage reuse.
              name: '[path][name].[md5:hash:hex:12].[ext]',
            },
          },
        ],
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
    modules: ['app/static/app/', 'node_modules'],
    alias: {},
  },
  plugins: [
    new ExtractTextPlugin({
      filename: '[name]-[contenthash].css',
      allChunks: true,
    }),
    new BundleTracker({
      filename: './static/webpack-stats-prod.json',
    }),
    new webpack.HashedModuleIdsPlugin(),
  ],
}
  • Put this configuration into webpack.config.js
  • Configure the url-loader to use the same naming schema ManifestStaticFilesStorage uses (see above)
  • Add a app/static/app/main.js entrypoint, and enjoy that require('img/logo.png') and {% static 'app/img/logo.png' %} generate the same URL.