Repository: phoenixframework/phoenix
Pull request #4801 (2022-04-20 00:08:57)

Use `Application.compile_env` for compile-time config values on the endpoint
This is an attempt at #4451 

Any feedback is welcome!

escobera: Unfortunately I don't think this is the right solution, maybe I haven't grokked how `compile_env` works on this scenario. 

I had some expectations that were proven false:

1 - Changing one of the values at `runtime.exs` would trigger an error. Not the case.
2 - Calling `Application.put_env/2` inside the generated `DevAppWeb.Endpoint ` would prevent the application from booting up.  Didn't happen either.

I'm a little lost right now, do you have any hints how to go about doing this @josevalim ? 
josevalim: 1. This should happen but you need to call compile_env inside the generated code (not inside Phoenix).
2. This is not verified because put_env is a regular runtime call we can't check
escobera: Got it! Thanks for the clarification. I'll move the compile-time checks to the generated `Endpoint` and submit another version of this.
escobera: Ok, now it seem to be working. But I think I hit some kind of race condition on the app initialization process.

Some times it just errors out without showing the complete error message like so:
```
$ _build/dev/rel/dev_app/bin/dev_app start
{"init terminating in do_boot",{<<"aborting boot">>,[{'Elixir.Config.Provider',boot,2,[]}]}}
init terminating in do_boot ({,[{Elixir.Config.Provider,boot,2,[]}]})

Crash dump is being written to: erl_crash.dump...done
```

Most of the times it works as it should:
```
$ _build/dev/rel/dev_app/bin/dev_app start
ERROR! the application :dev_app has a different value set for path [:debug_errors] inside key DevAppWeb.Endpoint during runtime compared to compile time. Since this application environment entry was marked as compile time, this difference can lead to different behaviour than expected:

  * Compile time value was set to: true
  * Runtime value was set to: "test"

To fix this error, you might:

  * Make the runtime value match the compile time one

  * Recompile your project. If the misconfigured application is a dependency, you may need to run "mix deps.compile dev_app --force"

  * Alternatively, you can disable this check. If you are using releases, you can set :validate_compile_env to false in your release configuration. If you are using Mix to start your system, you can pass the --no-validate-compile-env flag


{"init terminating in do_boot",{<<"aborting boot">>,[{'Elixir.Config.Provider',boot,2,[]}]}}
init terminating in do_boot ({,[{Elixir.Config.Provider,boot,2,[]}]})

Crash dump is being written to: erl_crash.dump...done
```

Steps to reproduce:

1 - Clone this branch
2 - Generate a new phoenix app 
```
$ cd phoenix/installer
$ mix phx.new dev_app --dev
```
3 - Add `config :dev_app, DevAppWeb.Endpoint, debug_errors: "test"` to `config/runtime.exs`
4 - `mix release`
5 - run `_build/dev/rel/dev_app/bin/dev_app start` a bunch of times.



josevalim: Hi @escobera! This is the way to go but you want to read those values inside the `quote do` sections in `lib/phoenix/endpoint.ex`. That will have the same effect of running inside the user endpoint.

You will also need to bump Phoenix minimum Elixir require to v1.10 first.
escobera: Sorry about the confusion, I moved the `compile_env` calls back into `lib/phoenix/endpoint.ex`  and it is working as intended. I also bumped the required Elixir version to 1.10.

About it not showing the output properly I was able to reproduce the problem without phoenix, so I'll open an issue at the Elixir repo.

Thanks for the guidance @josevalim , if you need me to change anything just let me know.
josevalim: I have dropped a comment but it seems we may need to require Elixir v1.13.0.
josevalim: Unfortunately we cannot merge this yet because Elixir v1.11 still required the app to be a compile-time atom. This has been fixed on v1.13, so we will need to wait for that release.
escobera: @josevalim I had to reintroduce `Phoenix.Endpoint.Supervisor.config/2` not to break tests.
SteffenDE: > @josevalim I had to reintroduce `Phoenix.Endpoint.Supervisor.config/2` to not break tests.

My question is: if we only need it for a test, why not remove the test instead?

Patch:
diff --git a/guides/controllers.md b/guides/controllers.md
index 3bb93589e..833c2032b 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -182,7 +182,7 @@ What it doesn't have is a view for rendering JSON. Phoenix Controller hands off
     quote do
       use Phoenix.Controller,
         formats: [:html, :json],
