Packages

History API fallback plug for web applications with client side routing.

Current section

Files

Jump to
phoenix_histo lib phoenix_histo.ex
Raw

lib/phoenix_histo.ex

defmodule PhoenixHisto do
@moduledoc """
A plug for handling client side routing in Phoenix, a.k.a. History Api fallback.
It rewrites requests for client side routes to `index.html` file.
## Requests exempt from rewrite
The fallback algorithm is limited only to certain requests. In particular
it will not rewrite if:
* request method is not `GET` or `HEAD`.
* it is a file request, e.g. `"/path/file.ext"`; note that if `PhoenixHisto`
is plugged after `Plug.Static` and we get a file request than most likely
the file does not exist and `404` response will be returned
* the client does not accept `text/html` response MIME type
* the requested path starts with one of the paths in `:blacklist`
## Options
* `:index` - path to index.html fallback relative to the dir passed in
`:static_opts` (defaults to `"index.html"`).
* `:blacklist` - list of path prefixes exempt from fallback algorithm
(defaults to `[]`).
* `:static_opts` - options to forward to `Plug.Static` (required), refer to
`Plug.Static` docs. Note that `:only` and `:only_matching`
are not respected.
## Examples
The best place to mount this plug is in `Phoenix.Endpoint`, just after all
`Plug.Static` entries:
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
@static_opts [at: "/", from: :my_app, gzip: false]
plug Plug.Static,
(@static_opts ++
[only: ~w(css fonts img js favicon.ico robots.txt manifest.json)])
plug PhoenixHisto,
static_opts: @static_opts,
blacklist: ["/api"]
...
end
"""
import Phoenix.Controller
@behaviour Plug
@allowed_methods ~w(GET HEAD)
def init(opts) do
index = Keyword.get(opts, :index, "index.html")
static_opts =
opts
|> Keyword.fetch!(:static_opts)
|> Keyword.put(:only, [index])
|> Keyword.delete(:only_matching)
|> Plug.Static.init()
%{
static_opts: static_opts,
blacklist: Keyword.get(opts, :blacklist, []),
index_path: index |> String.split("/")
}
end
defp html_accepted?(conn) do
try do
_ = accepts(conn, ["html"])
true
rescue
Phoenix.NotAcceptableError ->
false
end
end
defp file_request?(conn) do
case conn.path_info |> Enum.at(-1) do
nil -> false
last -> String.contains?(last, ".")
end
end
defp blacklisted?(conn, blacklist) do
blacklist
|> Enum.any?(&(conn.request_path |> String.starts_with?(&1)))
end
def call(conn, %{static_opts: static_opts, blacklist: blacklist, index_path: index_path}) do
if conn.method in @allowed_methods and not file_request?(conn) and html_accepted?(conn) and
not blacklisted?(conn, blacklist) do
%Plug.Conn{conn | path_info: index_path}
|> Plug.Static.call(static_opts)
else
conn
end
end
end