PhoenixAssetPipeline

Build, optimize, cache, and serve Phoenix assets from one manifest.

Hex.pm Documentation

Requirements

Elixir 1.18+, Erlang/OTP 28+, Rust 1.98+, and Phoenix LiveView. The pipeline installs and manages its pinned Bun version.

Installation

Add the dependency and compilers to mix.exs:

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

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

Configure the application:

config :phoenix, template_engines: [heex: PhoenixAssetPipeline.HTML.Engine]

config :phoenix_asset_pipeline,
  endpoint: MyAppWeb.Endpoint,
  otp_app: :my_app

config :my_app, MyAppWeb.Endpoint,
  adapter: PhoenixAssetPipeline.EndpointAdapter,
  server_adapter: Bandit.PhoenixAdapter

Enable a precompiled manifest in config/prod.exs:

config :phoenix_asset_pipeline, precompiled_manifest: true

The default is a cached manifest. Use your application's name for otp_app. The adapter starts the pipeline after endpoint configuration is ready and before the HTTP server. When server: false, start the pipeline after the endpoint in your application's supervision tree:

children = [MyAppWeb.Endpoint]

children =
  if Phoenix.Endpoint.server?(:my_app, MyAppWeb.Endpoint),
    do: children,
    else: children ++ [PhoenixAssetPipeline]

Set server_adapter to your web server's Phoenix adapter. Bandit remains an application dependency.

HTML

Add these imports to the application's HTML helpers:

use PhoenixAssetPipeline.HTML.Macros

import PhoenixAssetPipeline.Components
import PhoenixAssetPipeline.Helpers

Render assets:

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

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

Classes are minified consistently in CSS, HEEx, and Elixir. Use class/1 in Elixir expressions:

@container {:div, class: class("h-full")}

class(["button", {"enabled", enabled}])

Literal class and *_class component attributes are handled automatically.

Include the library's component utilities in assets/css/app.css:

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

The prepare compiler resolves module attributes and component defaults before Elixir compilation. Function and HEEx expressions use the current manifest at runtime.

Formatting

Enable canonical Tailwind classes in .formatter.exs:

[
  plugins: [Phoenix.LiveView.HTMLFormatter, PhoenixAssetPipeline.HTML.Formatter],
  tailwind: "assets/css/app.css"
]

The formatter uses the project's installed @tailwindcss/node (4.3 or later), which is also included by @tailwindcss/cli. It processes literal class and *_class attributes, including conditional classes, in HEEx files and ~H sigils. mix format --check-formatted reports required changes without writing files. The same plugin works when formatting through an editor.

Classes keep the formatter's existing sorting and grouping rules. Unknown classes and conflicting utilities are not removed. Conditions and helper arguments are left unchanged. Omit :tailwind to only sort and group classes.

Assets

Default inputs:

Declare Bun packages in assets/package.json. Dependencies are installed when the package or lockfile changes; production requires assets/bun.lock and uses --frozen-lockfile.

Images are auto-oriented and converted to AVIF, WebP, and PNG with libvips. The source is the highest density: with image_densities: [1, 2], a 40×20 source produces 20×10 and 40×20 variants. <.picture> renders responsive sources and a PNG fallback; width and height reserve space during loading.

<.picture src="hero" alt="Welcome" width="640" height="480" />

Images default to densities [1, 2] and a 40,000,000-pixel input limit. Override image_densities or image_max_pixels when needed.

Brotli, gzip, deflate, and Zstandard variants are kept only when smaller than the original. Already compressed files use Cache-Control: no-transform.

Hidden static files are excluded except for files under the root .well-known directory. Add .well-known to the static plug's :only list when serving it.

Generated static files

For files such as robots.txt and sitemap.xml that need the endpoint's runtime URL, register generators in the pipeline configuration:

config :phoenix_asset_pipeline,
  generated: %{
    "robots.txt" => {MyAppWeb.Crawlers, :robots, []},
    "sitemap.xml" => {MyAppWeb.Crawlers, :sitemap, []}
  }

Include both filenames in static_paths/0. Each generator returns iodata and runs during pipeline startup, before the endpoint accepts requests. Generated files are added to the manifest's :static_files section with the same contents, compression, ETags, and HTTP handling as files from disk. Lookups only read ready assets; the production manifest is stored in persistent_term.

With the startup order above, generators may read runtime application configuration and call MyAppWeb.Endpoint.url/0. Generated content must be shared by all requests and use the configured endpoint URL.

Development rebuilds generated files when the manifest changes, including when a generator module is recompiled. Runtime configuration changes require restarting the application. Generated paths take precedence over files with the same path. A generator failure prevents startup or leaves the previous development manifest intact during a rebuild.

SVG sprites

Select external SVGs by basename with svg_sprites; paths are relative to the project root:

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 to prevent collisions. Use namespace_ids: false only for sources without internal IDs. Optional metadata_file text is escaped and inserted as root metadata; changes invalidate the cache.

Browser headers

Use secure_browser_headers/1 to generate cached headers. To allow DOMPurify, replace the Trusted Types policy list at compile time:

config :phoenix_asset_pipeline, trusted_types: ~w(decodeHTMLEntitiesPolicy default dompurify)

Build

Rebuild the manifest before rendering reloaded code in config/dev.exs:

config :my_app, MyAppWeb.Endpoint,
  reloadable_compilers: [
    :phoenix_live_view,
    :elixir,
    :app,
    :phoenix_asset_pipeline
  ]
# Watch and rebuild in development
mix phx.server

# Build the production manifest
MIX_ENV=prod mix release

# Rebuild the manifest manually
mix phoenix_asset_pipeline.manifest

Production uses PhoenixAssetPipeline.Manifest.Precompiled; no separate asset build or deploy task is needed.

License

MIT.