Repository: phoenixframework/phoenix
Pull request #6138 (2025-03-24 20:17:38)

Update EEF Security WG Links
Follow up of erlef/security-wg#48


Patch:
diff --git a/guides/components.md b/guides/components.md
index 5e98f9213..2ca5075ed 100644
--- a/guides/components.md
+++ b/guides/components.md
@@ -75,7 +75,7 @@ Overall, function components are the essential building block of Phoenix renderi
 In fact, every template embedded into `HelloHTML` is a function component in itself. `show.html.heex` simply becomes a function component named `show`. This also means you can directly render function components directly from the controller, skipping the `show.html.heex` template:
 
 ```elixir
-def HelloWeb.HelloController do
+defmodule HelloWeb.HelloController do
   use HelloWeb, :controller
 
   def show(conn, %{"messenger" => messenger}) do
@@ -186,7 +186,7 @@ Likewise, for comprehensions may be written as:
 
 ## Layouts
 
-Layouts are just function components. They are defined in a module, just like all other function component templates. In a newly generated app, this is `lib/hello_web/components/layouts.ex`. You will also find a `layouts` folder with two built-in layouts generated by Phoenix. The default _root layout_ is called `root.html.heex`, and it is the layout into which all templates will be rendered by default. The second is the _app layout_, called `app.html.heex`, which is rendered within the root layout and includes our contents.
+Layouts are just function components. They are defined in a module, just like all other function component templates. In a newly generated app, this is `lib/hello_web/components/layouts.ex`. The default _root layout_ is called `root.html.heex`, and is stored in a separate `layouts` folder. It is the layout into which all templates will be rendered by default. The second is the _app layout_, directly defined in the `layouts.ex` module, which is explicitly rendered inside regular views and live views.
 
 You may be wondering how the string resulting from a rendered view ends up inside a layout. That's a great question! If we look at `lib/hello_web/components/layouts/root.html.heex`, just about at the end of the `<body>`, we will see this.
 
@@ -210,23 +210,53 @@ end
 
 After reloading [http://localhost:4000/hello](http://localhost:4000/hello), we should see a very different page, one with no title or CSS styling at all.
 
-To customize the application layout, we invoke a similar function named `put_layout/2`. Let's actually create another layout and render the index template into it. As an example, let's say we had a different layout for the admin section of our application which didn't have the logo image. To do this, copy the existing `app.html.heex` to a new file `admin.html.heex` in the same directory `lib/hello_web/components/layouts`. Then remove everything inside the `<header>...</header>` tags (or change it to whatever you desire) in the new file.
+To customize the application layout, we invoke a similar function named `put_layout/2`. Let's actually create another layout and render the index template into it. As an example, let's say we had a different layout for the admin section of our application which didn't have the logo image. To do this, copy the existing `app` component to a new `admin` component in the `lib/hello_web/components/layouts.ex` module. Then remove everything inside the `<header>...</header>` tags (or change it to whatever you desire) in the new component.
 
-Now, in the `index` action of the controller of `lib/hello_web/controllers/hello_controller.ex`, add the following:
+Now, change the `index` action of the controller of `lib/hello_web/controllers/hello_controller.ex`, to the following:
 
 ```elixir
 def index(conn, _params) do
-  conn
-  |> put_layout(html: :admin)
-  |> render(:index)
+  render(conn, :index)
 end
 ```
 
+And then adjust the `index.html.heex` file to invoke `Layouts.admin` instead of `Layouts.app`:
+
+```heex
+<Layouts.admin flash={@flash}>
+  ...
+</Layouts.admin>
+```
+
 When we load the page, we should be rendering the admin layout without the header (or a custom one that you wrote).
 
-At this point, you may be wondering, why does Phoenix have two layouts?
+By default, Phoenix creates one `root` layout that is rendered for all views and then each view explicitly includes the `app` layout - which is just a regular function component. It is also possible to automatically render the `app` layout in each view. To do this, you can either call `put_view(conn, html: {HelloWeb.Layouts, :app})` or define the `layouts` option when `use`ing `Phoenix.Controller`, which usually happens inside your `hello_web.ex` file:
+
+```elixir
+  def controller do
+    quote do
+      use Phoenix.Controller,
+        formats: [:html, :json],
+        layouts: [html: {HelloWeb.Layouts, :app}]
+      ...
+    end
+  end
+```
+
+For LiveViews, there's a similar option:
+
+```elixir
+  def live_view do
+    quote do
+      use Phoenix.LiveView,
+        layout: {HelloWeb.Layouts, :app}
+
+      ...
+    end
+  end
+```
 
-First of all, it gives us flexibility. In practice, we will hardly have multiple root layouts, as they often contain only HTML headers. This allows us to focus on different application layouts with only the parts that changes between them. Second of all, Phoenix ships with a feature called LiveView, which allows us to build rich and real-time user experiences with server-rendered HTML. LiveView is capable of dynamically changing the contents of the page, but it only ever changes the app layout, never the root layout. Check out [the LiveView documentation](https://hexdocs.pm/phoenix_live_view) to learn more.
+Note that if you change the `app` layout to be rendered implicitly, you'll need to replace `render_slot(@inner_block)` with `@inner_content`, as in the `root` layout.
 
 ## CoreComponents
 
diff --git a/guides/controllers.md b/guides/controllers.md
index 833c2032b..b5d60987c 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -181,8 +181,7 @@ What it doesn't have is a view for rendering JSON. Phoenix Controller hands off
   def controller do
     quote do
       use Phoenix.Controller,
-        formats: [:html, :json],
-        layouts: [html: {HelloWeb.Layouts, :app}]
+        formats: [:html, :json]
       ...
     end
   end
@@ -190,12 +189,6 @@ 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/guides/directory_structure.md b/guides/directory_structure.md
index 4e8ed6537..6a8fec2da 100644
--- a/guides/directory_structure.md
+++ b/guides/directory_structure.md
@@ -97,7 +97,6 @@ lib/hello_web
 │   ├── core_components.ex
 │   ├── layouts.ex
 │   └── layouts
-│       ├── app.html.heex
 │       └── root.html.heex
 ├── endpoint.ex
 ├── gettext.ex
diff --git a/guides/introduction/community.md b/guides/introduction/community.md
index 262d7c36d..e419df107 100644
--- a/guides/introduction/community.md
+++ b/guides/introduction/community.md
@@ -14,7 +14,7 @@ There are a number of places to connect with community members at all experience
 
   * Follow the Phoenix Framework on [Twitter](https://twitter.com/elixirphoenix).
 
-The Security Working Group of the Erlang Ecosystem Foundation also publishes in-depth documents about [security best practices for Erlang, Elixir, and Phoenix](https://erlef.github.io/security-wg/web_app_security_best_practices_beam/).
+The Security Working Group of the Erlang Ecosystem Foundation also publishes in-depth documents about [security best practices for Erlang, Elixir, and Phoenix](https://security.erlef.org/web_app_security_best_practices_beam/).
 
 ## Books
 
diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md
index d57bbe943..01f5b1a46 100644
--- a/guides/request_lifecycle.md
+++ b/guides/request_lifecycle.md
@@ -167,9 +167,11 @@ lib/hello_web
 A template file has the following structure: `NAME.FORMAT.TEMPLATING_LANGUAGE`. In our case, let's create an `index.html.heex` file at `lib/hello_web/controllers/hello_html/index.html.heex`:
 
 ```heex
-<section>
-  <h2>Hello World, from Phoenix!</h2>
-</section>
+<Layouts.app flash={@flash}>
+  <section>
+    <h2>Hello World, from Phoenix!</h2>
+  </section>
+</Layouts.app>
 ```
 
 Template files are compiled into the module as function components themselves, there is no runtime or performance difference between the two styles.
@@ -178,7 +180,7 @@ Now that we've got the route, controller, view, and template, we should be able
 
 ![Phoenix Greets Us](assets/images/hello-from-phoenix.png)
 
-There are a couple of interesting things to notice about what we just did. We didn't need to stop and restart the server while we made these changes. Yes, Phoenix has hot code reloading! Also, even though our `index.html.heex` file consists of only a single `section` tag, the page we get is a full HTML document. Our index template is actually rendered into layouts: first it renders `lib/hello_web/components/layouts/root.html.heex` which renders `lib/hello_web/components/layouts/app.html.heex` which finally includes our content. If you open those files, you'll see a line that looks like this at the bottom:
+There are a couple of interesting things to notice about what we just did. We didn't need to stop and restart the server while we made these changes. Yes, Phoenix has hot code reloading! Also, even though our `index.html.heex` file consists of only a single `section` tag, the page we get is a full HTML document. Our index template is actually rendered into a separate layout: `lib/hello_web/components/layouts/root.html.heex`, which contains the basic HTML skeleton of the page. If you open this files, you'll see a line that looks like this at the bottom:
 
 ```heex
 {@inner_content}
@@ -186,6 +188,8 @@ There are a couple of interesting things to notice about what we just did. We di
 
 This line injects our template into the layout before the HTML is sent off to the browser. We will talk more about layouts in the Controllers guide.
 
+The rest of the page structure is included in the `app` component the is defined in the `lib/hello_web/components/layouts.ex` module.
+
 > A note on hot code reloading: some editors with their automatic linters may prevent hot code reloading from working. If it's not working for you, please see the discussion in [this issue](https://github.com/phoenixframework/phoenix/issues/1165).
 
 ## From endpoint to views
diff --git a/installer/lib/phx_new/single.ex b/installer/lib/phx_new/single.ex
index 48f06ad3d..87773130a 100644
--- a/installer/lib/phx_new/single.ex
+++ b/installer/lib/phx_new/single.ex
@@ -52,8 +52,6 @@ defmodule Phx.New.Single do
        "test/:lib_web_name/controllers/page_controller_test.exs",
      "phx_web/components/layouts/root.html.heex":
        "lib/:lib_web_name/components/layouts/root.html.heex",
-     "phx_web/components/layouts/app.html.heex":
-       "lib/:lib_web_name/components/layouts/app.html.heex",
      "phx_web/components/layouts.ex": "lib/:lib_web_name/components/layouts.ex"},
     {:eex, :web, "phx_assets/logo.svg": "priv/static/images/logo.svg"}
   ])
diff --git a/installer/lib/phx_new/web.ex b/installer/lib/phx_new/web.ex
index 3a899e119..f4915f480 100644
--- a/installer/lib/phx_new/web.ex
+++ b/installer/lib/phx_new/web.ex
@@ -52,8 +52,7 @@ defmodule Phx.New.Web do
        "test/:web_app/controllers/page_controller_test.exs",
      "phx_test/controllers/error_html_test.exs": "test/:web_app/controllers/error_html_test.exs",
      "phx_assets/topbar.js": "assets/vendor/topbar.js",
-     "phx_web/components/layouts/root.html.heex": "lib/:web_app/components/layouts/root.html.heex",
-     "phx_web/components/layouts/app.html.heex": "lib/:web_app/components/layouts/app.html.heex"},
+     "phx_web/components/layouts/root.html.heex": "lib/:web_app/components/layouts/root.html.heex"},
     {:eex, :web, "phx_assets/logo.svg": "priv/static/images/logo.svg"}
   ])
 
diff --git a/installer/templates/phx_single/lib/app_name/application.ex b/installer/templates/phx_single/lib/app_name/application.ex
index e195ff6a4..eca30e413 100644
--- a/installer/templates/phx_single/lib/app_name/application.ex
+++ b/installer/templates/phx_single/lib/app_name/application.ex
@@ -11,8 +11,7 @@ defmodule <%= @app_module %>.Application do
       <%= @web_namespace %>.Telemetry,<%= if @ecto do %>
       <%= @app_module %>.Repo,<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
       {Ecto.Migrator,
-        repos: Application.fetch_env!(<%= inspect(String.to_atom(@app_name)) %>, :ecto_repos),
-        skip: skip_migrations?()},<% end %>
+       repos: Application.fetch_env!(<%= inspect(String.to_atom(@app_name)) %>, :ecto_repos), skip: skip_migrations?()},<% end %>
       {DNSCluster, query: Application.get_env(<%= inspect(String.to_atom(@app_name)) %>, :dns_cluster_query) || :ignore},
       {Phoenix.PubSub, name: <%= @app_module %>.PubSub},
       # Start a worker by calling: <%= @app_module %>.Worker.start_link(arg)
diff --git a/installer/templates/phx_single/lib/app_name_web.ex b/installer/templates/phx_single/lib/app_name_web.ex
index bc25934ca..06c762714 100644
--- a/installer/templates/phx_single/lib/app_name_web.ex
+++ b/installer/templates/phx_single/lib/app_name_web.ex
@@ -38,9 +38,7 @@ defmodule <%= @web_namespace %> do
 
   def controller do
     quote do
-      use Phoenix.Controller,
-        formats: [:html, :json],
-        layouts: [html: {<%= @web_namespace %>.Layouts, :app}]<%= if @gettext do %>
+      use Phoenix.Controller, formats: [:html, :json]<%= if @gettext do %>
 
       use Gettext, backend: <%= @web_namespace %>.Gettext<% end %>
 
@@ -52,8 +50,7 @@ defmodule <%= @web_namespace %> do
 
   def live_view do
     quote do
-      use Phoenix.LiveView,
-        layout: {<%= @web_namespace %>.Layouts, :app}
+      use Phoenix.LiveView
 
       unquote(html_helpers())
     end
@@ -90,8 +87,9 @@ defmodule <%= @web_namespace %> do
       # Core UI components
       import <%= @web_namespace %>.CoreComponents
 
-      # Shortcut for generating JS commands
+      # Common modules used in templates
       alias Phoenix.LiveView.JS
+      alias <%= @web_namespace %>.Layouts
 
       # Routes generation with the ~p sigil
       unquote(verified_routes())
diff --git a/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex b/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
index d455ad2cb..10569b5b2 100644
--- a/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
+++ b/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
@@ -10,8 +10,7 @@ defmodule <%= @app_module %>.Application do
     children = [<%= if @ecto do %>
       <%= @app_module %>.Repo,<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
       {Ecto.Migrator,
-        repos: Application.fetch_env!(<%= inspect(String.to_atom(@app_name)) %>, :ecto_repos),
-        skip: skip_migrations?()},<% end %>
+       repos: Application.fetch_env!(<%= inspect(String.to_atom(@app_name)) %>, :ecto_repos), skip: skip_migrations?()},<% end %>
       {DNSCluster, query: Application.get_env(<%= inspect(String.to_atom(@app_name)) %>, :dns_cluster_query) || :ignore},
       {Phoenix.PubSub, name: <%= @app_module %>.PubSub}
       # Start a worker by calling: <%= @app_module %>.Worker.start_link(arg)
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 4f2d8b44e..d8b1f9543 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
@@ -38,9 +38,7 @@ defmodule <%= @web_namespace %> do
 
   def controller do
     quote do
-      use Phoenix.Controller,
-        formats: [:html, :json],
-        layouts: [html: {<%= @web_namespace %>.Layouts, :app}]<%= if @gettext do %>
+      use Phoenix.Controller, formats: [:html, :json]<%= if @gettext do %>
 
       use Gettext, backend: <%= @web_namespace %>.Gettext<% end %>
 
@@ -52,8 +50,7 @@ defmodule <%= @web_namespace %> do
 
   def live_view do
     quote do
-      use Phoenix.LiveView,
-        layout: {<%= @web_namespace %>.Layouts, :app}
+      use Phoenix.LiveView
 
       unquote(html_helpers())
     end
@@ -90,8 +87,9 @@ defmodule <%= @web_namespace %> do
       # Core UI components
       import <%= @web_namespace %>.CoreComponents
 
-      # Shortcut for generating JS commands
+      # Common modules used in templates
       alias Phoenix.LiveView.JS
+      alias <%= @web_namespace %>.Layouts
 
       # Routes generation with the ~p sigil
       unquote(verified_routes())
diff --git a/installer/templates/phx_web/components/layouts.ex b/installer/templates/phx_web/components/layouts.ex
index fdb2d4e0b..972c85554 100644
--- a/installer/templates/phx_web/components/layouts.ex
+++ b/installer/templates/phx_web/components/layouts.ex
@@ -4,11 +4,154 @@ defmodule <%= @web_namespace %>.Layouts do
 
   See the `layouts` directory for all templates available.
   The "root" layout is a skeleton rendered as part of the
