### Sven Gehring's Blog

I write about software, engineering and stupidly fun side projects.

# Serving static assets on a subpath in Phoenix

If you create a new Phoenix project, without using the --no-html flag, a static plug will be added to your endpoint. Because of this, a lot of people recommend to just edit that, if you want to serve static files from a subdirectory. However, this can get a bit tricky if you have data stored in different directories - or use Phoenix purely as an API.

You don’t have to edit the endpoint, though, you can just use Plug.Static in a (sub)scope in your controller

I remembered I did get this working once but wasn’t quite sure how anymore, so I wanted to quickly test if my answer was correct…. and there was some caveats to it. I haven’t found a comprehensive guide on how to do this, so here’s what I learned.

This article assumes you already have a Phoenix project up and running, however, if you don’t, you can create one with mix phx.new --no-ecto --no-webpack phxstatic - leaving out the database and frontend JS components for the sake of simplicity. For testing, we add a file at priv/test/hello.txt that contains hello world.

For serving static assets, we need to add a pipeline with the Plug.Static plug to our router. We will also use that pipeline in the scope where we want to serve those files, using the pipe_through macro.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  defmodule PhxstaticWeb.Router do use PhxstaticWeb, :router pipeline :static do plug Plug.Static, at: "/static", from: {:phxstatic, "priv/test"} end scope "/", PhxstaticWeb do scope "/static" do pipe_through :static end end end

Caveat 1: Even though the plug is nested in the /static/ scope, the :at option has to be set to the full path!
Caveat 2: It doesn’t do anything yet, now that’s unfortunate.

## Making it work

Caveat 3: Now this is where I got stuck the first time. Everything looks right but why does it just display the phoenix 404 page? The reason for this is, that a pipeline is only invoked, once a route in the scope that uses it matches, as explained by José in this post.

So the solution is reasonably simple, we just add a catchall route to the respective scope. I am not including the source code of ErrorController.notfound here, since you can use any controller/function here for rendering a 404.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  defmodule PhxstaticWeb.Router do use PhxstaticWeb, :router pipeline :static do plug Plug.Static, at: "/static", from: {:phxstatic, "priv/test"} end scope "/", PhxstaticWeb do scope "/static" do pipe_through :static get "/*path", ErrorController, :notfound end end end

If we now open localhost:4000/static/hello.txt, we get hello world - yay! And if we try some other path in that scope, we will just get the response rendered by ErrorController.notfound.