Static Assets and Frontend Integration

Using Build Tools like Vite or Webpack

Modern build tools transform the frontend development experience by providing features like hot module replacement, code splitting, automatic optimization, and seamless integration with CSS preprocessors and JavaScript transpilers. This chapter covers integrating Vite and Webpack with Django to create efficient development workflows and optimized production builds.

Using Build Tools like Vite or Webpack

Modern build tools transform the frontend development experience by providing features like hot module replacement, code splitting, automatic optimization, and seamless integration with CSS preprocessors and JavaScript transpilers. This chapter covers integrating Vite and Webpack with Django to create efficient development workflows and optimized production builds.

Vite Integration with Django

Setting Up Vite

// package.json
{
  "name": "django-vite-frontend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "watch": "vite build --watch"
  },
  "devDependencies": {
    "vite": "^5.0.0",
    "@vitejs/plugin-legacy": "^5.0.0",
    "sass": "^1.69.0",
    "autoprefixer": "^10.4.16",
    "postcss": "^8.4.31",
    "tailwindcss": "^3.3.5"
  },
  "dependencies": {
    "alpinejs": "^3.13.0",
    "axios": "^1.6.0"
  }
}
// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';
import legacy from '@vitejs/plugin-legacy';

export default defineConfig({
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11']
    })
  ],
  
  // Build configuration
  build: {
    // Output directory (Django's STATIC_ROOT)
    outDir: 'staticfiles/dist',
    
    // Generate manifest for Django integration
    manifest: true,
    
    // Entry points
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'frontend/js/main.js'),
        admin: resolve(__dirname, 'frontend/js/admin.js'),
        blog: resolve(__dirname, 'frontend/js/blog.js')
      },
      output: {
        // Organize output files
        assetFileNames: (assetInfo) => {
          const info = assetInfo.name.split('.');
          const ext = info[info.length - 1];
          
          if (/\.(css)$/.test(assetInfo.name)) {
            return `css/[name]-[hash].${ext}`;
          }
          if (/\.(png|jpe?g|svg|gif|tiff|bmp|ico)$/i.test(assetInfo.name)) {
            return `images/[name]-[hash].${ext}`;
          }
          if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
            return `fonts/[name]-[hash].${ext}`;
          }
          return `assets/[name]-[hash].${ext}`;
        },
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js'
      }
    },
    
    // Code splitting
    chunkSizeWarningLimit: 1000,
    
    // Source maps for production debugging
    sourcemap: true
  },
  
  // Development server
  server: {
    host: 'localhost',
    port: 3000,
    
    // Proxy API requests to Django
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true
      },
      '/admin': {
        target: 'http://localhost:8000',
        changeOrigin: true
      }
    }
  },
  
  // CSS configuration
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "frontend/scss/variables.scss";`
      }
    },
    postcss: {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer')
      ]
    }
  },
  
  // Resolve configuration
  resolve: {
    alias: {
      '@': resolve(__dirname, 'frontend'),
      '@components': resolve(__dirname, 'frontend/js/components'),
      '@utils': resolve(__dirname, 'frontend/js/utils'),
      '@styles': resolve(__dirname, 'frontend/scss')
    }
  }
});

Django Integration with Vite

# settings.py
import os
import json
from pathlib import Path

# Vite configuration
VITE_DEV_MODE = DEBUG and os.environ.get('VITE_DEV_MODE', 'False').lower() == 'true'
VITE_DEV_SERVER_URL = 'http://localhost:3000'

if VITE_DEV_MODE:
    # Development: Use Vite dev server
    STATICFILES_DIRS = [
        BASE_DIR / 'frontend',
    ]
else:
    # Production: Use built assets
    STATICFILES_DIRS = [
        BASE_DIR / 'staticfiles' / 'dist',
    ]

# Vite manifest helper
def get_vite_manifest():
    """Load Vite manifest for production asset URLs."""
    if VITE_DEV_MODE:
        return {}
    
    manifest_path = BASE_DIR / 'staticfiles' / 'dist' / 'manifest.json'
    if manifest_path.exists():
        with open(manifest_path) as f:
            return json.load(f)
    return {}

VITE_MANIFEST = get_vite_manifest()
# templatetags/vite_tags.py
from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
from django.templatetags.static import static

register = template.Library()

@register.simple_tag
def vite_asset(entry_name):
    """
    Generate script/link tags for Vite assets.
    
    Usage:
    {% vite_asset 'main.js' %}
    {% vite_asset 'main.css' %}
    """
    if settings.VITE_DEV_MODE:
        # Development mode: use Vite dev server
        if entry_name.endswith('.js'):
            return mark_safe(
                f'<script type="module" src="{settings.VITE_DEV_SERVER_URL}/{entry_name}"></script>'
            )
        elif entry_name.endswith('.css'):
            return mark_safe(
                f'<link rel="stylesheet" href="{settings.VITE_DEV_SERVER_URL}/{entry_name}">'
            )
    else:
        # Production mode: use manifest
        manifest = getattr(settings, 'VITE_MANIFEST', {})
        
        if entry_name in manifest:
            file_info = manifest[entry_name]
            file_url = static(file_info['file'])
            
            if entry_name.endswith('.js'):
                html = f'<script type="module" src="{file_url}"></script>'
                
                # Add CSS imports
                if 'css' in file_info:
                    for css_file in file_info['css']:
                        css_url = static(css_file)
                        html += f'\n<link rel="stylesheet" href="{css_url}">'
                
                return mark_safe(html)
            elif entry_name.endswith('.css'):
                return mark_safe(f'<link rel="stylesheet" href="{file_url}">')
    
    return ''

@register.simple_tag
def vite_hmr():
    """Add Vite HMR client in development mode."""
    if settings.VITE_DEV_MODE:
        return mark_safe(
            f'<script type="module" src="{settings.VITE_DEV_SERVER_URL}/@vite/client"></script>'
        )
    return ''
<!-- templates/base.html -->
{% load vite_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Django + Vite{% endblock %}</title>
    
    <!-- Vite HMR in development -->
    {% vite_hmr %}
    
    <!-- Main CSS -->
    {% vite_asset 'frontend/js/main.js' %}
    
    {% block extra_css %}{% endblock %}
</head>
<body>
    {% block content %}{% endblock %}
    
    <!-- Page-specific JavaScript -->
    {% block extra_js %}{% endblock %}
</body>
</html>

Frontend Structure

frontend/
├── js/
│   ├── main.js              # Main entry point
│   ├── components/          # Reusable components
│   │   ├── modal.js
│   │   ├── dropdown.js
│   │   └── form-validator.js
│   ├── pages/              # Page-specific modules
│   │   ├── blog.js
│   │   ├── contact.js
│   │   └── dashboard.js
│   └── utils/              # Utility functions
│       ├── api.js
│       ├── dom.js
│       └── validation.js
├── scss/
│   ├── main.scss           # Main stylesheet
│   ├── _variables.scss     # SCSS variables
│   ├── _mixins.scss        # SCSS mixins
│   ├── components/         # Component styles
│   └── pages/              # Page-specific styles
└── images/
    └── icons/
// frontend/js/main.js
import '../scss/main.scss';
import Alpine from 'alpinejs';
import { initializeAPI } from './utils/api.js';
import { initializeComponents } from './components/index.js';

// Initialize Alpine.js
window.Alpine = Alpine;
Alpine.start();

// Initialize API utilities
initializeAPI();

// Initialize components
initializeComponents();

// Global utilities
window.app = {
    csrfToken: document.querySelector('[name=csrfmiddlewaretoken]')?.value,
    
    // API helper
    async fetch(url, options = {}) {
        const defaultOptions = {
            headers: {
                'X-CSRFToken': this.csrfToken,
                'Content-Type': 'application/json',
            },
            credentials: 'same-origin'
        };
        
        return fetch(url, { ...defaultOptions, ...options });
    }
};

Webpack Integration with Django

Webpack Configuration

// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

const isDevelopment = process.env.NODE_ENV !== 'production';

module.exports = {
  mode: isDevelopment ? 'development' : 'production',
  
  entry: {
    main: './frontend/js/main.js',
    admin: './frontend/js/admin.js',
    blog: './frontend/js/blog.js'
  },
  
  output: {
    path: path.resolve(__dirname, 'staticfiles/dist'),
    filename: isDevelopment ? 'js/[name].js' : 'js/[name].[contenthash].js',
    chunkFilename: isDevelopment ? 'js/[name].chunk.js' : 'js/[name].[contenthash].chunk.js',
    publicPath: '/static/dist/',
    clean: true
  },
  
  module: {
    rules: [
      // JavaScript
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-syntax-dynamic-import']
          }
        }
      },
      
      // CSS/SCSS
      {
        test: /\.(css|scss|sass)$/,
        use: [
          isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ]
      },
      
      // Images
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'images/[name].[hash][ext]'
        }
      },
      
      // Fonts
      {
        test: /\.(woff2?|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[hash][ext]'
        }
      }
    ]
  },
  
  plugins: [
    new CleanWebpackPlugin(),
    
    new MiniCssExtractPlugin({
      filename: isDevelopment ? 'css/[name].css' : 'css/[name].[contenthash].css',
      chunkFilename: isDevelopment ? 'css/[name].chunk.css' : 'css/[name].[contenthash].chunk.css'
    }),
    
    new WebpackManifestPlugin({
      fileName: 'manifest.json',
      publicPath: '/static/dist/'
    })
  ],
  
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: !isDevelopment
          }
        }
      }),
      new CssMinimizerPlugin()
    ],
    
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  },
  
  devtool: isDevelopment ? 'eval-source-map' : 'source-map',
  
  devServer: {
    static: {
      directory: path.join(__dirname, 'staticfiles')
    },
    port: 3000,
    hot: true,
    proxy: {
      '/api': 'http://localhost:8000',
      '/admin': 'http://localhost:8000'
    }
  },
  
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'frontend'),
      '@components': path.resolve(__dirname, 'frontend/js/components'),
      '@utils': path.resolve(__dirname, 'frontend/js/utils'),
      '@styles': path.resolve(__dirname, 'frontend/scss')
    }
  }
};

Django Webpack Integration

# utils/webpack.py
import json
import os
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage

class WebpackLoader:
    """Helper class for loading Webpack assets in Django templates."""
    
    def __init__(self):
        self.manifest = self._load_manifest()
    
    def _load_manifest(self):
        """Load Webpack manifest file."""
        if settings.DEBUG and os.environ.get('WEBPACK_DEV_MODE'):
            return {}
        
        manifest_path = os.path.join(settings.STATIC_ROOT or '', 'dist', 'manifest.json')
        
        try:
            with open(manifest_path, 'r') as f:
                return json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            return {}
    
    def get_bundle(self, bundle_name):
        """Get bundle URLs from manifest."""
        if settings.DEBUG and os.environ.get('WEBPACK_DEV_MODE'):
            # Development mode
            return {
                'js': [f'http://localhost:3000/js/{bundle_name}.js'],
                'css': []
            }
        
        # Production mode
        js_files = []
        css_files = []
        
        # Main bundle
        if f'{bundle_name}.js' in self.manifest:
            js_files.append(staticfiles_storage.url(self.manifest[f'{bundle_name}.js']))
        
        if f'{bundle_name}.css' in self.manifest:
            css_files.append(staticfiles_storage.url(self.manifest[f'{bundle_name}.css']))
        
        # Vendor bundle
        if 'vendors.js' in self.manifest and bundle_name == 'main':
            js_files.insert(0, staticfiles_storage.url(self.manifest['vendors.js']))
        
        if 'vendors.css' in self.manifest and bundle_name == 'main':
            css_files.insert(0, staticfiles_storage.url(self.manifest['vendors.css']))
        
        return {
            'js': js_files,
            'css': css_files
        }

webpack_loader = WebpackLoader()
# templatetags/webpack_tags.py
from django import template
from django.utils.safestring import mark_safe
from utils.webpack import webpack_loader

register = template.Library()

@register.simple_tag
def webpack_bundle(bundle_name):
    """Render script and link tags for a Webpack bundle."""
    bundle = webpack_loader.get_bundle(bundle_name)
    
    html = []
    
    # CSS files
    for css_url in bundle['css']:
        html.append(f'<link rel="stylesheet" href="{css_url}">')
    
    # JavaScript files
    for js_url in bundle['js']:
        html.append(f'<script src="{js_url}"></script>')
    
    return mark_safe('\n'.join(html))

@register.simple_tag
def webpack_css(bundle_name):
    """Render only CSS tags for a Webpack bundle."""
    bundle = webpack_loader.get_bundle(bundle_name)
    
    html = []
    for css_url in bundle['css']:
        html.append(f'<link rel="stylesheet" href="{css_url}">')
    
    return mark_safe('\n'.join(html))

@register.simple_tag
def webpack_js(bundle_name):
    """Render only JavaScript tags for a Webpack bundle."""
    bundle = webpack_loader.get_bundle(bundle_name)
    
    html = []
    for js_url in bundle['js']:
        html.append(f'<script src="{js_url}"></script>')
    
    return mark_safe('\n'.join(html))

Development Workflow

Development Scripts

// package.json scripts
{
  "scripts": {
    "dev": "concurrently \"python manage.py runserver\" \"npm run vite:dev\"",
    "vite:dev": "VITE_DEV_MODE=true vite",
    "webpack:dev": "WEBPACK_DEV_MODE=true webpack serve",
    "build": "vite build",
    "build:webpack": "NODE_ENV=production webpack",
    "watch": "vite build --watch",
    "preview": "vite preview",
    "lint": "eslint frontend/js --ext .js",
    "lint:fix": "eslint frontend/js --ext .js --fix",
    "format": "prettier --write frontend/js/**/*.js frontend/scss/**/*.scss"
  }
}

Environment Configuration

# .env.development
DEBUG=True
VITE_DEV_MODE=True
WEBPACK_DEV_MODE=True
# .env.production
DEBUG=False
VITE_DEV_MODE=False
WEBPACK_DEV_MODE=False

Django Management Command

# management/commands/build_frontend.py
from django.core.management.base import BaseCommand
import subprocess
import os

class Command(BaseCommand):
    help = 'Build frontend assets'
    
    def add_arguments(self, parser):
        parser.add_argument(
            '--tool',
            choices=['vite', 'webpack'],
            default='vite',
            help='Build tool to use'
        )
        parser.add_argument(
            '--watch',
            action='store_true',
            help='Watch for changes'
        )
    
    def handle(self, *args, **options):
        tool = options['tool']
        watch = options['watch']
        
        if tool == 'vite':
            cmd = ['npm', 'run', 'build']
            if watch:
                cmd = ['npm', 'run', 'watch']
        else:
            cmd = ['npm', 'run', 'build:webpack']
            if watch:
                cmd.extend(['--', '--watch'])
        
        self.stdout.write(f'Building frontend assets with {tool}...')
        
        try:
            subprocess.run(cmd, check=True, cwd=os.getcwd())
            self.stdout.write(
                self.style.SUCCESS(f'Successfully built frontend assets with {tool}')
            )
        except subprocess.CalledProcessError as e:
            self.stdout.write(
                self.style.ERROR(f'Failed to build frontend assets: {e}')
            )

Production Deployment

Build Process

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build frontend assets
        run: npm run build
      
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      
      - name: Install Python dependencies
        run: |
          pip install -r requirements.txt
      
      - name: Collect static files
        run: |
          python manage.py collectstatic --noinput
      
      - name: Deploy to production
        run: |
          # Your deployment commands here

Docker Configuration

# Dockerfile
FROM node:18-alpine AS frontend-builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY frontend/ ./frontend/
COPY vite.config.js ./
RUN npm run build

FROM python:3.11-slim

WORKDIR /app

# Copy Python requirements and install
COPY requirements.txt .
RUN pip install -r requirements.txt

# Copy Django application
COPY . .

# Copy built frontend assets
COPY --from=frontend-builder /app/staticfiles/dist ./staticfiles/dist

# Collect static files
RUN python manage.py collectstatic --noinput

EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]

Performance Optimization

Code Splitting

// Dynamic imports for code splitting
const loadBlogModule = () => import('./pages/blog.js');
const loadDashboardModule = () => import('./pages/dashboard.js');

// Route-based code splitting
const routes = {
  '/blog/': loadBlogModule,
  '/dashboard/': loadDashboardModule
};

// Load modules based on current page
const currentPath = window.location.pathname;
if (routes[currentPath]) {
  routes[currentPath]().then(module => {
    module.default();
  });
}

Asset Optimization

// vite.config.js - Advanced optimization
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['axios', 'alpinejs'],
          utils: ['./frontend/js/utils/api.js', './frontend/js/utils/dom.js']
        }
      }
    },
    
    // Compression
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  }
});

Modern build tools like Vite and Webpack transform Django frontend development by providing hot module replacement, automatic optimization, and seamless integration with CSS preprocessors and JavaScript transpilers. Choose Vite for faster development builds and simpler configuration, or Webpack for more complex build requirements and extensive plugin ecosystem. Both tools integrate well with Django's static files system and can be configured for efficient production deployments.