Repository: phoenixframework/phoenix
Pull request #5910 (2024-08-25 06:50:19)

Enable web console logger for new projects
This article describes a new and useful feature in Live reload 1.5 and later.
I think it could make sense to enable it by default for new projects...

https://fly.io/phoenix-files/phoenix-dev-blog-server-logs-in-the-browser-console/

SteffenDE: Thank you, @ponychicken! I think we'll need to add `web_console_logger: true` here as well:

https://github.com/phoenixframework/phoenix/blob/010a73848d6e11948f91d8c6960cbf7806960ccf/installer/templates/phx_single/config/dev.exs#L47

```diff
config :my_app, MyAppWeb.Endpoint,
  live_reload: [
+   web_console_logger: true,
    patterns: [
      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
      ~r"priv/gettext/.*(po)$",
      ~r"lib/my_app_web/(controllers|live|components)/.*(ex|heex)$"
    ]
  ]
```
ponychicken: @SteffenDE 
I think you meant the file installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs  right?

I pushed it.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dc08e0205..c11117b32 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,8 +22,8 @@ jobs:
           - elixir: 1.14.5
             otp: 25.3.2.9
 
-          - elixir: 1.17.2
-            otp: 27.0
+          - elixir: 1.17.3
+            otp: 27.1
             lint: true
             installer: true
 
@@ -111,13 +111,13 @@ jobs:
       matrix:
         include:
           # look for correct alpine image here: https://hub.docker.com/r/hexpm/elixir/tags
-          - elixir: 1.14.5
-            otp: 25.3.2.12
-            suffix: "alpine-3.19.1"
+          - elixir: 1.15.8
+            otp: 24.3.4.17
+            suffix: "alpine-3.20.3"
 
-          - elixir: 1.16.2
-            otp: 26.2.5
-            suffix: "alpine-3.19.1"
+          - elixir: 1.17.3
+            otp: 27.1.2
+            suffix: "alpine-3.20.3"
 
     container:
       image: hexpm/elixir:${{ matrix.elixir }}-erlang-${{ matrix.otp }}-${{ matrix.suffix }}
diff --git a/guides/authentication/mix_phx_gen_auth.md b/guides/authentication/mix_phx_gen_auth.md
index 952cca8d1..05b7cd533 100644
--- a/guides/authentication/mix_phx_gen_auth.md
+++ b/guides/authentication/mix_phx_gen_auth.md
@@ -1,12 +1,14 @@
 # mix phx.gen.auth
 
+> This guide assumes that you have gone through the [introductory guides](overview.html) and have a Phoenix application [up and running](up_and_running.html).
+
 The `mix phx.gen.auth` command generates a flexible, pre-built authentication system into your Phoenix app. This generator allows you to quickly move past the task of adding authentication to your codebase and stay focused on the real-world problem your application is trying to solve.
 
 ## Getting started
 
 > Before running this command, consider committing your work as it generates multiple files.
 
-Let's start by running the following command from the root of our app (or `apps/my_app_web` in an umbrella app):
+Let's start by running the following command from the root of our app:
 
 ```console
 $ mix phx.gen.auth Accounts User users
@@ -28,10 +30,10 @@ Since this generator installed additional dependencies in `mix.exs`, let's fetch
 $ mix deps.get
 ```
 
-Now we need to verify the database connection details for the development and test environments in `config/` so the migrator and tests can run properly. Then run the following to create the database:
+Now run the pending repository migrations:
 
 ```console
-$ mix ecto.setup
+$ mix ecto.migrate
 ```
 
 Let's run the tests to make sure our new authentication system works as expected.
@@ -40,7 +42,7 @@ Let's run the tests to make sure our new authentication system works as expected
 $ mix test
 ```
 
-And finally, let's start our Phoenix server and try it out.
+And finally, let's start our Phoenix server and try it out (note the new `Register` and `Log in` links at the top right of the default page).
 
 ```console
 $ mix phx.server
diff --git a/guides/components.md b/guides/components.md
index f0a3c1805..f78aa38a3 100644
--- a/guides/components.md
+++ b/guides/components.md
@@ -62,11 +62,7 @@ Next we need to update `show.html.heex`:
 </section>
 ```
 
-When we reload `http://localhost:4000/hello/Frank`, we should see the same content as before.
-
-Since templates are embedded inside the `HelloHTML` module, we were able to invoke the view function simply as `<.greet messenger="..." />`.
-
-If the component was defined elsewhere, we can also type `<HelloWeb.HelloHTML.greet messenger="..." />`.
+When we reload `http://localhost:4000/hello/Frank`, we should see the same content as before. Since the `show.html.heex` template is embedded within the `HelloHTML` module, we were able to invoke the function component directly as `<.greet messenger="..." />`. If the component was defined elsewhere, we would need to give its full name: `<HelloWeb.HelloHTML.greet messenger="..." />`.
 
 By declaring attributes as required, Phoenix will warn at compile time if we call the `<.greet />` component without passing attributes. If an attribute is optional, you can specify the `:default` option with a value:
 
@@ -74,19 +70,22 @@ By declaring attributes as required, Phoenix will warn at compile time if we cal
 attr :messenger, :string, default: nil
 ```
 
-Although this is a quick example, it shows the different roles function components play in Phoenix:
-
-* Function components can be defined as functions that receive `assigns` as argument and call the `~H` sigil, as we did in `greet/1`
-
-* Function components can be embedded from template files, that's how we load `show.html.heex` into `HelloWeb.HelloHTML`
+Overall, function components are the essential building block of Phoenix rendering stack. The majority of the times, they are functions that receive a single argument called `assigns` and call the `~H` sigil, as we did in `greet/1`. They can also be invoked from templates, with compile-time validation of its attributes declared via `attr`.
 
-* Function components can declare which attributes are expected, which are validated at compilation time
+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:
 
-* Function components can be directly rendered from controllers
+```elixir
+def HelloWeb.HelloController do
+  use HelloWeb, :controller
 
-* Function components can be directly rendered from other function components, as we called `<.greet messenger={@messenger} />` from `show.html.heex`
+  def show(conn, %{"messenger" => messenger}) do
+    # Render the HelloWeb.HelloHTML.greet/1 component
+    render(conn, :greet, messenger: messenger)
+  end
+end
+```
 
-And there's more. Before we go deeper, let's fully understand the expressive power behind the HEEx template language.
+Next, let's fully understand the expressive power behind the HEEx template language.
 
 ## HEEx
 
diff --git a/guides/contexts.md b/guides/contexts.md
index ec3439452..333eaa209 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -20,8 +20,6 @@ By giving modules that expose and group related functionality the name **context
 
 In Phoenix, contexts often encapsulate data access and data validation. They often talk to a database or APIs. Overall, think of them as boundaries to decouple and isolate parts of your application. Let's use these ideas to build out our web application. Our goal is to build an ecommerce system where we can showcase products, allow users to add products to their cart, and complete their orders.
 
-> How to read this guide: Using the context generators is a great way for beginners and intermediate Elixir programmers alike to get up and running quickly while thoughtfully writing their applications. This guide focuses on those readers.
-
 ### Adding a Catalog Context
 
 An ecommerce platform has wide-reaching coupling across a codebase so it's important to think about writing well-defined modules. With that in mind, our goal is to build a product catalog API that handles creating, updating, and deleting the products available in our system. We'll start off with the basic features of showcasing our products, and we will add shopping cart features later. We'll see how starting with a solid foundation with isolated boundaries allows us to grow our application naturally as we add functionality.
@@ -30,9 +28,6 @@ Phoenix includes the `mix phx.gen.html`, `mix phx.gen.json`, `mix phx.gen.live`,
 
 In order to run the context generators, we need to come up with a module name that groups the related functionality that we're building. In the [Ecto guide](ecto.html), we saw how we can use Changesets and Repos to validate and persist user schemas, but we didn't integrate this with our application at large. In fact, we didn't think about where a "user" in our application should live at all. Let's take a step back and think about the different parts of our system. We know that we'll have products to showcase on pages for sale, along with descriptions, pricing, etc. Along with selling products, we know we'll need to support carting, order checkout, and so on. While the products being purchased are related to the cart and checkout processes, showcasing a product and managing the *exhibition* of our products is distinctly different than tracking what a user has placed in their cart or how an order is placed. A `Catalog` context is a natural place for the management of our product details and the showcasing of those products we have for sale.
 
-> #### Naming things is hard {: .tip}
-> If you're stuck when trying to come up with a context name when the grouped functionality in your system isn't yet clear, you can simply use the plural form of the resource you're creating. For example, a `Products` context for managing products. As you grow your application and the parts of your system become clear, you can simply rename the context to a more refined one.
-
 To jump-start our catalog context, we'll use `mix phx.gen.html` which creates a context module that wraps up Ecto access for creating, updating, and deleting products, along with web files like controllers and templates for the web interface into our context. Run the following command at your project root:
 
 ```console
@@ -60,7 +55,6 @@ Add the resource to your browser scope in lib/hello_web/router.ex:
 
     resources "/products", ProductController
 
-
 Remember to update your repository by running migrations:
 
     $ mix ecto.migrate
@@ -123,6 +117,14 @@ Views: 0
 
 If we follow the "Back" link, we get a list of all products, which should contain the one we just created. Likewise, we can update this record or delete it. Now that we've seen how it works in the browser, it's time to take a look at the generated code.
 
+> #### Naming things is hard {: .tip}
+>
+> When starting a web application, it may be hard to draw lines or name its different contexts, especially when the domain you are working with is not as well established as e-commerce.
+>
+> If you're stuck when defining or naming a context, you can simply create a new context using the plural form of the resource you're creating. For example, a `Products` context for managing products. You will find that, even in such cases, you will organically discover other resources that belong to the `Products` context, such as categories or image galleries.
+>
+> As your applications grows and the different parts of your system become clear, you can simply rename the context or move resources around. The beauty of Elixir modules is that they are stateless, so moving them around should be simply a matter of renaming the module names (and renaming the files for consistency).
+
 ## Starting with generators
 
 That little `mix phx.gen.html` command packed a surprising punch. We got a lot of functionality out-of-the-box for creating, updating, and deleting products in our catalog. This is far from a full-featured app, but remember, generators are first and foremost learning tools and a starting point for you to begin building real features. Code generation can't solve all your problems, but it will teach you the ins and outs of Phoenix and nudge you towards the proper mindset when designing your application.
diff --git a/guides/controllers.md b/guides/controllers.md
index 6ca334642..3bb93589e 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -47,7 +47,7 @@ defmodule HelloWeb.PageController do
   ...
 
   def index(conn, _params) do
-    render(conn, :index)
+    render(conn, :home)
   end
 end
 ```
diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md
index b2560b088..9ba3838f2 100644
--- a/guides/deployment/heroku.md
+++ b/guides/deployment/heroku.md
@@ -97,11 +97,11 @@ The buildpack uses a predefined Elixir and Erlang version, but to avoid surprise
 
 ```console
 # Elixir version
-elixir_version=1.14.0
+elixir_version=1.15.0
 
 # Erlang version
 # https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions
-erlang_version=24.3
+erlang_version=25.3
 
 # Invoke assets.deploy defined in your mix.exs to deploy assets with esbuild
 # Note we nuke the esbuild executable from the image
diff --git a/guides/deployment/releases.md b/guides/deployment/releases.md
index c89ef2add..7e4d1668f 100644
--- a/guides/deployment/releases.md
+++ b/guides/deployment/releases.md
@@ -169,11 +169,11 @@ If you call `mix phx.gen.release --docker` you'll see a new file with these cont
 #   - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
 #   - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20230612-slim - for the release image
 #   - https://pkgs.org/ - resource for finding needed packages
-#   - Ex: hexpm/elixir:1.14.5-erlang-25.3.2.4-debian-bullseye-20230612-slim
+#   - Ex: hexpm/elixir:1.15.8-erlang-25.3.2.15-debian-bookworm-20241016-slim
 #
-ARG ELIXIR_VERSION=1.14.5
-ARG OTP_VERSION=25.3.2.4
-ARG DEBIAN_VERSION=bullseye-20230612-slim
+ARG ELIXIR_VERSION=1.15.8
+ARG OTP_VERSION=25.3.2.15
+ARG DEBIAN_VERSION=bookworm-20241016-slim
 
 ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
 ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
diff --git a/guides/introduction/installation.md b/guides/introduction/installation.md
index e7e96a9f9..11b2c3969 100644
--- a/guides/introduction/installation.md
+++ b/guides/introduction/installation.md
@@ -8,7 +8,7 @@ In order to build a Phoenix application, we will need a few dependencies install
 
 Please take a look at this list and make sure to install anything necessary for your system. Having dependencies installed in advance can prevent frustrating problems later on.
 
-## Elixir 1.14 or later
+## Elixir 1.15 or later
 
 Phoenix is written in Elixir, and our application code will also be written in Elixir. We won't get far in a Phoenix app without it! The Elixir site maintains a great [Installation Page](https://elixir-lang.org/install.html) to help.
 