-  application router. The "app" layout is set as the default
-  layout on both `use <%= @web_namespace %>, :controller` and
-  `use <%= @web_namespace %>, :live_view`.
+  application router. The "app" layout is rendered as component
+  in regular views and live views.
   """
   use <%= @web_namespace %>, :html
 
   embed_templates "layouts/*"
+
+  def app(assigns) do
+    ~H"""
+    <header class="navbar px-4 sm:px-6 lg:px-8">
+      <div class="flex-1">
+        <a href="/" class="flex-1 flex items-center gap-2">
+          <img src={~p"/images/logo.svg"} width="36" />
+          <span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
+        </a>
+      </div>
+      <div class="flex-none">
+        <ul class="flex flex-column px-1 space-x-4">
+          <li>
+            <a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
+          </li>
+          <li>
+            <a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">GitHub</a>
+          </li><%= if @css do %>
+          <li>
+            <.theme_switcher />
+          </li><% end %>
+          <li>
+            <a href="https://hexdocs.pm/phoenix/overview.html" class="btn btn-primary">
+              Get Started <span aria-hidden="true">&rarr;</span>
+            </a>
+          </li>
+        </ul>
+      </div>
+    </header>
+
+    <main class="px-4 py-20 sm:px-6 lg:px-8">
+      <div class="mx-auto max-w-2xl">
+        {render_slot(@inner_block)}
+      </div>
+    </main>
+
+    <.flash_group flash={@flash} />
+    """
+  end
+
+  @doc """
+  Shows the flash group with standard titles and content.
+
+  ## Examples
+
+      <.flash_group flash={@flash} />
+  """
+  attr :flash, :map, required: true, doc: "the map of flash messages"
+  attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
+
+  def flash_group(assigns) do
+    ~H"""
+    <div id={@id} aria-live="polite">
+      <.flash kind={:info} flash={@flash} />
+      <.flash kind={:error} flash={@flash} />
+
+      <.flash
+        id="client-error"
+        kind={:error}
+        title=<%= maybe_heex_attr_gettext.("We can't find the internet", @gettext) %>
+        phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
+        phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
+        hidden
+      >
+        <%= maybe_eex_gettext.("Attempting to reconnect", @gettext) %>
+        <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 motion-safe:animate-spin" />
+      </.flash>
+
+      <.flash
+        id="server-error"
+        kind={:error}
+        title=<%= maybe_heex_attr_gettext.("Something went wrong!", @gettext) %>
+        phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
+        phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
+        hidden
+      >
+        <%= maybe_eex_gettext.("Hang in there while we get back on track", @gettext) %>
+        <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 motion-safe:animate-spin" />
+      </.flash>
+    </div>
+    """
+  end
+
+  # Example daisyUI theme switcher, powered by the theme_switcher_sceipt/1
+  # included in layouts/root.html.heex
+  def theme_switcher(assigns) do
+    ~H"""
+    <div class="dropdown dropdown-end">
+      <div tabindex="0" role="button" class="btn">
+        Theme
+        <span aria-hidden="true" class="text-xs font-normal">
+          <.icon class="size-4" name="hero-chevron-down-micro" />
+        </span>
+      </div>
+      <ul
+        tabindex="0"
+        class="dropdown-content menu bg-base-100 flex flex-row rounded-field z-1 w-52 mt-2 shadow-sm h-64 overflow-y-scroll"
+      >
+        <li
+          :for={
+            theme <-
+              ~w(system light dark abyss acid aqua autumn black bumblebee business caramellatte cmyk coffee) ++
+                ~w(corporate cupcake cyberpunk dark dim dracula emerald fantasy forest garden halloween) ++
+                ~w(lemonade lofi luxurynight nord pastel retro silk sunset synthwave valentine winter wireframe)
+          }
+          class="w-full"
+        >
+          <a class="capitalize" phx-click={JS.dispatch("phx:set-theme", detail: %{theme: theme})}>
+            {theme}
+          </a>
+        </li>
+      </ul>
+    </div>
+    """
+  end<%= if @css do %>
+
+  # Script used to power the theme switcher in the app layout + home page
+  defp theme_switcher_script(assigns) do
+    ~H"""
+    <script>
+      (() => {
+        const handleTheme = (theme) => {
+          if (!theme || theme === "system") {
+            window.localStorage.removeItem("theme")
+            document.documentElement.removeAttribute("data-theme", theme);
+            return;
+          }
+          window.localStorage.setItem("theme", theme)
+          document.documentElement.setAttribute("data-theme", theme);
+        }
+
+        handleTheme(localStorage.getItem("theme"));
+
+        window.addEventListener('storage', (e) => {
+          e.key === "theme" && handleTheme(e.newValue);
+        });
+
+        window.addEventListener("phx:set-theme", ({ detail: { theme }}) => {
+          handleTheme(theme);
+        });
+      })();
+    </script>
+    """
+  end<% end %>
 end
diff --git a/installer/templates/phx_web/components/layouts/app.html.heex b/installer/templates/phx_web/components/layouts/app.html.heex
deleted file mode 100644
index 8dd2e07f7..000000000
--- a/installer/templates/phx_web/components/layouts/app.html.heex
+++ /dev/null
@@ -1,67 +0,0 @@
-<header class="navbar px-4 sm:px-6 lg:px-8">
-  <div class="flex-1">
-    <a href="/" class="flex-1 flex items-center gap-2">
-      <img src={~p"/images/logo.svg"} width="36" />
-      <span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
-    </a>
-  </div>
-  <div class="flex-none">
-    <ul class="flex flex-column px-1 space-x-4">
-      <li>
-        <a href="https://twitter.com/elixirphoenix" class="btn btn-ghost">
-          @elixirphoenix
-        </a>
-      </li>
-      <li>
-        <a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">
-          GitHub
-        </a>
-      </li><%= if @css do %>
-      <li>
-        <%%!-- Example theme switcher, powered by JavaScript in layouts/root.html.heex --%>
-        <div class="dropdown dropdown-end">
-          <div tabindex="0" role="button" class="btn">
-            Theme
-            <span aria-hidden="true" class="text-xs font-normal">
-              <.icon class="size-4" name="hero-chevron-down-micro" />
-            </span>
-          </div>
-          <ul
-            tabindex="0"
-            class="dropdown-content menu bg-base-100 flex flex-row rounded-field z-1 w-52 mt-2 shadow-sm h-64 overflow-y-scroll"
-          >
-            <li
-              :for={
-                theme <-
-                  ~w(system light dark abyss acid aqua autumn black bumblebee business caramellatte cmyk coffee) ++
-                    ~w(corporate cupcake cyberpunk dark dim dracula emerald fantasy forest garden halloween) ++
-                    ~w(lemonade lofi luxurynight nord pastel retro silk sunset synthwave valentine winter wireframe)
-              }
-              class="w-full"
-            >
-              <a
-                class="capitalize"
-                phx-click={JS.dispatch("phx:set-theme", detail: %{theme: theme})}
-              >
-                {theme}
-              </a>
-            </li>
-          </ul>
-        </div>
-      </li><% end %>
-      <li>
-        <a href="https://hexdocs.pm/phoenix/overview.html" class="btn btn-primary">
-          Get Started <span aria-hidden="true">&rarr;</span>
-        </a>
-      </li>
-    </ul>
-  </div>
-</header>
-
-<main class="px-4 py-20 sm:px-6 lg:px-8">
-  <div class="mx-auto max-w-2xl">
-    {@inner_content}
-  </div>
-</main>
-
-<.flash_group flash={@flash} />
diff --git a/installer/templates/phx_web/components/layouts/root.html.heex b/installer/templates/phx_web/components/layouts/root.html.heex
index 1c362ac70..df8cea893 100644
--- a/installer/templates/phx_web/components/layouts/root.html.heex
+++ b/installer/templates/phx_web/components/layouts/root.html.heex
@@ -11,30 +11,7 @@
     <link phx-track-static rel="stylesheet" href={~p"/assets/default.css"} /><% end %>
     <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
     </script><%= if @css do %>
-    <script>
-      // Script used to power the theme switcher in layouts/app.html.heex
-      (() => {
-        const handleTheme = (theme) => {
-          if (!theme || theme === "system") {
-            window.localStorage.removeItem("theme")
-            document.documentElement.removeAttribute("data-theme", theme);
-            return;
-          }
-          window.localStorage.setItem("theme", theme)
-          document.documentElement.setAttribute("data-theme", theme);
-        }
-
-        handleTheme(localStorage.getItem("theme"));
-
-        window.addEventListener('storage', (e) => {
-          e.key === "theme" && handleTheme(e.newValue);
-        });
-
-        window.addEventListener("phx:set-theme", ({ detail: { theme }}) => {
-          handleTheme(theme);
-        });
-      })();
-    </script><% end %>
+    <.theme_switcher_script /><% end %>
   </head>
   <body class="bg-base-100">
     {@inner_content}
diff --git a/installer/templates/phx_web/controllers/page_html/home.html.heex b/installer/templates/phx_web/controllers/page_html/home.html.heex
index 9d5c921a8..9fdfa2564 100644
--- a/installer/templates/phx_web/controllers/page_html/home.html.heex
+++ b/installer/templates/phx_web/controllers/page_html/home.html.heex
@@ -1,4 +1,4 @@
-<.flash_group flash={@flash} />
+<Layouts.flash_group flash={@flash} />
 <div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
   <svg
     viewBox="0 0 1480 957"
@@ -122,36 +122,7 @@ export PATH=$HOME/<%= @elixir_install_bin_path %>:$PATH</code><% end %>
           v{Application.spec(:phoenix, :vsn)}
         </small>
       </h1><%= if @css do %>
-      <%%!-- Example theme switcher, powered by JavaScript in layouts/root.html.heex --%>
-      <div class="dropdown dropdown-end">
-        <div tabindex="0" role="button" class="btn btn-sm">
-          Theme
-          <span aria-hidden="true" class="text-xs font-normal">
-            <.icon class="size-4" name="hero-chevron-down-micro" />
-          </span>
-        </div>
-        <ul
-          tabindex="0"
-          class="dropdown-content menu bg-base-100 flex flex-row rounded-field z-1 w-52 mt-2 shadow-sm h-64 overflow-y-scroll"
-        >
-          <li
-            :for={
-              theme <-
-                ~w(system light dark abyss acid aqua autumn black bumblebee business caramellatte cmyk coffee) ++
-                  ~w(corporate cupcake cyberpunk dark dim dracula emerald fantasy forest garden halloween) ++
-                  ~w(lemonade lofi luxurynight nord pastel retro silk sunset synthwave valentine winter wireframe)
-            }
-            class="w-full"
-          >
-            <a
-              class="capitalize"
-              phx-click={JS.dispatch("phx:set-theme", detail: %{theme: theme})}
-            >
-              {theme}
-            </a>
-          </li>
-        </ul>
-      </div><% end %>
+      <Layouts.theme_switcher /><% end %>
     </div>
 
     <p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-balance">
diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex
index b9d14404a..9db43ccdb 100644
--- a/lib/mix/tasks/phx.gen.auth.ex
+++ b/lib/mix/tasks/phx.gen.auth.ex
@@ -751,14 +751,14 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
 
       Mix.shell().error("""
 
-      Unable to find an application layout file to inject user menu items.
+      Unable to find the root layout file to inject user menu items.
 
       Missing files:
 
       #{missing}
 
       Please ensure this phoenix app was not generated with
-      --no-html. If you have changed the name of your application
+      --no-html. If you have changed the name of your root
       layout file, please add the following code to it where you'd
       like the #{schema.singular} menu items to be rendered.
 
@@ -778,7 +778,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   defp potential_layout_file_paths(%Context{context_app: ctx_app}) do
     web_prefix = Mix.Phoenix.web_path(ctx_app)
 
-    for file_name <- ~w(root.html.heex app.html.heex) do
+    for file_name <- ~w(root.html.heex) do
       Path.join([web_prefix, "components", "layouts", file_name])
     end
   end
diff --git a/lib/mix/tasks/phx.gen.release.ex b/lib/mix/tasks/phx.gen.release.ex
index c7fae0b95..3f3bf1e29 100644
--- a/lib/mix/tasks/phx.gen.release.ex
+++ b/lib/mix/tasks/phx.gen.release.ex
@@ -322,7 +322,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
       :httpc.set_options([{:https_proxy, {{String.to_charlist(host), port}, []}}])
     end
 
-    # https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets
+    # https://security.erlef.org/secure_coding_and_deployment_hardening/inets
     http_options = [
       ssl: [
         verify: :verify_peer,
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 2ce079253..d4f010d03 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -532,13 +532,13 @@ defmodule Phoenix.Controller do
 
   def put_view(%Plug.Conn{} = conn, module) do
     raise(AlreadySentError, """
-      the response was already sent.
+    the response was already sent.
 
-          Status code: #{conn.status}
-          Request path: #{conn.request_path}
-          Method: #{conn.method}
-          View module: #{inspect(module)}
-      """)
+        Status code: #{conn.status}
+        Request path: #{conn.request_path}
+        Method: #{conn.method}
+        View module: #{inspect(module)}
+    """)
   end
 
   defp put_private_view(conn, priv_key, kind, formats) when is_list(formats) do
@@ -578,13 +578,13 @@ defmodule Phoenix.Controller do
 
   def put_new_view(%Plug.Conn{} = conn, module) do
     raise(AlreadySentError, """
-      the response was already sent.
+    the response was already sent.
 
-          Status code: #{conn.status}
-          Request path: #{conn.request_path}
-          Method: #{conn.method}
-          View module: #{inspect(module)}
-      """)
+        Status code: #{conn.status}
+        Request path: #{conn.request_path}
+        Method: #{conn.method}
+        View module: #{inspect(module)}
+    """)
   end
 
   @doc """
@@ -745,6 +745,7 @@ defmodule Phoenix.Controller do
           Layout: #{inspect(layout)}
       """)
     end