-        layouts: [html: HelloWeb.Layouts]
+        layouts: [html: {HelloWeb.Layouts, :app}]
       ...
     end
   end
@@ -190,6 +190,12 @@ What it doesn't have is a view for rendering JSON. Phoenix Controller hands off
 
 So out of the box Phoenix will look for a `HTML` and `JSON` view modules based on the request format and the controller name. We can also explicitly tell Phoenix in our controller which view(s) to use for each format. For example, what Phoenix does by default can be explicitly set with the following in your controller:
 
+```elixir
+plug :put_view, html: {HelloWeb.PageHTML, :app}, json: {HelloWeb.PageJSON, :app}
+```
+
+The layout name can be omitted, in which case the default layout name `:app` is used, so the above is equivalent to:
+
 ```elixir
 plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON
 ```
diff --git a/installer/templates/phx_single/lib/app_name_web.ex b/installer/templates/phx_single/lib/app_name_web.ex
index b64696d3f..bc25934ca 100644
--- a/installer/templates/phx_single/lib/app_name_web.ex
+++ b/installer/templates/phx_single/lib/app_name_web.ex
@@ -40,7 +40,7 @@ defmodule <%= @web_namespace %> do
     quote do
       use Phoenix.Controller,
         formats: [:html, :json],
-        layouts: [html: <%= @web_namespace %>.Layouts]<%= if @gettext do %>
+        layouts: [html: {<%= @web_namespace %>.Layouts, :app}]<%= if @gettext do %>
 
       use Gettext, backend: <%= @web_namespace %>.Gettext<% end %>
 
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
index a8cad99c9..c1a11b34b 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
@@ -1,5 +1,3 @@
-import Config
-
 # The secret key base is used to sign/encrypt cookies and other secrets.
 # A default value is used in config/dev.exs and config/test.exs but you
 # want to use a different value for prod and you most likely don't want
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex b/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex
index 985dee1c0..4f2d8b44e 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex
+++ b/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex
@@ -40,7 +40,7 @@ defmodule <%= @web_namespace %> do
     quote do
       use Phoenix.Controller,
         formats: [:html, :json],
-        layouts: [html: <%= @web_namespace %>.Layouts]<%= if @gettext do %>
+        layouts: [html: {<%= @web_namespace %>.Layouts, :app}]<%= if @gettext do %>
 
       use Gettext, backend: <%= @web_namespace %>.Gettext<% end %>
 
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index f5ba02541..d18398a84 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -163,20 +163,25 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   def button(assigns) do
     ~H"""
-    <button
-      type={@type}
-      class={[
-        "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
-        "text-sm font-semibold leading-6 text-white active:text-white/80",
-        @class
-      ]}
-      {@rest}
-    >
+    <button type={@type} class={[button_classes(), @class]} {@rest}>
       {render_slot(@inner_block)}
     </button>
     """
   end
 
