Skip to main content

Serve static assets (CSS, JS, images, etc.) directly or from a CDN.

Project description

plain.assets

Serve static assets (CSS, JS, images, etc.) directly or from a CDN.

Overview

To serve assets, put them in app/assets or app/{package}/assets.

Then include the AssetsRouter in your own router, typically under the assets/ path.

# app/urls.py
from plain.assets.urls import AssetsRouter
from plain.urls import include, Router


class AppRouter(Router):
    namespace = ""
    urls = [
        include("assets/", AssetsRouter),
        # your other routes here...
    ]

Now in your template you can use the asset() function to get the URL, which will output the fully compiled and fingerprinted URL.

<link rel="stylesheet" href="{{ asset('css/style.css') }}">

Local development

When you're working with settings.DEBUG = True, the assets will be served directly from their original location. You don't need to run plain assets compile or configure anything else.

Production deployment

In production, one of your deployment steps should be to compile the assets.

plain assets compile

By default, this generates "fingerprinted" and compressed versions of the assets, which are then served by your app. This means that a file like main.css will result in two new files, like main.d0db67b.css and main.d0db67b.css.gz.

The purpose of fingerprinting the assets is to allow the browser to cache them indefinitely. When the content of the file changes, the fingerprint will change, and the browser will use the newer file. This cuts down on the number of requests that your app has to handle related to assets.

Pre-compile hooks

plain assets compile runs three things in order:

  1. User-defined shell commands from [tool.plain.assets.run] in your pyproject.toml. Useful for codegen that produces files into app/assets/:

    [tool.plain.assets.run]
    openapi = {cmd = "plain api generate-openapi --validate > app/assets/openapi.json"}
    
  2. Package-registered hooks via the plain.assets.compile entry-point group. Packages like plain.tailwind and plain.esbuild ship one of these to compile CSS / JS before the asset fingerprinter runs.

  3. The asset compile itself (fingerprinting, compression).

Build inputs and outputs

Two top-level subdirectory names are reserved inside any assets dir, so build tools have a place for their source and their pre-hashed output:

  • src/ — build inputs. A top-level app/assets/src/ is skipped by discovery and never served. It holds source consumed by a build step — a bundler's entry points and the modules they import — not deployable assets.
  • dist/ — build outputs. A top-level app/assets/dist/ holds files a build tool has already content-hashed. Plain serves them immutable, as-is, and does not re-fingerprint them — it trusts the build tool's hash. Gitignore this directory; it's generated.

Everything else in app/assets/ is static passthrough, fingerprinted by Plain as usual. Only the top-level src/ and dist/ are special — a nested foo/src/ is treated normally.

Using AssetView directly

In some situations you may want to use the AssetView at a custom URL, for example to serve a favicon.ico. You can do this quickly by using the AssetView.as_view() class method.

from plain.assets.views import AssetView
from plain.urls import path, Router


class AppRouter(Router):
    namespace = ""
    urls = [
        path("favicon.ico", AssetView.as_view(asset_path="favicon.ico")),
    ]

FAQs

How do you reference assets in Python code?

There is a get_asset_url function that you can use to get the URL of an asset in Python code. This is useful if you need to reference an asset in a non-template context, such as in a redirect or an API response.

from plain.assets.urls import get_asset_url

url = get_asset_url("css/style.css")

What if I need the files in a different location?

The generated/copied files are stored in {repo}/.plain/assets/compiled. If you need them to be somewhere else, try simply moving them after compilation.

plain assets compile
mv .plain/assets/compiled /path/to/your/static

How do I upload the assets to a CDN?

The steps for this will vary, but the general idea is to compile them, and then upload the compiled assets from their compiled location.

# Compile the assets
plain assets compile

# List the newly compiled files
ls .plain/assets/compiled

# Upload the files to your CDN
./example-upload-to-cdn-script

Use the ASSETS_CDN_URL setting to tell the {{ asset() }} template function where to point.

# app/settings.py
ASSETS_CDN_URL = "https://cdn.example.com/"

When ASSETS_CDN_URL is set, the {{ asset() }} function returns full CDN URLs directly (e.g., https://cdn.example.com/css/style.d0db67b.css).

If you also have AssetsRouter included in your URLs, requests to local asset paths will redirect to the CDN:

  • Original paths (e.g., /assets/css/style.css) use a 302 temporary redirect to the fingerprinted CDN URL. The mapping can change on rebuild.
  • Terminal paths (fingerprinted or non-fingerprinted final paths) use a 301 permanent redirect. The path itself is stable.

Only assets in the manifest are redirected. Other assets (like page assets from plain-pages) are served directly by your app.

How do I control asset access logging?

By default, 304 Not Modified responses for assets are excluded from the server access log to reduce noise. Set ASSETS_LOG_304 = True in your settings to include them.

Why aren't the originals copied to the compiled directory?

The default behavior is to fingerprint assets, which is an exact copy of the original file but with a different filename. The originals aren't copied over because you should generally always use this fingerprinted path (that automatically uses longer-lived caching).

If you need the originals for any reason, you can use plain assets compile --keep-original, though this will typically be combined with --no-fingerprint otherwise the fingerprinted files will still get priority in {{ asset() }} template calls.

Note that by default, the ASSETS_REDIRECT_ORIGINAL setting is True, which will redirect requests for the original file to the fingerprinted file.

Installation

Install the plain.assets package from PyPI:

uv add plain.assets

Add to your INSTALLED_PACKAGES:

# app/settings.py
INSTALLED_PACKAGES = [
    ...
    "plain.assets",
]

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

plain_assets-0.4.0.tar.gz (18.6 kB view details)

Uploaded Source

Built Distribution

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

plain_assets-0.4.0-py3-none-any.whl (20.4 kB view details)

Uploaded Python 3

File details

Details for the file plain_assets-0.4.0.tar.gz.

File metadata

  • Download URL: plain_assets-0.4.0.tar.gz
  • Upload date:
  • Size: 18.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for plain_assets-0.4.0.tar.gz
Algorithm Hash digest
SHA256 9aa8c57a1f13329d4c76b227bb8632a6d1c9073c5f5642a29aea605a2e1db534
MD5 db734d6466a5c9f8c2ce80948d0f8afe
BLAKE2b-256 b56132f8625d6ab23e9570321b6cc776d4ac9680eb7cd9b65376e1bfa58c11e8

See more details on using hashes here.

File details

Details for the file plain_assets-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: plain_assets-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 20.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for plain_assets-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8a514170eda9d39ba7ab5e54e610748436f2062f12b5bf828ed0bf4276c91cc2
MD5 48ff542290a08d773fd74babeb0e5075
BLAKE2b-256 6ce62645890a68bb51334991e6fd3b0faf616e5191694a6009a8c1abd382f82c

See more details on using hashes here.

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