Sven Gehring's Blog

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

Serving static assets on a subpath in Phoenix

2019-04-26 3 min read programming Sven Gehring

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.

When someone asked about this on the elixir-lang Slack, my first response was:

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.

Adjusting our router

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.

comments powered by Disqus