How do you create responsive images in your Phoenix apps?

How do you create responsive images with phoenix?
I’d love to merge my separate 11ty/netlify landing page into the Phoenix app.

Responsive images are the last big stumbling block for me. Most JAMStack frameworks have good out-of-the-box solutions, e.g. next.js and gatsby.

It would be great to have an easy to use, performant solution for Phoenix.
That doesn’t impact page loading speeds/page rank.

Here’s my best solution so far. Resized using Image at compile time.
Stored in priv/static/resized, served and cached via normal Plug.Static setup.

Would love to get some feedback and to hear about how you guys handle this.

Usage:

use ResponsiveImage ~H(<img src={src("input.jpg", 300)} />) ~H(<img srcset={srcset("input.jpg", [300, 600, 900])} src={...} sizes="50vw" />) 

Module:

defmodule ResponsiveImage do defmacro __using__(_opts) do quote do import unquote(__MODULE__) Module.register_attribute(__MODULE__, :image, accumulate: true) @before_compile unquote(__MODULE__) end end defmacro src(path, width) do Module.put_attribute(__CALLER__.module, :image, {path, width}) quote do unquote(img_src(path, width)) end end defmacro srcset(path, widths) do for width <- widths, do: Module.put_attribute(__CALLER__.module, :image, {path, width}) quote do unquote(widths |> Enum.map(&"#{img_src(path, &1)} #{&1}w") |> Enum.join(", ")) end end defmacro __before_compile__(_env) do {duration, _} = :timer.tc(fn -> ResponsiveImage.resize(Module.get_attribute(__CALLER__.module, :image)) end) IO.puts("Image resize took #{duration / 1_000_000}s") end def resize(attr) when is_list(attr) do for {path, width} <- attr, do: resize(path, width) end def resize(path, width) do out_path = out_path(path, width) if not File.exists?(out_path) do IO.puts("Writing #{out_path}") out_path |> Path.dirname() |> File.mkdir_p!() path |> in_path() |> Image.open!() |> then(&Image.resize!(&1, width / Image.width(&1))) |> Image.write!(out_path) end end defp without_ext(path), do: "#{Path.dirname(path)}/#{Path.basename(path, Path.extname(path))}" defp in_path(path), do: "priv/static/images/#{path}" defp img_src(path, width), do: "/resized/#{without_ext(path)}_#{width}.avif" defp out_path(path, width), do: "priv/static#{img_src(path, width)}" end 
2 Likes

I use a mix compiler to resize images:

I also have played with on demand resizing behind a cdn using image in a plug:

10 Likes

Thanks for sharing!

This looks like a nice & clean solution if you usually need the same standard sizes everywhere. Pro: Resize on dev machine, commit resized images → saves pipeline time.
I have more variance in the required image sizes. Then I think its nicer to collocate the template and which image sizes I require.

Do I understand correctly that you have a minimal Elixir/Image implementation of the Thumbor “path protocol”? And put a CDN in front to handle caching? Also clever, I quite like that idea.

This might be a useful reference

Author avatar looks familiar :slight_smile: