Skip to content

Commit

Permalink
render error page with root layout
Browse files Browse the repository at this point in the history
compile a function for each part of the layout tree: root, layout, and page
where root takes the content of page+layout
  • Loading branch information
leandrocp committed Aug 31, 2023
1 parent 4aa9fd4 commit e6928c1
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 63 deletions.
5 changes: 4 additions & 1 deletion dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ Logger.configure(level: :debug)

Application.put_env(:phoenix, :json_library, Jason)

display_error_pages? = false

Application.put_env(:beacon, SamplePhoenix.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64),
debug_errors: true,
debug_errors: !display_error_pages?,
render_errors: [formats: [html: BeaconWeb.ErrorHTML]],
check_origin: false,
pubsub_server: SamplePhoenix.PubSub,
live_reload: [
Expand Down
3 changes: 2 additions & 1 deletion lib/beacon/loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,8 @@ defmodule Beacon.Loader do

@doc false
def handle_info(:error_pages_updated, config) do
:ok = load_error_pages(config.site)
# FIXME: race condition on tests
# :ok = load_error_pages(config.site)
{:noreply, config}
end

Expand Down
74 changes: 23 additions & 51 deletions lib/beacon/loader/error_module_loader.ex
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
defmodule Beacon.Loader.ErrorModuleLoader do
@moduledoc false
alias Beacon.Content
alias Beacon.Content.ErrorPage
alias Beacon.Loader

@root_layout """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title>
Error
</.live_title>
<link id="beacon-runtime-stylesheet" rel="stylesheet" href={asset_path(@conn, :css)} />
<script defer src={asset_path(@conn, :js)}>
</script>
</head>
<body>
<%= @inner_content %>
</body>
</html>
"""

def load_error_pages!(error_pages, site) do
error_module = Loader.error_module_for_site(site)
component_module = Loader.component_module_for_site(site)

render_functions = Enum.map(error_pages, &build_render_fn/1)
layout_functions = Enum.map(error_pages, &build_layout_fn/1)
render_functions = Enum.map(error_pages, &build_render_fn(&1, error_module))

ast =
quote do
defmodule unquote(error_module) do
use Phoenix.HTML
require EEx
import Phoenix.Component
unquote(Loader.maybe_import_my_component(component_module, render_functions))
require Logger

# One function per error page
unquote_splicing(layout_functions)

# One function per error page
unquote_splicing(render_functions)

Expand All @@ -45,8 +31,12 @@ defmodule Beacon.Loader.ErrorModuleLoader do
Plug.Conn.Status.reason_phrase(var!(status))
end

# One function per error page
unquote_splicing(layout_functions)
EEx.function_from_file(
:def,
:root_layout,
Path.join([__DIR__, "lib", "beacon_web", "components", "layouts", "runtime_error.html.heex"]),
[:assigns]
)
end
end

Expand All @@ -55,44 +45,26 @@ defmodule Beacon.Loader.ErrorModuleLoader do
{:ok, error_module, ast}
end

defp build_layout_fn(error_page) do
%{site: site, template: page_template, layout: %{id: layout_id}, status: status} = error_page
layout = Content.get_published_layout(site, layout_id)

file = "site-#{site}-error-page-#{status}"
ast = Beacon.Template.HEEx.compile_heex_template!(file, page_template)

layout_module = Loader.layout_module_for_site(layout.id)
defp build_layout_fn(%ErrorPage{} = error_page) do
%{site: site, layout: %{id: layout_id}, status: status} = error_page
%{template: template} = Content.get_published_layout(site, layout_id)

rendered_layout =
layout_module.render(%{
inner_content: ast,
beacon_live_data: %{},
title: layout.title,
meta_tags: layout.meta_tags,
resource_links: layout.resource_links
})

ast = EEx.compile_string(@root_layout, inner_content: rendered_layout)
compiled = EEx.compile_string(template)

quote do
def layout(unquote(status), var!(assigns)) do
unquote(ast)
def layout(unquote(status), var!(assigns)) when is_map(var!(assigns)) do
unquote(compiled)
end
end
end

defp build_render_fn(error_page) do
%{site: site, template: page_template, status: status} = error_page

error_module = Loader.error_module_for_site(site)
file = "site-#{site}-error-page-#{status}"
ast = Beacon.Template.HEEx.compile_heex_template!(file, page_template)
defp build_render_fn(%ErrorPage{} = error_page, error_module) do
%{template: template, status: status} = error_page

quote do
def render(unquote(error_page.status)) do
var!(assigns) = %{}
apply(unquote(error_module), :layout, [unquote(status), %{inner_content: unquote(ast)}])
def render(unquote(status)) do
var!(assigns) = %{inner_content: unquote(error_module).layout(unquote(status), %{inner_content: unquote(template)})}
unquote(error_module).root_layout(var!(assigns))
end
end
end
Expand Down
13 changes: 13 additions & 0 deletions lib/beacon_web/components/layouts/runtime_error.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="csrf-token" content={get_csrf_token()} />
<title>Error</title>
<link id="beacon-runtime-stylesheet" rel="stylesheet" href={asset_path(@conn, :css)} />
<script defer src={asset_path(@conn, :js)}>
</script>
</head>
<body>
<%= @inner_content %>
</body>
</html>
2 changes: 1 addition & 1 deletion lib/beacon_web/controllers/error_html.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ defmodule BeaconWeb.ErrorHTML do
|> hd()
|> String.to_integer()

error_module.render(status)
{:safe, error_module.render(status)}
end
end
94 changes: 85 additions & 9 deletions test/beacon/loader/error_module_loader_test.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
defmodule Beacon.Loader.ErrorModuleLoaderTest do
use Beacon.DataCase, async: false

import Beacon.Fixtures

alias Beacon.Loader.ErrorModuleLoader

@site :my_site
Expand All @@ -12,21 +10,99 @@ defmodule Beacon.Loader.ErrorModuleLoaderTest do
:ok
end

test "render default error pages" do
setup do
:ok = Beacon.Loader.populate_layouts(@site)
:ok = Beacon.Loader.populate_error_pages(@site)
error_module = load_error_pages_module(@site)
[error_module: error_module]
end

test "root layout", %{error_module: error_module} do
layout_template = "#inner_content#"

assert error_module.root_layout(%{inner_content: layout_template}) == """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="csrf-token" content={get_csrf_token()} />
<title>Error</title>
<link id="beacon-runtime-stylesheet" rel="stylesheet" href={asset_path(@conn, :css)} />
<script defer src={asset_path(@conn, :js)}>
</script>
</head>
<body>
#{layout_template}
</body>
</html>
"""
end

test "default layouts", %{error_module: error_module} do
assert error_module.layout(404, %{inner_content: "Not Found"}) == "Not Found"
assert error_module.layout(500, %{inner_content: "Internal Server Error"}) == "Internal Server Error"
end

test "custom layout" do
layout = published_layout_fixture(template: "#custom_layout#<%= @inner_content %>", site: @site)
error_page = error_page_fixture(layout: layout, template: "error_501", status: 501, site: @site)
error_module = load_error_pages_module(@site)
assert error_module.layout(501, %{inner_content: error_page.template}) == "#custom_layout#error_501"
end

test "default error pages", %{error_module: error_module} do
assert error_module.render(404) == """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="csrf-token" content={get_csrf_token()} />
<title>Error</title>
<link id="beacon-runtime-stylesheet" rel="stylesheet" href={asset_path(@conn, :css)} />
<script defer src={asset_path(@conn, :js)}>
</script>
</head>
<body>
Not Found
</body>
</html>
"""

assert error_module.render(404) == "Not Found"
assert error_module.render(500) == "Internal Server Error"
assert error_module.render(500) == """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="csrf-token" content={get_csrf_token()} />
<title>Error</title>
<link id="beacon-runtime-stylesheet" rel="stylesheet" href={asset_path(@conn, :css)} />
<script defer src={asset_path(@conn, :js)}>
</script>
</head>
<body>
Internal Server Error
</body>
</html>
"""
end

test "render custom error page with layout" do
layout = published_layout_fixture(template: "Wow\n<%= @inner_content %>\nLayout", site: @site)
error_page = error_page_fixture(layout: layout, template: "Error", site: @site)
test "custom error page" do
layout = published_layout_fixture(template: "#custom_layout#<%= @inner_content %>", site: @site)
_error_page = error_page_fixture(layout: layout, template: "error_501", status: 501, site: @site)
error_module = load_error_pages_module(@site)

assert error_module.render(error_page.status) == "Wow\nError\nLayout"
assert error_module.render(501) == """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="csrf-token" content={get_csrf_token()} />
<title>Error</title>
<link id="beacon-runtime-stylesheet" rel="stylesheet" href={asset_path(@conn, :css)} />
<script defer src={asset_path(@conn, :js)}>
</script>
</head>
<body>
#custom_layout#error_501
</body>
</html>
"""
end

defp load_error_pages_module(site) do
Expand Down

0 comments on commit e6928c1

Please sign in to comment.