Skip to main content

Include Django Media assets into <head> using template tags

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_include_media-0.2.1.tar.gz (9.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_include_media-0.2.1-py3-none-any.whl (11.5 kB view details)

Uploaded Python 3

File details

Details for the file django_include_media-0.2.1.tar.gz.

File metadata

  • Download URL: django_include_media-0.2.1.tar.gz
  • Upload date:
  • Size: 9.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_include_media-0.2.1.tar.gz
Algorithm Hash digest
SHA256 c188256630dd40d444cc6e5356e661ab736a523b85486a68dcc1052d9744e255
MD5 2c91c01736533e2674451a72677a883b
BLAKE2b-256 7c7a21f3315a670440274569c31aa505371db041622ef8192fe26ab1af90c395

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_include_media-0.2.1.tar.gz:

Publisher: pypi-release.yml on blighj/django-include-media

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_include_media-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_include_media-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 60f8d23c927819a2c0d96a3870df3bc0f63439961d4b7e88cdabf77c7efae7a2
MD5 e59d7843d41360e68e14deb20c0733b7
BLAKE2b-256 3dbf087f698afa3277ed21eb8702bf052545b5709257404438bf33fac0f03580

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_include_media-0.2.1-py3-none-any.whl:

Publisher: pypi-release.yml on blighj/django-include-media

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page