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
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
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.
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