+
     put_private_layout(conn, :phoenix_layout, :new, layout)
   end
 
@@ -806,12 +807,12 @@ defmodule Phoenix.Controller do
 
   def put_layout_formats(%Plug.Conn{} = conn, _formats) do
     raise(AlreadySentError, """
-      the response was already sent.
+    the response was already sent.
 
-          Status code: #{conn.status}
-          Request path: #{conn.request_path}
-          Method: #{conn.method}
-      """)
+        Status code: #{conn.status}
+        Request path: #{conn.request_path}
+        Method: #{conn.method}
+    """)
   end
 
   @doc false
diff --git a/lib/phoenix/endpoint/render_errors.ex b/lib/phoenix/endpoint/render_errors.ex
index 0e0d494bf..2ee87cdb3 100644
--- a/lib/phoenix/endpoint/render_errors.ex
+++ b/lib/phoenix/endpoint/render_errors.ex
@@ -118,10 +118,9 @@ defmodule Phoenix.Endpoint.RenderErrors do
       |> Controller.put_layout(opts[:layout] || false)
 
     format = Controller.get_format(conn)
-
     reason = Exception.normalize(kind, reason, stack)
     template = "#{conn.status}.#{format}"
-    assigns = %{kind: kind, reason: reason, stack: stack, status: conn.status}
+    assigns = %{kind: kind, reason: reason, stack: stack, status: conn.status, __changed__: nil}
 
     Controller.render(conn, template, assigns)
   end
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index 809af7fcf..2634fec5a 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -5,49 +5,51 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def render(assigns) do
     ~H"""
-    <div class="mx-auto max-w-sm">
-      <.header class="text-center">Welcome {@<%= schema.singular %>.email}</.header>
+    <Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
+      <div class="mx-auto max-w-sm">
+        <.header class="text-center">Welcome {@<%= schema.singular %>.email}</.header>
 
-      <.form
-        :if={!@<%= schema.singular %>.confirmed_at}
-        for={@form}
-        id="confirmation_form"
-        phx-submit="submit"
-        action={~p"<%= schema.route_prefix %>/log-in?_action=confirmed"}
-        phx-trigger-action={@trigger_submit}
-      >
-        <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
-        <.input
-          :if={!@current_scope}
-          field={@form[:remember_me]}
-          type="checkbox"
-          label="Keep me logged in"
-        />
-        <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
-      </.form>
+        <.form
+          :if={!@<%= schema.singular %>.confirmed_at}
+          for={@form}
+          id="confirmation_form"
+          phx-submit="submit"
+          action={~p"<%= schema.route_prefix %>/log-in?_action=confirmed"}
+          phx-trigger-action={@trigger_submit}
+        >
+          <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+          <.input
+            :if={!@current_scope}
+            field={@form[:remember_me]}
+            type="checkbox"
+            label="Keep me logged in"
+          />
+          <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
+        </.form>
 
-      <.form
-        :if={@<%= schema.singular %>.confirmed_at}
-        for={@form}
-        id="login_form"
-        phx-submit="submit"
-        action={~p"<%= schema.route_prefix %>/log-in"}
-        phx-trigger-action={@trigger_submit}
-      >
-        <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
-        <.input
-          :if={!@current_scope}
-          field={@form[:remember_me]}
-          type="checkbox"
-          label="Keep me logged in"
-        />
-        <.button phx-disable-with="Logging in..." class="w-full">Log in</.button>
-      </.form>
+        <.form
+          :if={@<%= schema.singular %>.confirmed_at}
+          for={@form}
+          id="login_form"
+          phx-submit="submit"
+          action={~p"<%= schema.route_prefix %>/log-in"}
+          phx-trigger-action={@trigger_submit}
+        >
+          <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+          <.input
+            :if={!@current_scope}
+            field={@form[:remember_me]}
+            type="checkbox"
+            label="Keep me logged in"
+          />
+          <.button phx-disable-with="Logging in..." class="w-full">Log in</.button>
+        </.form>
 
-      <p :if={!@<%= schema.singular %>.confirmed_at} class="alert alert-outline mt-8">
-        Tip: If you prefer passwords, you can enable them in the <%= schema.singular %> settings.
-      </p>
-    </div>
+        <p :if={!@<%= schema.singular %>.confirmed_at} class="alert alert-outline mt-8">
+          Tip: If you prefer passwords, you can enable them in the <%= schema.singular %> settings.
+        </p>
+      </div>
+    </Layouts.app>
     """
   end
 
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index 309d9fbb6..9b8084774 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -5,86 +5,88 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def render(assigns) do
     ~H"""
-    <div class="mx-auto max-w-sm">
-      <.header class="text-center">
-        <p>Log in</p>
-        <:subtitle>
-          <%%= if @current_scope do %>
-            You need to reauthenticate to perform sensitive actions on your account.
-          <%% else %>
-            Don't have an account? <.link
-              navigate={~p"<%= schema.route_prefix %>/register"}
-              class="font-semibold text-brand hover:underline"
-              phx-no-format
-            >Sign up</.link> for an account now.
-          <%% end %>
-        </:subtitle>
-      </.header>
+    <Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
+      <div class="mx-auto max-w-sm">
+        <.header class="text-center">
+          <p>Log in</p>
+          <:subtitle>
+            <%%= if @current_scope do %>
+              You need to reauthenticate to perform sensitive actions on your account.
+            <%% else %>
+              Don't have an account? <.link
+                navigate={~p"<%= schema.route_prefix %>/register"}
+                class="font-semibold text-brand hover:underline"
+                phx-no-format
+              >Sign up</.link> for an account now.
+            <%% end %>
+          </:subtitle>
+        </.header>
 
-      <.form
-        :let={f}
-        for={@form}
-        id="login_form_magic"
-        action={~p"<%= schema.route_prefix %>/log-in"}
-        phx-submit="submit_magic"
-      >
-        <.input
-          readonly={!!@current_scope}
-          field={f[:email]}
-          type="email"
-          label="Email"
-          autocomplete="username"
-          required
-        />
-        <.button class="w-full">
-          Log in with email <span aria-hidden="true">→</span>
-        </.button>
-      </.form>
+        <.form
+          :let={f}
+          for={@form}
+          id="login_form_magic"
+          action={~p"<%= schema.route_prefix %>/log-in"}
+          phx-submit="submit_magic"
+        >
+          <.input
+            readonly={!!@current_scope}
+            field={f[:email]}
+            type="email"
+            label="Email"
+            autocomplete="username"
+            required
+          />
+          <.button class="w-full">
+            Log in with email <span aria-hidden="true">→</span>
+          </.button>
+        </.form>
 
-      <div class="divider">or</div>
+        <div class="divider">or</div>
 
-      <.form
-        :let={f}
-        for={@form}
-        id="login_form_password"
-        action={~p"<%= schema.route_prefix %>/log-in"}
-        phx-submit="submit_password"
-        phx-trigger-action={@trigger_submit}
-      >
-        <.input
-          readonly={!!@current_scope}
-          field={f[:email]}
-          type="email"
-          label="Email"
-          autocomplete="username"
-          required
-        />
-        <.input
-          field={@form[:password]}
-          type="password"
-          label="Password"
-          autocomplete="current-password"
-        />
-        <.input
-          :if={!@current_scope}
-          field={f[:remember_me]}
-          type="checkbox"
-          label="Keep me logged in"
-        />
-        <.button class="w-full">
-          Log in <span aria-hidden="true">→</span>
-        </.button>
-      </.form>
+        <.form
+          :let={f}
+          for={@form}
+          id="login_form_password"
+          action={~p"<%= schema.route_prefix %>/log-in"}
+          phx-submit="submit_password"
+          phx-trigger-action={@trigger_submit}
+        >
+          <.input
+            readonly={!!@current_scope}
+            field={f[:email]}
+            type="email"
+            label="Email"
+            autocomplete="username"
+            required
+          />
+          <.input
+            field={@form[:password]}
+            type="password"
+            label="Password"
+            autocomplete="current-password"
+          />
+          <.input
+            :if={!@current_scope}
+            field={f[:remember_me]}
+            type="checkbox"
+            label="Keep me logged in"
+          />
+          <.button class="w-full">
+            Log in <span aria-hidden="true">→</span>
+          </.button>
+        </.form>
 
-      <div :if={local_mail_adapter?()} class="alert alert-outline mt-8">
-        <div>
-          <p>You are running the local mail adapter.</p>
-          <p>
-            To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
-          </p>
+        <div :if={local_mail_adapter?()} class="alert alert-outline mt-8">
+          <div>
+            <p>You are running the local mail adapter.</p>
+            <p>
+              To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
+            </p>
+          </div>
         </div>
       </div>
-    </div>
+    </Layouts.app>
     """
   end
 
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index 04de630b5..bdb81a46d 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -6,24 +6,26 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def render(assigns) do
     ~H"""
-    <div class="mx-auto max-w-sm">
-      <.header class="text-center">
-        Register for an account
-        <:subtitle>
-          Already registered?
-          <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
-            Log in
-          </.link>
-          to your account now.
-        </:subtitle>
-      </.header>
+    <Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
+      <div class="mx-auto max-w-sm">
+        <.header class="text-center">
+          Register for an account
+          <:subtitle>
+            Already registered?
+            <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
+              Log in
+            </.link>
+            to your account now.
+          </:subtitle>
+        </.header>
 
