An app for Django that allows templates and views to add Script/Stylesheets to
a page using Django's forms.Media object, with automatic collection and
deduplication, outputting assets into <head>. Inspired by django-sekizai.
pip install django-include-mediaAdd to INSTALLED_APPS:
INSTALLED_APPS = [
...
"include_media",
]Place {% include_media %} in <head> of your base template. Then use the
use_media templatetag or page_media context to add the assets you need.
All your sub-templates or templates from templatetags can now reliably add assets to the page.
{# base.html #}
{% load include_media_tags %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
{% include_media %}
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>Declare assets inline with {% use_media %}. Assets are deduplicated by
object identity, including the same component twice only renders its
assets once:
{% load include_media_tags %}
{% use_media form.media %}
{% use_media css="myapp/widget.css" %}
{% use_media js="myapp/script.js" %}Extra HTML attributes can be passed as keyword arguments and are forwarded
to the rendered tag. Add csp_nonce_attr to opt a specific asset into
Django's CSP nonce (Django 6.0+); the nonce is applied if csp_nonce is
present in the template context and is a no-op otherwise:
{% use_media js="myapp/widget.js" type="module" %}
{% use_media js="myapp/widget.js" type="module" csp_nonce_attr %}
{% use_media css="myapp/widget.css" media="print" %}
{% use_media form.media csp_nonce_attr %}Pass page_media via get_context_data. If a site-wide context processor
also sets page_media, the two are merged automatically:
from django.forms import Media
from django.forms.widgets import Script
from include_media import Stylesheet
from django.views.generic import TemplateView
class DatePickerView(TemplateView):
template_name = "datepicker.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["page_media"] = Media(
css={"all":[Stylesheet("datepicker/datepicker.css")]},
js=[Script("datepicker/datepicker.js", type="module")],
)
return ctxDeclare assets required on every page in a context processor:
# myproject/context_processors.py
from django.forms import Media
from django.forms.widgets import Script
from include_media import Stylesheet
def site_media(request):
nonce = getattr(request, "csp_nonce", None)
attrs = {"nonce": nonce} if nonce else {}
media = Media(
css={"all":[Stylesheet("base.css", **attrs)]},
js=[Script("base.js", type="module", **attrs)],
)
if request.user.is_authenticated:
media += Media(js=[Script("dashboard.js", type="module", **attrs)])
return {"page_media": media}TEMPLATES = [{
"OPTIONS": {
"context_processors": [
...
"myproject.context_processors.site_media",
],
},
}]Apps can use the register() method to add app level assets that are linked to
a registered_media context_processor.
# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
from django.forms import Media
from django.forms.widgets import Script
from include_media import register, Stylesheet
register(Media(
css={"all": [Stylesheet("myapp/base.css")]},
js=[Script("myapp/base.js", type="module")],
))Add the single bundled context processor to TEMPLATES once, regardless of
how many apps use register():
TEMPLATES = [{
"OPTIONS": {
"context_processors": [
...
"include_media.context_processors.registered_media",
],
},
}]{% include_media %} can generate a <script type="importmap"> tag,
letting templates and reusable components declare their ES module specifiers
in the same places they already declare CSS and JS assets. All entries from
across the template hierarchy are merged into a single importmap tag, placed
before other assets in <head>.
Template tag — for one-off or inline declarations:
{% use_media js="vendor/htmx.js" importmap="htmx" %}
{% use_media js="https://cdn.example.com/lodash.js" importmap="lodash" %}ImportmapScript — for reusable widgets and forms that need a module
specifier wherever they are used:
from django.forms import Form
from include_media import ImportmapScript
class DatePickerForm(Form):
class Media:
js = [
ImportmapScript("vendor/pikaday.js", specifier="pikaday"),
Script("datepicker/widget.js", type="module"),
]To hook up a JS build system's manifest, you could add ImportmapScript
entries to page_media from a context processor:
import json
from pathlib import Path
from django.forms import Media
from include_media import ImportmapScript
_manifest = json.loads((BASE_DIR / "static/dist/manifest.json").read_text())
def importmap(request):
return {
"page_media": Media(js=[
ImportmapScript(f"/static/dist/{entry['file']}", specifier=name)
for name, entry in _manifest.items()
])
}Merging and precedence — all sources (template tags, ImportmapScript in
page_media) are merged into one <script type="importmap"> tag with
first-wins semantics: page_media is processed before template tags, so
view-level declarations take precedence when the same specifier appears in
multiple places.
Set INCLUDE_MEDIA_POSTPROCESSOR to a dotted Python path to intercept the
collected asset HTML before it is written to the page. The callable receives
the fully-rendered asset tags — after deduplication and nonce application —
and must return a string:
# settings.py
INCLUDE_MEDIA_POSTPROCESSOR = "myproject.assets.postprocess"# myproject/assets.py
def postprocess(assets_html: str, context) -> str:
# assets_html contains the rendered <link> / <script> / importmap tags.
# context is the template Context, giving access to request, user, etc.
return assets_html # return a modified stringcontext is the full Django template Context. Access the current request
via context.get("request") if you need per-request information (user,
headers, etc.).
The setting is validated at startup by Django's system check framework; a misconfigured path or non-callable target raises an error before the first request is served.
- Python 3.10+
- Django 5.2, 6.0+ (
Stylesheetis backported for Django < 6.1)
Contributions are welcome! Please feel free to submit a Pull Request. The aim of this repo is to explore this idea and if it feels right to propose it back to django core, where it could be implemented cleaner. Any feedback is welcome.
BSD 3-Clause License
- Add a post processing hook
- Add an app level register for media assets
- Provide template tags
{% include_media %}and{% use_media %}for collecting Django Media assets into<head> - Supports CSS
<link rel="stylesheet">and JavaScript<script>tags and setting any attributes. - Sets correct path with
django.contrib.staticfilesif relevant. - CSP nonce support available via
csp_nonce_attrflag on{% use_media %} - Import map support via
importmap=keyword andImportmapScript - Site-wide media via
page_mediacontext variable; merges with template-level assets {% use_media %}falls back to rendering inline with debug-mode warning, if the include is not there.