PhoenixImage
On-the-fly image resizing for Phoenix with disk caching and a responsive <.image> component.
Images are resized on first request using libvips (via the image library), cached to disk, and served with immutable cache headers. The <.image> component renders <img> tags with correct width, height, and srcset attributes to prevent layout shift and support retina displays.
Installation
Add phoenix_image to your list of dependencies in mix.exs:
def deps do
[
{:phoenix_image, "~> 0.2.0"}
]
endSetup
1. Configure
# config/config.exs
config :phoenix_image, otp_app: :my_app2. Add to supervision tree
# lib/my_app/application.ex
def start(_type, _args) do
children = [
# ...
PhoenixImage,
MyAppWeb.Endpoint
]
Supervisor.start_link(children, strategy: :one_for_one)
end3. Add route
# lib/my_app_web/router.ex
scope "/" do
forward "/resize", PhoenixImage.Plug
end4. Import the component
# lib/my_app_web.ex
defp html_helpers do
quote do
# ...
import PhoenixImage.Component
end
endUsage
The <.image> component
Place your source images in priv/static/ as usual. Then use the component in your templates:
<%# Crop to exact dimensions (attention-based crop) %>
<.image src="/images/photo.jpg" fill={{400, 300}} alt="A photo" class="rounded" />
<%# Resize by width, height calculated proportionally %>
<.image src="/images/photo.jpg" width={800} alt="A photo" />
<%# Resize by height, width calculated proportionally %>
<.image src="/images/photo.jpg" height={600} alt="A photo" />
<%# Fit within max dimensions, no crop %>
<.image src="/images/photo.jpg" max={{800, 600}} alt="A photo" />
<%# Serve original size (still sets width/height to prevent layout shift) %>
<.image src="/images/photo.jpg" original alt="A photo" />The component automatically:
-
Generates signed URLs like
/resize/fill-400x300/images/photo.jpg?s=abc123... -
Sets
widthandheightattributes on the<img>tag to prevent layout shift -
Generates a
srcsetwith 1x and 2x variants for retina displays (when the source image is large enough) -
Passes through all other HTML attributes (
class,alt,loading, etc.)
Resize operations
| Operation | URL pattern | Description |
|---|---|---|
fill | /resize/fill-400x300/path | Crop to exact dimensions using attention detection |
width | /resize/width-800/path | Resize by width, proportional height |
height | /resize/height-600/path | Resize by height, proportional width |
max | /resize/max-800x600/path | Fit within bounds, no crop |
original | /resize/original/path | Serve original file |
URL signing
All image URLs are automatically signed with an HMAC-SHA256 signature. Only URLs generated by the <.image> component are accepted — manually crafted or tampered URLs return a 403 error. The signature is deterministic, so URLs are stable and fully cacheable by CDNs.
Configuration
All options are set via application config under :phoenix_image:
config :phoenix_image,
# Required: which OTP app's priv/static to read source images from
otp_app: :my_app,
# URL prefix for resize routes (must match your `forward` path)
# Default: "/resize"
prefix: "/resize",
# Signing secret for URL HMAC signatures
# Falls back to the Phoenix endpoint's secret_key_base if not set
# Generate with: mix phx.gen.secret 32
secret: "your-random-secret-here",
# JPEG output quality (1-100)
# Default: 90
quality: 90,
# Directory within priv/ where source images live
# Default: "priv/static"
source_dir: "priv/static",
# Directory within priv/ for cached resized images
# Default: "priv/static/cache/resize"
cache_dir: "priv/static/cache/resize"Cache management
Resized images are cached to disk on first request. Subsequent requests serve the cached file directly with Cache-Control: public, max-age=31536000, immutable.
To clear the cache and force regeneration:
mix phoenix_image.reset_cacheHow it works
-
A request hits
/resize/fill-400x300/images/photo.jpg?s=abc123... PhoenixImage.Plugverifies the URL signature and parses the operationPhoenixImage.Processorchecks for a cached version on disk-
If not cached, it resizes the source image using
Image.thumbnail/3(libvips) - The result is written to the cache directory and served with immutable headers
-
Meanwhile,
PhoenixImage.Componentreads the original image dimensions (cached in ETS viaPhoenixImage.Dimensions) to set correctwidth/heightattributes at render time
Requirements
- Elixir ~> 1.15
-
libvips (system dependency for the
imagelibrary)
On macOS: brew install libvips
On Ubuntu/Debian: apt-get install libvips-dev
License
MIT