-      <.form for={@form} id="registration_form" phx-submit="save" phx-change="validate">
-        <.input field={@form[:email]} type="email" label="Email" autocomplete="username" required />
+        <.form for={@form} id="registration_form" phx-submit="save" phx-change="validate">
+          <.input field={@form[:email]} type="email" label="Email" autocomplete="username" required />
 
-        <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
-      </.form>
-    </div>
+          <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
+        </.form>
+      </div>
+    </Layouts.app>
     """
   end
 
diff --git a/priv/templates/phx.gen.auth/registration_new.html.heex b/priv/templates/phx.gen.auth/registration_new.html.heex
index fe2269b82..e06b3c39b 100644
--- a/priv/templates/phx.gen.auth/registration_new.html.heex
+++ b/priv/templates/phx.gen.auth/registration_new.html.heex
@@ -1,18 +1,20 @@
-<div class="mx-auto max-w-sm">
-  <.header class="text-center">
-    Register for an account
-    <:subtitle>
-      Already registered?
-      <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
-        Log in
-      </.link>
-      to your account now.
-    </:subtitle>
-  </.header>
+<Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
+  <div class="mx-auto max-w-sm">
+    <.header class="text-center">
+      Register for an account
+      <:subtitle>
+        Already registered?
+        <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
+          Log in
+        </.link>
+        to your account now.
+      </:subtitle>
+    </.header>
 
-  <.form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/register"}>
-    <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
+    <.form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/register"}>
+      <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
 
-    <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
-  </.form>
-</div>
+      <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
+    </.form>
+  </div>
+</Layouts.app>
diff --git a/priv/templates/phx.gen.auth/session_confirm.html.heex b/priv/templates/phx.gen.auth/session_confirm.html.heex
index fb84e8539..47c0a35c1 100644
--- a/priv/templates/phx.gen.auth/session_confirm.html.heex
+++ b/priv/templates/phx.gen.auth/session_confirm.html.heex
@@ -1,34 +1,36 @@
-<div class="mx-auto max-w-sm">
-  <.header class="text-center">Welcome {@<%= schema.singular %>.email}</.header>
+<Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
+  <div class="mx-auto max-w-sm">
+    <.header class="text-center">Welcome {@<%= schema.singular %>.email}</.header>
 
-  <.form
-    :if={!@<%= schema.singular %>.confirmed_at}
-    for={@form}
-    id="confirmation_form"
-    action={~p"<%= schema.route_prefix %>/log-in?_action=confirmed"}
-  >
-    <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
-    <.input
-      :if={!@current_scope}
-      field={@form[:remember_me]}
-      type="checkbox"
-      label="Keep me logged in"
-    />
-    <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
-  </.form>
+    <.form
+      :if={!@<%= schema.singular %>.confirmed_at}
+      for={@form}
+      id="confirmation_form"
+      action={~p"<%= schema.route_prefix %>/log-in?_action=confirmed"}
+    >
+      <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+      <.input
+        :if={!@current_scope}
+        field={@form[:remember_me]}
+        type="checkbox"
+        label="Keep me logged in"
+      />
+      <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
+    </.form>
 
-  <.form :if={@<%= schema.singular %>.confirmed_at} for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"}>
-    <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
-    <.input
-      :if={!@current_scope}
-      field={@form[:remember_me]}
-      type="checkbox"
-      label="Keep me logged in"
-    />
-    <.button phx-disable-with="Logging in..." class="w-full">Log in</.button>
-  </.form>
+    <.form :if={@<%= schema.singular %>.confirmed_at} for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"}>
+      <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+      <.input
+        :if={!@current_scope}
+        field={@form[:remember_me]}
+        type="checkbox"
+        label="Keep me logged in"
+      />
+      <.button phx-disable-with="Logging in..." class="w-full">Log in</.button>
+    </.form>
 
-  <p :if={!@<%= schema.singular %>.confirmed_at} class="alert alert-outline mt-8">
-    Tip: If you prefer passwords, you can enable them in the <%= schema.singular %> settings.
-  </p>
-</div>
+    <p :if={!@<%= schema.singular %>.confirmed_at} class="alert alert-outline mt-8">
+      Tip: If you prefer passwords, you can enable them in the <%= schema.singular %> settings.
+    </p>
+  </div>
+</Layouts.app>
diff --git a/priv/templates/phx.gen.auth/session_new.html.heex b/priv/templates/phx.gen.auth/session_new.html.heex
index e6aae8bf5..927b2c950 100644
--- a/priv/templates/phx.gen.auth/session_new.html.heex
+++ b/priv/templates/phx.gen.auth/session_new.html.heex
@@ -1,62 +1,69 @@
-<div class="mx-auto max-w-sm">
-  <.header class="text-center">
-    <p>Log in</p>
-    <:subtitle>
-      <%%= if @current_scope do %>
-        You need to reauthenticate to perform sensitive actions on your account.
-      <%% else %>
-        Don't have an account? <.link
-          navigate={~p"<%= schema.route_prefix %>/register"}
-          class="font-semibold text-brand hover:underline"
-          phx-no-format
-        >Sign up</.link> for an account now.
-      <%% end %>
-    </:subtitle>
-  </.header>
+<Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
+  <div class="mx-auto max-w-sm">
+    <.header class="text-center">
+      <p>Log in</p>
+      <:subtitle>
+        <%%= if @current_scope do %>
+          You need to reauthenticate to perform sensitive actions on your account.
+        <%% else %>
+          Don't have an account? <.link
+            navigate={~p"<%= schema.route_prefix %>/register"}
+            class="font-semibold text-brand hover:underline"
+            phx-no-format
+          >Sign up</.link> for an account now.
+        <%% end %>
+      </:subtitle>
+    </.header>
 
-  <.form :let={f} for={@form} as={:<%= schema.singular %>} id="login_form_magic" action={~p"<%= schema.route_prefix %>/log-in"}>
-    <.input
-      readonly={!!@current_scope}
-      field={f[:email]}
-      type="email"
-      label="Email"
-      autocomplete="username"
-      required
-    />
-    <.button class="w-full">
-      Log in with email <span aria-hidden="true">→</span>
-    </.button>
-  </.form>
+    <.form :let={f} for={@form} as={:<%= schema.singular %>} id="login_form_magic" action={~p"<%= schema.route_prefix %>/log-in"}>
+      <.input
+        readonly={!!@current_scope}
+        field={f[:email]}
+        type="email"
+        label="Email"
+        autocomplete="username"
+        required
+      />
+      <.button class="w-full">
+        Log in with email <span aria-hidden="true">→</span>
+      </.button>
+    </.form>
 
-  <div class="divider">or</div>
+    <div class="divider">or</div>
 
-  <.form :let={f} for={@form} as={:<%= schema.singular %>} id="login_form_password" action={~p"<%= schema.route_prefix %>/log-in"}>
-    <.input
-      readonly={!!@current_scope}
-      field={f[:email]}
-      type="email"
-      label="Email"
-      autocomplete="username"
-      required
-    />
-    <.input field={f[:password]} type="password" label="Password" autocomplete="current-password" />
-    <.input
-      :if={!@current_scope}
-      field={f[:remember_me]}
-      type="checkbox"
-      label="Keep me logged in"
-    />
-    <.button class="w-full">
-      Log in <span aria-hidden="true">→</span>
-    </.button>
-  </.form>
+    <.form :let={f} for={@form} as={:<%= schema.singular %>} id="login_form_password" action={~p"<%= schema.route_prefix %>/log-in"}>
+      <.input
+        readonly={!!@current_scope}
+        field={f[:email]}
+        type="email"
+        label="Email"
+        autocomplete="username"
+        required
+      />
+      <.input
+        field={f[:password]}
+        type="password"
+        label="Password"
+        autocomplete="current-password"
+      />
+      <.input
+        :if={!@current_scope}
+        field={f[:remember_me]}
+        type="checkbox"
+        label="Keep me logged in"
+      />
+      <.button class="w-full">
+        Log in <span aria-hidden="true">→</span>
+      </.button>
+    </.form>
 
-  <div :if={local_mail_adapter?()} class="alert alert-outline mt-8">
-    <div>
-      <p>You are running the local mail adapter.</p>
-      <p>
-        To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
-      </p>
+    <div :if={local_mail_adapter?()} class="alert alert-outline mt-8">
+      <div>
+        <p>You are running the local mail adapter.</p>
+        <p>
+          To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
+        </p>
+      </div>
     </div>
   </div>
-</div>
+</Layouts.app>
diff --git a/priv/templates/phx.gen.auth/settings_edit.html.heex b/priv/templates/phx.gen.auth/settings_edit.html.heex
index 8db95e9a1..29c88122d 100644
--- a/priv/templates/phx.gen.auth/settings_edit.html.heex
+++ b/priv/templates/phx.gen.auth/settings_edit.html.heex
@@ -1,36 +1,38 @@
-<.header class="text-center">
-  Account Settings
-  <:subtitle>Manage your account email address and password settings</:subtitle>
-</.header>
+<Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
+  <.header class="text-center">
+    Account Settings
+    <:subtitle>Manage your account email address and password settings</:subtitle>
+  </.header>
 
-<.form :let={f} for={@email_changeset} action={~p"<%= schema.route_prefix %>/settings"} id="update_email">
-  <input type="hidden" name="action" value="update_email" />
+  <.form :let={f} for={@email_changeset} action={~p"<%= schema.route_prefix %>/settings"} id="update_email">
+    <input type="hidden" name="action" value="update_email" />
 
-  <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
+    <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
 
-  <.button phx-disable-with="Changing...">Change Email</.button>
-</.form>
+    <.button phx-disable-with="Changing...">Change Email</.button>
+  </.form>
 
-<div class="divider" />
+  <div class="divider" />
 
-<.form :let={f} for={@password_changeset} action={~p"<%= schema.route_prefix %>/settings"} id="update_password">
-  <input type="hidden" name="action" value="update_password" />
+  <.form :let={f} for={@password_changeset} action={~p"<%= schema.route_prefix %>/settings"} id="update_password">
+    <input type="hidden" name="action" value="update_password" />
 
-  <.input
-    field={f[:password]}
-    type="password"
-    label="New password"
-    autocomplete="new-password"
-    required
-  />
-  <.input
-    field={f[:password_confirmation]}
-    type="password"
-    label="Confirm new password"
-    autocomplete="new-password"
-    required
-  />
-  <.button phx-disable-with="Changing...">
-    Save Password
-  </.button>
-</.form>
+    <.input
+      field={f[:password]}
+      type="password"
+      label="New password"
+      autocomplete="new-password"
+      required
+    />
+    <.input
+      field={f[:password_confirmation]}
+      type="password"
+      label="Confirm new password"
+      autocomplete="new-password"
+      required
+    />
+    <.button phx-disable-with="Changing...">
+      Save Password
+    </.button>
+  </.form>
+</Layouts.app>
diff --git a/priv/templates/phx.gen.auth/settings_live.ex b/priv/templates/phx.gen.auth/settings_live.ex
index e4f6a4a11..ad94ae380 100644
--- a/priv/templates/phx.gen.auth/settings_live.ex
+++ b/priv/templates/phx.gen.auth/settings_live.ex
@@ -7,51 +7,59 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def render(assigns) do
     ~H"""
