djust docs
Browse documentation
intermediate

Vendoring third-party JS

Declare the third-party JS/CSS your app or component package serves, with Subresource Integrity and a startup check for every version.

On this page

Any Django app that serves someone else's JavaScript or CSS to a browser — djust itself, a component package, or your own project — declares it in a djust_assets.json manifest. djust renders it with Subresource Integrity (SRI), checks it at startup, and can list it in a CycloneDX SBOM (see Scanning a djust app). This is ADR-040.

Who it's for

  • Component packages that bundle a JS library (a chart widget, a rich text editor, a terminal) and want it declared once, checked at every consumer's startup, and visible to a scanner.
  • Apps that vendor their own third-party files, or that need to override a library djust already bundles — for example shipping a security fix for highlight.js before djust releases one.

If you never write <script src="..."> or <link href="..."> by hand for third-party code, you don't need this guide — djust's own components already declare their assets, and {% code_block %} and the ttyd terminal already use them.

The schema

A manifest is <app>/djust_assets.json, next to the app's apps.py:

{
  "schema": 1,
  "assets": {
    "markdown-visual": {
      "files": [
        {
          "path": "djust_components/markdown-visual.js",
          "integrity": "sha384-…"
        }
      ],
      "license_file": "djust_components/markdown-visual.LICENSE.txt",
      "packages": [
        {"purl": "pkg:npm/%40tiptap/core@3.31.3", "license": "MIT"}
      ]
    },
    "chart.js": {
      "files": [
        {
          "url": "https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js",
          "integrity": "sha384-…"
        }
      ],
      "packages": [{"purl": "pkg:npm/chart.js@4.4.0", "license": "MIT"}]
    }
  }
}

Rules:

  • "schema" must be 1.
  • Each entry in "files" has exactly one of "path" (a staticfiles path — vendored) or "url" (external), plus "integrity" in SRI form (sha256-, sha384- or sha512-). A file may also carry "variant" (for example a highlight.js theme) and "type" (script, module or style); "type" is inferred from the extension when omitted (.js → script, .mjs → module, .css → style).
  • "packages" must be non-empty. Each "purl" includes an exact @version — it's the only record of name and version djust keeps. Non-npm sources use pkg:github/<owner>/<repo>@<tag> or pkg:generic/<name>@<version>?download_url=<url>. "license" is an SPDX expression.
  • "license_file" is optional, for a bundle whose build generates one.

An invalid asset is dropped, and its neighbours in the same manifest still load — startup checks report each problem (djust.B001–B002) rather than crashing.

Where manifests live

  • App-owned: <app>/djust_assets.json, discovered automatically from every installed app via apps.get_app_configs(). This is how djust's own components and admin_ext apps declare their bundles, and how a component package ships one.
  • Project-owned: list extra manifest paths in DJUST_ASSET_MANIFESTS (default []), for files under your STATICFILES_DIRS rather than an installed app.
# settings.py
DJUST_ASSET_MANIFESTS = [
    BASE_DIR / "static" / "djust_assets.json",
]

Names are global. When two manifests declare the same asset name, project manifests win over apps (in DJUST_ASSET_MANIFESTS list order), and apps earlier in INSTALLED_APPS win over later ones — the same order Django already uses for templates and static files.

How to compute an integrity

openssl dgst -sha384 -binary path/to/file.js | openssl base64 -A

Prefix the result with sha384- (or use -sha256/-sha512 and prefix accordingly) for the manifest's "integrity" value.

Using assets

{% load djust_assets %}, then:

{% djust_asset "highlight.js" variant="github-dark" %}
{% djust_asset_url "xterm" file_type="module" %}
  • {% djust_asset "name" [variant="…"] %} renders every tag the asset needs — <script integrity="…">, <link rel="stylesheet" integrity="…"> or <link rel="modulepreload" integrity="…"> — computed from the stored static file (so a post-collectstatic rewrite, like ManifestStaticFilesStorage's hashed CSS url()s, doesn't invalidate the hash).
  • {% djust_asset_url "name" [variant="…"] [file_type="module"] %} returns just the URL, for JavaScript that needs to import() a module itself — import() can't carry SRI, so the module is rendered as a modulepreload link and imported by its URL from a data- attribute instead.
  • A component declares what it needs and djust checks it exists:
from djust.components.base import Component


class ChartWidget(Component):
    requires_assets = ["chart.js"]

An undeclared name in requires_assets fails startup with djust.B007.

Rendering a component's assets

requires_assets only declares and checks; it renders nothing by itself. Something has to output the tags, and you choose where.

In the page template, through the variable holding the component instance — for a component the view exposes as chart:

{{ chart.asset_tags }}

In the component's own template (or _render_custom), from Python with self.asset_tags():

