Using Django’s template loaders to configure Tailwind

Tailwind has a config option to tell it where your HTML is:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js}"],  // <--- Which files to scan?
  theme: {
    extend: {},
  },
  plugins: [],
}

So for a basic Django project you might think it’s going to look something like this:

    content: [
        '../../templates/**/*.html',  // Project level templates folder.
        '../../../**/templates/**/*.html',  // App template folders.
    ],

That double ../.. is bad enough: you’re backing out of a project level static/css folder where you’ve got your tailwind.config.js. (“I don’t do it like that”, you scream — it’s only an example!)

You have nested folders, for includes and whatnot.

Then you have third-party apps which might be shipping templates too — are you really adding paths to you venv’s site-packages?

Frankly it’s a pain.

What you want is Django to tell Tailwind where your templates are. It knows.

A list_templates command

First you need to list all templates. Django can do that:

import os

from django.conf import settings
from django.core.management.base import BaseCommand
from django.template.utils import get_app_template_dirs


class Command(BaseCommand):
    help = "List all template files"

    def handle(self, *args, **options):
        template_files = []
        app_template_dirs = get_app_template_dirs("templates")
        for app_template_dir in app_template_dirs:
            template_files += self.list_template_files(app_template_dir)
        for template_dir in settings.TEMPLATES[0]["DIRS"]:
            template_files += self.list_template_files(template_dir)
        self.stdout.write("\n".join(template_files))

    def list_template_files(self, template_dir):
        template_files = []
        # TODO: Look into using pathlib.Path.rglob() instead. 🤔
        for dirpath, _, filenames in os.walk(str(template_dir)):
            for filename in filenames:
                if filename.endswith(".html") or filename.endswith(".txt"):
                    template_files.append(os.path.join(dirpath, filename))
        return template_files

This would be a management command, so somewhere like myapp/management/commands/list_templates.py.

Telling Tailwind

Then we need to use that in our Tailwind config.

Something like this in your tailwind.config.js:

// Resolve path to directory containing manage.py file.
// This is the root of the project.
// Then assumed layout of <main-app>/static/css/tailwind.config.js, so up 3 levels.
// Adjust for your needs.
const path = require('path');
const projectRoot = path.resolve(__dirname, '../../..');

const { spawnSync } = require('child_process');

// Function to execute the Django management command and capture its output
const getTemplateFiles = () => {
  const command = 'python'; // Requires virtualenv to be activated.
  const args = ['manage.py', 'list_templates']; // Requires cwd to be set.
  const options = { cwd: projectRoot };
  const result = spawnSync(command, args, options);

  if (result.error) {
    throw result.error;
  }

  if (result.status !== 0) {
    console.log(result.stdout.toString(), result.stderr.toString());
    throw new Error(`Django management command exited with code ${result.status}`);
  }

  const templateFiles = result.stdout.toString()
    .split('\n')
    .map((file) => file.trim())
    .filter(function(e){return e});  // Remove empty strings, including last empty line.
  return templateFiles;
};

module.exports = {
  // Allow configuring some folders manually, and then concatenate with the
  // output of the Django management command.
  content: [].concat(getTemplateFiles()),
  theme: {
    extend: {},
  },
  plugins: [],
}
// console.log(module.exports)

Enough messing around. Enjoy! 🚀