Getting started with Phoenix

Copy Markdown View Source

In this guide we'll add ImagePipe to a new Phoenix app, serve resized copies of an image from it, and show one on a page, with its URL built in the template.

You need Elixir 1.18 or newer, the Phoenix installer (mix archive.install hex phx_new), and a JPEG image.

Creating the app

mix phx.new gallery --no-ecto --install
cd gallery

Add ImagePipe to the dependencies in mix.exs:

# mix.exs
defp deps do
  [
    {:image_pipe, "~> 0.1.0"},
    # ...the generated dependencies
  ]
end

ImagePipe needs req 0.8.0-rc.0

ImagePipe depends on req ~> 0.8.0-rc.0, and a new Phoenix app locks an older version. Unlock it before fetching, or mix deps.get stops with a version conflict over req:

mix deps.unlock req
mix deps.get

Adding a folder of images

ImagePipe reads originals from a folder and makes processed copies of them on request. Create priv/originals and copy your image into it as cat.jpg:

mkdir -p priv/originals
cp /path/to/your/image.jpg priv/originals/cat.jpg

The examples below use a 4000×2667 image. With your image, the widths match and the heights follow its proportions.

Starting ImagePipe

Next, we start ImagePipe in the app's supervision tree and tell it where the originals are. Add it to the children in lib/gallery/application.ex, before the endpoint:

# lib/gallery/application.ex
children = [
  # ...
  {ImagePipe,
   name: Gallery.Images,
   sources: [
     originals: [
       adapter: ImagePipe.Source.File,
       match: :path,
       options: [root: Application.app_dir(:gallery, "priv/originals"), root_id: "originals"]
     ]
   ]},
  # Start to serve requests, typically the last entry
  GalleryWeb.Endpoint
]

This starts an ImagePipe instance named Gallery.Images with one source named originals. A source is a place ImagePipe reads originals from:

  • adapter: ImagePipe.Source.File reads files from a directory.
  • match: :path sends every image path in a URL to this source.
  • root is that directory, priv/originals in our app.
  • root_id is a stable name for this directory.

Mounting the Plug

ImagePipe.Plug answers image requests for the instance. Add a forward to lib/gallery_web/router.ex, outside the :browser scope:

# lib/gallery_web/router.ex
forward "/media", ImagePipe.Plug, instance: Gallery.Images

scope "/", GalleryWeb do
  pipe_through :browser

  get "/", PageController, :home
end

Image requests don't need the session, CSRF protection, or HTML layout of the :browser pipeline. We use /media rather than /images because a new Phoenix app already serves its static files under /images.

Resizing an image

Start the app:

mix phx.server
[info] Running GalleryWeb.Endpoint with Bandit 1.x.x at 127.0.0.1:4000 (http)

An image URL lists processing options, then src/, then the image's path in the source. Let's ask for cat.jpg at 400 pixels wide. Open this URL:

http://localhost:4000/media/w=400/src/cat.jpg

GET /media/w=400/src/cat.jpg (in Chrome)
200 OK, content-type: image/avif, 400×267

Notice that:

  • /media is where we mounted the Plug, and src/cat.jpg is priv/originals/cat.jpg.
  • The original is a JPEG, but Chrome got AVIF. Without a format option, ImagePipe picks AVIF or WebP when the browser accepts it.

If you mistype the file name, ImagePipe answers 404 with the body source not found. If you leave out src/, as in /media/w=400/cat.jpg, it answers 400, and the body points at the problem:

invalid transformation options

/w=400/cat.jpg
              ^
              |
              missing src/, src64/, or enc/ before the image path

Showing the image on a page

Image URLs are ordinary paths in the app, so templates use them with ~p. Replace the contents of lib/gallery_web/controllers/page_html/home.html.heex with:

<Layouts.app flash={@flash}>
  <img src={~p"/media/w=400/h=300/fit=cover/src/cat.jpg"} alt="A cat" />
</Layouts.app>

Open http://localhost:4000. The page shows a 400×300 copy of the image, cropped to fill the box by fit=cover.

Building URLs in templates

ImagePipe can build URLs from the instance's settings. First, set base_url to the path of the forward:

# lib/gallery/application.ex
{ImagePipe,
 name: Gallery.Images,
 base_url: "/media",
 sources: [
   # ...
 ]},

Then make ImagePipe's image_url helper available in every template. In lib/gallery_web.ex, add a line to html_helpers:

# lib/gallery_web.ex
defp html_helpers do
  quote do
    # ... the generated imports and aliases
    use ImagePipe.URL.Helpers, instance: Gallery.Images
  end
end

Stop the server (Ctrl+C twice) and start it again with mix phx.server, so the instance picks up base_url. Then replace the hand-written path in the template:

<Layouts.app flash={@flash}>
  <img
    src={image_url("cat.jpg", group: [resize: [width: 400, height: 300, fit: :cover]])}
    alt="A cat"
  />
</Layouts.app>

Reload http://localhost:4000. The page looks the same, and the image's address is /media/w=400/h=300/fit=cover/src/cat.jpg, the path we wrote by hand before. Each group: holds the options of one processing step, and resize: writes w=, h=, and fit=.

Now change fit: :cover to fit: :bogus and save. When Phoenix recompiles the template, the terminal shows a warning that points at its line:

warning: image_url: invalid_value at {:group, 0, :fit}: invalid value for :fit option: expected one of [:contain, :cover, :stretch, :auto], got: :bogus
└─ lib/gallery_web/controllers/page_html/home.html.heex:3: GalleryWeb.PageHTML.home/1

Notice that the mistake shows up when the template compiles. The page still renders: the server rejects the image's URL with 400, so the browser shows a broken image in place of the cat, and each render logs:

[warning] image_url (lib/gallery_web/controllers/page_html/home.html.heex:3): invalid_value at {:group, 0, :fit}

Change it back to fit: :cover.

Next steps

We started an ImagePipe instance, mounted it in the router, and served resized copies of an image to a page, with URLs written by hand and built in the template. From here:

Before deploying, set up URL signing so only your app can create image URLs, and caching so each copy is processed once.