Metadata-Version: 2.4
Name: plain.assets
Version: 0.3.0
Summary: Serve static assets (CSS, JS, images, etc.) directly or from a CDN.
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
License-Expression: BSD-3-Clause
License-File: LICENSE
Requires-Python: >=3.13
Requires-Dist: plain<1.0.0
Description-Content-Type: text/markdown

# plain.assets

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

- [Overview](#overview)
- [Local development](#local-development)
- [Production deployment](#production-deployment)
- [Using `AssetView` directly](#using-assetview-directly)
- [FAQs](#faqs)
- [Installation](#installation)

## Overview

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

Then include the [`AssetsRouter`](./urls.py#AssetsRouter) in your own router, typically under the `assets/` path.

```python
# 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.

```html
<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.

```bash
plain assets compile
```

By default, this [generates "fingerprinted" and compressed versions of the assets](./manifest.py#compute_fingerprint), 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/`:

    ```toml
    [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).

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

```python
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`](./urls.py#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.

```python
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.

```bash
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.py#get_compiled_path).

```bash
# 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`](./default_settings.py#ASSETS_CDN_URL) setting to tell the `{{ asset() }}` template function where to point.

```python
# 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`](./default_settings.py#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](https://pypi.org/project/plain.assets/):

```bash
uv add plain.assets
```

Add to your `INSTALLED_PACKAGES`:

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