+  @doc """
+  Returns the default button classes.
+
+  ## Examples
+
+      <.link class={button_classes()} href={~p"/items/new"}>
+        New Item
+      </.link>
+  """
+  def button_classes do
+    "inline-block phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80"
+  end
+
   @doc """
   Renders an input with label and error messages.
 
diff --git a/lib/phoenix/config.ex b/lib/phoenix/config.ex
index e31961059..7c68c6367 100644
--- a/lib/phoenix/config.ex
+++ b/lib/phoenix/config.ex
@@ -93,13 +93,6 @@ defmodule Phoenix.Config do
     merge(defaults, config)
   end
 
-  @doc """
-  Take 2 keyword lists and merge them recursively.
-
-  Used to merge configuration values into defaults.
-  """
-  def merge(a, b), do: Keyword.merge(a, b, &merger/3)
-
   defp fetch_config(otp_app, module) do
     case Application.fetch_env(otp_app, module) do
       {:ok, conf} -> conf
@@ -107,6 +100,13 @@ defmodule Phoenix.Config do
     end
   end
 
+  @doc """
+  Take 2 keyword lists and merge them recursively.
+
+  Used to merge configuration values into defaults.
+  """
+  def merge(a, b), do: Keyword.merge(a, b, &merger/3)
+
   defp merger(_k, v1, v2) do
     if Keyword.keyword?(v1) and Keyword.keyword?(v2) do
       Keyword.merge(v1, v2, &merger/3)
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index b13ac956d..7a08b0ee9 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -92,7 +92,7 @@ defmodule Phoenix.Controller do
 
       use Phoenix.Controller,
         formats: [:html, :json],
-        layouts: [html: MyAppWeb.Layouts]
+        layouts: [html: {MyAppWeb.Layouts, :app}]
 
   You can also specify formats and layouts to render by calling
   `put_view/2` and `put_layout/2` directly with a connection.
@@ -125,7 +125,10 @@ defmodule Phoenix.Controller do
       set, the default view is set to `MyAppWeb.UserView`
 
     * `:layouts` - which layouts to render for each format,
-      for example: `[html: DemoWeb.Layouts]`
+      for example: `[html: DemoWeb.Layouts]`. The value for each
+      format can be a tuple with the layout module and the layout name,
+      or just a module. When the name is omitted, the default layout
+      name `:app` is used.
 
   Deprecated options:
 
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 1419e1902..48ffae62b 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -421,11 +421,16 @@ defmodule Phoenix.Endpoint do
   defp config(opts) do
     quote do
       @otp_app unquote(opts)[:otp_app] || raise("endpoint expects :otp_app to be given")
-      var!(config) = Phoenix.Endpoint.Supervisor.config(@otp_app, __MODULE__)
-      var!(code_reloading?) = var!(config)[:code_reloader]
+
+      # Compile-time configuration checking
+      # This ensures that if a compile-time configuration is overwritten at runtime the application won't boot.
+      var!(code_reloading?) = Application.compile_env(@otp_app, [__MODULE__, :code_reloader], false)
+      var!(debug_errors?) =  Application.compile_env(@otp_app, [__MODULE__, :debug_errors], false)
+      var!(force_ssl) =  Application.compile_env(@otp_app, [__MODULE__, :force_ssl])
 
       # Avoid unused variable warnings
       _ = var!(code_reloading?)
+      _ = var!(force_ssl)
     end
   end
 
@@ -477,11 +482,11 @@ defmodule Phoenix.Endpoint do
 
       Module.register_attribute(__MODULE__, :phoenix_sockets, accumulate: true)
 
-      if force_ssl = Phoenix.Endpoint.__force_ssl__(__MODULE__, var!(config)) do
+      if force_ssl = Phoenix.Endpoint.__force_ssl__(__MODULE__, var!(force_ssl)) do
         plug Plug.SSL, force_ssl
       end
 
-      if var!(config)[:debug_errors] do
+      if var!(debug_errors?) do
         logo = "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K"
 
         use Plug.Debugger,
@@ -628,8 +633,8 @@ defmodule Phoenix.Endpoint do
   end
 
   @doc false
-  def __force_ssl__(module, config) do
-    if force_ssl = config[:force_ssl] do
+  def __force_ssl__(module, force_ssl) do
+    if force_ssl do
       Keyword.put_new(force_ssl, :host, {module, :host, []})
     end
   end
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index da2ffdb83..8f8a52bcf 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -27,7 +27,7 @@ defmodule Phoenix.Endpoint.Supervisor do
   @doc false
   def init({otp_app, mod, opts}) do
     default_conf = Phoenix.Config.merge(defaults(otp_app, mod), opts)
-    env_conf = config(otp_app, mod, default_conf)
+    env_conf = Phoenix.Config.from_env(otp_app, mod, default_conf)
 
     secret_conf =
       cond do
@@ -190,17 +190,6 @@ defmodule Phoenix.Endpoint.Supervisor do
     end
   end
 
-  @doc """
-  The endpoint configuration used at compile time.
-  """
-  def config(otp_app, endpoint) do
-    config(otp_app, endpoint, defaults(otp_app, endpoint))
-  end
-
-  defp config(otp_app, endpoint, defaults) do
-    Phoenix.Config.from_env(otp_app, endpoint, defaults)
-  end
-
   @doc """
   Checks if Endpoint's web server has been configured to start.
   """
diff --git a/priv/templates/phx.gen.html/index.html.heex b/priv/templates/phx.gen.html/index.html.heex
index feec52ee5..3e70d52aa 100644
--- a/priv/templates/phx.gen.html/index.html.heex
+++ b/priv/templates/phx.gen.html/index.html.heex
@@ -1,11 +1,9 @@
 <.header>
   Listing <%= schema.human_plural %>
   <:actions>
-    <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
-      <.link href={~p"<%= schema.route_prefix %>/new"}>
-        New <%= schema.human_singular %>
-      </.link>
-    </.button>
+    <.link class={button_classes()} href={~p"<%= schema.route_prefix %>/new"}>
+      New <%= schema.human_singular %>
+    </.link>
   </:actions>
 </.header>
 
diff --git a/priv/templates/phx.gen.html/show.html.heex b/priv/templates/phx.gen.html/show.html.heex
index ec3f7774a..a9e15ad98 100644
--- a/priv/templates/phx.gen.html/show.html.heex
+++ b/priv/templates/phx.gen.html/show.html.heex
@@ -2,11 +2,9 @@
   <%= schema.human_singular %> {@<%= schema.singular %>.id}
   <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
   <:actions>
-    <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
-      <.link href={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
-        Edit <%= schema.singular %>
-      </.link>
-    </.button>
+    <.link class={button_classes()} href={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
+      Edit <%= schema.singular %>
+    </.link>
   </:actions>
 </.header>
 
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index f5ba02541..d18398a84 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -163,20 +163,25 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   def button(assigns) do
     ~H"""
