Skip to content

blighj/django-include-media

Repository files navigation

Django include media

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.

Installation

pip install django-include-media

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "include_media",
]

Usage

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>

Component and template assets

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 %}

View-level assets

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 ctx

Site-wide assets

Declare 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",
        ],
    },
}]

App-level registration

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",
        ],
    },
}]

Import maps

{% 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.

Asset post-processing

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 string

context 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.

Compatibility

  • Python 3.10+
  • Django 5.2, 6.0+ (Stylesheet is backported for Django < 6.1)

Contributing

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.

License

BSD 3-Clause License

Changelog

0.2.0

  • Add a post processing hook
  • Add an app level register for media assets

0.1.0 — (Initial version)

  • 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.staticfiles if relevant.
  • CSP nonce support available via csp_nonce_attr flag on {% use_media %}
  • Import map support via importmap= keyword and ImportmapScript
  • Site-wide media via page_media context variable; merges with template-level assets
  • {% use_media %} falls back to rendering inline with debug-mode warning, if the include is not there.

About

Include Django Media assets into <head> using template tags

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages