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 will 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 avialable 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.0.tar.gz (9.8 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.0-py3-none-any.whl (11.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: django_include_media-0.2.0.tar.gz
  • Upload date:
  • Size: 9.8 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.0.tar.gz
Algorithm Hash digest
SHA256 0f4fc3ebe33625c8c9bd1e0db91560e3e94c7958f9558de4ee0c4e47c4a141f1
MD5 829131a92c4f459dfe38f45701a62521
BLAKE2b-256 c3052581887c3fdeeda6b26657a9c3c574a858eb2a1d331df88fa8450d7f8b09

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_include_media-0.2.0.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.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_include_media-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 59072d667a33bbb76a1deb66958820918ef8c5409289989099a3026bd67a3272
MD5 0045ccbee939b12f020ecc6e6159611f
BLAKE2b-256 10ce404b98f309ef3c62e58bdde5a921e124897d1c569a3b0f8abc6e948fed58

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_include_media-0.2.0-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