class ChartWidget(Component):
    requires_assets = ["chart.js"]
    template = "<div>{{ asset_tags }}<canvas></canvas></div>"

    def get_context_data(self):
        return {"asset_tags": self.asset_tags()}

asset_tags is per instance: every instance that renders it emits its own tags. When several instances on one page share an asset, render the tags once in a page-level template ({% djust_asset "chart.js" %} in the base template, or one instance's asset_tags) instead of in each instance.

LiveComponent does not inherit from Component, so it has neither the asset_tags helper nor the djust.B007 check for requires_assets. Put {% load djust_assets %}{% djust_asset "chart.js" %} in its template instead — an unknown name there raises ImproperlyConfigured when it renders.

Overriding a djust-bundled library for a security fix

Because resolution follows INSTALLED_APPS/DJUST_ASSET_MANIFESTS order and the first declaration of a name wins, you can ship a fixed version of a library djust bundles — for example highlight.js — before djust cuts a release with the fix: add your own djust_assets.json entry for the same asset name in a manifest that resolves before djust's.

An override replaces djust's declaration whole — djust does not merge the two. Overriding highlight.js in particular means your entry must re-declare all thirteen theme files as "variant" entries (see Supported highlight.js themes) alongside the script, or {% code_block theme="…" %} raises ImproperlyConfigured for every theme you left out.

Keep the override under its own path, not djust's: a copy of a djust static file at djust's own path (for example an old djust_components/vendor/highlight/highlight.js in STATICFILES_DIRS) shadows the vendored file, no longer matches djust's manifest, and fails startup with djust.B004. Delete such a copy.

The override is never silent: djust.B009 (a warning, not an error) names both sources and both versions, so the fact that something is shadowing djust's own copy stays visible in manage.py check output. Silence it deliberately with DJUST_CONFIG = {"suppress_checks": ["B009"]} once you've confirmed it's intentional.

External assets

Vendoring is the default; loading from another origin needs an explicit opt-in:

# settings.py
DJUST_ALLOW_EXTERNAL_ASSETS = True

Without it, any manifest entry using "url" fails startup with djust.B005. With it, every external file still needs "integrity" — djust.B006 catches one that doesn't.

If your STATIC_URL is absolute (serving your own vendored files from S3, a CDN, or any host other than the app itself), djust adds crossorigin="anonymous" to the rendered tag automatically — the browser requires it before it will apply an SRI check to a cross-origin resource. Your static host must send an Access-Control-Allow-Origin header back for that fetch, or the browser blocks the file outright rather than silently skipping the integrity check. Check that against your CDN/S3 CORS configuration before you switch STATIC_URL to an absolute host.

Origins that can't be pinned

Some third-party SDKs must load from their vendor's own origin and change without notice, so they can be neither vendored nor given an integrity: Stripe.js (js.stripe.com), Cloudflare Turnstile (challenges.cloudflare.com), Google Tag Manager (www.googletagmanager.com), the Google Fonts CSS API (fonts.googleapis.com). An external asset declaration needs an integrity (djust.B006), which these can't have, and leaving them undeclared makes djust.B010 warn about every template that loads them.

List those hosts explicitly instead:

# settings.py
DJUST_ALLOWED_EXTERNAL_ORIGINS = [
    "js.stripe.com",
    "challenges.cloudflare.com",
]

djust.B010 then accepts templates that load from exactly those hosts. The list is empty by default, and it matches whole hosts only: js.stripe.com does not cover m.stripe.com, and there are no wildcards. Each entry is a bare host (js.stripe.com), optionally written as an origin (https://js.stripe.com, with or without a trailing /). The scheme is ignored, so http:// and https:// entries mean the same host, and case doesn't matter. An entry with an empty host, a port, userinfo, a path or a query is ignored, and B010 names it; so is the whole setting when it isn't a list of strings. Hosts are compared as written, with no normalisation: write an internationalised domain and a host with a trailing dot exactly as they appear in the template's URL (for example the xn-- form if that is what the template uses).

This is an exception list for a security review to read, not a way to switch the check off: every entry is an origin whose code your pages run unverified and that no SBOM lists. It affects only djust.B010's template scan. It does not declare an asset, {% djust_asset %} still renders only declared assets, and it does not change your Content-Security-Policy.

Supported highlight.js themes

{% code_block %} renders one of a curated set of vendored highlight.js themes, not the full ~250 upstream themes:

  • github
  • github-dark
  • atom-one-dark
  • atom-one-light
  • monokai
  • vs2015
  • nord
  • default
  • dark
  • a11y-dark
  • a11y-light
  • stackoverflow-light
  • stackoverflow-dark

{% code_block theme="dracula" %} (or any other name outside this list) raises ImproperlyConfigured naming the thirteen available themes — never a silently unhighlighted block.

See also