-    <button
-      type={@type}
-      class={[
-        "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
-        "text-sm font-semibold leading-6 text-white active:text-white/80",
-        @class
-      ]}
-      {@rest}
-    >
+    <button type={@type} class={[button_classes(), @class]} {@rest}>
       {render_slot(@inner_block)}
     </button>
     """
   end
 
+  @doc """
+  Returns the default button classes.
+
+  ## Examples
+
+      <.link class={button_classes()} href={~p"/items/new"}>
+        New Item
+      </.link>
+  """
+  def button_classes do
+    "inline-block phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3 text-sm font-semibold leading-6 text-white active:text-white/80"
+  end
+
   @doc """
   Renders an input with label and error messages.
 
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
index fd8a7d97d..8c16389d7 100644
--- a/priv/templates/phx.gen.live/index.ex
+++ b/priv/templates/phx.gen.live/index.ex
@@ -9,11 +9,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     <.header>
       Listing <%= schema.human_plural %>
       <:actions>
-        <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
-          <.link navigate={~p"<%= schema.route_prefix %>/new"}>
-            New <%= schema.human_singular %>
-          </.link>
-        </.button>
+        <.link class={button_classes()} navigate={~p"<%= schema.route_prefix %>/new"}>
+          New <%= schema.human_singular %>
+        </.link>
       </:actions>
     </.header>
 
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
index 46379eb69..e5c449b0a 100644
--- a/priv/templates/phx.gen.live/show.ex
+++ b/priv/templates/phx.gen.live/show.ex
@@ -10,11 +10,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       <%= schema.human_singular %> {@<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>}
       <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
       <:actions>
-        <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
-          <.link navigate={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
-            Edit <%= schema.singular %>
-          </.link>
-        </.button>
+        <.link class={button_classes()} navigate={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
+          Edit <%= schema.singular %>
+        </.link>
       </:actions>
     </.header>
 


Test patch:
diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs
index 1c8c24693..5a31718b3 100644
--- a/test/phoenix/endpoint/endpoint_test.exs
+++ b/test/phoenix/endpoint/endpoint_test.exs
@@ -25,9 +25,9 @@ defmodule Phoenix.Endpoint.EndpointTest do
     use Phoenix.Endpoint, otp_app: :phoenix
 
     # Assert endpoint variables
-    assert is_list(config)
     assert @otp_app == :phoenix
     assert code_reloading? == false
+    assert debug_errors? == false
   end
 
   defmodule NoConfigEndpoint do
diff --git a/test/phoenix/endpoint/supervisor_test.exs b/test/phoenix/endpoint/supervisor_test.exs
index 3b2b958db..a2ff64158 100644
--- a/test/phoenix/endpoint/supervisor_test.exs
+++ b/test/phoenix/endpoint/supervisor_test.exs
@@ -58,15 +58,6 @@ defmodule Phoenix.Endpoint.SupervisorTest do
 
   defp persistent!(endpoint), do: :persistent_term.get({Phoenix.Endpoint, endpoint})
 
-  test "loads router configuration" do
-    config = Supervisor.config(:phoenix, SupervisorApp.Endpoint)
-    assert config[:otp_app] == :phoenix
-    assert config[:custom] == true
-
-    assert config[:render_errors] ==
-             [view: SupervisorApp.ErrorView, accepts: ~w(html), layout: false]
-  end
-
   test "generates the static url based on the static host configuration" do
     assert persistent!(StaticURLEndpoint).static_url == "http://static.example.com"
   end