Importing an html file to render

I’m currently trying to build a catch all for the backend.

CONTEXT:
Why? I’m building an SPA application which has run into a router issue. I tried fixing this through webpack, but won’t be a solution for production which lead to modifying the backend.

How? (Implementation) When I first created the project. I opted to not include any html files, controllers etc since I was expecting to use Absinthe/GraphQL to handle all my queries. Currently I have two folds, BACKEND and FRONTEND. The frontend is where my HTML file lives and where we should be finding/hitting for the catch all route.

What? What I’ve done so far for this catch all is as follows:

Implemented a new router to handle a catch all

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", HuntersWeb do
    pipe_through(:browser)
    get("/*path", PageController, :index)
  end

Next create a controller (which never gets hit since my IO.inspect never runs)

defmodule HuntersWeb.PageController do
  use HuntersWeb, :controller

  def index(conn, _params) do
    render(conn, "index.html") <- Problem since I don't know how to get the right html file
  end
end
defmodule HuntersWeb.PageView do
  use HuntersWeb, :view
end

All advice is appreciated.

My two main questions are:
1 - How can I use the right html file to render or to point to should a route (refresh) were to be hit.
2 - Did I do something wrong, that is causing the controller not to be hit? (it feels like it isn;t)

Also seeing Cannot GET /store/2

Thanks again for all the help and advice for this one.

  1. You do not need controller at all. You can use simple plug.
  2. If you generate your file somewhere in the /priv directory then the best way to send it will be:
    conn
    |> put_resp_content_type("text/html")
    |> send_file(200, Path.join(:code.priv_dir(:app_name), "path/to/index.html")
    

This will send file stored in priv/path/to/index.html as a a UTF-8 encoded HTML file.

1 Like
  1. You should get it from priv directory, my webpack build production assets in priv/static
  2. If You build with --no-html flag, then there is no view or template support, but You can render like this…
  def index(conn, _params) do
    priv = :code.priv_dir(:your_app_name)
    file = Path.join(priv, "static/index.html")
    {:ok, binary} = File.read(file)
    html(conn, binary)
  end

Or You can use plug, as mentionned in the previous post :slight_smile:

Just to mention, it is much better to use send_file/3 than load whole file to memory and send it later, as the first approach can use sendfile function, which in turn use splice which allows to copy file to socket solely in kernel space without swaps to the user space. This will reduce amount of used space in case if file is large, and additionally reduce amount of time spent on swapping kernel/user boundaries (especially since Spectre).

3 Likes

Thank you @hauleth and @kokolegorille for providing those two answers. The --no-html part is what had me worried. I do have a few questions about the approaches mentioned,

Do I need a different path or same graphql path in order to get this resolved? The reason for asking is I’m noticing that the backend never gets hit (from what I can tell) but I’m getting a 404 Cannot GET /store/2

My route currently looks like this

    plug(:accepts, ["json"])
    plug(HuntersWeb.Plugs.SetCurrentUser)
  end

  scope "/api/v1" do
    pipe_through(:api)

    forward("/api", Absinthe.Plug, schema: HuntersWeb.Schema.Schema)

    forward("/graphiql", Absinthe.Plug.GraphiQL,
      schema: HuntersWeb.Schema.Schema,
      socket: HuntersWeb.UserSocket
    )
  end

  pipeline :browser do
    plug(:accepts, ["html"])
    plug(:fetch_session)
    plug(:fetch_flash)
    plug(:protect_from_forgery)
    plug(:put_secure_browser_headers)
    plug(HuntersWeb.Plugs.CatchAll)
  end

  scope "/", HuntersWeb do
    IO.inspect("hit")
    pipe_through(:browser)
  end

Next question is the approach mentioned. I noticed both are approaches if the priv folder exist within the Phoenix folder. Can this still be done if it’s outside?

The structure is
Backend (folder) -> Phoenix
Frontend (folder) -> dist -> index.html

My route is like this… as I build with --no-html

  scope "/api", ThreeApiWeb do
    pipe_through :api
  end

  scope "/", ThreeApiWeb do
    get "/*path", PageController, :index
  end

priv as a significant meaning for release, but You could link it to anywhere in your disk. I find it easier to specify the path in webpack.

BTW I do not use browser plugs, as it makes no real sense for SPA…

1 Like

Thank you for that info and sorry for my late reply.

I removed the browser plugin and additional routes since it wasn’t necessary. I’ve also been thinking to not make things very complicating (separating Frontend and Backend to two different folders and potentially move the frontend back into the backend directory)

Quick question. As I was looking around, I noticed the assets directory and the priv directory, specifically static look very similar. Where should I be putting my Frontend? Also what is the difference between the two?

Thank you again for the help and providing knowledge from your own experience.

When building an API, I usually do with --no-html and --no-webpack. The later does not add assets directory.

The assets directory is normally there when You use webpack pipeline. It is the place where You put frontend code, that is ultimately transformed and outputed into priv/static.

This is configured in the standard webpack.config.js. This one is mine, but You get the idea… entry point is assets/js/app.js, output dir is priv/static.

  entry: {
    app: './js/app.js',
    vendor: VENDOR_LIBS.concat(glob.sync('./vendor/**/*.js'))
  },
  output: {
    filename: 'js/[name].js',
    chunkFilename: 'js/[name].js',
    path: path.resolve(__dirname, '../priv/static')
  }

So, to resume…

If You want to clearly separate, use --no-html, --no-webpack.
If You still want to use webpack pipeline, use --no-html, put the code in assets and compile to priv/static

I have been doing both… and both have their advantages.

In the first case, I use nginx to serve frontend, and reverse proxy to serve backend. Complete independance between frontend and backend.
In the second, the backend hold the frontend inside priv/static, and is served by the controller I show You previously. It has the advantage to hold both in the same project.