@@ -28,13 +28,13 @@ When we install Elixir using instructions from the Elixir [Installation Page](ht
 
 ## Phoenix
 
-To check that we are on Elixir 1.14 and Erlang 24 or later, run:
+To check that we are on Elixir 1.15 and Erlang 24 or later, run:
 
 ```console
 elixir -v
 Erlang/OTP 24 [erts-12.0] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
 
-Elixir 1.14.0
+Elixir 1.15.0
 ```
 
 Once we have Elixir and Erlang, we are ready to install the Phoenix application generator:
diff --git a/guides/introduction/up_and_running.md b/guides/introduction/up_and_running.md
index 8d07024b1..df1871d49 100644
--- a/guides/introduction/up_and_running.md
+++ b/guides/introduction/up_and_running.md
@@ -86,7 +86,7 @@ And finally, we'll start the Phoenix server:
 
 ```console
 $ mix phx.server
-[info] Running HelloWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
+[info] Running HelloWeb.Endpoint with Bandit 1.5.7 at 127.0.0.1:4000 (http)
 [info] Access HelloWeb.Endpoint at http://localhost:4000
 [watch] build finished, watching for changes...
 ...
diff --git a/guides/json_and_apis.md b/guides/json_and_apis.md
index c6638dfdf..5994229dc 100644
--- a/guides/json_and_apis.md
+++ b/guides/json_and_apis.md
@@ -173,7 +173,7 @@ end
 
 This view is very simple. The `index` function receives all URLs, and converts them into a list of maps. Those maps are placed inside the data key at the root, exactly as we saw when interfacing with our application from `cURL`. In other words, our JSON view converts our complex data into simple Elixir data-structures. Once our view layer returns, Phoenix uses the `Jason` library to encode JSON and send the response to the client.
 
-If you explore the remaining the controller, you will learn the `show` action is similar to the `index` one. For `create`, `update`, and `delete` actions, Phoenix uses one other important feature, called "Action fallback".
+If you explore the remaining controller, you will learn the `show` action is similar to the `index` one. For `create`, `update`, and `delete` actions, Phoenix uses one other important feature, called "Action fallback".
 
 ## Action fallback
 
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index 899a9f84b..dc4807b65 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -223,9 +223,7 @@ defmodule Mix.Tasks.Phx.New do
             Task.async(fn -> cmd(project, cmd, log: false, cd: project.web_path) end)
           end)
 
-        if rebar_available?() do
-          cmd(project, "mix deps.compile")
-        end
+        cmd(project, "mix deps.compile")
 
         Task.await_many(tasks, :infinity)
       end
@@ -254,22 +252,13 @@ defmodule Mix.Tasks.Phx.New do
   defp maybe_cd(path, func), do: path && File.cd!(path, func)
 
   defp install_mix(project, install?) do
-    if install? && hex_available?() do
+    if install? do
       cmd(project, "mix deps.get")
     else
       ["$ mix deps.get"]
     end
   end
 
-  # TODO: Elixir v1.15 automatically installs Hex/Rebar if missing, so we can simplify this.
-  defp hex_available? do
-    Code.ensure_loaded?(Hex)
-  end
-
-  defp rebar_available? do
-    Mix.Rebar.rebar_cmd(:rebar3)
-  end
-
   defp print_missing_steps(steps) do
     Mix.shell().info("""
 
@@ -389,9 +378,9 @@ defmodule Mix.Tasks.Phx.New do
   end
 
   defp elixir_version_check! do
-    unless Version.match?(System.version(), "~> 1.14") do
+    unless Version.match?(System.version(), "~> 1.15") do
       Mix.raise(
-        "Phoenix v#{@version} requires at least Elixir v1.14\n " <>
+        "Phoenix v#{@version} requires at least Elixir v1.15\n " <>
           "You have #{System.version()}. Please update accordingly"
       )
     end
diff --git a/installer/mix.exs b/installer/mix.exs
index 98ca0634c..518f3a243 100644
--- a/installer/mix.exs
+++ b/installer/mix.exs
@@ -17,7 +17,7 @@ defmodule Phx.New.MixProject do
   #   4. test/test_helper.exs at the root
   #   5. installer/lib/mix/tasks/phx.new.ex
   #
-  @elixir_requirement "~> 1.14"
+  @elixir_requirement "~> 1.15"
 
   def project do
     [
diff --git a/installer/templates/phx_assets/app.js b/installer/templates/phx_assets/app.js
index 0d9f8cdd1..9ab0855a5 100644
--- a/installer/templates/phx_assets/app.js
+++ b/installer/templates/phx_assets/app.js
@@ -33,6 +33,11 @@ import "phoenix_html"
 <%= @live_comment %>window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
 <%= @live_comment %>window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
 
+// Enable server log streaming to client.
+<%= @live_comment %>window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
+<%= @live_comment %>  reloader.enableServerLogs()
+<%= @live_comment %>  window.liveReloader = reloader
+<%= @live_comment %>})
 // connect if there are any LiveViews on the page
 <%= @live_comment %>liveSocket.connect()
 
diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs
index 661291d54..97c1bbf53 100644
--- a/installer/templates/phx_single/config/config.exs
+++ b/installer/templates/phx_single/config/config.exs
@@ -56,7 +56,7 @@ config :tailwind,
   ]<% end %>
 
 # Configures Elixir's Logger
-config :logger, :console,
+config :logger, :default_formatter,
   format: "$time $metadata[$level] $message\n",
   metadata: [:request_id]
 
diff --git a/installer/templates/phx_single/config/dev.exs b/installer/templates/phx_single/config/dev.exs
index 73deeaf34..3df396383 100644
--- a/installer/templates/phx_single/config/dev.exs
+++ b/installer/templates/phx_single/config/dev.exs
@@ -45,6 +45,7 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
 # Watch static and templates for browser reloading.
 config :<%= @app_name %>, <%= @endpoint_module %>,
   live_reload: [
+    web_console_logger: true,
     patterns: [
       ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",<%= if @gettext do %>
       ~r"priv/gettext/.*(po)$",<% end %>
@@ -56,7 +57,7 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
 config :<%= @app_name %>, dev_routes: true
 
 # Do not include metadata nor timestamps in development logs
-config :logger, :console, format: "[$level] $message\n"
+config :logger, :default_formatter, format: "[$level] $message\n"
 
 # Set a higher stacktrace during development. Avoid configuring such
 # in production as building large stacktraces may be expensive.
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index 60fe5b267..ed631a252 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -9,11 +9,12 @@ defmodule <%= @app_module %>.MixProject do
       config_path: "../../config/config.exs",
       deps_path: "../../deps",
       lockfile: "../../mix.lock",<% end %>
-      elixir: "~> 1.14",
+      elixir: "~> 1.15",
       elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       aliases: aliases(),
-      deps: deps()
+      deps: deps(),
+      listeners: [Phoenix.CodeReloader]
     ]
   end
 
diff --git a/installer/templates/phx_umbrella/apps/app_name/mix.exs b/installer/templates/phx_umbrella/apps/app_name/mix.exs
index b1510c8c2..5ba7b9323 100644
--- a/installer/templates/phx_umbrella/apps/app_name/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/mix.exs
@@ -9,7 +9,7 @@ defmodule <%= @app_module %>.MixProject do
       config_path: "../../config/config.exs",
       deps_path: "../../deps",
       lockfile: "../../mix.lock",
-      elixir: "~> 1.14",
+      elixir: "~> 1.15",
       elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       aliases: aliases(),
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
index bcc1304b4..d73204be7 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
@@ -45,6 +45,7 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,
 # Watch static and templates for browser reloading.
 config :<%= @web_app_name %>, <%= @endpoint_module %>,
   live_reload: [
+    web_console_logger: true,
     patterns: [
       ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",<%= if @gettext do %>
       ~r"priv/gettext/.*(po)$",<% end %>
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
index 12f347b4d..f0bd3d0cd 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -9,11 +9,12 @@ defmodule <%= @web_namespace %>.MixProject do
       config_path: "../../config/config.exs",
       deps_path: "../../deps",
       lockfile: "../../mix.lock",
-      elixir: "~> 1.14",
+      elixir: "~> 1.15",
       elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       aliases: aliases(),
-      deps: deps()
+      deps: deps(),
+      listeners: [Phoenix.CodeReloader]
     ]
   end
 
diff --git a/installer/templates/phx_umbrella/config/dev.exs b/installer/templates/phx_umbrella/config/dev.exs
index c48d06047..a049be27d 100644
--- a/installer/templates/phx_umbrella/config/dev.exs
+++ b/installer/templates/phx_umbrella/config/dev.exs
@@ -1,7 +1,7 @@
 import Config
 
 # Do not include metadata nor timestamps in development logs
-config :logger, :console, format: "[$level] $message\n"
+config :logger, :default_formatter, format: "[$level] $message\n"
 
 # Initialize plugs at runtime for faster development compilation
 config :phoenix, :plug_init_mode, :runtime<%= if @html do %>
diff --git a/installer/templates/phx_umbrella/config/extra_config.exs b/installer/templates/phx_umbrella/config/extra_config.exs
index 68d1c6605..a34ed28f2 100644
--- a/installer/templates/phx_umbrella/config/extra_config.exs
+++ b/installer/templates/phx_umbrella/config/extra_config.exs
@@ -1,7 +1,7 @@
 import Config
 
 # Configures Elixir's Logger
-config :logger, :console,
+config :logger, :default_formatter,
   format: "$time $metadata[$level] $message\n",
   metadata: [:request_id]
 
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index 83ac2a9f3..c604ef553 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -76,7 +76,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   def flash_group(assigns) do
     ~H"""
-    <div id={@id}>
+    <div id={@id} aria-live="polite">
       <.flash kind={:info} title=<%= maybe_heex_attr_gettext.("Success!", @gettext) %> flash={@flash} />
       <.flash kind={:error} title=<%= maybe_heex_attr_gettext.("Error!", @gettext) %> flash={@flash} />
       <.flash
@@ -120,7 +120,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       </.simple_form>
   """
   attr :for, :any, required: true, doc: "the data structure for the form"
-  attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+  attr :as, :any, doc: "the server side parameter to collect all input under"
 
   attr :rest, :global,
     include: ~w(autocomplete name rel action enctype method novalidate target multipart),
@@ -130,8 +130,10 @@ defmodule <%= @web_namespace %>.CoreComponents do
   slot :actions, doc: "the slot for form actions, such as a submit button"
 
   def simple_form(assigns) do
+    assigns = assign(assigns, :as, if(assigns[:as], do: %{as: assigns[:as]}, else: %{}))
+
     ~H"""
-    <.form :let={f} for={@for} as={@as} {@rest}>
+    <.form :let={f} for={@for} {@as} {@rest}>
       <div class="mt-10 space-y-8 bg-white">
         <%%= render_slot(@inner_block, f) %>
         <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
@@ -265,7 +267,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       <select
         id={@id}
         name={@name}
-        class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
+        class="mt-2 block w-full rounded-md border border-zinc-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
         multiple={@multiple}
         {@rest}
       >
@@ -339,7 +341,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   def error(assigns) do
     ~H"""
-    <p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
+    <p class="mt-1.5 flex gap-1.5 text-sm leading-6 text-rose-600">
       <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
       <%%= render_slot(@inner_block) %>
     </p>
diff --git a/installer/templates/phx_web/components/layouts/app.html.heex b/installer/templates/phx_web/components/layouts/app.html.heex
index c50aa21d2..d16409073 100644
--- a/installer/templates/phx_web/components/layouts/app.html.heex
+++ b/installer/templates/phx_web/components/layouts/app.html.heex
@@ -26,7 +26,7 @@
 </header>
 <main class="px-4 py-20 sm:px-6 lg:px-8">
   <div class="mx-auto max-w-2xl">
-    <.flash_group flash={@flash} />
     <%%= @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 ae33a8b50..12f7f1d87 100644
--- a/installer/templates/phx_web/components/layouts/root.html.heex
+++ b/installer/templates/phx_web/components/layouts/root.html.heex
@@ -4,8 +4,8 @@
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="csrf-token" content={get_csrf_token()} />
-    <.live_title suffix=" · Phoenix Framework">
-      <%%= assigns[:page_title] || "<%= @app_module %>" %>
+    <.live_title default="<%= @app_module %>" suffix=" · Phoenix Framework">
+      <%%= assigns[:page_title] %>
     </.live_title>
     <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
     <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
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 fe4507e46..071a7104d 100644
--- a/installer/templates/phx_web/controllers/page_html/home.html.heex
+++ b/installer/templates/phx_web/controllers/page_html/home.html.heex
@@ -201,6 +201,21 @@
               Join our Discord server
             </a>
           </div>
+          <div>
+            <a
+              href="https://elixir-slack.community/"
+              class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
+            >
+              <svg
+                viewBox="0 0 16 16"
+                aria-hidden="true"
+                class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
+              >
+                <path d="M3.361 10.11a1.68 1.68 0 1 1-1.68-1.681h1.68v1.682ZM4.209 10.11a1.68 1.68 0 1 1 3.361 0v4.21a1.68 1.68 0 1 1-3.361 0v-4.21ZM5.89 3.361a1.68 1.68 0 1 1 1.681-1.68v1.68H5.89ZM5.89 4.209a1.68 1.68 0 1 1 0 3.361H1.68a1.68 1.68 0 1 1 0-3.361h4.21ZM12.639 5.89a1.68 1.68 0 1 1 1.68 1.681h-1.68V5.89ZM11.791 5.89a1.68 1.68 0 1 1-3.361 0V1.68a1.68 1.68 0 0 1 3.361 0v4.21ZM10.11 12.639a1.68 1.68 0 1 1-1.681 1.68v-1.68h1.682ZM10.11 11.791a1.68 1.68 0 1 1 0-3.361h4.21a1.68 1.68 0 1 1 0 3.361h-4.21Z" />
+              </svg>
+              Join us on Slack
+            </a>
+          </div>
           <div>
             <a
               href="https://fly.io/docs/elixir/getting-started/"
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index 3201c4ce1..7fec805e3 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -425,7 +425,7 @@ defmodule Mix.Phoenix.Schema do
       mix phx.gen.schema Comment comments body:text status:enum:published:unpublished
   """
 
-  defp validate_attr!({name, :datetime}), do: validate_attr!({name, :naive_datetime})
+  defp validate_attr!({name, :datetime}), do: {name, :naive_datetime}
 
   defp validate_attr!({name, :array}) do
     Mix.raise("""
@@ -438,7 +438,6 @@ defmodule Mix.Phoenix.Schema do
 
   defp validate_attr!({_name, :enum}), do: Mix.raise(@enum_missing_value_error)
   defp validate_attr!({_name, type} = attr) when type in @valid_types, do: attr
-  defp validate_attr!({_name, {:enum, _vals}} = attr), do: attr
   defp validate_attr!({_name, {type, _}} = attr) when type in @valid_types, do: attr
 
   defp validate_attr!({_, type}) do
diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex
index 4e2910573..04cb5d234 100644
--- a/lib/mix/tasks/phx.gen.auth.ex
+++ b/lib/mix/tasks/phx.gen.auth.ex
@@ -274,69 +274,91 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
     case Keyword.fetch(context.opts, :live) do
       {:ok, true} ->
         live_files = [
-          "registration_live.ex": [web_pre, "live", web_path, "#{singular}_registration_live.ex"],
+          "registration_live.ex": [
+            web_pre,
+            "live",
+            web_path,
+            "#{singular}_live",
+            "registration.ex"
+          ],
           "registration_live_test.exs": [
             web_test_pre,
             "live",
             web_path,
-            "#{singular}_registration_live_test.exs"
+            "#{singular}_live",
+            "registration_test.exs"
           ],
-          "login_live.ex": [web_pre, "live", web_path, "#{singular}_login_live.ex"],
+          "login_live.ex": [web_pre, "live", web_path, "#{singular}_live", "login.ex"],
           "login_live_test.exs": [
             web_test_pre,
             "live",
             web_path,
-            "#{singular}_login_live_test.exs"
+            "#{singular}_live",
+            "login_test.exs"
           ],
           "reset_password_live.ex": [
             web_pre,
             "live",
             web_path,
-            "#{singular}_reset_password_live.ex"
+            "#{singular}_live",
+            "reset_password.ex"
           ],
           "reset_password_live_test.exs": [
             web_test_pre,
             "live",
             web_path,
-            "#{singular}_reset_password_live_test.exs"
+            "#{singular}_live",
+            "reset_password_test.exs"
           ],
           "forgot_password_live.ex": [
             web_pre,
             "live",
             web_path,
-            "#{singular}_forgot_password_live.ex"
+            "#{singular}_live",
+            "forgot_password.ex"
           ],
           "forgot_password_live_test.exs": [
             web_test_pre,
             "live",
             web_path,
-            "#{singular}_forgot_password_live_test.exs"
+            "#{singular}_live",
+            "forgot_password_test.exs"
           ],
-          "settings_live.ex": [web_pre, "live", web_path, "#{singular}_settings_live.ex"],
+          "settings_live.ex": [web_pre, "live", web_path, "#{singular}_live", "settings.ex"],
           "settings_live_test.exs": [
             web_test_pre,
             "live",
             web_path,
-            "#{singular}_settings_live_test.exs"
+            "#{singular}_live",
+            "settings_test.exs"
+          ],
+          "confirmation_live.ex": [
+            web_pre,
+            "live",
+            web_path,
+            "#{singular}_live",
+            "confirmation.ex"
           ],
-          "confirmation_live.ex": [web_pre, "live", web_path, "#{singular}_confirmation_live.ex"],
           "confirmation_live_test.exs": [
             web_test_pre,
             "live",
             web_path,
-            "#{singular}_confirmation_live_test.exs"
+            "#{singular}_live",
+            "confirmation_test.exs"
           ],
           "confirmation_instructions_live.ex": [
             web_pre,
             "live",
             web_path,
-            "#{singular}_confirmation_instructions_live.ex"
+            "#{singular}_live",
+            "confirmation_instructions.ex"
           ],
           "confirmation_instructions_live_test.exs": [
             web_test_pre,
             "live",
             web_path,
-            "#{singular}_confirmation_instructions_live_test.exs"
+            "#{singular}_live",
+            "confirmation_instructions_test.exs"
           ]
         ]
 
diff --git a/lib/mix/tasks/phx.gen.auth/hashing_library.ex b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
index cd1d2b7c7..2d37afc26 100644
--- a/lib/mix/tasks/phx.gen.auth/hashing_library.ex
+++ b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
@@ -40,7 +40,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
     lib = %__MODULE__{
       name: :argon2,
       module: Argon2,
-      mix_dependency: ~s|{:argon2_elixir, "~> 3.0"}|,
+      mix_dependency: ~s|{:argon2_elixir, "~> 4.0"}|,
       test_config: """
       config :argon2_elixir, t_cost: 1, m_cost: 8
       """
diff --git a/lib/mix/tasks/phx.gen.auth/injector.ex b/lib/mix/tasks/phx.gen.auth/injector.ex
index 90a2116c8..48ae16782 100644
--- a/lib/mix/tasks/phx.gen.auth/injector.ex
+++ b/lib/mix/tasks/phx.gen.auth/injector.ex
@@ -161,7 +161,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
   Menu code to inject into the application layout template.
   """
   def app_layout_menu_code_to_inject(%Schema{} = schema, padding \\ 4, newline \\ "\n") do
-    already_injected_str = "#{schema.route_prefix}/log_in"
+    already_injected_str = "#{schema.route_prefix}/log-in"
 
     base_tailwind_classes = "text-[0.8125rem] leading-6 text-zinc-900"
     link_tailwind_classes = "#{base_tailwind_classes} font-semibold hover:text-zinc-700"
@@ -182,7 +182,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
         </li>
         <li>
           <.link
-            href={~p"#{schema.route_prefix}/log_out"}
+            href={~p"#{schema.route_prefix}/log-out"}
             method="delete"
             class="#{link_tailwind_classes}"
           >
@@ -200,7 +200,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
         </li>
         <li>
           <.link
-            href={~p"#{schema.route_prefix}/log_in"}
+            href={~p"#{schema.route_prefix}/log-in"}
             class="#{link_tailwind_classes}"
           >
             Log in
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index b435d3a4d..76fe2aa38 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -198,7 +198,7 @@ defmodule Mix.Tasks.Phx.Gen.Html do
         ~s(<.input field={f[#{inspect(key)}]} type="checkbox" label="#{label(key)}" />)
 
       {key, :text} ->
-        ~s(<.input field={f[#{inspect(key)}]} type="text" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="textarea" label="#{label(key)}" />)
 
       {key, :date} ->
         ~s(<.input field={f[#{inspect(key)}]} type="date" label="#{label(key)}" />)
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
index 59a51514d..f0b6aa28a 100644
--- a/lib/mix/tasks/phx.gen.live.ex
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -285,7 +285,7 @@ defmodule Mix.Tasks.Phx.Gen.Live do
         ~s(<.input field={@form[#{inspect(key)}]} type="checkbox" label="#{label(key)}" />)
 
       {key, :text} ->
-        ~s(<.input field={@form[#{inspect(key)}]} type="text" label="#{label(key)}" />)
+        ~s(<.input field={@form[#{inspect(key)}]} type="textarea" label="#{label(key)}" />)
 
       {key, :date} ->
         ~s(<.input field={@form[#{inspect(key)}]} type="date" label="#{label(key)}" />)
diff --git a/lib/phoenix/code_reloader.ex b/lib/phoenix/code_reloader.ex
index 809d55525..433cc7547 100644
--- a/lib/phoenix/code_reloader.ex
+++ b/lib/phoenix/code_reloader.ex
@@ -31,6 +31,19 @@ defmodule Phoenix.CodeReloader do
 
   This function is a no-op and returns `:ok` if Mix is not available.
 
+  The reloader should also be configured as a Mix listener in project's
+  mix.exs file (since Elixir v1.18):
+
+      def project do
+        [
+          ...,
+          listeners: [Phoenix.CodeReloader]
+        ]
+      end
+
+  This way the reloader can notice whenever the project is compiled
+  concurrently.
+
   ## Options
 
     * `:reloadable_args` - additional CLI args to pass to the compiler tasks.
@@ -57,6 +70,10 @@ defmodule Phoenix.CodeReloader do
   @spec sync :: :ok
   defdelegate sync, to: Phoenix.CodeReloader.Server
 
+  @doc false
+  @spec child_spec(keyword) :: Supervisor.child_spec()
+  defdelegate child_spec(opts), to: Phoenix.CodeReloader.MixListener
+
   ## Plug
 
   @behaviour Plug
diff --git a/lib/phoenix/code_reloader/mix_listener.ex b/lib/phoenix/code_reloader/mix_listener.ex
new file mode 100644
index 000000000..f136314ee
--- /dev/null
+++ b/lib/phoenix/code_reloader/mix_listener.ex
@@ -0,0 +1,72 @@
+defmodule Phoenix.CodeReloader.MixListener do
+  @moduledoc false
+
+  use GenServer
+
+  @name __MODULE__
+
+  @spec start_link(keyword) :: GenServer.on_start()
+  def start_link(_opts) do
+    GenServer.start_link(__MODULE__, {}, name: @name)
+  end
+
+  @spec started? :: boolean()
+  def started? do
+    Process.whereis(Phoenix.CodeReloader.MixListener) != nil
+  end
+
+  @doc """
+  Unloads all modules invalidated by external compilations.
+
+  Only reloads modules from the given apps.
+  """
+  @spec purge([atom()]) :: :ok
+  def purge(apps) do
+    GenServer.call(@name, {:purge, apps}, :infinity)
+  end
+
+  @impl true
+  def init({}) do
+    {:ok, %{to_purge: %{}}}
+  end
+
+  @impl true
+  def handle_call({:purge, apps}, _from, state) do
+    for app <- apps, modules = state.to_purge[app] do
+      purge_modules(modules)
+    end
+
+    {:reply, :ok, %{state | to_purge: %{}}}
+  end
+
+  @impl true
+  def handle_info({:modules_compiled, info}, state) do
+    if info.os_pid == System.pid() do
+      # Ignore compilations from ourselves, because the modules are
+      # already updated in memory
+      {:noreply, state}
+    else
+      %{changed: changed, removed: removed} = info.modules_diff
+
+      state =
+        update_in(state.to_purge[info.app], fn to_purge ->
+          to_purge = to_purge || MapSet.new()
+          to_purge = Enum.into(changed, to_purge)
+          Enum.into(removed, to_purge)
+        end)
+
+      {:noreply, state}
+    end
+  end
+
+  def handle_info(_message, state) do
+    {:noreply, state}
+  end
+
+  defp purge_modules(modules) do
+    for module <- modules do
+      :code.purge(module)
+      :code.delete(module)
+    end
+  end
+end
diff --git a/lib/phoenix/code_reloader/server.ex b/lib/phoenix/code_reloader/server.ex
index 8f56f423c..16ff92bf1 100644
--- a/lib/phoenix/code_reloader/server.ex
+++ b/lib/phoenix/code_reloader/server.ex
@@ -63,23 +63,39 @@ defmodule Phoenix.CodeReloader.Server do
     apps = endpoint.config(:reloadable_apps) || default_reloadable_apps()
     args = Keyword.get(opts, :reloadable_args, ["--no-all-warnings"])
 
-    # We do a backup of the endpoint in case compilation fails.
-    # If so we can bring it back to finish the request handling.
-    backup = load_backup(endpoint)
     froms = all_waiting([from], endpoint)
 
-    {res, out} =
-      proxy_io(fn ->
-        try do
-          mix_compile(Code.ensure_loaded(Mix.Task), compilers, apps, args, state.timestamp)
-        catch
-          :exit, {:shutdown, 1} ->
-            :error
-
-          kind, reason ->
-            IO.puts(Exception.format(kind, reason, __STACKTRACE__))
-            :error
-        end
+    {backup, res, out} =
+      with_build_lock(fn ->
+        purge_fallback? =
+          if Phoenix.CodeReloader.MixListener.started?() do
+            Phoenix.CodeReloader.MixListener.purge(apps)
+            false
+          else
+            warn_missing_mix_listener()
+            true
+          end
+
+        # We do a backup of the endpoint in case compilation fails.
+        # If so we can bring it back to finish the request handling.
+        backup = load_backup(endpoint)
+
+        {res, out} =
+          proxy_io(fn ->
+            try do
+              task_loaded = Code.ensure_loaded(Mix.Task)
+              mix_compile(task_loaded, compilers, apps, args, state.timestamp, purge_fallback?)
+            catch
+              :exit, {:shutdown, 1} ->
+                :error
+
+              kind, reason ->
+                IO.puts(Exception.format(kind, reason, __STACKTRACE__))
+                :error
+            end
+          end)
+
+        {backup, res, out}
       end)
 
     reply =
@@ -175,7 +191,34 @@ defmodule Phoenix.CodeReloader.Server do
     defp purge_protocols(_path), do: :ok
   end
 
-  defp mix_compile({:module, Mix.Task}, compilers, apps_to_reload, compile_args, timestamp) do
+  defp warn_missing_mix_listener do
+    listeners_supported? = Version.match?(System.version(), ">= 1.18.0-dev")
+
+    if listeners_supported? do
+      IO.warn("""
+      a Mix listener expected by Phoenix.CodeReloader is missing.
+
+      Please add the listener to your mix.exs configuration, like so:
+
+          def project do
+            [
+              ...,
+              listeners: [Phoenix.CodeReloader]
+            ]
+          end
+
+      """)
+    end
+  end
+
+  defp mix_compile(
+         {:module, Mix.Task},
+         compilers,
+         apps_to_reload,
+         compile_args,
+         timestamp,
+         purge_fallback?
+       ) do
     config = Mix.Project.config()
     path = Mix.Project.consolidation_path(config)
 
@@ -184,8 +227,25 @@ defmodule Phoenix.CodeReloader.Server do
       purge_protocols(path)
     end
 
-    mix_compile_deps(Mix.Dep.cached(), apps_to_reload, compile_args, compilers, timestamp, path)
-    mix_compile_project(config[:app], apps_to_reload, compile_args, compilers, timestamp, path)
+    mix_compile_deps(
+      Mix.Dep.cached(),
+      apps_to_reload,
+      compile_args,
+      compilers,
+      timestamp,
+      path,
+      purge_fallback?
+    )
+
+    mix_compile_project(
+      config[:app],
+      apps_to_reload,
+      compile_args,
+      compilers,
+      timestamp,
+      path,
+      purge_fallback?
+    )
 
     if config[:consolidate_protocols] do
       # If we are consolidating protocols, we need to purge all of its modules
@@ -198,30 +258,46 @@ defmodule Phoenix.CodeReloader.Server do
     :ok
   end
 
-  defp mix_compile({:error, _reason}, _, _, _, _) do
+  defp mix_compile({:error, _reason}, _, _, _, _, _) do
     raise "the Code Reloader is enabled but Mix is not available. If you want to " <>
             "use the Code Reloader in production or inside an escript, you must add " <>
             ":mix to your applications list. Otherwise, you must disable code reloading " <>
             "in such environments"
   end
 
-  defp mix_compile_deps(deps, apps_to_reload, compile_args, compilers, timestamp, path) do
+  defp mix_compile_deps(
+         deps,
+         apps_to_reload,
+         compile_args,
+         compilers,
+         timestamp,
+         path,
+         purge_fallback?
+       ) do
     for dep <- deps, dep.app in apps_to_reload do
       Mix.Dep.in_dependency(dep, fn _ ->
-        mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
+        mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?)
       end)
     end
   end
 
-  defp mix_compile_project(nil, _, _, _, _, _), do: :ok
-
-  defp mix_compile_project(app, apps_to_reload, compile_args, compilers, timestamp, path) do
+  defp mix_compile_project(nil, _, _, _, _, _, _), do: :ok
+
+  defp mix_compile_project(
+         app,
+         apps_to_reload,
+         compile_args,
+         compilers,
+         timestamp,
+         path,
+         purge_fallback?
+       ) do
     if app in apps_to_reload do
-      mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
+      mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?)
     end
   end
 
-  defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path) do
+  defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?) do
     manifests = Mix.Tasks.Compile.Elixir.manifests()
     configs = Mix.Project.config_files()
     config = Mix.Project.config()
@@ -230,7 +306,8 @@ defmodule Phoenix.CodeReloader.Server do
       [] ->
         # If the manifests are more recent than the timestamp,
         # someone updated this app behind the scenes, so purge all beams.
-        if Mix.Utils.stale?(manifests, [timestamp]) do
+        # TODO: remove once we depend on Elixir 1.18
+        if purge_fallback? and Mix.Utils.stale?(manifests, [timestamp]) do
           purge_modules(Path.join(Mix.Project.app_path(config), "ebin"))
         end
 
@@ -351,4 +428,11 @@ defmodule Phoenix.CodeReloader.Server do
       Logger.configure(compile_time_application: logger_config_app)
     end
   end
+
+  # TODO: remove once we depend on Elixir 1.18
+  if Code.ensure_loaded?(Mix.Project) and function_exported?(Mix.Project, :with_build_lock, 1) do
+    defp with_build_lock(fun), do: Mix.Project.with_build_lock(fun)
+  else
+    defp with_build_lock(fun), do: fun.()
+  end
 end
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 95cb139e5..b13ac956d 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -1094,7 +1094,7 @@ defmodule Phoenix.Controller do
       conn
     else
       content_type = content_type <> "; charset=utf-8"
-      %Plug.Conn{conn | resp_headers: [{"content-type", content_type} | resp_headers]}
+      %{conn | resp_headers: [{"content-type", content_type} | resp_headers]}
     end
   end
 
@@ -1274,7 +1274,7 @@ defmodule Phoenix.Controller do
   end
 
   defp encode_filename(filename, false), do: filename
-  defp encode_filename(filename, true), do: URI.encode(filename)
+  defp encode_filename(filename, true), do: URI.encode(filename, &URI.char_unreserved?/1)
 
   defp get_disposition_type(:attachment), do: "attachment"
   defp get_disposition_type(:inline), do: "inline"
@@ -1323,7 +1323,7 @@ defmodule Phoenix.Controller do
 
   """
   @spec scrub_params(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
-  def scrub_params(conn, required_key) when is_binary(required_key) do
+  def scrub_params(%Plug.Conn{} = conn, required_key) when is_binary(required_key) do
     param = Map.get(conn.params, required_key) |> scrub_param()
 
     unless param do
@@ -1331,7 +1331,7 @@ defmodule Phoenix.Controller do
     end
 
     params = Map.put(conn.params, required_key, param)
-    %Plug.Conn{conn | params: params}
+    %{conn | params: params}
   end
 
   defp scrub_param(%{__struct__: mod} = struct) when is_atom(mod) do
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 465420ac2..a923be9a8 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -859,6 +859,13 @@ defmodule Phoenix.Endpoint do
       The MFA is invoked with the request `%URI{}` as the first argument,
       followed by arguments in the MFA list, and must return a boolean.
 
+    * `:check_csrf` - if the transport should perform CSRF check. To avoid
+      "Cross-Site WebSocket Hijacking", you must have at least one of
+      `check_origin` and `check_csrf` enabled. If you set both to `false`,
+      Phoenix will raise, but it is still possible to disable both by passing
+      a custom MFA to `check_origin`. In such cases, it is your responsibility
+      to ensure at least one of them is enabled. Defaults to `true`
+
     * `:code_reloader` - enable or disable the code reloader. Defaults to your
       endpoint configuration
 
diff --git a/lib/phoenix/endpoint/render_errors.ex b/lib/phoenix/endpoint/render_errors.ex
index 340039901..0e0d494bf 100644
--- a/lib/phoenix/endpoint/render_errors.ex
+++ b/lib/phoenix/endpoint/render_errors.ex
@@ -52,12 +52,12 @@ defmodule Phoenix.Endpoint.RenderErrors do
   end
 
   @doc false
-  def __catch__(conn, kind, reason, stack, opts) do
+  def __catch__(%Plug.Conn{} = conn, kind, reason, stack, opts) do
     conn =
       receive do
         @already_sent ->
           send(self(), @already_sent)
-          %Plug.Conn{conn | state: :sent}
+          %{conn | state: :sent}
       after
         0 ->
           instrument_render_and_send(conn, kind, reason, stack, opts)
@@ -126,13 +126,13 @@ defmodule Phoenix.Endpoint.RenderErrors do
     Controller.render(conn, template, assigns)
   end
 
-  defp maybe_fetch_query_params(conn) do
+  defp maybe_fetch_query_params(%Plug.Conn{} = conn) do
     fetch_query_params(conn)
   rescue
     Plug.Conn.InvalidQueryError ->
       case conn.params do
-        %Plug.Conn.Unfetched{} -> %Plug.Conn{conn | query_params: %{}, params: %{}}
-        params -> %Plug.Conn{conn | query_params: %{}, params: params}
+        %Plug.Conn.Unfetched{} -> %{conn | query_params: %{}, params: %{}}
+        params -> %{conn | query_params: %{}, params: params}
       end
   end
 
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index 5ee904dcb..597f175d9 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -84,11 +84,10 @@ defmodule Phoenix.Endpoint.Supervisor do
     children =
       config_children(mod, secret_conf, default_conf) ++
         pubsub_children(mod, conf) ++
-        socket_children(mod, :child_spec) ++
+        socket_children(mod, conf, :child_spec) ++
         server_children(mod, conf, server?) ++
-        socket_children(mod, :drainer_spec) ++
+        socket_children(mod, conf, :drainer_spec) ++
         watcher_children(mod, conf, server?)
-
     Supervisor.init(children, strategy: :one_for_one)
   end
 
@@ -118,8 +117,9 @@ defmodule Phoenix.Endpoint.Supervisor do
     end
   end
 
-  defp socket_children(endpoint, fun) do
+  defp socket_children(endpoint, conf, fun) do
     for {_, socket, opts} <- Enum.uniq_by(endpoint.__sockets__(), &elem(&1, 1)),
+        _ = check_origin_or_csrf_checked!(conf, opts),
         spec = apply_or_ignore(socket, fun, [[endpoint: endpoint] ++ opts]),
         spec != :ignore do
       spec
@@ -135,6 +135,22 @@ defmodule Phoenix.Endpoint.Supervisor do
     end
   end
 
+  defp check_origin_or_csrf_checked!(endpoint_conf, socket_opts) do
+    check_origin = endpoint_conf[:check_origin]
+
+    for {transport, transport_opts} <- socket_opts, is_list(transport_opts) do
+      check_origin = Keyword.get(transport_opts, :check_origin, check_origin)
+
+      check_csrf = transport_opts[:check_csrf]
+
+      if check_origin == false and check_csrf == false do
+        raise ArgumentError,
+              "one of :check_origin and :check_csrf must be set to non-false value for " <>
+                "transport #{inspect(transport)}"
+      end
+    end
+  end
+
   defp config_children(mod, conf, default_conf) do
     args = {mod, conf, default_conf, name: Module.concat(mod, "Config")}
     [{Phoenix.Config, args}]
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index 77286a8db..7963ef31e 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -332,24 +332,32 @@ defmodule Phoenix.Socket do
   ## USER API
 
   @doc """
-  Adds key-value pairs to socket assigns.
+  Adds a `key`/`value` pair to `socket` assigns.
 
-  A single key-value pair may be passed, a keyword list or map
-  of assigns may be provided to be merged into existing socket
-  assigns.
+  See also `assign/2` to add multiple key/value pairs.
 
   ## Examples
 
       iex> assign(socket, :name, "Elixir")
-      iex> assign(socket, name: "Elixir", logo: "💧")
   """
   def assign(%Socket{} = socket, key, value) do
     assign(socket, [{key, value}])
   end
 
-  def assign(%Socket{} = socket, attrs)
-      when is_map(attrs) or is_list(attrs) do
-    %{socket | assigns: Map.merge(socket.assigns, Map.new(attrs))}
+  @doc """
+  Adds key/value pairs to socket assigns.
+
+  A keyword list or a map of assigns must be given as argument to be merged into existing assigns.
+
+  ## Examples
+
+      iex> assign(socket, name: "Elixir", logo: "💧")
+      iex> assign(socket, %{name: "Elixir"})
+
+  """
+  def assign(%Socket{} = socket, keyword_or_map)
+      when is_map(keyword_or_map) or is_list(keyword_or_map) do
+    %{socket | assigns: Map.merge(socket.assigns, Map.new(keyword_or_map))}
   end
 
   @doc """
@@ -509,7 +517,7 @@ defmodule Phoenix.Socket do
   defp result({:error, _}), do: :error
 
   def __init__({state, %{id: id, endpoint: endpoint} = socket}) do
-    _ = id && endpoint.subscribe(id, link: true)
+    _ = id && endpoint.subscribe(id)
     {:ok, {state, %{socket | transport_pid: self()}}}
   end
 
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index a1c5fd546..c9ab5dd95 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -458,8 +458,9 @@ defmodule Phoenix.Socket.Transport do
 
     * `:user_agent` - the value of the "user-agent" request header
 
+  The CSRF check can be disabled by setting the `:check_csrf` option to `false`.
   """
-  def connect_info(conn, endpoint, keys) do
+  def connect_info(conn, endpoint, keys, opts \\ []) do
     for key <- keys, into: %{} do
       case key do
         :peer_data ->
@@ -478,7 +479,7 @@ defmodule Phoenix.Socket.Transport do
           {:user_agent, fetch_user_agent(conn)}
 
         {:session, session} ->
-          {:session, connect_session(conn, endpoint, session)}
+          {:session, connect_session(conn, endpoint, session, opts)}
 
         {key, val} ->
           {key, val}
@@ -486,26 +487,24 @@ defmodule Phoenix.Socket.Transport do
     end
   end
 
-  defp connect_session(conn, endpoint, {key, store, {csrf_token_key, init}}) do
+  defp connect_session(conn, endpoint, {key, store, {csrf_token_key, init}}, opts) do
     conn = Plug.Conn.fetch_cookies(conn)
+    check_csrf = Keyword.get(opts, :check_csrf, true)
 
-    with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"],
-         cookie when is_binary(cookie) <- conn.cookies[key],
+    with cookie when is_binary(cookie) <- conn.cookies[key],
          conn = put_in(conn.secret_key_base, endpoint.config(:secret_key_base)),
          {_, session} <- store.get(conn, cookie, init),
-         csrf_state when is_binary(csrf_state) <-
-           Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]),
-         true <- Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) do
+         true <- not check_csrf or csrf_token_valid?(conn, session, csrf_token_key) do
       session
     else
       _ -> nil
     end
   end
 
-  defp connect_session(conn, endpoint, {:mfa, {module, function, args}}) do
+  defp connect_session(conn, endpoint, {:mfa, {module, function, args}}, opts) do
     case apply(module, function, args) do
       session_config when is_list(session_config) ->
-        connect_session(conn, endpoint, init_session(session_config))
+        connect_session(conn, endpoint, init_session(session_config), opts)
 
       other ->
         raise ArgumentError,
@@ -542,6 +541,14 @@ defmodule Phoenix.Socket.Transport do
     end
   end
 
+  defp csrf_token_valid?(conn, session, csrf_token_key) do
+    with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"],
+         csrf_state when is_binary(csrf_state) <-
+           Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]) do
+       Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token)
+    end
+  end
+
   defp check_origin_config(handler, endpoint, opts) do
     Phoenix.Config.cache(endpoint, {:check_origin, handler}, fn _ ->
       check_origin =
diff --git a/lib/phoenix/transports/long_poll.ex b/lib/phoenix/transports/long_poll.ex
index de3365cee..3a9915bb4 100644
--- a/lib/phoenix/transports/long_poll.ex
+++ b/lib/phoenix/transports/long_poll.ex
@@ -4,6 +4,7 @@ defmodule Phoenix.Transports.LongPoll do
 
   # 10MB
   @max_base64_size 10_000_000
+  @connect_info_opts [:check_csrf]
 
   import Plug.Conn
   alias Phoenix.Socket.{V1, V2, Transport}
@@ -136,7 +137,10 @@ defmodule Phoenix.Transports.LongPoll do
         (System.system_time(:millisecond) |> Integer.to_string())
 
     keys = Keyword.get(opts, :connect_info, [])
-    connect_info = Transport.connect_info(conn, endpoint, keys)
+
+    connect_info =
+      Transport.connect_info(conn, endpoint, keys, Keyword.take(opts, @connect_info_opts))
+
     arg = {endpoint, handler, opts, conn.params, priv_topic, connect_info}
     spec = {Phoenix.Transports.LongPoll.Server, arg}
 
@@ -226,7 +230,7 @@ defmodule Phoenix.Transports.LongPoll do
   defp client_ref(pid) when is_pid(pid), do: self()
 
   defp subscribe(endpoint, topic) when is_binary(topic),
-    do: Phoenix.PubSub.subscribe(endpoint.config(:pubsub_server), topic, link: true)
+    do: Phoenix.PubSub.subscribe(endpoint.config(:pubsub_server), topic)
 
   defp subscribe(_endpoint, pid) when is_pid(pid),
     do: :ok
diff --git a/lib/phoenix/transports/long_poll_server.ex b/lib/phoenix/transports/long_poll_server.ex
index 37e8c90bd..e3a5f23d8 100644
--- a/lib/phoenix/transports/long_poll_server.ex
+++ b/lib/phoenix/transports/long_poll_server.ex
@@ -33,7 +33,7 @@ defmodule Phoenix.Transports.LongPoll.Server do
           client_ref: nil
         }
 
-        :ok = PubSub.subscribe(state.pubsub_server, priv_topic, link: true)
+        :ok = PubSub.subscribe(state.pubsub_server, priv_topic)
         schedule_inactive_shutdown(state.window_ms)
         {:ok, state}
 
diff --git a/lib/phoenix/transports/websocket.ex b/lib/phoenix/transports/websocket.ex
index f04cc4350..dfc7bd250 100644
--- a/lib/phoenix/transports/websocket.ex
+++ b/lib/phoenix/transports/websocket.ex
@@ -15,6 +15,8 @@ defmodule Phoenix.Transports.WebSocket do
   #
   @behaviour Plug
 
+  @connect_info_opts [:check_csrf]
+
   import Plug.Conn
 
   alias Phoenix.Socket.{V1, V2, Transport}
@@ -45,7 +47,9 @@ defmodule Phoenix.Transports.WebSocket do
 
       %{params: params} = conn ->
         keys = Keyword.get(opts, :connect_info, [])
-        connect_info = Transport.connect_info(conn, endpoint, keys)
+
+        connect_info =
+          Transport.connect_info(conn, endpoint, keys, Keyword.take(opts, @connect_info_opts))
 
         config = %{
           endpoint: endpoint,
diff --git a/priv/templates/phx.gen.auth/auth.ex b/priv/templates/phx.gen.auth/auth.ex
index 472a0214c..af36929d2 100644
--- a/priv/templates/phx.gen.auth/auth.ex
+++ b/priv/templates/phx.gen.auth/auth.ex
@@ -158,7 +158,7 @@ defmodule <%= inspect auth_module %> do
       socket =
         socket
         |> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
-        |> Phoenix.LiveView.redirect(to: ~p"<%= schema.route_prefix %>/log_in")
+        |> Phoenix.LiveView.redirect(to: ~p"<%= schema.route_prefix %>/log-in")
 
       {:halt, socket}
     end
@@ -208,7 +208,7 @@ defmodule <%= inspect auth_module %> do
       conn
       |> put_flash(:error, "You must log in to access this page.")
       |> maybe_store_return_to()
-      |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")
+      |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
       |> halt()
     end
   end
diff --git a/priv/templates/phx.gen.auth/confirmation_edit.html.heex b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
index ea7d8d170..0f6d96d41 100644
--- a/priv/templates/phx.gen.auth/confirmation_edit.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
@@ -7,8 +7,8 @@
     </:actions>
   </.simple_form>
 
-  <p class="text-center mt-4">
+  <p :if={!@current_<%= schema.singular %>} class="text-center mt-4">
     <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-    | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
+    | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
   </p>
 </div>
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
index 2917a418f..ac8d9b298 100644
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationInstructionsLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ConfirmationInstructions do
   use <%= inspect context.web_module %>, :live_view
 
   alias <%= inspect context.module %>
@@ -20,9 +20,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         </:actions>
       </.simple_form>
 
-      <p class="text-center mt-4">
+      <p :if={!@current_<%= schema.singular %>} class="text-center mt-4">
         <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-        | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
+        | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
       </p>
     </div>
     """
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index 0ef2b6225..251afacec 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Confirmation do
   use <%= inspect context.web_module %>, :live_view
 
   alias <%= inspect context.module %>
@@ -15,9 +15,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         </:actions>
       </.simple_form>
 
-      <p class="text-center mt-4">
+      <p :if={!@current_<%= schema.singular %>} class="text-center mt-4">
         <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-        | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
+        | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
       </p>
     </div>
     """
diff --git a/priv/templates/phx.gen.auth/confirmation_new.html.heex b/priv/templates/phx.gen.auth/confirmation_new.html.heex
index e1d75c787..97b952445 100644
--- a/priv/templates/phx.gen.auth/confirmation_new.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_new.html.heex
@@ -13,8 +13,8 @@
     </:actions>
   </.simple_form>
 
-  <p class="text-center mt-4">
+  <p :if={!@current_<%= schema.singular %>} class="text-center mt-4">
     <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-    | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
+    | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
   </p>
 </div>
diff --git a/priv/templates/phx.gen.auth/context_functions.ex b/priv/templates/phx.gen.auth/context_functions.ex
index 3d4e25936..e3a2c090f 100644
--- a/priv/templates/phx.gen.auth/context_functions.ex
+++ b/priv/templates/phx.gen.auth/context_functions.ex
@@ -154,7 +154,7 @@
 
   ## Examples
 
-      iex> deliver_<%= schema.singular %>_update_email_instructions(<%= schema.singular %>, current_email, &url(~p"<%= schema.route_prefix %>/settings/confirm_email/#{&1}"))
+      iex> deliver_<%= schema.singular %>_update_email_instructions(<%= schema.singular %>, current_email, &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}"))
       {:ok, %{to: ..., body: ...}}
 
   """
@@ -288,7 +288,7 @@
 
   ## Examples
 
-      iex> deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, &url(~p"<%= schema.route_prefix %>/reset_password/#{&1}"))
+      iex> deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}"))
       {:ok, %{to: ..., body: ...}}
 
   """
diff --git a/priv/templates/phx.gen.auth/forgot_password_live.ex b/priv/templates/phx.gen.auth/forgot_password_live.ex
index fb5ba7aa5..dc4a03a59 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live.ex
+++ b/priv/templates/phx.gen.auth/forgot_password_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ForgotPasswordLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ForgotPassword do
   use <%= inspect context.web_module %>, :live_view
 
   alias <%= inspect context.module %>
@@ -21,7 +21,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </.simple_form>
       <p class="text-center text-sm mt-4">
         <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-        | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
+        | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
       </p>
     </div>
     """
@@ -35,7 +35,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
       <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(
         <%= schema.singular %>,
-        &url(~p"<%= schema.route_prefix %>/reset_password/#{&1}")
+        &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}")
       )
     end
 
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index dd9003c97..e9957f607 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>LoginLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Login do
   use <%= inspect context.web_module %>, :live_view
 
   def render(assigns) do
@@ -15,18 +15,18 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         </:subtitle>
       </.header>
 
-      <.simple_form for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log_in"} phx-update="ignore">
+      <.simple_form for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"} phx-update="ignore">
         <.input field={@form[:email]} type="email" label="Email" required />
         <.input field={@form[:password]} type="password" label="Password" required />
 
         <:actions>
           <.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
-          <.link href={~p"<%= schema.route_prefix %>/reset_password"} class="text-sm font-semibold">
+          <.link href={~p"<%= schema.route_prefix %>/reset-password"} class="text-sm font-semibold">
             Forgot your password?
           </.link>
         </:actions>
         <:actions>
-          <.button phx-disable-with="Logging in..." class="w-full">
+          <.button class="w-full">
             Log in <span aria-hidden="true">→</span>
           </.button>
         </:actions>
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index 3f7f422fe..f5c5ef830 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>RegistrationLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Registration do
   use <%= inspect context.web_module %>, :live_view
 
   alias <%= inspect context.module %>
@@ -11,7 +11,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         Register for an account
         <:subtitle>
           Already registered?
-          <.link navigate={~p"<%= schema.route_prefix %>/log_in"} class="font-semibold text-brand hover:underline">
+          <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
             Log in
           </.link>
           to your account now.
@@ -24,7 +24,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         phx-submit="save"
         phx-change="validate"
         phx-trigger-action={@trigger_submit}
-        action={~p"<%= schema.route_prefix %>/log_in?_action=registered"}
+        action={~p"<%= schema.route_prefix %>/log-in?_action=registered"}
         method="post"
       >
         <.error :if={@check_errors}>
diff --git a/priv/templates/phx.gen.auth/registration_new.html.heex b/priv/templates/phx.gen.auth/registration_new.html.heex
index 6f036442e..ae1c5fbb8 100644
--- a/priv/templates/phx.gen.auth/registration_new.html.heex
+++ b/priv/templates/phx.gen.auth/registration_new.html.heex
@@ -3,7 +3,7 @@
     Register for an account
     <:subtitle>
       Already registered?
-      <.link navigate={~p"<%= schema.route_prefix %>/log_in"} class="font-semibold text-brand hover:underline">
+      <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
         Log in
       </.link>
       to your account now.
diff --git a/priv/templates/phx.gen.auth/reset_password_controller.ex b/priv/templates/phx.gen.auth/reset_password_controller.ex
index 5dc1c60fc..e572e4d5f 100644
--- a/priv/templates/phx.gen.auth/reset_password_controller.ex
+++ b/priv/templates/phx.gen.auth/reset_password_controller.ex
@@ -13,7 +13,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
       <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(
         <%= schema.singular %>,
-        &url(~p"<%= schema.route_prefix %>/reset_password/#{&1}")
+        &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}")
       )
     end
 
@@ -36,7 +36,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       {:ok, _} ->
         conn
         |> put_flash(:info, "Password reset successfully.")
-        |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")
+        |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
 
       {:error, changeset} ->
         render(conn, :edit, changeset: changeset)
diff --git a/priv/templates/phx.gen.auth/reset_password_edit.html.heex b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
index a4699b854..29615463d 100644
--- a/priv/templates/phx.gen.auth/reset_password_edit.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
@@ -3,7 +3,7 @@
     Reset Password
   </.header>
 
-  <.simple_form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/reset_password/#{@token}"}>
+  <.simple_form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/reset-password/#{@token}"}>
     <.error :if={@changeset.action}>
       Oops, something went wrong! Please check the errors below.
     </.error>
@@ -24,6 +24,6 @@
 
   <p class="text-center text-sm mt-4">
     <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-    | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
+    | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
   </p>
 </div>
diff --git a/priv/templates/phx.gen.auth/reset_password_live.ex b/priv/templates/phx.gen.auth/reset_password_live.ex
index 32845c9ee..8d07fd8a8 100644
--- a/priv/templates/phx.gen.auth/reset_password_live.ex
+++ b/priv/templates/phx.gen.auth/reset_password_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ResetPasswordLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ResetPassword do
   use <%= inspect context.web_module %>, :live_view
 
   alias <%= inspect context.module %>
@@ -32,7 +32,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       <p class="text-center text-sm mt-4">
         <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-        | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
+        | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
       </p>
     </div>
     """
@@ -61,7 +61,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         {:noreply,
          socket
          |> put_flash(:info, "Password reset successfully.")
-         |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")}
+         |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")}
 
       {:error, changeset} ->
         {:noreply, assign_form(socket, Map.put(changeset, :action, :insert))}
diff --git a/priv/templates/phx.gen.auth/reset_password_new.html.heex b/priv/templates/phx.gen.auth/reset_password_new.html.heex
index 739078687..3d2e40b0d 100644
--- a/priv/templates/phx.gen.auth/reset_password_new.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_new.html.heex
@@ -4,7 +4,7 @@
     <:subtitle>We'll send a password reset link to your inbox</:subtitle>
   </.header>
 
-  <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/reset_password"}>
+  <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/reset-password"}>
     <.input field={f[:email]} type="email" placeholder="Email" required />
     <:actions>
       <.button phx-disable-with="Sending..." class="w-full">
@@ -15,6 +15,6 @@
 
   <p class="text-center text-sm mt-4">
     <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-    | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
+    | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
   </p>
 </div>
diff --git a/priv/templates/phx.gen.auth/routes.ex b/priv/templates/phx.gen.auth/routes.ex
index 60ced3360..928ea70c4 100644
--- a/priv/templates/phx.gen.auth/routes.ex
+++ b/priv/templates/phx.gen.auth/routes.ex
@@ -6,22 +6,22 @@
 
     live_session :redirect_if_<%= schema.singular %>_is_authenticated,
       on_mount: [{<%= inspect auth_module %>, :redirect_if_<%= schema.singular %>_is_authenticated}] do
-      live "/<%= schema.plural %>/register", <%= inspect schema.alias %>RegistrationLive, :new
-      live "/<%= schema.plural %>/log_in", <%= inspect schema.alias %>LoginLive, :new
-      live "/<%= schema.plural %>/reset_password", <%= inspect schema.alias %>ForgotPasswordLive, :new
-      live "/<%= schema.plural %>/reset_password/:token", <%= inspect schema.alias %>ResetPasswordLive, :edit
+      live "/<%= schema.plural %>/register", <%= inspect schema.alias %>Live.Registration, :new
+      live "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>Live.Login, :new
+      live "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>Live.ForgotPassword, :new
+      live "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>Live.ResetPassword, :edit
     end
 
-    post "/<%= schema.plural %>/log_in", <%= inspect schema.alias %>SessionController, :create<% else %>
+    post "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :create<% else %>
 
     get "/<%= schema.plural %>/register", <%= inspect schema.alias %>RegistrationController, :new
     post "/<%= schema.plural %>/register", <%= inspect schema.alias %>RegistrationController, :create
-    get "/<%= schema.plural %>/log_in", <%= inspect schema.alias %>SessionController, :new
-    post "/<%= schema.plural %>/log_in", <%= inspect schema.alias %>SessionController, :create
-    get "/<%= schema.plural %>/reset_password", <%= inspect schema.alias %>ResetPasswordController, :new
-    post "/<%= schema.plural %>/reset_password", <%= inspect schema.alias %>ResetPasswordController, :create
-    get "/<%= schema.plural %>/reset_password/:token", <%= inspect schema.alias %>ResetPasswordController, :edit
-    put "/<%= schema.plural %>/reset_password/:token", <%= inspect schema.alias %>ResetPasswordController, :update<% end %>
+    get "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :new
+    post "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :create
+    get "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>ResetPasswordController, :new
+    post "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>ResetPasswordController, :create
+    get "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>ResetPasswordController, :edit
+    put "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>ResetPasswordController, :update<% end %>
   end
 
   scope <%= router_scope %> do
@@ -29,24 +29,24 @@
 
     live_session :require_authenticated_<%= schema.singular %>,
       on_mount: [{<%= inspect auth_module %>, :ensure_authenticated}] do
-      live "/<%= schema.plural %>/settings", <%= inspect schema.alias %>SettingsLive, :edit
-      live "/<%= schema.plural %>/settings/confirm_email/:token", <%= inspect schema.alias %>SettingsLive, :confirm_email
+      live "/<%= schema.plural %>/settings", <%= inspect schema.alias %>Live.Settings, :edit
+      live "/<%= schema.plural %>/settings/confirm-email/:token", <%= inspect schema.alias %>Live.Settings, :confirm_email
     end<% else %>
 
     get "/<%= schema.plural %>/settings", <%= inspect schema.alias %>SettingsController, :edit
     put "/<%= schema.plural %>/settings", <%= inspect schema.alias %>SettingsController, :update
-    get "/<%= schema.plural %>/settings/confirm_email/:token", <%= inspect schema.alias %>SettingsController, :confirm_email<% end %>
+    get "/<%= schema.plural %>/settings/confirm-email/:token", <%= inspect schema.alias %>SettingsController, :confirm_email<% end %>
   end
 
   scope <%= router_scope %> do
     pipe_through [:browser]
 
-    delete "/<%= schema.plural %>/log_out", <%= inspect schema.alias %>SessionController, :delete<%= if live? do %>
+    delete "/<%= schema.plural %>/log-out", <%= inspect schema.alias %>SessionController, :delete<%= if live? do %>
 
     live_session :current_<%= schema.singular %>,
       on_mount: [{<%= inspect auth_module %>, :mount_current_<%= schema.singular %>}] do
-      live "/<%= schema.plural %>/confirm/:token", <%= inspect schema.alias %>ConfirmationLive, :edit
-      live "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>ConfirmationInstructionsLive, :new
+      live "/<%= schema.plural %>/confirm/:token", <%= inspect schema.alias %>Live.Confirmation, :edit
+      live "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>Live.ConfirmationInstructions, :new
     end<% else %>
     get "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>ConfirmationController, :new
     post "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>ConfirmationController, :create
diff --git a/priv/templates/phx.gen.auth/schema_token.ex b/priv/templates/phx.gen.auth/schema_token.ex
index f7a792a66..d862d1d28 100644
--- a/priv/templates/phx.gen.auth/schema_token.ex
+++ b/priv/templates/phx.gen.auth/schema_token.ex
@@ -134,7 +134,7 @@ defmodule <%= inspect schema.module %>Token do
   @doc """
   Checks if the token is valid and returns its underlying lookup query.
 
-  The query returns the <%= schema.singular %> found by the token, if any.
+  The query returns the <%= schema.singular %>_token found by the token, if any.
 
   This is used to validate requests to change the <%= schema.singular %>
   email. It is different from `verify_email_token_query/2` precisely because
diff --git a/priv/templates/phx.gen.auth/session_controller.ex b/priv/templates/phx.gen.auth/session_controller.ex
index cb76ac3ea..bab413a3c 100644
--- a/priv/templates/phx.gen.auth/session_controller.ex
+++ b/priv/templates/phx.gen.auth/session_controller.ex
@@ -8,7 +8,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     create(conn, params, "Account created successfully!")
   end
 
-  def create(conn, %{"_action" => "password_updated"} = params) do
+  def create(conn, %{"_action" => "password-updated"} = params) do
     conn
     |> put_session(:<%= schema.singular %>_return_to, ~p"<%= schema.route_prefix %>/settings")
     |> create(params, "Password updated successfully!")
@@ -30,7 +30,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       conn
       |> put_flash(:error, "Invalid email or password")
       |> put_flash(:email, String.slice(email, 0, 160))
-      |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")
+      |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
     end
   end<% else %>
 
diff --git a/priv/templates/phx.gen.auth/session_new.html.heex b/priv/templates/phx.gen.auth/session_new.html.heex
index 341cf0b73..abddb592e 100644
--- a/priv/templates/phx.gen.auth/session_new.html.heex
+++ b/priv/templates/phx.gen.auth/session_new.html.heex
@@ -10,7 +10,7 @@
     </:subtitle>
   </.header>
 
-  <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/log_in"}>
+  <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/log-in"}>
     <.error :if={@error_message}><%%= @error_message %></.error>
 
     <.input field={f[:email]} type="email" label="Email" required />
@@ -18,7 +18,7 @@
 
     <:actions :let={f}>
       <.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" />
-      <.link href={~p"<%= schema.route_prefix %>/reset_password"} class="text-sm font-semibold">
+      <.link href={~p"<%= schema.route_prefix %>/reset-password"} class="text-sm font-semibold">
         Forgot your password?
       </.link>
     </:actions>
diff --git a/priv/templates/phx.gen.auth/settings_controller.ex b/priv/templates/phx.gen.auth/settings_controller.ex
index 971c12df0..c046cf779 100644
--- a/priv/templates/phx.gen.auth/settings_controller.ex
+++ b/priv/templates/phx.gen.auth/settings_controller.ex
@@ -19,7 +19,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         <%= inspect context.alias %>.deliver_<%= schema.singular %>_update_email_instructions(
           applied_<%= schema.singular %>,
           <%= schema.singular %>.email,
-          &url(~p"<%= schema.route_prefix %>/settings/confirm_email/#{&1}")
+          &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}")
         )
 
         conn
diff --git a/priv/templates/phx.gen.auth/settings_live.ex b/priv/templates/phx.gen.auth/settings_live.ex
index b6958f43a..a1deeb080 100644
--- a/priv/templates/phx.gen.auth/settings_live.ex
+++ b/priv/templates/phx.gen.auth/settings_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>SettingsLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Settings do
   use <%= inspect context.web_module %>, :live_view
 
   alias <%= inspect context.module %>
@@ -37,7 +37,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         <.simple_form
           for={@password_form}
           id="password_form"
-          action={~p"<%= schema.route_prefix %>/log_in?_action=password_updated"}
+          action={~p"<%= schema.route_prefix %>/log-in?_action=password-updated"}
           method="post"
           phx-change="validate_password"
           phx-submit="update_password"
@@ -124,7 +124,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         <%= inspect context.alias %>.deliver_<%= schema.singular %>_update_email_instructions(
           applied_<%= schema.singular %>,
           <%= schema.singular %>.email,
-          &url(~p"<%= schema.route_prefix %>/settings/confirm_email/#{&1}")
+          &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}")
         )
 
         info = "A link to confirm your email change has been sent to the new address."
diff --git a/priv/templates/phx.gen.html/index.html.heex b/priv/templates/phx.gen.html/index.html.heex
index b6273d48a..772a71077 100644
--- a/priv/templates/phx.gen.html/index.html.heex
+++ b/priv/templates/phx.gen.html/index.html.heex
@@ -1,9 +1,11 @@
 <.header>
   Listing <%= schema.human_plural %>
   <:actions>
-    <.link href={~p"<%= schema.route_prefix %>/new"}>
-      <.button>New <%= schema.human_singular %></.button>
-    </.link>
+    <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}> 
+      <.link href={~p"<%= schema.route_prefix %>/new"}>
+        New <%= schema.human_singular %>
+      </.link>
+    </.button>
   </:actions>
 </.header>
 
diff --git a/priv/templates/phx.gen.html/show.html.heex b/priv/templates/phx.gen.html/show.html.heex
index 344289278..9b46db567 100644
--- a/priv/templates/phx.gen.html/show.html.heex
+++ b/priv/templates/phx.gen.html/show.html.heex
@@ -2,9 +2,11 @@
   <%= schema.human_singular %> <%%= @<%= schema.singular %>.id %>
   <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
   <:actions>
-    <.link href={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
-      <.button>Edit <%= schema.singular %></.button>
-    </.link>
+    <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
+      <.link href={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
+        Edit <%= schema.singular %>
+      </.link>
+    </.button>
   </:actions>
 </.header>
 
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index 83ac2a9f3..c604ef553 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -76,7 +76,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   def flash_group(assigns) do
     ~H"""
-    <div id={@id}>
+    <div id={@id} aria-live="polite">
       <.flash kind={:info} title=<%= maybe_heex_attr_gettext.("Success!", @gettext) %> flash={@flash} />
       <.flash kind={:error} title=<%= maybe_heex_attr_gettext.("Error!", @gettext) %> flash={@flash} />
       <.flash
@@ -120,7 +120,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       </.simple_form>
   """
   attr :for, :any, required: true, doc: "the data structure for the form"
-  attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+  attr :as, :any, doc: "the server side parameter to collect all input under"
 
   attr :rest, :global,
     include: ~w(autocomplete name rel action enctype method novalidate target multipart),
@@ -130,8 +130,10 @@ defmodule <%= @web_namespace %>.CoreComponents do
   slot :actions, doc: "the slot for form actions, such as a submit button"
 
   def simple_form(assigns) do
+    assigns = assign(assigns, :as, if(assigns[:as], do: %{as: assigns[:as]}, else: %{}))
+
     ~H"""
-    <.form :let={f} for={@for} as={@as} {@rest}>
+    <.form :let={f} for={@for} {@as} {@rest}>
       <div class="mt-10 space-y-8 bg-white">
         <%%= render_slot(@inner_block, f) %>
         <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
@@ -265,7 +267,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       <select
         id={@id}
         name={@name}
-        class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
+        class="mt-2 block w-full rounded-md border border-zinc-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
         multiple={@multiple}
         {@rest}
       >
@@ -339,7 +341,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   def error(assigns) do
     ~H"""
-    <p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
+    <p class="mt-1.5 flex gap-1.5 text-sm leading-6 text-rose-600">
       <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
       <%%= render_slot(@inner_block) %>
     </p>
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
index d6055a669..ff23665ab 100644
--- a/priv/templates/phx.gen.live/index.ex
+++ b/priv/templates/phx.gen.live/index.ex
@@ -9,9 +9,11 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     <.header>
       Listing <%= schema.human_plural %>
       <:actions>
-        <.link navigate={~p"<%= schema.route_prefix %>/new"}>
-          <.button>New <%= schema.human_singular %></.button>
-        </.link>
+        <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
+          <.link navigate={~p"<%= schema.route_prefix %>/new"}>
+            New <%= schema.human_singular %>
+          </.link>
+        </.button>
       </:actions>
     </.header>
 
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
index 6d6e1ac27..140016c74 100644
--- a/priv/templates/phx.gen.live/show.ex
+++ b/priv/templates/phx.gen.live/show.ex
@@ -10,9 +10,11 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       <%= schema.human_singular %> <%%= @<%= schema.singular %>.id %>
       <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
       <:actions>
-        <.link navigate={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
-          <.button>Edit <%= schema.singular %></.button>
-        </.link>
+        <.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>
       </:actions>
     </.header>
 


Test patch:
diff --git a/installer/templates/phx_test/controllers/error_html_test.exs b/installer/templates/phx_test/controllers/error_html_test.exs
index 6d3396470..74098fc30 100644
--- a/installer/templates/phx_test/controllers/error_html_test.exs
+++ b/installer/templates/phx_test/controllers/error_html_test.exs
@@ -2,7 +2,7 @@ defmodule <%= @web_namespace %>.ErrorHTMLTest do
   use <%= @web_namespace %>.ConnCase, async: true
 
   # Bring render_to_string/4 for testing custom views
-  import Phoenix.Template
+  import Phoenix.Template, only: [render_to_string: 4]
 
   test "renders 404.html" do
     assert render_to_string(<%= @web_namespace %>.ErrorHTML, "404", "html", []) == "Not Found"
diff --git a/integration_test/docker.sh b/integration_test/docker.sh
index ab4441ca7..c2a296514 100755
--- a/integration_test/docker.sh
+++ b/integration_test/docker.sh
@@ -2,9 +2,9 @@
 
 # adapt with versions from .github/versions/ci.yml if necessary;
 # you can also override these with environment variables
-ELIXIR="${ELIXIR:-1.16.2}"
-ERLANG="${ERLANG:-26.2.5}"
-SUFFIX="${SUFFIX:-alpine-3.19.1}"
+ELIXIR="${ELIXIR:-1.17.3}"
+ERLANG="${ERLANG:-27.1.2}"
+SUFFIX="${SUFFIX:-alpine-3.20.3}"
 
 # Get the directory of the script
 SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
diff --git a/integration_test/mix.exs b/integration_test/mix.exs
index 81012c0c8..63cedf531 100644
--- a/integration_test/mix.exs
+++ b/integration_test/mix.exs
@@ -10,7 +10,7 @@ defmodule Phoenix.Integration.MixProject do
     [
       app: :phoenix_integration,
       version: "0.1.0",
-      elixir: "~> 1.14",
+      elixir: "~> 1.15",
       elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       deps: deps()
@@ -42,7 +42,7 @@ defmodule Phoenix.Integration.MixProject do
       {:ecto_sqlite3, ">= 0.0.0"},
       {:phoenix_html, "~> 4.1"},
       # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
-      {:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
+      {:phoenix_live_view, "~> 1.0.0-rc.0", override: true},
       {:dns_cluster, "~> 0.1.1"},
       {:floki, ">= 0.30.0"},
       {:phoenix_live_reload, "~> 1.2"},
@@ -54,7 +54,7 @@ defmodule Phoenix.Integration.MixProject do
       {:swoosh, "~> 1.16"},
       {:bandit, "~> 1.0"},
       {:bcrypt_elixir, "~> 3.0"},
-      {:argon2_elixir, "~> 3.0"},
+      {:argon2_elixir, "~> 4.0"},
       {:pbkdf2_elixir, "~> 2.0"},
       {:tailwind, "~> 0.2"},
       {:heroicons,
diff --git a/integration_test/mix.lock b/integration_test/mix.lock
index 59445ecf5..6ce1c9a9c 100644
--- a/integration_test/mix.lock
+++ b/integration_test/mix.lock
@@ -1,27 +1,27 @@
 %{
-  "argon2_elixir": {:hex, :argon2_elixir, "3.2.1", "f47740bf9f2a39ffef79ba48eb25dea2ee37bcc7eadf91d49615591d1a6fce1a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "a813b78217394530b5fcf4c8070feee43df03ffef938d044019169c766315690"},
+  "argon2_elixir": {:hex, :argon2_elixir, "4.1.0", "2f242afe47c373663cb404eb75e792f749507075ed737b49685a9f2edcb401df", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ecb6f2ca2cca34b28e546224661bf2a85714516d2713c7313c5ffe8bdade7cf"},
   "bandit": {:hex, :bandit, "1.5.4", "8e56e7cfc06f3c57995be0d9bf4e45b972d8732f5c7e96ef8ec0735f52079527", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "04c2b38874769af67fe7f10034f606ad6dda1d8f80c4d7a0c616b347584d5aff"},
   "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
-  "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
+  "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
   "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
-  "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
+  "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
   "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
-  "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
+  "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"},
   "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
   "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
   "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"},
   "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"},
   "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
   "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
-  "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
+  "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
   "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
   "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
   "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
-  "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
-  "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
+  "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
+  "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
   "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
   "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"},
-  "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
+  "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
   "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
   "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
   "myxql": {:hex, :myxql, "0.7.0", "3382f139b0b0da977a8fc33c8cded125e20df2e400f8d7b7e674fa62a7e077dd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "40e4b7ad4973c8b895e86a3de04ff7a79c2cf72b9f2bddef7717afb4ab36d8c0"},
@@ -32,7 +32,7 @@
   "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
   "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
   "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
-  "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.1", "adfb0ab3edbad3bac63661ee0367c803d96178b8e8bbb8ff2b6c7cebdb820491", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ffc940d7af43c3d6c26faa7693872f95178f4ca886434224d29d4b54d3a38ae3"},
+  "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.7", "d2abca526422adea88896769529addb6443390b1d4f1ff9cbe694312d8875fb2", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01"},
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
   "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
   "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
@@ -42,7 +42,7 @@
   "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"},
   "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
   "tds": {:hex, :tds, "2.3.5", "fedfb96d53206f01eac62ead859e47e1541a62e1553e9eb7a8801c7dca59eae8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "52e350f5dd5584bbcff9859e331be144d290b41bd4c749b936014a17660662f2"},
-  "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
+  "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
   "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
   "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
   "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
diff --git a/priv/templates/phx.gen.auth/auth_test.exs b/priv/templates/phx.gen.auth/auth_test.exs
index bafdfd6b7..cb7943b5e 100644
--- a/priv/templates/phx.gen.auth/auth_test.exs
+++ b/priv/templates/phx.gen.auth/auth_test.exs
@@ -231,7 +231,7 @@ defmodule <%= inspect auth_module %>Test do
       conn = conn |> fetch_flash() |> <%= inspect schema.alias %>Auth.require_authenticated_<%= schema.singular %>([])
       assert conn.halted
 
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
 
       assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
                "You must log in to access this page."
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs b/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
index c1ac8b654..d8d1d2376 100644
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationInstructionsLiveTest do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ConfirmationInstructionsTest do
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   import Phoenix.LiveViewTest
diff --git a/priv/templates/phx.gen.auth/confirmation_live_test.exs b/priv/templates/phx.gen.auth/confirmation_live_test.exs
index fc3112882..0eac2e43c 100644
--- a/priv/templates/phx.gen.auth/confirmation_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_live_test.exs
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationLiveTest do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ConfirmationTest do
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   import Phoenix.LiveViewTest
diff --git a/priv/templates/phx.gen.auth/forgot_password_live_test.exs b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
index 4ad1455f6..2a7f5b362 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live_test.exs
+++ b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ForgotPasswordLiveTest do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ForgotPasswordTest do
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   import Phoenix.LiveViewTest
@@ -9,18 +9,18 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   describe "Forgot password page" do
     test "renders email page", %{conn: conn} do
-      {:ok, lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password")
+      {:ok, lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
 
       assert html =~ "Forgot your password?"
       assert has_element?(lv, ~s|a[href="#{~p"<%= schema.route_prefix %>/register"}"]|, "Register")
-      assert has_element?(lv, ~s|a[href="#{~p"<%= schema.route_prefix %>/log_in"}"]|, "Log in")
+      assert has_element?(lv, ~s|a[href="#{~p"<%= schema.route_prefix %>/log-in"}"]|, "Log in")
     end
 
     test "redirects if already logged in", %{conn: conn} do
       result =
         conn
         |> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
-        |> live(~p"<%= schema.route_prefix %>/reset_password")
+        |> live(~p"<%= schema.route_prefix %>/reset-password")
         |> follow_redirect(conn, ~p"/")
 
       assert {:ok, _conn} = result
@@ -33,7 +33,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "sends a new reset password token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
 
       {:ok, conn} =
         lv
@@ -48,7 +48,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "does not send reset password token if email is invalid", %{conn: conn} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
 
       {:ok, conn} =
         lv
diff --git a/priv/templates/phx.gen.auth/login_live_test.exs b/priv/templates/phx.gen.auth/login_live_test.exs
index 6b9d14268..11516b78a 100644
--- a/priv/templates/phx.gen.auth/login_live_test.exs
+++ b/priv/templates/phx.gen.auth/login_live_test.exs
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>LoginLiveTest do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.LoginTest do
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   import Phoenix.LiveViewTest
@@ -6,7 +6,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   describe "Log in page" do
     test "renders log in page", %{conn: conn} do
-      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       assert html =~ "Log in"
       assert html =~ "Register"
@@ -17,7 +17,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       result =
         conn
         |> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
-        |> live(~p"<%= schema.route_prefix %>/log_in")
+        |> live(~p"<%= schema.route_prefix %>/log-in")
         |> follow_redirect(conn, ~p"/")
 
       assert {:ok, _conn} = result
@@ -29,7 +29,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       password = "123456789abcd"
       <%= schema.singular %> = <%= schema.singular %>_fixture(%{password: password})
 
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       form =
         form(lv, "#login_form", <%= schema.singular %>: %{email: <%= schema.singular %>.email, password: password, remember_me: true})
@@ -42,7 +42,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "redirects to login page with a flash error if there are no valid credentials", %{
       conn: conn
     } do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       form =
         form(lv, "#login_form",
@@ -53,13 +53,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
 
-      assert redirected_to(conn) == "<%= schema.route_prefix %>/log_in"
+      assert redirected_to(conn) == "<%= schema.route_prefix %>/log-in"
     end
   end
 
   describe "login navigation" do
     test "redirects to registration page when the Register button is clicked", %{conn: conn} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       {:ok, _login_live, login_html} =
         lv
@@ -73,13 +73,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "redirects to forgot password page when the Forgot Password button is clicked", %{
       conn: conn
     } do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       {:ok, conn} =
         lv
         |> element(~s|main a:fl-contains("Forgot your password?")|)
         |> render_click()
-        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/reset_password")
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/reset-password")
 
       assert conn.resp_body =~ "Forgot your password?"
     end
diff --git a/priv/templates/phx.gen.auth/registration_controller_test.exs b/priv/templates/phx.gen.auth/registration_controller_test.exs
index 2682277b0..53dfbb2e6 100644
--- a/priv/templates/phx.gen.auth/registration_controller_test.exs
+++ b/priv/templates/phx.gen.auth/registration_controller_test.exs
@@ -8,7 +8,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       conn = get(conn, ~p"<%= schema.route_prefix %>/register")
       response = html_response(conn, 200)
       assert response =~ "Register"
-      assert response =~ ~p"<%= schema.route_prefix %>/log_in"
+      assert response =~ ~p"<%= schema.route_prefix %>/log-in"
       assert response =~ ~p"<%= schema.route_prefix %>/register"
     end
 
@@ -37,7 +37,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       response = html_response(conn, 200)
       assert response =~ email
       assert response =~ ~p"<%= schema.route_prefix %>/settings"
-      assert response =~ ~p"<%= schema.route_prefix %>/log_out"
+      assert response =~ ~p"<%= schema.route_prefix %>/log-out"
     end
 
     test "render errors for invalid data", %{conn: conn} do
diff --git a/priv/templates/phx.gen.auth/registration_live_test.exs b/priv/templates/phx.gen.auth/registration_live_test.exs
index 1d1246819..8c878d301 100644
--- a/priv/templates/phx.gen.auth/registration_live_test.exs
+++ b/priv/templates/phx.gen.auth/registration_live_test.exs
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>RegistrationLiveTest do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.RegistrationTest do
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   import Phoenix.LiveViewTest
@@ -79,7 +79,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         lv
         |> element(~s|main a:fl-contains("Log in")|)
         |> render_click()
-        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log_in")
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       assert login_html =~ "Log in"
     end
diff --git a/priv/templates/phx.gen.auth/reset_password_controller_test.exs b/priv/templates/phx.gen.auth/reset_password_controller_test.exs
index f622b51e0..dcf0dc5c9 100644
--- a/priv/templates/phx.gen.auth/reset_password_controller_test.exs
+++ b/priv/templates/phx.gen.auth/reset_password_controller_test.exs
@@ -9,19 +9,19 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
   end
 
-  describe "GET <%= schema.route_prefix %>/reset_password" do
+  describe "GET <%= schema.route_prefix %>/reset-password" do
     test "renders the reset password page", %{conn: conn} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/reset_password")
+      conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password")
       response = html_response(conn, 200)
       assert response =~ "Forgot your password?"
     end
   end
 
-  describe "POST <%= schema.route_prefix %>/reset_password" do
+  describe "POST <%= schema.route_prefix %>/reset-password" do
     @tag :capture_log
     test "sends a new reset password token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
-        post(conn, ~p"<%= schema.route_prefix %>/reset_password", %{
+        post(conn, ~p"<%= schema.route_prefix %>/reset-password", %{
           "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email}
         })
 
@@ -35,7 +35,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
     test "does not send reset password token if email is invalid", %{conn: conn} do
       conn =
-        post(conn, ~p"<%= schema.route_prefix %>/reset_password", %{
+        post(conn, ~p"<%= schema.route_prefix %>/reset-password", %{
           "<%= schema.singular %>" => %{"email" => "unknown@example.com"}
         })
 
@@ -48,7 +48,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
   end
 
-  describe "GET <%= schema.route_prefix %>/reset_password/:token" do
+  describe "GET <%= schema.route_prefix %>/reset-password/:token" do
     setup %{<%= schema.singular %>: <%= schema.singular %>} do
       token =
         extract_<%= schema.singular %>_token(fn url ->
@@ -59,12 +59,12 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "renders reset password", %{conn: conn, token: token} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+      conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
       assert html_response(conn, 200) =~ "Reset password"
     end
 
     test "does not render reset password with invalid token", %{conn: conn} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/reset_password/oops")
+      conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password/oops")
       assert redirected_to(conn) == ~p"/"
 
       assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
@@ -72,7 +72,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
   end
 
-  describe "PUT <%= schema.route_prefix %>/reset_password/:token" do
+  describe "PUT <%= schema.route_prefix %>/reset-password/:token" do
     setup %{<%= schema.singular %>: <%= schema.singular %>} do
       token =
         extract_<%= schema.singular %>_token(fn url ->
@@ -84,14 +84,14 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
     test "resets password once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>, token: token} do
       conn =
-        put(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}", %{
+        put(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}", %{
           "<%= schema.singular %>" => %{
             "password" => "new valid password",
             "password_confirmation" => "new valid password"
           }
         })
 
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
       refute get_session(conn, :<%= schema.singular %>_token)
 
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
@@ -102,7 +102,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
     test "does not reset password on invalid data", %{conn: conn, token: token} do
       conn =
-        put(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}", %{
+        put(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}", %{
           "<%= schema.singular %>" => %{
             "password" => "too short",
             "password_confirmation" => "does not match"
@@ -113,7 +113,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "does not reset password with invalid token", %{conn: conn} do
-      conn = put(conn, ~p"<%= schema.route_prefix %>/reset_password/oops")
+      conn = put(conn, ~p"<%= schema.route_prefix %>/reset-password/oops")
       assert redirected_to(conn) == ~p"/"
 
       assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
diff --git a/priv/templates/phx.gen.auth/reset_password_live_test.exs b/priv/templates/phx.gen.auth/reset_password_live_test.exs
index 4e4995dd3..6e73bfe8e 100644
--- a/priv/templates/phx.gen.auth/reset_password_live_test.exs
+++ b/priv/templates/phx.gen.auth/reset_password_live_test.exs
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ResetPasswordLiveTest do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ResetPasswordTest do
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   import Phoenix.LiveViewTest
@@ -19,13 +19,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   describe "Reset password page" do
     test "renders reset password with valid token", %{conn: conn, token: token} do
-      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
 
       assert html =~ "Reset Password"
     end
 
     test "does not render reset password with invalid token", %{conn: conn} do
-      {:error, {:redirect, to}} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/invalid")
+      {:error, {:redirect, to}} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/invalid")
 
       assert to == %{
                flash: %{"error" => "Reset password link is invalid or it has expired."},
@@ -34,7 +34,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "renders errors for invalid data", %{conn: conn, token: token} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
 
       result =
         lv
@@ -50,7 +50,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   describe "Reset Password" do
     test "resets password once", %{conn: conn, token: token, <%= schema.singular %>: <%= schema.singular %>} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
 
       {:ok, conn} =
         lv
@@ -61,7 +61,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           }
         )
         |> render_submit()
-        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log_in")
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       refute get_session(conn, :<%= schema.singular %>_token)
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password reset successfully"
@@ -69,7 +69,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "does not reset password on invalid data", %{conn: conn, token: token} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
 
       result =
         lv
@@ -89,13 +89,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   describe "Reset password navigation" do
     test "redirects to login page when the Log in button is clicked", %{conn: conn, token: token} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
 
       {:ok, conn} =
         lv
         |> element(~s|main a:fl-contains("Log in")|)
         |> render_click()
-        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log_in")
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       assert conn.resp_body =~ "Log in"
     end
@@ -104,7 +104,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       conn: conn,
       token: token
     } do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
 
       {:ok, conn} =
         lv
diff --git a/priv/templates/phx.gen.auth/session_controller_test.exs b/priv/templates/phx.gen.auth/session_controller_test.exs
index 70473c2ce..8004208b1 100644
--- a/priv/templates/phx.gen.auth/session_controller_test.exs
+++ b/priv/templates/phx.gen.auth/session_controller_test.exs
@@ -7,9 +7,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
   end<%= if not live? do %>
 
-  describe "GET <%= schema.route_prefix %>/log_in" do
+  describe "GET <%= schema.route_prefix %>/log-in" do
     test "renders log in page", %{conn: conn} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/log_in")
+      conn = get(conn, ~p"<%= schema.route_prefix %>/log-in")
       response = html_response(conn, 200)
       assert response =~ "Log in"
       assert response =~ ~p"<%= schema.route_prefix %>/register"
@@ -17,15 +17,15 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "redirects if already logged in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> get(~p"<%= schema.route_prefix %>/log_in")
+      conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> get(~p"<%= schema.route_prefix %>/log-in")
       assert redirected_to(conn) == ~p"/"
     end
   end<% end %>
 
-  describe "POST <%= schema.route_prefix %>/log_in" do
+  describe "POST <%= schema.route_prefix %>/log-in" do
     test "logs the <%= schema.singular %> in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
-        post(conn, ~p"<%= schema.route_prefix %>/log_in", %{
+        post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
           "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email, "password" => valid_<%= schema.singular %>_password()}
         })
 
@@ -37,12 +37,12 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       response = html_response(conn, 200)
       assert response =~ <%= schema.singular %>.email
       assert response =~ ~p"<%= schema.route_prefix %>/settings"
-      assert response =~ ~p"<%= schema.route_prefix %>/log_out"
+      assert response =~ ~p"<%= schema.route_prefix %>/log-out"
     end
 
     test "logs the <%= schema.singular %> in with remember me", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
-        post(conn, ~p"<%= schema.route_prefix %>/log_in", %{
+        post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
           "<%= schema.singular %>" => %{
             "email" => <%= schema.singular %>.email,
             "password" => valid_<%= schema.singular %>_password(),
@@ -58,7 +58,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       conn =
         conn
         |> init_test_session(<%= schema.singular %>_return_to: "/foo/bar")
-        |> post(~p"<%= schema.route_prefix %>/log_in", %{
+        |> post(~p"<%= schema.route_prefix %>/log-in", %{
           "<%= schema.singular %>" => %{
             "email" => <%= schema.singular %>.email,
             "password" => valid_<%= schema.singular %>_password()
@@ -72,7 +72,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "login following registration", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
         conn
-        |> post(~p"<%= schema.route_prefix %>/log_in", %{
+        |> post(~p"<%= schema.route_prefix %>/log-in", %{
           "_action" => "registered",
           "<%= schema.singular %>" => %{
             "email" => <%= schema.singular %>.email,
@@ -87,8 +87,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "login following password update", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
         conn
-        |> post(~p"<%= schema.route_prefix %>/log_in", %{
-          "_action" => "password_updated",
+        |> post(~p"<%= schema.route_prefix %>/log-in", %{
+          "_action" => "password-updated",
           "<%= schema.singular %>" => %{
             "email" => <%= schema.singular %>.email,
             "password" => valid_<%= schema.singular %>_password()
@@ -101,17 +101,17 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
     test "redirects to login page with invalid credentials", %{conn: conn} do
       conn =
-        post(conn, ~p"<%= schema.route_prefix %>/log_in", %{
+        post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
           "<%= schema.singular %>" => %{"email" => "invalid@email.com", "password" => "invalid_password"}
         })
 
       assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
     end<% else %>
 
     test "emits error message with invalid credentials", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
-        post(conn, ~p"<%= schema.route_prefix %>/log_in", %{
+        post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
           "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email, "password" => "invalid_password"}
         })
 
@@ -121,16 +121,16 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end<% end %>
   end
 
-  describe "DELETE <%= schema.route_prefix %>/log_out" do
+  describe "DELETE <%= schema.route_prefix %>/log-out" do
     test "logs the <%= schema.singular %> out", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> delete(~p"<%= schema.route_prefix %>/log_out")
+      conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> delete(~p"<%= schema.route_prefix %>/log-out")
       assert redirected_to(conn) == ~p"/"
       refute get_session(conn, :<%= schema.singular %>_token)
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
     end
 
     test "succeeds even if the <%= schema.singular %> is not logged in", %{conn: conn} do
-      conn = delete(conn, ~p"<%= schema.route_prefix %>/log_out")
+      conn = delete(conn, ~p"<%= schema.route_prefix %>/log-out")
       assert redirected_to(conn) == ~p"/"
       refute get_session(conn, :<%= schema.singular %>_token)
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
diff --git a/priv/templates/phx.gen.auth/settings_controller_test.exs b/priv/templates/phx.gen.auth/settings_controller_test.exs
index 7c83ed554..6f5ce220d 100644
--- a/priv/templates/phx.gen.auth/settings_controller_test.exs
+++ b/priv/templates/phx.gen.auth/settings_controller_test.exs
@@ -16,7 +16,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "redirects if <%= schema.singular %> is not logged in" do
       conn = build_conn()
       conn = get(conn, ~p"<%= schema.route_prefix %>/settings")
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
     end
   end
 
@@ -96,7 +96,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
   end
 
-  describe "GET <%= schema.route_prefix %>/settings/confirm_email/:token" do
+  describe "GET <%= schema.route_prefix %>/settings/confirm-email/:token" do
     setup %{<%= schema.singular %>: <%= schema.singular %>} do
       email = unique_<%= schema.singular %>_email()
 
@@ -109,7 +109,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "updates the <%= schema.singular %> email once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>, token: token, email: email} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+      conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
       assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/settings"
 
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
@@ -118,7 +118,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(<%= schema.singular %>.email)
       assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email)
 
-      conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+      conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
 
       assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/settings"
 
@@ -127,7 +127,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "does not update email with invalid token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/oops")
+      conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/oops")
       assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/settings"
 
       assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
@@ -138,8 +138,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
     test "redirects if <%= schema.singular %> is not logged in", %{token: token} do
       conn = build_conn()
-      conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+      conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
     end
   end
 end
diff --git a/priv/templates/phx.gen.auth/settings_live_test.exs b/priv/templates/phx.gen.auth/settings_live_test.exs
index e4a25d1f2..32f4ebd92 100644
--- a/priv/templates/phx.gen.auth/settings_live_test.exs
+++ b/priv/templates/phx.gen.auth/settings_live_test.exs
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>SettingsLiveTest do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.SettingsTest do
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   alias <%= inspect context.module %>
@@ -20,7 +20,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       assert {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings")
 
       assert {:redirect, %{to: path, flash: flash}} = redirect
-      assert path == ~p"<%= schema.route_prefix %>/log_in"
+      assert path == ~p"<%= schema.route_prefix %>/log-in"
       assert %{"error" => "You must log in to access this page."} = flash
     end
   end
@@ -172,7 +172,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "updates the <%= schema.singular %> email once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>, token: token, email: email} do
-      {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+      {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
 
       assert {:live_redirect, %{to: path, flash: flash}} = redirect
       assert path == ~p"<%= schema.route_prefix %>/settings"
@@ -182,7 +182,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email)
 
       # use confirm token again
-      {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+      {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
       assert {:live_redirect, %{to: path, flash: flash}} = redirect
       assert path == ~p"<%= schema.route_prefix %>/settings"
       assert %{"error" => message} = flash
@@ -190,7 +190,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "does not update email with invalid token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/oops")
+      {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/oops")
       assert {:live_redirect, %{to: path, flash: flash}} = redirect
       assert path == ~p"<%= schema.route_prefix %>/settings"
       assert %{"error" => message} = flash
@@ -200,9 +200,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
     test "redirects if <%= schema.singular %> is not logged in", %{token: token} do
       conn = build_conn()
-      {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+      {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
       assert {:redirect, %{to: path, flash: flash}} = redirect
-      assert path == ~p"<%= schema.route_prefix %>/log_in"
+      assert path == ~p"<%= schema.route_prefix %>/log-in"
       assert %{"error" => message} = flash
       assert message == "You must log in to access this page."
     end
diff --git a/test/mix/tasks/phx.gen.auth/injector_test.exs b/test/mix/tasks/phx.gen.auth/injector_test.exs
index 761493c59..6f43d5d98 100644
--- a/test/mix/tasks/phx.gen.auth/injector_test.exs
+++ b/test/mix/tasks/phx.gen.auth/injector_test.exs
@@ -680,7 +680,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                              </li>
                              <li>
                                <.link
-                                 href={~p"/users/log_out"}
+                                 href={~p"/users/log-out"}
                                  method="delete"
                                  class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
                                >
@@ -698,7 +698,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                              </li>
                              <li>
                                <.link
-                                 href={~p"/users/log_in"}
+                                 href={~p"/users/log-in"}
                                  class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
                                >
                                  Log in
@@ -774,7 +774,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                              </li>\r
                              <li>\r
                                <.link\r
-                                 href={~p"/users/log_out"}\r
+                                 href={~p"/users/log-out"}\r
                                  method="delete"\r
                                  class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"\r
                                >\r
@@ -792,7 +792,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                              </li>\r
                              <li>\r
                                <.link\r
-                                 href={~p"/users/log_in"}\r
+                                 href={~p"/users/log-in"}\r
                                  class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"\r
                                >\r
                                  Log in\r
@@ -852,7 +852,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                        </li>
                        <li>
                          <.link
-                           href={~p"/users/log_out"}
+                           href={~p"/users/log-out"}
                            method="delete"
                            class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
                          >
@@ -870,7 +870,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                        </li>
                        <li>
                          <.link
-                           href={~p"/users/log_in"}
+                           href={~p"/users/log-in"}
                            class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
                          >
                            Log in
@@ -932,7 +932,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                        </li>\r
                        <li>\r
                          <.link\r
-                           href={~p"/users/log_out"}\r
+                           href={~p"/users/log-out"}\r
                            method="delete"\r
                            class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"\r
                          >\r
@@ -950,7 +950,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
                        </li>\r
                        <li>\r
                          <.link\r
-                           href={~p"/users/log_in"}\r
+                           href={~p"/users/log-in"}\r
                            class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"\r
                          >\r
                            Log in\r
@@ -983,10 +983,10 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
               <%= if @current_user do %>
                 <li><%= @current_user.email %></li>
                 <li><.link href={~p"/users/settings"}>Settings</.link></li>
-                <li><.link href={~p"/users/log_out"} method="delete">Log out</.link></li>
+                <li><.link href={~p"/users/log-out"} method="delete">Log out</.link></li>
               <% else %>
                 <li><.link href={~p"/users/register"}>Register</.link></li>
-                <li><.link href={~p"/users/log_in"}>Log in</.link></li>
+                <li><.link href={~p"/users/log-in"}>Log in</.link></li>
               <% end %>
             </ul>
           </div>
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index d0944d8f7..773ec9a27 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -161,12 +161,12 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/register", UserRegistrationController, :new
                    post "/users/register", UserRegistrationController, :create
-                   get "/users/log_in", UserSessionController, :new
-                   post "/users/log_in", UserSessionController, :create
-                   get "/users/reset_password", UserResetPasswordController, :new
-                   post "/users/reset_password", UserResetPasswordController, :create
-                   get "/users/reset_password/:token", UserResetPasswordController, :edit
-                   put "/users/reset_password/:token", UserResetPasswordController, :update
+                   get "/users/log-in", UserSessionController, :new
+                   post "/users/log-in", UserSessionController, :create
+                   get "/users/reset-password", UserResetPasswordController, :new
+                   post "/users/reset-password", UserResetPasswordController, :create
+                   get "/users/reset-password/:token", UserResetPasswordController, :edit
+                   put "/users/reset-password/:token", UserResetPasswordController, :update
                  end
 
                  scope "/", MyAppWeb do
@@ -174,13 +174,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/settings", UserSettingsController, :edit
                    put "/users/settings", UserSettingsController, :update
-                   get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
+                   get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email
                  end
 
                  scope "/", MyAppWeb do
                    pipe_through [:browser]
 
-                   delete "/users/log_out", UserSessionController, :delete
+                   delete "/users/log-out", UserSessionController, :delete
                    get "/users/confirm", UserConfirmationController, :new
                    post "/users/confirm", UserConfirmationController, :create
                    get "/users/confirm/:token", UserConfirmationController, :edit
@@ -194,13 +194,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  ~r|<.link.*href={~p"/users/settings"}.*>|s
 
         assert file =~
-                 ~r|<.link.*href={~p"/users/log_out"}.*method="delete".*>|s
+                 ~r|<.link.*href={~p"/users/log-out"}.*method="delete".*>|s
 
         assert file =~
                  ~r|<.link.*href={~p"/users/register"}.*>|s
 
         assert file =~
-                 ~r|<.link.*href={~p"/users/log_in"}.*>|s
+                 ~r|<.link.*href={~p"/users/log-in"}.*>|s
       end)
 
       assert_file("test/support/conn_case.ex", fn file ->
@@ -249,20 +249,20 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert file =~ ~s|deliver(user.email, "Update email instructions",|
       end)
 
-      assert_file("lib/my_app_web/live/user_registration_live.ex")
-      assert_file("test/my_app_web/live/user_registration_live_test.exs")
-      assert_file("lib/my_app_web/live/user_login_live.ex")
-      assert_file("test/my_app_web/live/user_login_live_test.exs")
-      assert_file("lib/my_app_web/live/user_reset_password_live.ex")
-      assert_file("test/my_app_web/live/user_reset_password_live_test.exs")
-      assert_file("lib/my_app_web/live/user_forgot_password_live.ex")
-      assert_file("test/my_app_web/live/user_forgot_password_live_test.exs")
-      assert_file("lib/my_app_web/live/user_settings_live.ex")
-      assert_file("test/my_app_web/live/user_settings_live_test.exs")
-      assert_file("lib/my_app_web/live/user_confirmation_live.ex")
-      assert_file("test/my_app_web/live/user_confirmation_live_test.exs")
-      assert_file("lib/my_app_web/live/user_confirmation_instructions_live.ex")
-      assert_file("test/my_app_web/live/user_confirmation_instructions_live_test.exs")
+      assert_file("lib/my_app_web/live/user_live/registration.ex")
+      assert_file("test/my_app_web/live/user_live/registration_test.exs")
+      assert_file("lib/my_app_web/live/user_live/login.ex")
+      assert_file("test/my_app_web/live/user_live/login_test.exs")
+      assert_file("lib/my_app_web/live/user_live/reset_password.ex")
+      assert_file("test/my_app_web/live/user_live/reset_password_test.exs")
+      assert_file("lib/my_app_web/live/user_live/forgot_password.ex")
+      assert_file("test/my_app_web/live/user_live/forgot_password_test.exs")
+      assert_file("lib/my_app_web/live/user_live/settings.ex")
+      assert_file("test/my_app_web/live/user_live/settings_test.exs")
+      assert_file("lib/my_app_web/live/user_live/confirmation.ex")
+      assert_file("test/my_app_web/live/user_live/confirmation_test.exs")
+      assert_file("lib/my_app_web/live/user_live/confirmation_instructions.ex")
+      assert_file("test/my_app_web/live/user_live/confirmation_instructions_test.exs")
 
       assert_file("lib/my_app_web/user_auth.ex")
       assert_file("test/my_app_web/user_auth_test.exs")
@@ -290,13 +290,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    live_session :redirect_if_user_is_authenticated,
                      on_mount: [{MyAppWeb.UserAuth, :redirect_if_user_is_authenticated}] do
-                     live "/users/register", UserRegistrationLive, :new
-                     live "/users/log_in", UserLoginLive, :new
-                     live "/users/reset_password", UserForgotPasswordLive, :new
-                     live "/users/reset_password/:token", UserResetPasswordLive, :edit
+                     live "/users/register", UserLive.Registration, :new
+                     live "/users/log-in", UserLive.Login, :new
+                     live "/users/reset-password", UserLive.ForgotPassword, :new
+                     live "/users/reset-password/:token", UserLive.ResetPassword, :edit
                    end
 
-                   post "/users/log_in", UserSessionController, :create
+                   post "/users/log-in", UserSessionController, :create
                  end
 
                  scope "/", MyAppWeb do
@@ -304,20 +304,20 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    live_session :require_authenticated_user,
                      on_mount: [{MyAppWeb.UserAuth, :ensure_authenticated}] do
-                     live "/users/settings", UserSettingsLive, :edit
-                     live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
+                     live "/users/settings", UserLive.Settings, :edit
+                     live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
                    end
                  end
 
                  scope "/", MyAppWeb do
                    pipe_through [:browser]
 
-                   delete "/users/log_out", UserSessionController, :delete
+                   delete "/users/log-out", UserSessionController, :delete
 
                    live_session :current_user,
                      on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
-                     live "/users/confirm/:token", UserConfirmationLive, :edit
-                     live "/users/confirm", UserConfirmationInstructionsLive, :new
+                     live "/users/confirm/:token", UserLive.Confirmation, :edit
+                     live "/users/confirm", UserLive.ConfirmationInstructions, :new
                    end
                  end
                """
@@ -328,13 +328,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  ~r|<\.link.*href={~p"/users/settings"}.*>|s
 
         assert file =~
-                 ~r|<\.link.*href={~p"/users/log_out"}.*method="delete".*>|s
+                 ~r|<\.link.*href={~p"/users/log-out"}.*method="delete".*>|s
 
         assert file =~
                  ~r|<\.link.*href={~p"/users/register"}.*>|s
 
         assert file =~
-                 ~r|<\.link.*href={~p"/users/log_in"}.*>|s
+                 ~r|<\.link.*href={~p"/users/log-in"}.*>|s
       end)
 
       assert_file("test/support/conn_case.ex", fn file ->
@@ -366,13 +366,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  ~r|<.link.*href={~p"/users/settings"}.*>|s
 
         assert file =~
-                 ~r|<.link.*href={~p"/users/log_out"}.*method="delete".*>|s
+                 ~r|<.link.*href={~p"/users/log-out"}.*method="delete".*>|s
 
         assert file =~
                  ~r|<.link.*href={~p"/users/register"}.*>|s
 
         assert file =~
-                 ~r|<.link.*href={~p"/users/log_in"}.*>|s
+                 ~r|<.link.*href={~p"/users/log-in"}.*>|s
       end)
 
       assert_file("lib/my_app_web/router.ex", fn file ->
@@ -387,13 +387,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    live_session :redirect_if_user_is_authenticated,
                      on_mount: [{MyAppWeb.UserAuth, :redirect_if_user_is_authenticated}] do
-                     live "/users/register", UserRegistrationLive, :new
-                     live "/users/log_in", UserLoginLive, :new
-                     live "/users/reset_password", UserForgotPasswordLive, :new
-                     live "/users/reset_password/:token", UserResetPasswordLive, :edit
+                     live "/users/register", UserLive.Registration, :new
+                     live "/users/log-in", UserLive.Login, :new
+                     live "/users/reset-password", UserLive.ForgotPassword, :new
+                     live "/users/reset-password/:token", UserLive.ResetPassword, :edit
                    end
 
-                   post "/users/log_in", UserSessionController, :create
+                   post "/users/log-in", UserSessionController, :create
                  end
 
                  scope "/", MyAppWeb do
@@ -401,20 +401,20 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    live_session :require_authenticated_user,
                      on_mount: [{MyAppWeb.UserAuth, :ensure_authenticated}] do
-                     live "/users/settings", UserSettingsLive, :edit
-                     live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
+                     live "/users/settings", UserLive.Settings, :edit
+                     live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
                    end
                  end
 
                  scope "/", MyAppWeb do
                    pipe_through [:browser]
 
-                   delete "/users/log_out", UserSessionController, :delete
+                   delete "/users/log-out", UserSessionController, :delete
 
                    live_session :current_user,
                      on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
-                     live "/users/confirm/:token", UserConfirmationLive, :edit
-                     live "/users/confirm", UserConfirmationInstructionsLive, :new
+                     live "/users/confirm/:token", UserLive.Confirmation, :edit
+                     live "/users/confirm", UserLive.ConfirmationInstructions, :new
                    end
                  end
                """
@@ -434,13 +434,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  ~r|<.link.*href={~p"/users/settings"}.*>|s
 
         assert file =~
-                 ~r|<.link.*href={~p"/users/log_out"}.*method="delete".*>|s
+                 ~r|<.link.*href={~p"/users/log-out"}.*method="delete".*>|s
 
         assert file =~
                  ~r|<.link.*href={~p"/users/register"}.*>|s
 
         assert file =~
-                 ~r|<.link.*href={~p"/users/log_in"}.*>|s
+                 ~r|<.link.*href={~p"/users/log-in"}.*>|s
       end)
 
       assert_file("lib/my_app_web/router.ex", fn file ->
@@ -455,12 +455,12 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/register", UserRegistrationController, :new
                    post "/users/register", UserRegistrationController, :create
-                   get "/users/log_in", UserSessionController, :new
-                   post "/users/log_in", UserSessionController, :create
-                   get "/users/reset_password", UserResetPasswordController, :new
-                   post "/users/reset_password", UserResetPasswordController, :create
-                   get "/users/reset_password/:token", UserResetPasswordController, :edit
-                   put "/users/reset_password/:token", UserResetPasswordController, :update
+                   get "/users/log-in", UserSessionController, :new
+                   post "/users/log-in", UserSessionController, :create
+                   get "/users/reset-password", UserResetPasswordController, :new
+                   post "/users/reset-password", UserResetPasswordController, :create
+                   get "/users/reset-password/:token", UserResetPasswordController, :edit
+                   put "/users/reset-password/:token", UserResetPasswordController, :update
                  end
 
                  scope "/", MyAppWeb do
@@ -468,13 +468,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/settings", UserSettingsController, :edit
                    put "/users/settings", UserSettingsController, :update
-                   get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
+                   get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email
                  end
 
                  scope "/", MyAppWeb do
                    pipe_through [:browser]
 
-                   delete "/users/log_out", UserSessionController, :delete
+                   delete "/users/log-out", UserSessionController, :delete
                    get "/users/confirm", UserConfirmationController, :new
                    post "/users/confirm", UserConfirmationController, :create
                    get "/users/confirm/:token", UserConfirmationController, :edit
@@ -526,7 +526,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  ~r|<\.link.*href={~p"/warehouse/users/register"}.*>|s
 
         assert file =~
-                 ~r|<\.link.*href={~p"/warehouse/users/log_in"}.*>|s
+                 ~r|<\.link.*href={~p"/warehouse/users/log-in"}.*>|s
       end)
 
       assert_file(
@@ -548,13 +548,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  ~r|<\.link.*href={~p"/warehouse/users/settings"}.*>|s
 
         assert file =~
-                 ~r|<\.link.*href={~p"/warehouse/users/log_out"}.*method="delete".*>|s
+                 ~r|<\.link.*href={~p"/warehouse/users/log-out"}.*method="delete".*>|s
 
         assert file =~
                  ~r|<\.link.*href={~p"/warehouse/users/register"}.*>|s
 
         assert file =~
-                 ~r|<\.link.*href={~p"/warehouse/users/log_in"}.*>|s
+                 ~r|<\.link.*href={~p"/warehouse/users/log-in"}.*>|s
       end)
 
       assert_file(
@@ -586,7 +586,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         "lib/my_app_web/controllers/warehouse/user_reset_password_html/edit.html.heex",
         fn file ->
           assert file =~
-                   ~S|<.simple_form :let={f} for={@changeset} action={~p"/warehouse/users/reset_password/#{@token}"}>|
+                   ~S|<.simple_form :let={f} for={@changeset} action={~p"/warehouse/users/reset-password/#{@token}"}>|
         end
       )
 
@@ -594,7 +594,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         "lib/my_app_web/controllers/warehouse/user_reset_password_html/new.html.heex",
         fn file ->
           assert file =~
-                   ~S(<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/reset_password"}>)
+                   ~S(<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/reset-password"}>)
         end
       )
 
@@ -615,13 +615,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
       assert_file("lib/my_app_web/controllers/warehouse/user_session_html/new.html.heex", fn file ->
         assert file =~
-                 ~S|<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/log_in"}>|
+                 ~S|<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/log-in"}>|
 
         assert file =~
                  ~S|<.link navigate={~p"/warehouse/users/register"}|
 
         assert file =~
-                 ~S|<.link href={~p"/warehouse/users/reset_password"}|
+                 ~S|<.link href={~p"/warehouse/users/reset-password"}|
       end)
 
       assert_file(
@@ -677,12 +677,12 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/register", UserRegistrationController, :new
                    post "/users/register", UserRegistrationController, :create
-                   get "/users/log_in", UserSessionController, :new
-                   post "/users/log_in", UserSessionController, :create
-                   get "/users/reset_password", UserResetPasswordController, :new
-                   post "/users/reset_password", UserResetPasswordController, :create
-                   get "/users/reset_password/:token", UserResetPasswordController, :edit
-                   put "/users/reset_password/:token", UserResetPasswordController, :update
+                   get "/users/log-in", UserSessionController, :new
+                   post "/users/log-in", UserSessionController, :create
+                   get "/users/reset-password", UserResetPasswordController, :new
+                   post "/users/reset-password", UserResetPasswordController, :create
+                   get "/users/reset-password/:token", UserResetPasswordController, :edit
+                   put "/users/reset-password/:token", UserResetPasswordController, :update
                  end
 
                  scope "/warehouse", MyAppWeb.Warehouse, as: :warehouse do
@@ -690,13 +690,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/settings", UserSettingsController, :edit
                    put "/users/settings", UserSettingsController, :update
-                   get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
+                   get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email
                  end
 
                  scope "/warehouse", MyAppWeb.Warehouse, as: :warehouse do
                    pipe_through [:browser]
 
-                   delete "/users/log_out", UserSessionController, :delete
+                   delete "/users/log-out", UserSessionController, :delete
                    get "/users/confirm", UserConfirmationController, :new
                    post "/users/confirm", UserConfirmationController, :create
                    get "/users/confirm/:token", UserConfirmationController, :edit
@@ -1050,7 +1050,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert_received {:mix_shell, :yes?, [@liveview_option_message]}
 
         assert_file("mix.exs", fn file ->
-          assert file =~ ~s|{:argon2_elixir, "~> 3.0"}|
+          assert file =~ ~s|{:argon2_elixir, "~> 4.0"}|
         end)
 
         assert_file("config/test.exs", fn file ->
@@ -1380,7 +1380,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                 </li>
                 <li>
                   <.link
-                    href={~p"/users/log_out"}
+                    href={~p"/users/log-out"}
                     method="delete"
                     class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
                   >
@@ -1398,7 +1398,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                 </li>
                 <li>
                   <.link
-                    href={~p"/users/log_in"}
+                    href={~p"/users/log-in"}
                     class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
                   >
                     Log in
@@ -1444,7 +1444,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                 </li>
                 <li>
                   <.link
-                    href={~p"/users/log_out"}
+                    href={~p"/users/log-out"}
                     method="delete"
                     class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
                   >
@@ -1462,7 +1462,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                 </li>
                 <li>
                   <.link
-                    href={~p"/users/log_in"}
+                    href={~p"/users/log-in"}
                     class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
                   >
                     Log in
diff --git a/test/mix/tasks/phx.gen.html_test.exs b/test/mix/tasks/phx.gen.html_test.exs
index b0eb2d309..450e0599b 100644
--- a/test/mix/tasks/phx.gen.html_test.exs
+++ b/test/mix/tasks/phx.gen.html_test.exs
@@ -44,7 +44,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
     datetime = %{DateTime.utc_now() | second: 0, microsecond: {0, 6}}
 
     in_tmp_project(config.test, fn ->
-      Gen.Html.run(~w(Blog Post posts title slug:unique votes:integer cost:decimal
+      Gen.Html.run(~w(Blog Post posts title content:text slug:unique votes:integer cost:decimal
                       tags:array:text popular:boolean drafted_at:datetime
                       status:enum:unpublished:published:deleted
                       published_at:utc_datetime
@@ -110,6 +110,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
       assert_file(path, fn file ->
         assert file =~ "create table(:posts)"
         assert file =~ "add :title, :string"
+        assert file =~ "add :content, :text"
         assert file =~ "add :status, :string"
         assert file =~ "create unique_index(:posts, [:slug])"
       end)
@@ -151,6 +152,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
       assert_file("lib/phoenix_web/controllers/post_html/post_form.html.heex", fn file ->
         assert file =~ ~S(<.simple_form :let={f} for={@changeset} action={@action}>)
         assert file =~ ~s(<.input field={f[:title]} type="text")
+        assert file =~ ~s(<.input field={f[:content]} type="textarea")
         assert file =~ ~s(<.input field={f[:votes]} type="number")
         assert file =~ ~s(<.input field={f[:cost]} type="number" label="Cost" step="any")
 
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index e67f57a30..dd5cb4756 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -55,7 +55,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
 
   test "generates live resource and handles existing contexts", config do
     in_tmp_live_project config.test, fn ->
-      Gen.Live.run(~w(Blog Post posts title slug:unique votes:integer cost:decimal
+      Gen.Live.run(~w(Blog Post posts title content:text slug:unique votes:integer cost:decimal
                       tags:array:text popular:boolean drafted_at:datetime
                       status:enum:unpublished:published:deleted
                       published_at:utc_datetime
@@ -88,6 +88,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
       assert_file path, fn file ->
         assert file =~ "create table(:posts)"
         assert file =~ "add :title, :string"
+        assert file =~ "add :content, :text"
         assert file =~ "create unique_index(:posts, [:slug])"
       end
 
@@ -102,6 +103,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
       assert_file "lib/phoenix_web/live/post_live/form.ex", fn file ->
         assert file =~ ~s(<.simple_form)
         assert file =~ ~s(<.input field={@form[:title]} type="text")
+        assert file =~ ~s(<.input field={@form[:content]} type="textarea")
         assert file =~ ~s(<.input field={@form[:votes]} type="number")
         assert file =~ ~s(<.input field={@form[:cost]} type="number" label="Cost" step="any")
         assert file =~ """
diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs
index 279715fa7..4b2057f09 100644
--- a/test/phoenix/controller/controller_test.exs
+++ b/test/phoenix/controller/controller_test.exs
@@ -537,6 +537,17 @@ defmodule Phoenix.Controller.ControllerTest do
              "world"
     end
 
+    test "sends file for download for filename with unreserved characters" do
+      conn = send_download(conn(:get, "/"), {:file, @hello_txt}, filename: "hello, world.json")
+      assert conn.status == 200
+      assert get_resp_header(conn, "content-disposition") ==
+             ["attachment; filename=\"hello%2C%20world.json\"; filename*=utf-8''hello%2C%20world.json"]
+      assert get_resp_header(conn, "content-type") ==
+             ["application/json"]
+      assert conn.resp_body ==
+             "world"
+    end
+
     test "sends file supports UTF-8" do
       conn = send_download(conn(:get, "/"), {:file, @hello_txt}, filename: "测 试.txt")
       assert conn.status == 200
diff --git a/test/phoenix/endpoint/supervisor_test.exs b/test/phoenix/endpoint/supervisor_test.exs
index 235c5c0be..3b2b958db 100644
--- a/test/phoenix/endpoint/supervisor_test.exs
+++ b/test/phoenix/endpoint/supervisor_test.exs
@@ -180,4 +180,44 @@ defmodule Phoenix.Endpoint.SupervisorTest do
              end)
     end
   end
+
+  describe "origin & CSRF checks config" do
+    defmodule TestSocket do
+      @behaviour Phoenix.Socket.Transport
+      def child_spec(_), do: :ignore
+      def connect(_), do: {:ok, []}
+      def init(state), do: {:ok, state}
+      def handle_in(_, state), do: {:ok, state}
+      def handle_info(_, state), do: {:ok, state}
+      def terminate(_, _), do: :ok
+    end
+
+    defmodule SocketEndpoint do
+      use Phoenix.Endpoint, otp_app: :phoenix
+
+      socket "/ws", TestSocket, websocket: [check_csrf: false, check_origin: false]
+    end
+
+    Application.put_env(:phoenix, SocketEndpoint, [])
+
+    test "fails when CSRF and origin checks both disabled in transport" do
+      assert_raise ArgumentError, ~r/one of :check_origin and :check_csrf must be set/, fn ->
+        Supervisor.init({:phoenix, SocketEndpoint, []})
+      end
+    end
+
+    defmodule SocketEndpointOriginCheckDisabled do
+      use Phoenix.Endpoint, otp_app: :phoenix
+
+      socket "/ws", TestSocket, websocket: [check_csrf: false]
+    end
+
+    Application.put_env(:phoenix, SocketEndpointOriginCheckDisabled, check_origin: false)
+
+    test "fails when origin is disabled in endpoint config and CSRF disabled in transport" do
+      assert_raise ArgumentError, ~r/one of :check_origin and :check_csrf must be set/, fn ->
+        Supervisor.init({:phoenix, SocketEndpointOriginCheckDisabled, []})
+      end
+    end
+  end
 end
diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs
index 2a3e3df9f..05700342d 100644
--- a/test/phoenix/socket/transport_test.exs
+++ b/test/phoenix/socket/transport_test.exs
@@ -276,7 +276,7 @@ defmodule Phoenix.Socket.TransportTest do
     end
   end
 
-  describe "connect_info/3" do
+  describe "connect_info/4" do
     defp load_connect_info(connect_info) do
       [connect_info: connect_info] = Transport.load_config(connect_info: connect_info)
       connect_info
@@ -330,5 +330,31 @@ defmodule Phoenix.Socket.TransportTest do
               |> Transport.connect_info(Endpoint, connect_info)
     end
 
+    test "loads the session when CSRF is disabled despite CSRF token not being provided" do
+      conn = conn(:get, "https://foo.com/") |> Endpoint.call([])
+      session_cookie = conn.cookies["_hello_key"]
+
+      connect_info = load_connect_info(session: {Endpoint, :session_config, []})
+
+      assert %{session: %{"from_session" => "123"}} =
+        conn(:get, "https://foo.com/")
+        |> put_req_cookie("_hello_key", session_cookie)
+        |> fetch_query_params()
+        |> Transport.connect_info(Endpoint, connect_info, check_csrf: false)
+    end
+
+    test "doesn't load session when an invalid CSRF token is provided" do
+      conn = conn(:get, "https://foo.com/") |> Endpoint.call([])
+      invalid_csrf_token = "some invalid CSRF token"
+      session_cookie = conn.cookies["_hello_key"]
+
+      connect_info = load_connect_info(session: {Endpoint, :session_config, []})
+
+      assert %{session: nil} =
+        conn(:get, "https://foo.com/", _csrf_token: invalid_csrf_token)
+        |> put_req_cookie("_hello_key", session_cookie)
+        |> fetch_query_params()
+        |> Transport.connect_info(Endpoint, connect_info)
+    end
   end
 end
diff --git a/test/support/router_helper.exs b/test/support/router_helper.exs
index d306b3137..9cbf4be23 100644
--- a/test/support/router_helper.exs
+++ b/test/support/router_helper.exs
@@ -11,7 +11,8 @@ defmodule RouterHelper do
 
   defmacro __using__(_) do
     quote do
-      use Plug.Test
+      import Plug.Test
+      import Plug.Conn
       import RouterHelper
     end
   end
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 320240551..ae30db4b7 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -26,7 +26,7 @@ assert_timeout = String.to_integer(
 )
 
 excludes =
-  if Version.match?(System.version(), "~> 1.14") do
+  if Version.match?(System.version(), "~> 1.15") do
     []
   else
     [:mix_phx_new]