-    <.header class="text-center">
-      Account Settings
-      <:subtitle>Manage your account email address and password settings</:subtitle>
-    </.header>
-
-    <.form for={@email_form} id="email_form" phx-submit="update_email" phx-change="validate_email">
-      <.input field={@email_form[:email]} type="email" label="Email" autocomplete="username" required />
-      <.button phx-disable-with="Changing...">Change Email</.button>
-    </.form>
-
-    <div class="divider" />
-
-    <.form
-      for={@password_form}
-      id="password_form"
-      action={~p"<%= schema.route_prefix %>/update-password"}
-      method="post"
-      phx-change="validate_password"
-      phx-submit="update_password"
-      phx-trigger-action={@trigger_submit}
-    >
-      <input
-        name={@password_form[:email].name}
-        type="hidden"
-        id="hidden_<%= schema.singular %>_email"
-        autocomplete="username"
-        value={@current_email}
-      />
-      <.input
-        field={@password_form[:password]}
-        type="password"
-        label="New password"
-        autocomplete="new-password"
-        required
-      />
-      <.input
-        field={@password_form[:password_confirmation]}
-        type="password"
-        label="Confirm new password"
-        autocomplete="new-password"
-      />
-      <.button phx-disable-with="Saving...">
-        Save Password
-      </.button>
-    </.form>
+    <Layouts.app flash={@flash} <%= scope_config.scope.assign_key %>={@<%= scope_config.scope.assign_key %>}>
+      <.header class="text-center">
+        Account Settings
+        <:subtitle>Manage your account email address and password settings</:subtitle>
+      </.header>
+
+      <.form for={@email_form} id="email_form" phx-submit="update_email" phx-change="validate_email">
+        <.input
+          field={@email_form[:email]}
+          type="email"
+          label="Email"
+          autocomplete="username"
+          required
+        />
+        <.button phx-disable-with="Changing...">Change Email</.button>
+      </.form>
+
+      <div class="divider" />
+
+      <.form
+        for={@password_form}
+        id="password_form"
+        action={~p"<%= schema.route_prefix %>/update-password"}
+        method="post"
+        phx-change="validate_password"
+        phx-submit="update_password"
+        phx-trigger-action={@trigger_submit}
+      >
+        <input
+          name={@password_form[:email].name}
+          type="hidden"
+          id="hidden_<%= schema.singular %>_email"
+          autocomplete="username"
+          value={@current_email}
+        />
+        <.input
+          field={@password_form[:password]}
+          type="password"
+          label="New password"
+          autocomplete="new-password"
+          required
+        />
+        <.input
+          field={@password_form[:password_confirmation]}
+          type="password"
+          label="Confirm new password"
+          autocomplete="new-password"
+        />
+        <.button phx-disable-with="Saving...">
+          Save Password
+        </.button>
+      </.form>
+    </Layouts.app>
     """
   end
 
diff --git a/priv/templates/phx.gen.html/edit.html.heex b/priv/templates/phx.gen.html/edit.html.heex
index 7d9e9d392..e3f236d63 100644
--- a/priv/templates/phx.gen.html/edit.html.heex
+++ b/priv/templates/phx.gen.html/edit.html.heex
@@ -1,8 +1,10 @@
-<.header>
-  Edit <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
-  <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
-</.header>
+<Layouts.app flash={@flash}<%= if scope do %> <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
+  <.header>
+    Edit <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
+    <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
+  </.header>
 
-<.<%= schema.singular %>_form changeset={@changeset} action={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{@<%= schema.singular %>}"} />
+  <.<%= schema.singular %>_form changeset={@changeset} action={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{@<%= schema.singular %>}"} />
 
-<.back navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+  <.back navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+</Layouts.app>
diff --git a/priv/templates/phx.gen.html/index.html.heex b/priv/templates/phx.gen.html/index.html.heex
index b07c52e6c..94b8bfcb8 100644
--- a/priv/templates/phx.gen.html/index.html.heex
+++ b/priv/templates/phx.gen.html/index.html.heex
@@ -1,23 +1,25 @@
-<.header>
-  Listing <%= schema.human_plural %>
-  <:actions>
-    <.link class="btn" href={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/new"}>
-      New <%= schema.human_singular %>
-    </.link>
-  </:actions>
-</.header>
+<Layouts.app flash={@flash}<%= if scope do %> <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
+  <.header>
+    Listing <%= schema.human_plural %>
+    <:actions>
+      <.link class="btn" href={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/new"}>
+        New <%= schema.human_singular %>
+      </.link>
+    </:actions>
+  </.header>
 
-<.table id="<%= schema.plural %>" rows={@<%= schema.collection %>} row_click={&JS.navigate(~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{&1}")}><%= for {k, _} <- schema.attrs do %>
-  <:col :let={<%= schema.singular %>} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{<%= schema.singular %>.<%= k %>}</:col><% end %>
-  <:action :let={<%= schema.singular %>}>
-    <div class="sr-only">
-      <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show</.link>
-    </div>
-    <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit</.link>
-  </:action>
-  <:action :let={<%= schema.singular %>}>
-    <.link href={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}"} method="delete" data-confirm="Are you sure?">
-      Delete
-    </.link>
-  </:action>
-</.table>
+  <.table id="<%= schema.plural %>" rows={@<%= schema.collection %>} row_click={&JS.navigate(~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{&1}")}><%= for {k, _} <- schema.attrs do %>
+    <:col :let={<%= schema.singular %>} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{<%= schema.singular %>.<%= k %>}</:col><% end %>
+    <:action :let={<%= schema.singular %>}>
+      <div class="sr-only">
+        <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show</.link>
+      </div>
+      <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit</.link>
+    </:action>
+    <:action :let={<%= schema.singular %>}>
+      <.link href={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}"} method="delete" data-confirm="Are you sure?">
+        Delete
+      </.link>
+    </:action>
+  </.table>
+</Layouts.app>
diff --git a/priv/templates/phx.gen.html/new.html.heex b/priv/templates/phx.gen.html/new.html.heex
index 664019c2b..30e5c33fe 100644
--- a/priv/templates/phx.gen.html/new.html.heex
+++ b/priv/templates/phx.gen.html/new.html.heex
@@ -1,8 +1,10 @@
-<.header>
-  New <%= schema.human_singular %>
-  <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
-</.header>
+<Layouts.app flash={@flash}<%= if scope do %> <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
+  <.header>
+    New <%= schema.human_singular %>
+    <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
+  </.header>
 
-<.<%= schema.singular %>_form changeset={@changeset} action={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"} />
+  <.<%= schema.singular %>_form changeset={@changeset} action={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"} />
 
-<.back navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+  <.back navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+</Layouts.app>
diff --git a/priv/templates/phx.gen.html/show.html.heex b/priv/templates/phx.gen.html/show.html.heex
index 9bada4818..b9daf4f25 100644
--- a/priv/templates/phx.gen.html/show.html.heex
+++ b/priv/templates/phx.gen.html/show.html.heex
@@ -1,15 +1,17 @@
-<.header>
-  <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
-  <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
-  <:actions>
-    <.link class="btn" href={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
-      Edit <%= schema.singular %>
-    </.link>
-  </:actions>
-</.header>
+<Layouts.app flash={@flash}<%= if scope do %> <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
+  <.header>
+    <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
+    <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
+    <:actions>
+      <.link class="btn" href={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
+        Edit <%= schema.singular %>
+      </.link>
+    </:actions>
+  </.header>
 
-<.list><%= for {k, _} <- schema.attrs do %>
-  <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{@<%= schema.singular %>.<%= k %>}</:item><% end %>
-</.list>
+  <.list><%= for {k, _} <- schema.attrs do %>
+    <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{@<%= schema.singular %>.<%= k %>}</:item><% end %>
+  </.list>
 
-<.back navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+  <.back navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+</Layouts.app>
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index 3b0e95f2e..7f2e5ca5a 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -79,49 +79,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
     """
   end
 
-  @doc """
-  Shows the flash group with standard titles and content.
-
-  ## Examples
-
-      <.flash_group flash={@flash} />
-  """
-  attr :flash, :map, required: true, doc: "the map of flash messages"
-  attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
-
-  def flash_group(assigns) do
-    ~H"""
-    <div id={@id} aria-live="polite">
-      <.flash kind={:info} flash={@flash} />
-      <.flash kind={:error} flash={@flash} />
-
-      <.flash
-        id="client-error"
-        kind={:error}
-        title=<%= maybe_heex_attr_gettext.("We can't find the internet", @gettext) %>
-        phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
-        phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
-        hidden
-      >
-        <%= maybe_eex_gettext.("Attempting to reconnect", @gettext) %>
-        <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 motion-safe:animate-spin" />
-      </.flash>
-
-      <.flash
-        id="server-error"
-        kind={:error}
-        title=<%= maybe_heex_attr_gettext.("Something went wrong!", @gettext) %>
-        phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
-        phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
-        hidden
-      >
-        <%= maybe_eex_gettext.("Hang in there while we get back on track", @gettext) %>
-        <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 motion-safe:animate-spin" />
-      </.flash>
-    </div>
-    """
-  end
-
   @doc """
   Renders a button.
 
diff --git a/priv/templates/phx.gen.live/form.ex b/priv/templates/phx.gen.live/form.ex
index 3d0401a26..c2324e0f1 100644
--- a/priv/templates/phx.gen.live/form.ex
+++ b/priv/templates/phx.gen.live/form.ex
@@ -7,17 +7,19 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   @impl true
   def render(assigns) do
     ~H"""
-    <.header>
-      {@page_title}
-      <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
-    </.header>
-
-    <.form for={@form} id="<%= schema.singular %>-form" phx-change="validate" phx-submit="save">
-<%= Mix.Tasks.Phx.Gen.Html.indent_inputs(inputs, 6) %>
-      <.button phx-disable-with="Saving...">Save <%= schema.human_singular %></.button>
-    </.form>
-
-    <.back navigate={return_path(<%= assign_scope_prefix %>@return_to, @<%= schema.singular %>)}>Back</.back>
+    <Layouts.app flash={@flash}<%= if scope do %> <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
+      <.header>
+        {@page_title}
+        <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
+      </.header>
+
+      <.form for={@form} id="<%= schema.singular %>-form" phx-change="validate" phx-submit="save">
+<%= Mix.Tasks.Phx.Gen.Html.indent_inputs(inputs, 8) %>
+        <.button phx-disable-with="Saving...">Save <%= schema.human_singular %></.button>
+      </.form>
+
+      <.back navigate={return_path(<%= assign_scope_prefix %>@return_to, @<%= schema.singular %>)}>Back</.back>
+    </Layouts.app>
     """
   end
 
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
index 913964e14..6bced9670 100644
--- a/priv/templates/phx.gen.live/index.ex
+++ b/priv/templates/phx.gen.live/index.ex
@@ -6,36 +6,38 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   @impl true
   def render(assigns) do
     ~H"""
-    <.header>
-      Listing <%= schema.human_plural %>
-      <:actions>
-        <.link class="btn" navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/new"}>
-          New <%= schema.human_singular %>
-        </.link>
-      </:actions>
-    </.header>
+    <Layouts.app flash={@flash}<%= if scope do %> <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
+      <.header>
+        Listing <%= schema.human_plural %>
+        <:actions>
+          <.link class="btn" navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/new"}>
+            New <%= schema.human_singular %>
+          </.link>
+        </:actions>
+      </.header>
 
-    <.table
-      id="<%= schema.plural %>"
-      rows={@streams.<%= schema.collection %>}
-      row_click={fn {_id, <%= schema.singular %>} -> JS.navigate(~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}") end}
-    ><%= for {k, _} <- schema.attrs do %>
-      <:col :let={{_id, <%= schema.singular %>}} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{<%= schema.singular %>.<%= k %>}</:col><% end %>
-      <:action :let={{_id, <%= schema.singular %>}}>
-        <div class="sr-only">
-          <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show</.link>
-        </div>
-        <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit</.link>
-      </:action>
-      <:action :let={{id, <%= schema.singular %>}}>
-        <.link
-          phx-click={JS.push("delete", value: %{<%= primary_key %>: <%= schema.singular %>.<%= primary_key %>}) |> hide("##{id}")}
-          data-confirm="Are you sure?"
-        >
-          Delete
-        </.link>
-      </:action>
-    </.table>
+      <.table
+        id="<%= schema.plural %>"
+        rows={@streams.<%= schema.collection %>}
+        row_click={fn {_id, <%= schema.singular %>} -> JS.navigate(~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}") end}
+      ><%= for {k, _} <- schema.attrs do %>
+        <:col :let={{_id, <%= schema.singular %>}} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{<%= schema.singular %>.<%= k %>}</:col><% end %>
+        <:action :let={{_id, <%= schema.singular %>}}>
+          <div class="sr-only">
+            <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show</.link>
+          </div>
+          <.link navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit</.link>
+        </:action>
+        <:action :let={{id, <%= schema.singular %>}}>
+          <.link
+            phx-click={JS.push("delete", value: %{<%= primary_key %>: <%= schema.singular %>.<%= primary_key %>}) |> hide("##{id}")}
+            data-confirm="Are you sure?"
+          >
+            Delete
+          </.link>
+        </:action>
+      </.table>
+    </Layouts.app>
     """
   end
 
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
index 4901ef522..85f2b9bcf 100644
--- a/priv/templates/phx.gen.live/show.ex
+++ b/priv/templates/phx.gen.live/show.ex
@@ -6,21 +6,23 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   @impl true
   def render(assigns) do
     ~H"""
-    <.header>
-      <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
-      <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
-      <:actions>
-        <.link class="btn" navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
-          Edit <%= schema.singular %>
-        </.link>
-      </:actions>
-    </.header>
+    <Layouts.app flash={@flash}<%= if scope do %> <%= scope.assign_key %>={@<%= scope.assign_key %>}<% end %>>
+      <.header>
+        <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
+        <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
+        <:actions>
+          <.link class="btn" navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
+            Edit <%= schema.singular %>
+          </.link>
+        </:actions>
+      </.header>
 
-    <.list><%= for {k, _} <- schema.attrs do %>
-      <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{@<%= schema.singular %>.<%= k %>}</:item><% end %>
-    </.list>
+      <.list><%= for {k, _} <- schema.attrs do %>
+        <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>">{@<%= schema.singular %>.<%= k %>}</:item><% end %>
+      </.list>
 
-    <.back navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+      <.back navigate={~p"<%= scope_assign_route_prefix %><%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+    </Layouts.app>
     """
   end
 


Test patch:
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index 8cb432eba..41114272d 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -108,14 +108,13 @@ defmodule Mix.Tasks.Phx.NewTest do
         ~r/defmodule PhxBlogWeb.ErrorJSON/
       )
 
-      assert_file("phx_blog/lib/phx_blog_web/components/core_components.ex", fn file ->
-        assert file =~ "defmodule PhxBlogWeb.CoreComponents"
-        assert file =~ ~S|aria-label={gettext("close")}|
+      assert_file("phx_blog/lib/phx_blog_web/components/layouts.ex", fn file ->
+        assert file =~ "defmodule PhxBlogWeb.Layouts"
         assert file =~ ~S|gettext("Attempting to reconnect")|
       end)
 
-      assert_file("phx_blog/lib/phx_blog_web/components/layouts.ex", fn file ->
-        assert file =~ "defmodule PhxBlogWeb.Layouts"
+      assert_file("phx_blog/lib/phx_blog_web/components/core_components.ex", fn file ->
+        assert file =~ ~S|gettext("close")|
       end)
 
       assert_file("phx_blog/lib/phx_blog_web/router.ex", fn file ->
@@ -134,7 +133,6 @@ defmodule Mix.Tasks.Phx.NewTest do
         assert file =~ ~s|<meta name="csrf-token" content={get_csrf_token()} />|
       end)
 
-      assert_file("phx_blog/lib/phx_blog_web/components/layouts/app.html.heex")
       assert_file("phx_blog/lib/phx_blog_web/controllers/page_html/home.html.heex")
 
       # assets
@@ -452,10 +450,6 @@ defmodule Mix.Tasks.Phx.NewTest do
 
       assert_file("phx_blog/mix.exs", &refute(&1 =~ ~r":phoenix_live_dashboard"))
 
-      assert_file("phx_blog/lib/phx_blog_web/components/layouts/app.html.heex", fn file ->
-        refute file =~ ~s|LiveDashboard|
-      end)
-
       assert_file("phx_blog/lib/phx_blog_web/endpoint.ex", fn file ->
         assert file =~ ~s|defmodule PhxBlogWeb.Endpoint|
         assert file =~ ~s|  socket "/live"|
@@ -563,11 +557,15 @@ defmodule Mix.Tasks.Phx.NewTest do
     in_tmp("new with no_gettext", fn ->
       Mix.Tasks.Phx.New.run([@app_name, "--no-gettext"])
 
-      assert_file("phx_blog/lib/phx_blog_web/components/core_components.ex", fn file ->
-        assert file =~ ~S|aria-label="close"|
+      assert_file("phx_blog/lib/phx_blog_web/components/layouts.ex", fn file ->
         assert file =~ ~S|Attempting to reconnect|
         refute file =~ ~S|gettext("Attempting to reconnect")|
       end)
+
+      assert_file("phx_blog/lib/phx_blog_web/components/core_components.ex", fn file ->
+        assert file =~ ~S|aria-label="close"|
+        refute file =~ ~S|gettext("close")|
+      end)
     end)
   end
 
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 1c5542e40..5d7911887 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -175,8 +175,6 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
         assert file =~ ~s|<meta name="csrf-token" content={get_csrf_token()} />|
       end)
 
-      assert_file(web_path(@app, "lib/#{@app}_web/components/layouts/app.html.heex"))
-
       # assets
       assert_file(web_path(@app, ".gitignore"), "/priv/static/assets/")
       assert_file(web_path(@app, ".gitignore"), "#{@app}_web-*.tar")
@@ -437,10 +435,6 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file(web_path(@app, "mix.exs"), &refute(&1 =~ ~r":phoenix_live_dashboard"))
 
-      assert_file(web_path(@app, "lib/#{@app}_web/components/layouts/app.html.heex"), fn file ->
-        refute file =~ ~s|LiveDashboard|
-      end)
-
       assert_file(web_path(@app, "lib/#{@app}_web/endpoint.ex"), fn file ->
         assert file =~ ~s|defmodule PhxUmbWeb.Endpoint|
         assert file =~ ~s|socket "/live"|
@@ -823,7 +817,6 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
         assert_file("another/lib/another/router.ex", "defmodule Another.Router")
         assert_file("another/lib/another.ex", "defmodule Another")
         assert_file("another/lib/another/components/layouts/root.html.heex")
-        assert_file("another/lib/another/components/layouts/app.html.heex")
 
         # assets
         assert_file("another/.gitignore", ~r/\n$/)
diff --git a/integration_test/test/code_generation/app_with_sqlite3_adapter.exs b/integration_test/test/code_generation/app_with_sqlite3_adapter_test.exs
similarity index 96%
rename from integration_test/test/code_generation/app_with_sqlite3_adapter.exs
rename to integration_test/test/code_generation/app_with_sqlite3_adapter_test.exs
index 4576a6cfc..29dbf9fa6 100644
--- a/integration_test/test/code_generation/app_with_sqlite3_adapter.exs
+++ b/integration_test/test/code_generation/app_with_sqlite3_adapter_test.exs
@@ -13,7 +13,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithSQLite3AdapterTest do
         modify_file(Path.join(app_root_path, "lib/default_sqlite3_app_web/router.ex"), fn file ->
           inject_before_final_end(file, """
 
-            scope "/", DefaultMysqlAppWeb do
+            scope "/", DefaultSqlite3AppWeb do
               pipe_through [:browser]
 
               resources "/posts", PostController
@@ -39,7 +39,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithSQLite3AdapterTest do
         modify_file(Path.join(app_root_path, "lib/default_sqlite3_app_web/router.ex"), fn file ->
           inject_before_final_end(file, """
 
-            scope "/api", DefaultMysqlAppWeb do
+            scope "/api", DefaultSqlite3AppWeb do
               pipe_through [:api]
 
               resources "/posts", PostController, except: [:new, :edit]
@@ -65,7 +65,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithSQLite3AdapterTest do
         modify_file(Path.join(app_root_path, "lib/default_sqlite3_app_web/router.ex"), fn file ->
           inject_before_final_end(file, """
 
-            scope "/", DefaultMysqlAppWeb do
+            scope "/", DefaultSqlite3AppWeb do
               pipe_through [:browser]
 
               live "/posts", PostLive.Index, :index
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index 7f41db58b..59bb70f98 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -1188,7 +1188,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
     test "when layout file is not found", config do
       in_tmp_phx_project(config.test, fn ->
         File.rm!("lib/my_app_web/components/layouts/root.html.heex")
-        File.rm!("lib/my_app_web/components/layouts/app.html.heex")
 
         send(self(), {:mix_shell_input, :yes?, false})
 
@@ -1203,15 +1202,14 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
         assert error == """
 
-               Unable to find an application layout file to inject user menu items.
+               Unable to find the root layout file to inject user menu items.
 
                Missing files:
 
                  * lib/my_app_web/components/layouts/root.html.heex
-                 * lib/my_app_web/components/layouts/app.html.heex
 
                Please ensure this phoenix app was not generated with
-               --no-html. If you have changed the name of your application
+               --no-html. If you have changed the name of your root
                layout file, please add the following code to it where you'd
                like the user menu items to be rendered.
 
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index fdecd2bd8..8eb7cd2ea 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -109,10 +109,10 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ ~s(<.input field={@form[:votes]} type="number")
         assert file =~ ~s(<.input field={@form[:cost]} type="number" label="Cost" step="any")
         assert file =~ """
-              <.input
-                field={@form[:tags]}
-                type="select"
-                multiple
+                <.input
+                  field={@form[:tags]}
+                  type="select"
+                  multiple
         """
         assert file =~ ~s(<.input field={@form[:popular]} type="checkbox")
         assert file =~ ~s(<.input field={@form[:drafted_at]} type="datetime-local")
@@ -123,9 +123,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ ~s(<.input field={@form[:secret]} type="text" label="Secret" />)
         refute file =~ ~s(<field={@form[:metadata]})
         assert file =~ """
-              <.input
-                field={@form[:status]}
-                type="select"
+                <.input
+                  field={@form[:status]}
+                  type="select"
         """
         assert file =~ ~s|Ecto.Enum.values(Phoenix.Blog.Post, :status)|
 
diff --git a/test/phoenix/controller/render_test.exs b/test/phoenix/controller/render_test.exs
index 92506e2a1..fbe67d77d 100644
--- a/test/phoenix/controller/render_test.exs
+++ b/test/phoenix/controller/render_test.exs
@@ -1,4 +1,4 @@
-Code.require_file "../../fixtures/views.exs", __DIR__
+Code.require_file("../../fixtures/views.exs", __DIR__)
 
 defmodule Phoenix.Controller.RenderTest do
   use ExUnit.Case, async: true
@@ -48,7 +48,9 @@ defmodule Phoenix.Controller.RenderTest do
       |> put_root_layout({MyApp.LayoutView, "root.html"})
       |> render("index.html", title: "Hello")
 
-    assert conn.resp_body == "ROOTSTART[Hello]<html>\n  <title>Hello</title>\n  Hello\n\n</html>\nROOTEND\n"
+    assert conn.resp_body ==
+             "ROOTSTART[Hello]<html>\n  <title>Hello</title>\n  Hello\n\n</html>\nROOTEND\n"
+
     assert html_response?(conn)
   end
 
@@ -66,7 +68,9 @@ defmodule Phoenix.Controller.RenderTest do
       |> put_root_layout({MyApp.LayoutView, :root})
       |> render("index.html", title: "Hello")
 
-    assert conn.resp_body == "ROOTSTART[Hello]<html>\n  <title>Hello</title>\n  Hello\n\n</html>\nROOTEND\n"
+    assert conn.resp_body ==
+             "ROOTSTART[Hello]<html>\n  <title>Hello</title>\n  Hello\n\n</html>\nROOTEND\n"
+
     assert html_response?(conn)
   end
 
@@ -90,12 +94,16 @@ defmodule Phoenix.Controller.RenderTest do
 
   test "render with layout sets view_module/template for layout and inner view" do
     conn = render(conn(), "inner.html", title: "Hello", layout: {MyApp.LayoutView, :app})
-    assert conn.resp_body == "<html>\n  <title>Hello</title>\n  View module is Elixir.MyApp.UserView and view template is inner.html\n\n</html>\n"
+
+    assert conn.resp_body ==
+             "<html>\n  <title>Hello</title>\n  View module is Elixir.MyApp.UserView and view template is inner.html\n\n</html>\n"
   end
 
   test "render without layout sets inner view_module/template assigns" do
     conn = render(conn(), "inner.html", [])
-    assert conn.resp_body == "View module is Elixir.MyApp.UserView and view template is inner.html\n"
+
+    assert conn.resp_body ==
+             "View module is Elixir.MyApp.UserView and view template is inner.html\n"
   end
 
   test "renders with conn status code" do
@@ -126,14 +134,14 @@ defmodule Phoenix.Controller.RenderTest do
 
   test "uses action name" do
     conn = put_format(conn(), "html")
-    conn = put_in conn.private[:phoenix_action], :index
+    conn = put_in(conn.private[:phoenix_action], :index)
     conn = render(conn, title: "Hello")
     assert conn.resp_body == "Hello\n"
   end
 
   test "render/2 renders with View and Template with atom for template" do
     conn = put_format(conn(), "json")
-    conn = put_in conn.private[:phoenix_action], :show
+    conn = put_in(conn.private[:phoenix_action], :show)
     conn = put_view(conn, MyApp.UserView)
     conn = render(conn, :show)
     assert conn.resp_body == ~s({"foo":"bar"})
@@ -141,7 +149,7 @@ defmodule Phoenix.Controller.RenderTest do
 
   test "render/2 renders with View and Template" do
     conn = put_format(conn(), "json")
-    conn = put_in conn.private[:phoenix_action], :show
+    conn = put_in(conn.private[:phoenix_action], :show)
     conn = put_view(conn, MyApp.UserView)
     conn = render(conn, "show.json")
     assert conn.resp_body == ~s({"foo":"bar"})
diff --git a/test/phoenix/endpoint/render_errors_test.exs b/test/phoenix/endpoint/render_errors_test.exs
index cfd24fcb3..2aeea6db9 100644
--- a/test/phoenix/endpoint/render_errors_test.exs
+++ b/test/phoenix/endpoint/render_errors_test.exs
@@ -11,19 +11,47 @@ defmodule Phoenix.Endpoint.RenderErrorsTest do
     "Layout: " <> assigns.inner_content
   end
 
-  def render("404.html", %{kind: kind, reason: _reason, stack: _stack, status: 404, conn: conn}) do
+  def render("404.html", %{
+        kind: kind,
+        reason: _reason,
+        stack: _stack,
+        status: 404,
+        conn: conn,
+        __changed__: nil
+      }) do
     "Got 404 from #{kind} with #{conn.method}"
   end
 
-  def render("404.json", %{kind: kind, reason: _reason, stack: _stack, status: 404, conn: conn}) do
+  def render("404.json", %{
+        kind: kind,
+        reason: _reason,
+        stack: _stack,
+        status: 404,
+        conn: conn,
+        __changed__: nil
+      }) do
     %{error: "Got 404 from #{kind} with #{conn.method}"}
   end
 
-  def render("415.html", %{kind: kind, reason: _reason, stack: _stack, status: 415, conn: conn}) do
+  def render("415.html", %{
+        kind: kind,
+        reason: _reason,
+        stack: _stack,
+        status: 415,
+        conn: conn,
+        __changed__: nil
+      }) do
     "Got 415 from #{kind} with #{conn.method}"
   end
 
-  def render("500.html", %{kind: kind, reason: _reason, stack: _stack, status: 500, conn: conn}) do
+  def render("500.html", %{
+        kind: kind,
+        reason: _reason,
+        stack: _stack,
+        status: 500,
+        conn: conn,
+        __changed__: nil
+      }) do
     "Got 500 from #{kind} with #{conn.method}"
   end