How to add layout to 404 page in phoenix?

ErrorView which is used to handle errors doesn’t provide conn or stuff like that. Is there a way to attach layout anyway? Showing 404 error without any layout in current project is no-go.

The only way i see at this moment is to copy all the markup from app.html.eex which is less than ideal solution.

defmodule AppWeb.ErrorView do
  use AppWeb, :view

  def render("404.html", assigns) do
    render("404.html", assigns)
  end
...

P.S. Anything else, like redirect on 404/error is not possible as well

Also, it’s possible to add wildcard route at the end of every relevant section in router.ex, this is half of the solution.

The other half would be catching “not found” exceptions (or something like this). As far as i know FallbackController doesn’t work with exceptions?

I think you’re looking for render_layout/4.

3 Likes

Thank you. That’s what i was looking for. Hard to find :frowning:

For history, solution looks like this:

defmodule AppWeb.ErrorView do
  use AppWeb, :view

  def render("404.html", assigns) do
    Phoenix.View.render_layout AppWeb.LayoutView, "app.html", assigns do
      render("not_found.html", assigns)
    end
  end
...

Some hacking around get_flash is required though

1 Like

Thanks for positing the solution @thousandsofthem - it really helped me in arriving at my own version of solving this problem. This is what I went with:

defmodule AppWeb.ErrorView do
  use AppWeb, :view

  def template_not_found(template, assigns) do
    render(
      "error.html",
      Map.put(assigns, :message, Phoenix.Controller.status_message_from_template(template))
    )
  end
end

And a very stripped down version of my error/error.html.heex looks something like this:

<html>
    <body>
        <h1>ERROR</h1>

        <main>
            <%= @message %>
        </main>
    </body>
</html>

I found this approach helpful because it allows me to create a “generic” error page that:

  • Doesn’t rely on any other layouts (minimize risk of an error during the rendering of an error page).
  • Can still share things like styling between the error pages.
  • Allows for small customizations, like the message, via assigns.

I know it’s a different approach than the one you went for, and I think both approaches have merit. I just wanted to share this approach since it might be helpful for someone :).

2 Likes
config :demo, DemoWeb.Endpoint, 
  render_errors: [
    view: DemoWeb.ErrorView, 
    accepts: ~w(json),
    root_layout: false,
    layout: false
  ]

Phoenix.Endpoint has an option called render_errors, you can set root_layout or layout in config.

2 Likes

I set root_layout and layout to true and here’s what i got:

Server: localhost:4000 (http)
Request: GET /wrong/pathname
** (exit) an exception was raised:
    ** (RuntimeError) cannot use put_layout/2  or put_root_layout/2 with atom/binary when layout is false, use a tuple instead
        (phoenix 1.6.7) lib/phoenix/controller.ex:513: anonymous fn/3 in Phoenix.Controller.do_put_layout/3
        (elixir 1.13.4) lib/map.ex:830: Map.update!/3
        (phoenix 1.6.7) lib/phoenix/endpoint/render_errors.ex:106: Phoenix.Endpoint.RenderErrors.render/6
        (phoenix 1.6.7) lib/phoenix/endpoint/render_errors.ex:78: Phoenix.Endpoint.RenderErrors.instrument_render_and_send/5
        (phoenix 1.6.7) lib/phoenix/endpoint/render_errors.ex:64: Phoenix.Endpoint.RenderErrors.__catch__/5

You can’t set them to true.

Internally, it will call Controller.put_root_layout and Controller.put_layout. Inspect the source code at here: phoenix/render_errors.ex at 67f0083911e55fde971be56ec3d4d5d233377db6 · phoenixframework/phoenix · GitHub

Because of that, you should set something like:

{LayoutView, "error.html"}
1 Like

should i pass this tuple in render_errors from config.exs? where? sorry im lost with this part.

Setting it in config.exs would be fine.

For example:

config :demo, DemoWeb.Endpoint,
  render_errors: [
    accepts: ~w(html json),
    root_layout: {DemoWeb.LayoutView, :error},
    layout: {DemoWeb.LayoutView, :skip},
    view: DemoWeb.ErrorView,
  ],
  # ...

{DemoWeb.LayoutView, :error} corresponds to lib/demo_web/templates/layout/error.html.heex.

{DemoWeb.LayoutView, :skip} corresponds to an almost empty template (lib/demo_web/templates/layout/skip.html.heex):

<%= @inner_content %>

You may find that I’m using {DemoWeb.LayoutView, :error}(this is a customized root layout for error pages) instead of the default root layout {DemoWeb.LayoutView, :root}. The reason is from here:

It is worth noting that we did not render our 404.html.heex template through our application layout, even though we want our error page to have the look and feel of the rest of our site. This is to avoid circular errors. For example, what happens if our application failed due to an error in the layout? Attempting to render the layout again will just trigger another error. So ideally we want to minimize the amount of dependencies and logic in our error templates, sharing only what is necessary.

1 Like

Ah! makes sense now. All that’s left is fixing missing conn keys from my menu bar. Thanks alot for helping out!

1 Like