Django's static files system provides a robust foundation for managing CSS, JavaScript, images, and other assets in your web application. Understanding how to properly configure, organize, and serve static files is essential for building modern Django applications with rich frontend experiences.
# settings.py - Static files configuration
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
# Directory where collectstatic will collect static files for deployment
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Additional locations of static files
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
os.path.join(BASE_DIR, 'assets'),
# Add more directories as needed
]
# Static file finders
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# Optional: for advanced use cases
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
]
# Media files (user uploads)
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# settings/development.py
from .base import *
# Serve static files during development
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
# Enable debug mode for static files
DEBUG = True
# settings/production.py
from .base import *
# Use manifest static files storage for production
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# Disable debug mode
DEBUG = False
# Optional: Use whitenoise for serving static files
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Add this
# ... other middleware
]
# Whitenoise configuration
WHITENOISE_USE_FINDERS = True
WHITENOISE_AUTOREFRESH = True # Only in development
myproject/
├── myproject/
│ ├── settings/
│ ├── urls.py
│ └── wsgi.py
├── static/ # Global static files
│ ├── css/
│ │ ├── base.css
│ │ ├── components/
│ │ └── pages/
│ ├── js/
│ │ ├── main.js
│ │ ├── components/
│ │ └── utils/
│ ├── images/
│ │ ├── logo.png
│ │ └── icons/
│ └── fonts/
├── apps/
│ └── blog/
│ ├── static/blog/ # App-specific static files
│ │ ├── css/
│ │ │ └── blog.css
│ │ ├── js/
│ │ │ └── blog.js
│ │ └── images/
│ ├── templates/
│ └── views.py
├── media/ # User uploads
├── staticfiles/ # Collected static files (production)
└── manage.py
# blog/static/blog/css/blog.css
.blog-post {
margin-bottom: 2rem;
padding: 1.5rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.blog-post h2 {
color: #2c3e50;
margin-bottom: 1rem;
}
.blog-post .meta {
color: #7f8c8d;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.blog-post .content {
line-height: 1.6;
}
# blog/static/blog/js/blog.js
document.addEventListener('DOMContentLoaded', function() {
// Blog-specific JavaScript functionality
// Auto-expand text areas
const textareas = document.querySelectorAll('textarea');
textareas.forEach(textarea => {
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
});
});
// Smooth scrolling for anchor links
const anchorLinks = document.querySelectorAll('a[href^="#"]');
anchorLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
});
<!-- templates/base.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Django App{% endblock %}</title>
<!-- Global CSS -->
<link rel="stylesheet" href="{% static 'css/base.css' %}">
<!-- App-specific CSS -->
{% block extra_css %}{% endblock %}
<!-- Favicon -->
<link rel="icon" type="image/png" href="{% static 'images/favicon.png' %}">
</head>
<body>
<header>
<nav class="navbar">
<div class="container">
<a href="{% url 'home' %}" class="navbar-brand">
<img src="{% static 'images/logo.png' %}" alt="Logo" class="logo">
</a>
<ul class="navbar-nav">
<li><a href="{% url 'blog:list' %}">Blog</a></li>
<li><a href="{% url 'about' %}">About</a></li>
<li><a href="{% url 'contact' %}">Contact</a></li>
</ul>
</div>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<div class="container">
<p>© 2023 My Django App. All rights reserved.</p>
</div>
</footer>
<!-- Global JavaScript -->
<script src="{% static 'js/main.js' %}"></script>
<!-- App-specific JavaScript -->
{% block extra_js %}{% endblock %}
</body>
</html>
<!-- blog/templates/blog/post_list.html -->
{% extends 'base.html' %}
{% load static %}
{% block title %}Blog Posts{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'blog/css/blog.css' %}">
{% endblock %}
{% block content %}
<div class="container">
<h1>Latest Blog Posts</h1>
<div class="blog-posts">
{% for post in posts %}
<article class="blog-post">
{% if post.featured_image %}
<img src="{{ post.featured_image.url }}" alt="{{ post.title }}" class="featured-image">
{% endif %}
<h2><a href="{% url 'blog:detail' post.slug %}">{{ post.title }}</a></h2>
<div class="meta">
<span class="author">By {{ post.author.get_full_name }}</span>
<span class="date">{{ post.created_at|date:"F j, Y" }}</span>
<span class="category">in {{ post.category.name }}</span>
</div>
<div class="content">
{{ post.excerpt|safe }}
</div>
<a href="{% url 'blog:detail' post.slug %}" class="read-more">Read More</a>
</article>
{% empty %}
<p>No blog posts available.</p>
{% endfor %}
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'blog/js/blog.js' %}"></script>
{% endblock %}
<!-- templates/base.html -->
{% load static %}
{% block extra_css %}
<!-- Load different CSS based on user preferences or conditions -->
{% if user.profile.dark_mode %}
<link rel="stylesheet" href="{% static 'css/dark-theme.css' %}">
{% else %}
<link rel="stylesheet" href="{% static 'css/light-theme.css' %}">
{% endif %}
<!-- Load CSS based on page type -->
{% if 'admin' in request.path %}
<link rel="stylesheet" href="{% static 'css/admin-overrides.css' %}">
{% endif %}
{% endblock %}
{% block extra_js %}
<!-- Conditional JavaScript loading -->
{% if user.is_authenticated %}
<script src="{% static 'js/authenticated-user.js' %}"></script>
{% endif %}
<!-- Load JavaScript based on features needed -->
{% if form.media %}
{{ form.media }}
{% endif %}
{% endblock %}
# utils/static_helpers.py
from django.templatetags.static import static
from django.template import Library
from django.conf import settings
import os
register = Library()
@register.simple_tag
def static_exists(path):
"""Check if a static file exists"""
try:
static_path = static(path)
return True
except:
return False
@register.simple_tag
def versioned_static(path):
"""Add version parameter to static files for cache busting"""
static_url = static(path)
if settings.DEBUG:
# In development, add timestamp
import time
return f"{static_url}?v={int(time.time())}"
else:
# In production, use file hash or version from settings
version = getattr(settings, 'STATIC_VERSION', '1.0')
return f"{static_url}?v={version}"
@register.inclusion_tag('partials/css_bundle.html')
def css_bundle(bundle_name):
"""Load a bundle of CSS files"""
bundles = {
'base': [
'css/normalize.css',
'css/base.css',
'css/components.css'
],
'blog': [
'blog/css/blog.css',
'blog/css/syntax-highlighting.css'
]
}
return {'files': bundles.get(bundle_name, [])}
# templates/partials/css_bundle.html
{% load static %}
{% for file in files %}
<link rel="stylesheet" href="{% static file %}">
{% endfor %}
# utils/static_finders.py
from django.contrib.staticfiles.finders import BaseFinder
from django.core.files.storage import FileSystemStorage
from django.conf import settings
import os
class ThemeFinder(BaseFinder):
"""
Custom finder for theme-based static files
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.theme = getattr(settings, 'CURRENT_THEME', 'default')
self.theme_dir = os.path.join(settings.BASE_DIR, 'themes', self.theme)
self.storage = FileSystemStorage(location=self.theme_dir)
def find(self, path, all=False):
"""Find a static file in the current theme directory"""
matches = []
theme_path = os.path.join(self.theme_dir, path)
if os.path.exists(theme_path):
if not all:
return theme_path
matches.append(theme_path)
return matches
def list(self, ignore_patterns):
"""List all files in the theme directory"""
for root, dirs, files in os.walk(self.theme_dir):
for file in files:
yield os.path.relpath(os.path.join(root, file), self.theme_dir), self.storage
# settings.py
STATICFILES_FINDERS = [
'utils.static_finders.ThemeFinder', # Add custom finder
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
CURRENT_THEME = 'modern' # or 'classic', 'dark', etc.
# settings/production.py
# Using django-compressor for CSS/JS compression
INSTALLED_APPS = [
# ... other apps
'compressor',
]
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder', # Add compressor finder
]
# Compressor settings
COMPRESS_ENABLED = True
COMPRESS_OFFLINE = True # Pre-compress during deployment
COMPRESS_FILTERS = {
'css': [
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.rCSSMinFilter',
],
'js': [
'compressor.filters.jsmin.JSMinFilter',
]
}
# Cache compressed files
COMPRESS_STORAGE = 'compressor.storage.GzipCompressorFileStorage'
<!-- templates/base.html with compression -->
{% load static %}
{% load compress %}
<!DOCTYPE html>
<html>
<head>
{% compress css %}
<link rel="stylesheet" href="{% static 'css/normalize.css' %}">
<link rel="stylesheet" href="{% static 'css/base.css' %}">
<link rel="stylesheet" href="{% static 'css/components.css' %}">
{% endcompress %}
</head>
<body>
<!-- content -->
{% compress js %}
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/analytics.js' %}"></script>
{% endcompress %}
</body>
</html>
# utils/image_optimization.py
from PIL import Image
from django.core.files.storage import default_storage
from django.conf import settings
import os
class ImageOptimizer:
"""Optimize images for web delivery"""
def __init__(self):
self.quality = getattr(settings, 'IMAGE_QUALITY', 85)
self.max_width = getattr(settings, 'MAX_IMAGE_WIDTH', 1920)
self.max_height = getattr(settings, 'MAX_IMAGE_HEIGHT', 1080)
def optimize_image(self, image_path, output_path=None):
"""Optimize a single image"""
if not output_path:
output_path = image_path
with Image.open(image_path) as img:
# Convert to RGB if necessary
if img.mode in ('RGBA', 'LA', 'P'):
img = img.convert('RGB')
# Resize if too large
if img.width > self.max_width or img.height > self.max_height:
img.thumbnail((self.max_width, self.max_height), Image.Resampling.LANCZOS)
# Save with optimization
img.save(output_path, 'JPEG', quality=self.quality, optimize=True)
def create_responsive_images(self, image_path, sizes=[480, 768, 1024, 1920]):
"""Create multiple sizes for responsive images"""
base_name, ext = os.path.splitext(image_path)
responsive_images = {}
with Image.open(image_path) as img:
for size in sizes:
if img.width >= size:
# Create resized version
resized = img.copy()
resized.thumbnail((size, size), Image.Resampling.LANCZOS)
# Save resized image
output_path = f"{base_name}_{size}w{ext}"
resized.save(output_path, quality=self.quality, optimize=True)
responsive_images[size] = output_path
return responsive_images
# Template tag for responsive images
from django.template import Library
register = Library()
@register.inclusion_tag('partials/responsive_image.html')
def responsive_image(image_url, alt_text, css_class=''):
"""Generate responsive image HTML"""
base_url, ext = os.path.splitext(image_url)
# Generate srcset for different sizes
srcset_items = []
sizes = [480, 768, 1024, 1920]
for size in sizes:
responsive_url = f"{base_url}_{size}w{ext}"
srcset_items.append(f"{responsive_url} {size}w")
return {
'image_url': image_url,
'srcset': ', '.join(srcset_items),
'alt_text': alt_text,
'css_class': css_class
}
<!-- templates/partials/responsive_image.html -->
<img src="{{ image_url }}"
srcset="{{ srcset }}"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
alt="{{ alt_text }}"
class="{{ css_class }}"
loading="lazy">
# settings/production.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
# ... other middleware
]
# Whitenoise settings
WHITENOISE_USE_FINDERS = True
WHITENOISE_AUTOREFRESH = False # Disable in production
WHITENOISE_MAX_AGE = 31536000 # 1 year cache
# Serve compressed files
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'zip', 'gz', 'tgz', 'bz2', 'tbz', 'xz', 'br']
# Custom headers for static files
WHITENOISE_ADD_HEADERS_FUNCTION = 'myproject.utils.add_static_headers'
# utils.py
def add_static_headers(headers, path, url):
"""Add custom headers to static files"""
if path.endswith('.css') or path.endswith('.js'):
headers['Cache-Control'] = 'public, max-age=31536000, immutable'
elif path.endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
headers['Cache-Control'] = 'public, max-age=31536000'
elif path.endswith('.woff2'):
headers['Cache-Control'] = 'public, max-age=31536000, immutable'
# settings/production.py
# Using django-storages with AWS S3
INSTALLED_APPS = [
# ... other apps
'storages',
]
# AWS S3 settings
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'us-east-1')
# Static files on S3
STATICFILES_STORAGE = 'storages.backends.s3boto3.StaticS3Boto3Storage'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/static/'
# Media files on S3
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.MediaS3Boto3Storage'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'
# S3 settings
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
AWS_PRELOAD_METADATA = True
AWS_QUERYSTRING_AUTH = False
# CloudFront CDN (optional)
AWS_S3_CUSTOM_DOMAIN = os.environ.get('CLOUDFRONT_DOMAIN', AWS_S3_CUSTOM_DOMAIN)
<!-- templates/base.html -->
{% load static %}
<!-- Critical CSS inline -->
<style>
/* Critical above-the-fold styles */
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.header { background: #fff; border-bottom: 1px solid #eee; }
/* ... other critical styles */
</style>
<!-- Non-critical CSS loaded asynchronously -->
<link rel="preload" href="{% static 'css/main.css' %}" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{% static 'css/main.css' %}"></noscript>
<!-- Preload important resources -->
<link rel="preload" href="{% static 'fonts/main.woff2' %}" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="{% static 'js/main.js' %}" as="script">
<!-- DNS prefetch for external resources -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//www.google-analytics.com">
// static/js/lazy-loading.js
document.addEventListener('DOMContentLoaded', function() {
// Lazy load images
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
// Lazy load JavaScript modules
const loadScript = (src) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
};
// Load non-critical JavaScript on user interaction
let interactionEvents = ['mousedown', 'touchstart', 'keydown', 'scroll'];
let loadNonCriticalJS = () => {
loadScript('{% static "js/analytics.js" %}');
loadScript('{% static "js/social-sharing.js" %}');
// Remove event listeners after loading
interactionEvents.forEach(event => {
document.removeEventListener(event, loadNonCriticalJS);
});
};
interactionEvents.forEach(event => {
document.addEventListener(event, loadNonCriticalJS, { once: true });
});
});
# settings/development.py
# Enable static file debugging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.contrib.staticfiles': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
},
}
# Debug static file serving
if DEBUG:
import os
from django.conf.urls.static import static
from django.urls import include, path
urlpatterns = [
# ... your URL patterns
]
# Serve static and media files during development
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# management/commands/check_static_files.py
from django.core.management.base import BaseCommand
from django.contrib.staticfiles.finders import get_finders
from django.contrib.staticfiles import storage
from django.conf import settings
import os
class Command(BaseCommand):
help = 'Check static files configuration and find missing files'
def add_arguments(self, parser):
parser.add_argument(
'--missing',
action='store_true',
help='Show missing static files',
)
parser.add_argument(
'--duplicates',
action='store_true',
help='Show duplicate static files',
)
def handle(self, *args, **options):
self.stdout.write('Static Files Configuration:')
self.stdout.write(f'STATIC_URL: {settings.STATIC_URL}')
self.stdout.write(f'STATIC_ROOT: {settings.STATIC_ROOT}')
self.stdout.write(f'STATICFILES_DIRS: {settings.STATICFILES_DIRS}')
if options['missing']:
self.check_missing_files()
if options['duplicates']:
self.check_duplicate_files()
def check_missing_files(self):
"""Check for missing static files referenced in templates"""
# This would scan templates for {% static %} tags
# and verify the files exist
pass
def check_duplicate_files(self):
"""Check for duplicate static files across finders"""
file_locations = {}
for finder in get_finders():
for path, storage in finder.list([]):
if path in file_locations:
file_locations[path].append(storage.location)
else:
file_locations[path] = [storage.location]
duplicates = {path: locations for path, locations in file_locations.items() if len(locations) > 1}
if duplicates:
self.stdout.write(self.style.WARNING('Duplicate static files found:'))
for path, locations in duplicates.items():
self.stdout.write(f' {path}:')
for location in locations:
self.stdout.write(f' - {location}')
else:
self.stdout.write(self.style.SUCCESS('No duplicate static files found.'))
With a solid understanding of Django's static files system, you're ready to explore integrating CSS and JavaScript frameworks, build tools, and modern frontend development workflows. The next chapter will cover advanced CSS and JavaScript integration techniques, including preprocessors, module systems, and optimization strategies.
Key concepts covered:
These fundamentals provide the foundation for building sophisticated frontend experiences with Django while maintaining optimal performance and developer productivity.
Static Assets and Frontend Integration
Modern web applications require sophisticated frontend capabilities, from basic CSS and JavaScript to complex single-page applications (SPAs) built with frameworks like React or Vue. Django provides robust support for managing static assets and integrating with modern frontend toolchains, enabling you to build full-stack applications that deliver exceptional user experiences.
Integrating CSS and JavaScript
Modern web applications require sophisticated CSS and JavaScript integration to deliver rich user experiences. This chapter covers advanced techniques for organizing, processing, and optimizing CSS and JavaScript in Django applications, including preprocessors, module systems, and build workflows.