PhoenixAssetPipeline

Asset pipeline for Phoenix and Phoenix LiveView. It builds and caches application assets, minifies CSS/HTML classes, generates image and SVG variants, compresses static files, and serves everything from one manifest.

Hex.pmDocumentation

Requirements

Installation

def deps do
[{:phoenix_asset_pipeline, "~> 3.0"}]
end

Prepare module-scope classes before Elixir and build the manifest after the application compiler:

def project do
[
compilers:
[:phoenix_live_view, :phoenix_asset_pipeline_prepare] ++
Mix.compilers() ++
[:phoenix_asset_pipeline]
]
end

Configure the endpoint and HEEx engine:

manifest_mode =
case config_env() do
:dev -> :cached
:test -> :cached
:prod -> :precompiled
end
config :phoenix, template_engines: [heex: PhoenixAssetPipeline.HTML.Engine]
config :phoenix_asset_pipeline,
bun_version: "1.4.0",
endpoint: MyAppWeb.Endpoint,
manifest_mode: manifest_mode,
otp_app: :my_app

Start the pipeline before the endpoint:

children = [
PhoenixAssetPipeline,
MyAppWeb.Endpoint
]

HTML

Use the macros in the application's HTML surface:

def html do
quote do
use PhoenixAssetPipeline.HTML.Macros
import PhoenixAssetPipeline.Components
import PhoenixAssetPipeline.Helpers
end
end

Render manifest-backed assets:

<html data-d={asset_digest()}>
<head>
{script("app", async: true, crossorigin: true)}
{style("app")}
</head>
<body>{@inner_content}</body>
</html>

Include the packaged component utilities in the Tailwind source set from assets/css/app.css:

@source "../../deps/phoenix_asset_pipeline/lib/phoenix_asset_pipeline/components.ex";

Serve static files before the router:

plug PhoenixAssetPipeline.Plug, :put_private_phoenix_assigns
plug PhoenixAssetPipeline.Plug.Static, only: MyAppWeb.static_paths()
plug MyAppWeb.Router

Classes

Calls from functions and HEEx templates resolve through the current manifest at runtime. Module attributes and component defaults embed stable minified literals prepared before Elixir compilation; production builds allocate them deterministically.

@container {:div, class: class("h-full")}
def button(assigns) do
~H"""
<button class={class(["button", {"enabled", @enabled}])}>...</button>
"""
end

Component attributes ending in _class are extracted, obfuscated, and formatted like class, so literal values do not need an explicit class(...) call:

attr :img_class, :any, default: nil
attr :src, :string, required: true
def avatar(assigns) do
~H"""<img alt="" class={@img_class} src={@src} />"""
end
<.avatar img_class="h-36 object-contain w-auto" src="/avatar.png" />

The prepare and final compilers share the same mapping, so module values, runtime values, manifest entries, and CSS selectors remain consistent without a second Elixir compilation.

Assets

Default inputs:

Bun installs application-side dependencies when the package or lockfile changes. Production builds require assets/bun.lock and install with --frozen-lockfile. Image masters are auto-oriented and converted into AVIF, WebP, and PNG density variants with vix/libvips. Bun supplies the low-resolution geometry for each light-gray placeholder. Brotli, gzip, deflate, and Zstandard representations are stored only when they are smaller than the original.

The source image is the master for the highest configured density. With the default image_densities: [1, 2], a 40×20 source produces a 20×10 base image and a 40×20 -2x image in every output format. The picture component layers a content-addressed placeholder PNG beneath the responsive image, so pages request only the placeholders they render. Transparent masters receive a conservative inset mask that preserves internal transparency and stays inside their outer transparent edges.

Common options:

config :phoenix_asset_pipeline,
already_compressed_extensions: ~w(.avif .png .webp),
assets_dir: "assets",
image_densities: [1, 2],
image_max_pixels: 40_000_000,
static_dir: "priv/static"

already_compressed_extensions, assets_dir, bun_version, image_densities, image_max_pixels, manifest_mode, otp_app, and static_dir are compile-time settings. bun_version must be an exact semantic version and otp_app must match the application name from mix.exs. manifest_mode defaults to :cached; production builds must set it to :precompiled.

Hidden files and directories under static_dir are excluded, except for non-hidden files under the root .well-known directory. This directory is included automatically for standard files such as Digital Asset Links and Apple App Site Association. Add .well-known to :only when filtering requests in PhoenixAssetPipeline.Plug.Static.

Files matching already_compressed_extensions are served with Cache-Control: no-transform so the HTTP server does not compress them again dynamically.

SVG sprites

Use svg_sprites to select SVG files outside assets/svg/sprites. Paths are relative to the project root, and names selects files by basename without requiring literal references in application code:

config :phoenix_asset_pipeline,
svg_sprites: [
%{
file: "flags.svg",
src: "deps/flag_icons/flags/4x3",
names: ~w(ca jp us),
metadata_file: "deps/flag_icons/LICENSE"
}
]

Internal SVG IDs are namespaced by default so references from different source files cannot collide. Set namespace_ids: false only when every selected SVG is known to contain no internal IDs. When metadata_file is set, its XML-escaped text is inserted as one root <metadata> element after optimization. Changes to the metadata file invalidate the SVG cache and trigger development rebuilds.

Build

# Development
mix phx.server
# Production
MIX_ENV=prod mix release
# Manual manifest rebuild
mix phoenix_asset_pipeline.manifest

The development watcher rebuilds changed assets and broadcasts LiveReload events. Production compilation generates PhoenixAssetPipeline.Manifest.Precompiled; separate asset build/deploy tasks are not required.

License

PhoenixAssetPipeline is released under the MIT License. See LICENSE.