Repository: phoenixframework/phoenix
Pull request #5171 (2022-12-20 18:05:49)

Improve docs on custom errors from `Socket.connect/3`


Patch:
diff --git a/assets/package-lock.json b/assets/package-lock.json
index b168be3c2..392c190d0 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -2857,9 +2857,9 @@
       }
     },
     "node_modules/documentation": {
-      "version": "14.0.0",
-      "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.0.tgz",
-      "integrity": "sha512-4AwFzdiseEdtqR0KKLrruIQ5fvh7n5epg47P0ZyOidA5Fes5am+6xjqkDECHPwcv4pxJ6zITaHwzCoGblP0+JQ==",
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.1.tgz",
+      "integrity": "sha512-Y/brACCE3sNnDJPFiWlhXrqGY+NelLYVZShLGse5bT1KdohP4JkPf5T2KNq1YWhIEbDYl/1tebRLC0WYbPQxVw==",
       "dev": true,
       "dependencies": {
         "@babel/core": "^7.18.10",
@@ -2871,7 +2871,7 @@
         "chokidar": "^3.5.3",
         "diff": "^5.1.0",
         "doctrine-temporary-fork": "2.1.0",
-        "git-url-parse": "^12.0.0",
+        "git-url-parse": "^13.1.0",
         "github-slugger": "1.4.0",
         "glob": "^8.0.3",
         "globals-docs": "^2.4.1",
@@ -3999,22 +3999,22 @@
       }
     },
     "node_modules/git-up": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/git-up/-/git-up-6.0.0.tgz",
-      "integrity": "sha512-6RUFSNd1c/D0xtGnyWN2sxza2bZtZ/EmI9448n6rCZruFwV/ezeEn2fJP7XnUQGwf0RAtd/mmUCbtH6JPYA2SA==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz",
+      "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==",
       "dev": true,
       "dependencies": {
         "is-ssh": "^1.4.0",
-        "parse-url": "^7.0.2"
+        "parse-url": "^8.1.0"
       }
     },
     "node_modules/git-url-parse": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-12.0.0.tgz",
-      "integrity": "sha512-I6LMWsxV87vysX1WfsoglXsXg6GjQRKq7+Dgiseo+h0skmp5Hp2rzmcEIRQot9CPA+uzU7x1x7jZdqvTFGnB+Q==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz",
+      "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==",
       "dev": true,
       "dependencies": {
-        "git-up": "^6.0.0"
+        "git-up": "^7.0.0"
       }
     },
     "node_modules/github-slugger": {
@@ -4788,9 +4788,9 @@
       "dev": true
     },
     "node_modules/json5": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
-      "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
       "dev": true,
       "bin": {
         "json5": "lib/cli.js"
@@ -6500,18 +6500,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/normalize-url": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
-      "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/nwmatcher": {
       "version": "1.4.4",
       "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz",
@@ -6766,24 +6754,21 @@
       }
     },
     "node_modules/parse-path": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-5.0.0.tgz",
-      "integrity": "sha512-qOpH55/+ZJ4jUu/oLO+ifUKjFPNZGfnPJtzvGzKN/4oLMil5m9OH4VpOj6++9/ytJcfks4kzH2hhi87GL/OU9A==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz",
+      "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==",
       "dev": true,
       "dependencies": {
         "protocols": "^2.0.0"
       }
     },
     "node_modules/parse-url": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-7.0.2.tgz",
-      "integrity": "sha512-PqO4Z0eCiQ08Wj6QQmrmp5YTTxpYfONdOEamrtvK63AmzXpcavIVQubGHxOEwiIoDZFb8uDOoQFS0NCcjqIYQg==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz",
+      "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==",
       "dev": true,
       "dependencies": {
-        "is-ssh": "^1.4.0",
-        "normalize-url": "^6.1.0",
-        "parse-path": "^5.0.0",
-        "protocols": "^2.0.1"
+        "parse-path": "^7.0.0"
       }
     },
     "node_modules/parse5": {
@@ -7015,9 +7000,9 @@
       }
     },
     "node_modules/qs": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+      "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
       "dev": true,
       "engines": {
         "node": ">=0.6"
@@ -11520,9 +11505,9 @@
       }
     },
     "documentation": {
-      "version": "14.0.0",
-      "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.0.tgz",
-      "integrity": "sha512-4AwFzdiseEdtqR0KKLrruIQ5fvh7n5epg47P0ZyOidA5Fes5am+6xjqkDECHPwcv4pxJ6zITaHwzCoGblP0+JQ==",
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.1.tgz",
+      "integrity": "sha512-Y/brACCE3sNnDJPFiWlhXrqGY+NelLYVZShLGse5bT1KdohP4JkPf5T2KNq1YWhIEbDYl/1tebRLC0WYbPQxVw==",
       "dev": true,
       "requires": {
         "@babel/core": "^7.18.10",
@@ -11535,7 +11520,7 @@
         "chokidar": "^3.5.3",
         "diff": "^5.1.0",
         "doctrine-temporary-fork": "2.1.0",
-        "git-url-parse": "^12.0.0",
+        "git-url-parse": "^13.1.0",
         "github-slugger": "1.4.0",
         "glob": "^8.0.3",
         "globals-docs": "^2.4.1",
@@ -12400,22 +12385,22 @@
       }
     },
     "git-up": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/git-up/-/git-up-6.0.0.tgz",
-      "integrity": "sha512-6RUFSNd1c/D0xtGnyWN2sxza2bZtZ/EmI9448n6rCZruFwV/ezeEn2fJP7XnUQGwf0RAtd/mmUCbtH6JPYA2SA==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz",
+      "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==",
       "dev": true,
       "requires": {
         "is-ssh": "^1.4.0",
-        "parse-url": "^7.0.2"
+        "parse-url": "^8.1.0"
       }
     },
     "git-url-parse": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-12.0.0.tgz",
-      "integrity": "sha512-I6LMWsxV87vysX1WfsoglXsXg6GjQRKq7+Dgiseo+h0skmp5Hp2rzmcEIRQot9CPA+uzU7x1x7jZdqvTFGnB+Q==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz",
+      "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==",
       "dev": true,
       "requires": {
-        "git-up": "^6.0.0"
+        "git-up": "^7.0.0"
       }
     },
     "github-slugger": {
@@ -13026,9 +13011,9 @@
       "dev": true
     },
     "json5": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
-      "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
       "dev": true
     },
     "jsprim": {
@@ -14230,12 +14215,6 @@
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
       "dev": true
     },
-    "normalize-url": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
-      "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
-      "dev": true
-    },
     "nwmatcher": {
       "version": "1.4.4",
       "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz",
@@ -14431,24 +14410,21 @@
       }
     },
     "parse-path": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-5.0.0.tgz",
-      "integrity": "sha512-qOpH55/+ZJ4jUu/oLO+ifUKjFPNZGfnPJtzvGzKN/4oLMil5m9OH4VpOj6++9/ytJcfks4kzH2hhi87GL/OU9A==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz",
+      "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==",
       "dev": true,
       "requires": {
         "protocols": "^2.0.0"
       }
     },
     "parse-url": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-7.0.2.tgz",
-      "integrity": "sha512-PqO4Z0eCiQ08Wj6QQmrmp5YTTxpYfONdOEamrtvK63AmzXpcavIVQubGHxOEwiIoDZFb8uDOoQFS0NCcjqIYQg==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz",
+      "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==",
       "dev": true,
       "requires": {
-        "is-ssh": "^1.4.0",
-        "normalize-url": "^6.1.0",
-        "parse-path": "^5.0.0",
-        "protocols": "^2.0.1"
+        "parse-path": "^7.0.0"
       }
     },
     "parse5": {
@@ -14623,9 +14599,9 @@
       "dev": true
     },
     "qs": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+      "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
       "dev": true
     },
     "querystringify": {
diff --git a/guides/controllers.md b/guides/controllers.md
index 14b21474c..aeabe8d47 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -181,8 +181,8 @@ What it doesn't have is a view for rendering JSON. Phoenix Controller hands off
   def controller do
     quote do
       use Phoenix.Controller,
-        namespace: HelloWeb,
-        formats: [:html, :json]
+        formats: [:html, :json],
+        layouts: [html: HelloWeb.Layouts]
       ...
     end
   end
diff --git a/guides/ecto.md b/guides/ecto.md
index c2846bccc..67be98a37 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -562,7 +562,7 @@ The last change is to open up `lib/hello_phoenix/repo.ex` and make sure to set t
 Now all we need to do is fetch our new dependency, and we'll be ready to go.
 
 ```console
-$ mix do deps.get, compile
+$ mix deps.get
 ```
 
 With our new adapter installed and configured, we're ready to create our database.
diff --git a/guides/howto/custom_error_pages.md b/guides/howto/custom_error_pages.md
index 385e76fa6..bd94ea93c 100644
--- a/guides/howto/custom_error_pages.md
+++ b/guides/howto/custom_error_pages.md
@@ -14,10 +14,10 @@ defmodule HelloWeb.ErrorHTML do
   # uncomment the embed_templates/1 call below
   # and add pages to the error directory:
   #
-  #   * lib/<%= @lib_web_name %>/controllers/error/404.html.heex
-  #   * lib/<%= @lib_web_name %>/controllers/error/500.html.heex
+  #   * lib/<%= @lib_web_name %>/controllers/error_html/404.html.heex
+  #   * lib/<%= @lib_web_name %>/controllers/error_html/500.html.heex
   #
-  # embed_templates "error/*"
+  # embed_templates "error_html/*"
 
   # The default is to render a plain text page based on
   # the template name. For example, "404.html" becomes
diff --git a/guides/json_and_apis.md b/guides/json_and_apis.md
index 5daecdd8f..9d78fb0dd 100644
--- a/guides/json_and_apis.md
+++ b/guides/json_and_apis.md
@@ -266,23 +266,13 @@ The goal of this clause is to handle the `{:error, changeset}` return types from
 
 ```elixir
 defmodule HelloWeb.ChangesetJSON do
-  @doc """
-  Traverses and translates changeset errors.
-
-  See `Ecto.Changeset.traverse_errors/2` and
-  `HelloWeb.CoreCompponents.translate_error/1` for more details.
-  """
-  def translate_errors(changeset) do
-    Ecto.Changeset.traverse_errors(changeset, &HelloWeb.CoreComponents.translate_error/1)
-  end
-
   @doc """
   Renders changeset errors.
   """
   def error(%{changeset: changeset}) do
     # When encoded, the changeset returns its errors
     # as a JSON object. So we just pass it forward.
-    %{errors: translate_errors(changeset)}
+    %{errors: Ecto.Changeset.traverse_errors(changeset, &HelloWeb.CoreComponents.translate_error/1)}
   end
 end
 ```
diff --git a/guides/routing.md b/guides/routing.md
index 472b2f5b5..0ce693c63 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -28,7 +28,7 @@ defmodule HelloWeb.Router do
   scope "/", HelloWeb do
     pipe_through :browser
 
-    get "/", PageController, :index
+    get "/", PageController, :home
   end
 
   # Other scopes may use custom stacks.
@@ -48,7 +48,7 @@ Scopes have their own section in this guide, so we won't spend time on the `scop
 Inside the scope block, however, we have our first actual route:
 
 ```elixir
-get "/", PageController, :index
+get "/", PageController, :home
 ```
 
 `get` is a Phoenix macro that corresponds to the HTTP verb GET. Similar macros exist for other HTTP verbs, including POST, PUT, PATCH, DELETE, OPTIONS, CONNECT, TRACE, and HEAD.
@@ -61,11 +61,11 @@ Let's see how this works. Go to the root of a newly-generated Phoenix applicatio
 
 ```console
 $ mix phx.routes
-GET  /  HelloWeb.PageController :index
+GET  /  HelloWeb.PageController :home
 ...
 ```
 
-The route above tells us that any HTTP GET request for the root of the application will be handled by the `index` action of the `HelloWeb.PageController`.
+The route above tells us that any HTTP GET request for the root of the application will be handled by the `home` action of the `HelloWeb.PageController`.
 
 ## Resources
 
@@ -75,7 +75,7 @@ The router supports other macros besides those for HTTP verbs like [`get`](`Phoe
 scope "/", HelloWeb do
   pipe_through :browser
 
-  get "/", PageController, :index
+  get "/", PageController, :home
   resources "/users", UserController
   ...
 end
@@ -98,7 +98,7 @@ DELETE  /users/:id       HelloWeb.UserController :delete
 ...
 ```
 
-This is the standard matrix of HTTP verbs, paths, and controller actions. For a while, this was known as RESTful routes, but most consider this a misnomer nowadays. Let's look at them individually, in a slightly different order.
+This is the standard matrix of HTTP verbs, paths, and controller actions. For a while, this was known as RESTful routes, but most consider this a misnomer nowadays. Let's look at them individually.
 
 - A GET request to `/users` will invoke the `index` action to show all the users.
 - A GET request to `/users/:id/edit` will invoke the `edit` action with an ID to retrieve an individual user from the data store and present the information in a form for editing.
@@ -469,7 +469,7 @@ defmodule HelloWeb.Router do
   scope "/", HelloWeb do
     pipe_through :browser
 
-    get "/", PageController, :index
+    get "/", PageController, :home
   end
 
   # Other scopes may use custom stacks.
@@ -484,7 +484,7 @@ end
 
 When the server accepts a request, the request will always first pass through the plugs in our endpoint, after which it will attempt to match on the path and HTTP verb.
 
-Let's say that the request matches our first route: a GET to `/`. The router will first pipe that request through the `:browser` pipeline - which will fetch the session data, fetch the flash, and execute forgery protection - before it dispatches the request to `PageController`'s `index` action.
+Let's say that the request matches our first route: a GET to `/`. The router will first pipe that request through the `:browser` pipeline - which will fetch the session data, fetch the flash, and execute forgery protection - before it dispatches the request to `PageController`'s `home` action.
 
 Conversely, suppose the request matches any of the routes defined by the [`resources/2`](`Phoenix.Router.resources/2`) macro. In that case, the router will pipe it through the `:api` pipeline — which currently only performs content negotiation — before it dispatches further to the correct action of the `HelloWeb.ReviewController`.
 
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index 6afef65e4..cbc1fd406 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -170,8 +170,25 @@ defmodule Mix.Tasks.Phx.New do
     maybe_cd(path, fn ->
       mix_step = install_mix(project, install?)
 
-      if mix_step == [] and rebar_available?() do
-        cmd(project, "mix deps.compile")
+      if mix_step == [] do
+        tasks =
+          if Keyword.get(project.opts, :assets, true) do
+            # First compile only esbuild and tailwind so we can install in parallel
+            cmd(project, "mix deps.compile esbuild tailwind")
+
+            Enum.map(
+              ["mix tailwind.install", "mix esbuild.install"],
+              &Task.async(fn -> cmd(project, &1) end)
+            )
+          else
+            []
+          end
+
+        if rebar_available?() do
+          cmd(project, "mix deps.compile")
+        end
+
+        Task.await_many(tasks, :infinity)
       end
 
       print_missing_steps(cd_step ++ mix_step)
@@ -208,7 +225,11 @@ defmodule Mix.Tasks.Phx.New do
   defp switch_to_string({name, val}), do: name <> "=" <> val
 
   defp install_mix(project, install?) do
-    maybe_cmd(project, "mix deps.get", true, install? && hex_available?())
+    if install? && hex_available?() 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.
@@ -266,24 +287,12 @@ defmodule Mix.Tasks.Phx.New do
 
   ## Helpers
 
-  defp maybe_cmd(project, cmd, should_run?, can_run?) do
-    cond do
-      should_run? && can_run? ->
-        cmd(project, cmd)
-      should_run? ->
-        ["$ #{cmd}"]
-      true ->
-        []
-    end
-  end
-
   defp cmd(%Project{} = project, cmd) do
     Mix.shell().info [:green, "* running ", :reset, cmd]
+
     case Mix.shell().cmd(cmd, cmd_opts(project)) do
-      0 ->
-        []
-      _ ->
-        ["$ #{cmd}"]
+      0 -> []
+      _ -> ["$ #{cmd}"]
     end
   end
 
diff --git a/installer/lib/phx_new/single.ex b/installer/lib/phx_new/single.ex
index 649ebc271..9e0d77ddb 100644
--- a/installer/lib/phx_new/single.ex
+++ b/installer/lib/phx_new/single.ex
@@ -54,7 +54,7 @@ defmodule Phx.New.Single do
      "phx_web/components/layouts/app.html.heex":
        "lib/:lib_web_name/components/layouts/app.html.heex",
      "phx_web/components/layouts.ex": "lib/:lib_web_name/components/layouts.ex"},
-    {:eex, :web, "phx_live/assets/topbar.js": "assets/vendor/topbar.js"}
+    {:eex, :web, "phx_assets/topbar.js": "assets/vendor/topbar.js"}
   ])
 
   template(:ecto, [
diff --git a/installer/lib/phx_new/web.ex b/installer/lib/phx_new/web.ex
index 87a593979..2605d0386 100644
--- a/installer/lib/phx_new/web.ex
+++ b/installer/lib/phx_new/web.ex
@@ -51,7 +51,7 @@ defmodule Phx.New.Web do
        "lib/:web_app/controllers/page_html/home.html.heex",
      "phx_test/controllers/page_controller_test.exs":
        "test/:web_app/controllers/page_controller_test.exs",
-     "phx_live/assets/topbar.js": "assets/vendor/topbar.js",
+     "phx_assets/topbar.js": "assets/vendor/topbar.js",
      "phx_web/components/layouts/root.html.heex": "lib/:web_app/components/layouts/root.html.heex",
      "phx_web/components/layouts/app.html.heex": "lib/:web_app/components/layouts/app.html.heex"}
   ])
diff --git a/installer/templates/phx_live/assets/topbar.js b/installer/templates/phx_assets/topbar.js
similarity index 100%
rename from installer/templates/phx_live/assets/topbar.js
rename to installer/templates/phx_assets/topbar.js
diff --git a/installer/templates/phx_single/README.md b/installer/templates/phx_single/README.md
index cb0a1617b..5c96eb63a 100644
--- a/installer/templates/phx_single/README.md
+++ b/installer/templates/phx_single/README.md
@@ -2,8 +2,7 @@
 
 To start your Phoenix server:
 
-  * Install dependencies with `mix deps.get`<%= if @ecto do %>
-  * Create and migrate your database with `mix ecto.setup`<% end %>
+  * Run `mix setup` to install and setup dependencies
   * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
 
 Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
diff --git a/installer/templates/phx_single/lib/app_name_web.ex b/installer/templates/phx_single/lib/app_name_web.ex
index bf3382b4f..a2d52e1d3 100644
--- a/installer/templates/phx_single/lib/app_name_web.ex
+++ b/installer/templates/phx_single/lib/app_name_web.ex
@@ -39,7 +39,6 @@ defmodule <%= @web_namespace %> do
   def controller do
     quote do
       use Phoenix.Controller,
-        namespace: <%= @web_namespace %>,
         formats: [:html, :json],
         layouts: [html: <%= @web_namespace %>.Layouts]
 
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index bea1b5f68..34d446639 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -66,10 +66,11 @@ defmodule <%= @app_module %>.MixProject do
   # See the documentation for `Mix` for more info on aliases.
   defp aliases do
     [
-      setup: ["deps.get"<%= if @ecto do %>, "ecto.setup"<% end %>]<%= if @ecto do %>,
+      setup: ["deps.get"<%= if @ecto do %>, "ecto.setup"<% end %><%= if @assets do %>, "assets.setup"<% end %>]<%= if @ecto do %>,
       "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
       "ecto.reset": ["ecto.drop", "ecto.setup"],
       test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]<% end %><%= if @assets do %>,
+      "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
       "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]<% end %>
     ]
   end
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/README.md b/installer/templates/phx_umbrella/apps/app_name_web/README.md
index d462ec3e8..fdb87dfcf 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/README.md
+++ b/installer/templates/phx_umbrella/apps/app_name_web/README.md
@@ -2,8 +2,7 @@
 
 To start your Phoenix server:
 
-  * Install dependencies with `mix deps.get`<%= if @ecto do %>
-  * Create and migrate your database with `mix ecto.setup`<% end %>
+  * Run `mix setup` to install and setup dependencies
   * Start Phoenix endpoint with `mix phx.server`
 
 Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex b/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex
index bf3382b4f..a2d52e1d3 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex
+++ b/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex
@@ -39,7 +39,6 @@ defmodule <%= @web_namespace %> do
   def controller do
     quote do
       use Phoenix.Controller,
-        namespace: <%= @web_namespace %>,
         formats: [:html, :json],
         layouts: [html: <%= @web_namespace %>.Layouts]
 
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 711b02cf4..6f70f2200 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -60,8 +60,9 @@ defmodule <%= @web_namespace %>.MixProject do
   # See the documentation for `Mix` for more info on aliases.
   defp aliases do
     [
-      setup: ["deps.get"]<%= if @ecto do %>,
+      setup: ["deps.get"<%= if @assets do %>, "assets.setup"<% end %>]<%= if @ecto do %>,
       test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]<% end %><%= if @assets do %>,
+      "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
       "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]<% end %>
     ]
   end
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index f9264a307..4b632d353 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -147,7 +147,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
       id={@id}
       phx-mounted={@autoshow && show("##{@id}")}
-      phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("#flash")}
+      phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
       role="alert"
       class={[
         "fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
@@ -339,7 +339,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
         ]}
         {@rest}
       >
-
     <%%= @value %></textarea>
       <.error :for={msg <- @errors}><%%= msg %></.error>
     </div>
diff --git a/installer/templates/phx_web/controllers/error_html.ex b/installer/templates/phx_web/controllers/error_html.ex
index 0a9df6221..9d575e7da 100644
--- a/installer/templates/phx_web/controllers/error_html.ex
+++ b/installer/templates/phx_web/controllers/error_html.ex
@@ -5,10 +5,10 @@ defmodule <%= @web_namespace %>.ErrorHTML do
   # uncomment the embed_templates/1 call below
   # and add pages to the error directory:
   #
-  #   * lib/<%= @lib_web_name %>/controllers/error/404.html.heex
-  #   * lib/<%= @lib_web_name %>/controllers/error/500.html.heex
+  #   * lib/<%= @lib_web_name %>/controllers/error_html/404.html.heex
+  #   * lib/<%= @lib_web_name %>/controllers/error_html/500.html.heex
   #
-  # embed_templates "error/*"
+  # embed_templates "error_html/*"
 
   # The default is to render a plain text page based on
   # the template name. For example, "404.html" becomes
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 98f20d571..53bfdd4f4 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -6,7 +6,7 @@ defmodule Phoenix.Controller do
   require Phoenix.Endpoint
 
   @unsent [:unset, :set, :set_chunked, :set_file]
-  
+
   @type view :: atom()
   @type layout :: {module(), layout_name :: atom()} | false
 
@@ -25,7 +25,7 @@ defmodule Phoenix.Controller do
 
         def show(conn, %{"id" => id}) do
           user = Repo.get(User, id)
-          render(conn, "show.html", user: user)
+          render(conn, :show, user: user)
         end
       end
 
@@ -33,16 +33,9 @@ defmodule Phoenix.Controller do
   and the request parameters as arguments. The connection is a
   `Plug.Conn` struct, as specified by the Plug library.
 
-  ## Options
-
-  When used, the controller supports the following options:
-
-    * `:namespace` - sets the namespace to properly inflect
-      the layout view. By default it uses the base alias
-      in your controller name
-
-    * `:put_default_views` - controls whether the default view
-      and layout should be set or not
+  Then we invoke `render/3`, passing the connection, the template
+  to render (typically named after the action), and the `user: user`
+  as assigns. We will explore all of those concepts next.
 
   ## Connection
 
@@ -61,6 +54,73 @@ defmodule Phoenix.Controller do
   without fully implementing the controller, you can import both
   modules directly instead of `use Phoenix.Controller`.
 
+  ## Rendering and layouts
+
+  One of the main features provided by controllers is the ability
+  to perform content negotiation and render templates based on
+  information sent by the client.
+
+  There are two ways to render content in a controller. One option
+  is to invoke format-specific functions, such as `html/2` and `json/2`.
+
+  However, most commonly controllers invoke custom modules called
+  views. Views are modules capable of rendering a custom format.
+  This is done by specifying the option `:formats` when defining
+  the controller:
+
+      use Phoenix.Controller,
+        formats: [:html, :json]
+
+   Now, when invoking `render/3`, a controller named `MyAppWeb.UserController`
+   will invoke `MyAppWeb.UserHTML` and `MyAppWeb.UserJSON` respectively
+   when rendering each format:
+
+      def show(conn, %{"id" => id}) do
+        user = Repo.get(User, id)
+        # Will invoke UserHTML.show(%{user: user}) for html requests
+        # Will invoke UserJSON.show(%{user: user}) for json requests
+        render(conn, :show, user: user)
+      end
+
+  Some formats are also handy to have layouts, which render content
+  shared across all pages. We can also specify layouts on `use`:
+
+      use Phoenix.Controller,
+        formats: [:html, :json],
+        layouts: [html: MyAppWeb.Layouts]
+
+  You can also specify formats and layouts to render by calling
+  `put_view/2` and `put_layout/2` directly with a connection.
+  The line above can also be written directly in your actions as:
+
+      conn
+      |> put_view(html: MyAppWeb.UserHTML, json: MyAppWeb.UserJSON)
+      |> put_layout(html: MyAppWeb.Layouts)
+
+  ### Options
+
+  When used, the controller supports the following options to customize
+  template rendering:
+
+    * `:formats` - the formats this controller will render
+      by default. For example, specifying `formats: [:html, :json]`
+      for a controller named `MyAppWeb.UserController` will
+      invoke `MyAppWeb.UserHTML` and `MyAppWeb.UserJSON` when
+      respectively rendering each format. If `:formats` is not
+      set, the default view is set to `MyAppWeb.UserView`
+
+    * `:layouts` - which layouts to render for each format,
+      for example: `[html: DemoWeb.Layouts]`
+
+  Deprecated options:
+
+    * `:namespace` - sets the namespace for the layout. Use
+      `:layouts` instead
+
+    * `:put_default_views` - controls whether the default view
+      and layout should be set or not. Set `formats: []` and
+      `layouts: []` instead
+
   ## Plug pipeline
 
   As with routers, controllers also have their own plug pipeline.
@@ -150,18 +210,15 @@ defmodule Phoenix.Controller do
         # ...
       end
 
-  ## Rendering and layouts
-
-  One of the main features provided by controllers is the ability
-  to perform content negotiation and render templates based on
-  information sent by the client. Read `render/3` to learn more.
-
-  It is also important not to confuse `Phoenix.Controller.render/3`
-  with `Phoenix.View.render/3`. The former expects
-  a connection and relies on content negotiation while the latter is
-  connection-agnostic and typically invoked from your views.
   """
   defmacro __using__(opts) do
+    opts =
+      if Macro.quoted_literal?(opts) do
+        Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
+      else
+        opts
+      end
+
     quote bind_quoted: [opts: opts] do
       import Phoenix.Controller
 
@@ -177,6 +234,11 @@ defmodule Phoenix.Controller do
     end
   end
 
+  defp expand_alias({:__aliases__, _, _} = alias, env),
+    do: Macro.expand(alias, %{env | function: {:action, 2}})
+
+  defp expand_alias(other, _env), do: other
+
   @doc """
   Registers the plug to call as a fallback to the controller action.
 
@@ -446,15 +508,15 @@ defmodule Phoenix.Controller do
   Stores the view for rendering.
 
   Raises `Plug.Conn.AlreadySentError` if `conn` is already sent.
-  
+
   ## Examples
-  
+
       # Use single view module
       iex> put_view(conn, AppView)
-      
+
       # Use multiple view module for content negotiation
       iex> put_view(conn, html: AppHTML, json: AppJSON)
-      
+
   """
   @spec put_view(Plug.Conn.t(), [{format :: atom, view}] | view) :: Plug.Conn.t()
   def put_view(%Plug.Conn{state: state} = conn, formats) when state in @unsent do
@@ -536,15 +598,11 @@ defmodule Phoenix.Controller do
       iex> layout(conn)
       false
 
-      iex> conn = put_layout conn, {AppView, "application.html"}
-      iex> layout(conn)
-      {AppView, "application.html"}
-
-      iex> conn = put_layout conn, "print.html"
+      iex> conn = put_layout(conn, {AppView, :application})
       iex> layout(conn)
-      {AppView, "print.html"}
+      {AppView, :application}
 
-      iex> conn = put_layout conn, :print
+      iex> conn = put_layout(conn, :print)
       iex> layout(conn)
       {AppView, :print}
 
@@ -635,15 +693,11 @@ defmodule Phoenix.Controller do
       iex> root_layout(conn)
       false
 
-      iex> conn = put_root_layout conn, {AppView, "root.html"}
-      iex> root_layout(conn)
-      {AppView, "root.html"}
-
-      iex> conn = put_root_layout conn, "bare.html"
+      iex> conn = put_root_layout(conn, {AppView, :root})
       iex> root_layout(conn)
-      {AppView, "bare.html"}
+      {AppView, :root}
 
-      iex> conn = put_root_layout conn, :bare
+      iex> conn = put_root_layout(conn, :bare)
       iex> root_layout(conn)
       {AppView, :bare}
 
@@ -1758,7 +1812,7 @@ defmodule Phoenix.Controller do
 
           other ->
             raise ArgumentError, """
-            expected :layouts to be a list of format module pairs of the form: [html: DemoWEb.Layouts] or [html: {DemoWeb.Layouts, :app}]
+            expected :layouts to be a list of format module pairs of the form: [html: DemoWeb.Layouts] or [html: {DemoWeb.Layouts, :app}]
 
             Got: #{inspect(other)}
             """
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 5ea3e9ea9..5bbe7c9f3 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -188,8 +188,6 @@ defmodule Phoenix.Endpoint do
     * `:live_reload` - configuration for the live reload option.
       Configuration requires a `:patterns` option which should be a list of
       file patterns to watch. When these files change, it will trigger a reload.
-      If you are using a tool like [pow](http://pow.cx) in development,
-      you may need to set the `:url` option appropriately.
 
           live_reload: [
             url: "ws://localhost:4000",
@@ -606,7 +604,7 @@ defmodule Phoenix.Endpoint do
           {path, plug, conn_ast, plug_opts} <- socket_paths(module, path, socket, socket_opts) do
         quote do
           defp do_socket_dispatch(unquote(path), conn) do
-            unquote(plug).call(unquote(conn_ast), unquote(Macro.escape(plug_opts)))
+            halt(unquote(plug).call(unquote(conn_ast), unquote(Macro.escape(plug_opts))))
           end
         end
       end
@@ -866,7 +864,10 @@ defmodule Phoenix.Endpoint do
       the error handler must be a MFA tuple that receives a `Plug.Conn`, the
       error reason, and returns a `Plug.Conn` with a response. For example:
 
-          error_handler: {MySocket, :handle_error, []}
+          socket "/socket", MySocket,
+              websocket: [
+                error_handler: {MySocket, :handle_error, []}
+              ]
 
       and a `{:error, :rate_limit}` return may be handled on `MySocket` as:
 
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index 1e89621a6..796a7cf74 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -201,7 +201,10 @@ defmodule Phoenix.Socket do
 
       {:ok, assign(socket, :user_id, verified_user_id)}
 
-  To deny connection, return `:error`.
+  To deny connection, return `:error` or `{:error, term}`. To control the
+  response the client receives in that case, [define an error handler in the
+  websocket
+  configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration).
 
   See `Phoenix.Token` documentation for examples in
   performing token verification on connect.
diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex
index 69e734bd0..9856e8067 100644
--- a/lib/phoenix/verified_routes.ex
+++ b/lib/phoenix/verified_routes.ex
@@ -756,6 +756,10 @@ defmodule Phoenix.VerifiedRoutes do
     {static?, test_path, path_ast, static_ast}
   end
 
+  defp attr!(%{function: nil}, _) do
+    raise "Phoenix.VerifiedRoutes can only be used inside functions, please move your usage of ~p to functions"
+  end
+
   defp attr!(env, :endpoint) do
     Module.get_attribute(env.module, :endpoint) ||
       raise """
diff --git a/mix.exs b/mix.exs
index 17873b6a3..250c42bee 100644
--- a/mix.exs
+++ b/mix.exs
@@ -71,6 +71,7 @@ defmodule Phoenix.MixProject do
       {:plug_crypto, "~> 1.2"},
       {:telemetry, "~> 0.4 or ~> 1.0"},
       {:phoenix_pubsub, "~> 2.1"},
+      # TODO drop phoenix_view as an optional dependency in Phoenix v2.0
       {:phoenix_view, "~> 2.0", optional: true},
       {:phoenix_template, "~> 1.0"},
       {:websock_adapter, "~> 0.4"},
diff --git a/priv/templates/phx.gen.auth/confirmation_edit.html.heex b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
index 9e4128f2b..06d1b795e 100644
--- a/priv/templates/phx.gen.auth/confirmation_edit.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
@@ -1,13 +1,15 @@
-<.header>Confirm account</.header>
+<div class="mx-auto max-w-sm">
+  <.header class="text-center">Confirm account</.header>
 
-<.simple_form :let={_f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm/#{@token}"}>
-  <:actions>
-    <.button>Confirm my account</.button>
-  </:actions>
-</.simple_form>
+  <.simple_form :let={_f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm/#{@token}"}>
+    <:actions>
+      <.button class="w-full">Confirm my account</.button>
+    </:actions>
+  </.simple_form>
 
-<p>
-  <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-  |
-  <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
-</p>
+  <p 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>
+  </p>
+</div>
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index b6196baf3..125b67bf4 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -5,20 +5,22 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def render(%{live_action: :edit} = assigns) do
     ~H"""
-    <.header>Confirm Account</.header>
-
-    <.simple_form :let={f} for={:<%= schema.singular %>} id="confirmation_form" phx-submit="confirm_account">
-      <.input field={{f, :token}} type="hidden" value={@token} />
-      <:actions>
-        <.button phx-disable-with="Confirming...">Confirm my account</.button>
-      </:actions>
-    </.simple_form>
-
-    <p>
-      <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-      |
-      <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
-    </p>
+    <div class="mx-auto max-w-sm">
+      <.header class="text-center">Confirm Account</.header>
+
+      <.simple_form :let={f} for={:<%= schema.singular %>} id="confirmation_form" phx-submit="confirm_account">
+        <.input field={{f, :token}} type="hidden" value={@token} />
+        <:actions>
+          <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
+        </:actions>
+      </.simple_form>
+
+      <p 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>
+      </p>
+    </div>
     """
   end
 
diff --git a/priv/templates/phx.gen.auth/forgot_password_live.ex b/priv/templates/phx.gen.auth/forgot_password_live.ex
index ebed73e15..db0ce3725 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live.ex
+++ b/priv/templates/phx.gen.auth/forgot_password_live.ex
@@ -19,6 +19,11 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           </.button>
         </:actions>
       </.simple_form>
+      <p 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>
+      </p>
     </div>
     """
   end
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 00f0ab0eb..3dd5a966a 100644
--- a/priv/templates/phx.gen.auth/reset_password_edit.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
@@ -1,7 +1,6 @@
 <div class="mx-auto max-w-sm">
   <.header class="text-center">
-    Forgot your password?
-    <:subtitle>We'll send a password reset link to your inbox</:subtitle>
+    Reset Password
   </.header>
 
   <.simple_form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/reset_password/#{@token}"}>
@@ -9,11 +8,22 @@
       Oops, something went wrong! Please check the errors below.
     </.error>
 
-    <.input field={{f, :email}} type="email" placeholder="Email" required />
+    <.input field={{f, :password}} type="password" label="New Password" required />
+    <.input
+      field={{f, :password_confirmation}}
+      type="password"
+      label="Confirm new password"
+      required
+    />
     <:actions>
-      <.button phx-disable-with="Sending..." class="w-full">
-        Send password reset instructions
+      <.button phx-disable-with="Resetting..." class="w-full">
+        Reset password
       </.button>
     </:actions>
   </.simple_form>
+  <p 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>
+  </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 1785e4997..5d411c1ab 100644
--- a/priv/templates/phx.gen.auth/reset_password_live.ex
+++ b/priv/templates/phx.gen.auth/reset_password_live.ex
@@ -5,36 +5,38 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def render(assigns) do
     ~H"""
-    <.header>Reset Password</.header>
+    <div class="mx-auto max-w-sm">
+      <.header class="text-center">Reset Password</.header>
 
-    <.simple_form
-      :let={f}
-      for={@changeset}
-      id="reset_password_form"
-      phx-submit="reset_password"
-      phx-change="validate"
-    >
-      <.error :if={@changeset.action == :insert}>
-        Oops, something went wrong! Please check the errors below.
-      </.error>
+      <.simple_form
+        :let={f}
+        for={@changeset}
+        id="reset_password_form"
+        phx-submit="reset_password"
+        phx-change="validate"
+      >
+        <.error :if={@changeset.action == :insert}>
+          Oops, something went wrong! Please check the errors below.
+        </.error>
 
-      <.input field={{f, :password}} type="password" label="New password" required />
-      <.input
-        field={{f, :password_confirmation}}
-        type="password"
-        label="Confirm new password"
-        required
-      />
-      <:actions>
-        <.button phx-disable-with="Resetting...">Reset Password</.button>
-      </:actions>
-    </.simple_form>
+        <.input field={{f, :password}} type="password" label="New password" required />
+        <.input
+          field={{f, :password_confirmation}}
+          type="password"
+          label="Confirm new password"
+          required
+        />
+        <:actions>
+          <.button phx-disable-with="Resetting..." class="w-full">Reset Password</.button>
+        </:actions>
+      </.simple_form>
 
-    <p>
-      <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-      |
-      <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in</.link>
-    </p>
+      <p 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>
+      </p>
+    </div>
     """
   end
 
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 e2badda06..c7968829d 100644
--- a/priv/templates/phx.gen.auth/reset_password_new.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_new.html.heex
@@ -12,4 +12,9 @@
       </.button>
     </:actions>
   </.simple_form>
+  <p 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>
+  </p>
 </div>
diff --git a/priv/templates/phx.gen.embedded/embedded_schema.ex b/priv/templates/phx.gen.embedded/embedded_schema.ex
index e3acfb81a..fe815b932 100644
--- a/priv/templates/phx.gen.embedded/embedded_schema.ex
+++ b/priv/templates/phx.gen.embedded/embedded_schema.ex
@@ -3,9 +3,10 @@ defmodule <%= inspect schema.module %> do
   import Ecto.Changeset
   alias <%= inspect schema.module %>
 
-  embedded_schema do
-<%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %>
-  end
+  embedded_schema do <%= if !Map.equal?(schema.types, %{}) do %>
+<%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %><% end %>
+<%= for {_, k, _, _} <- schema.assocs do %>    field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>
+<% end %>  end
 
   @doc false
   def changeset(%<%= inspect schema.alias %>{} = <%= schema.singular %>, attrs) do
diff --git a/priv/templates/phx.gen.json/changeset_json.ex b/priv/templates/phx.gen.json/changeset_json.ex
index 867f22c4c..89b1aa1d1 100644
--- a/priv/templates/phx.gen.json/changeset_json.ex
+++ b/priv/templates/phx.gen.json/changeset_json.ex
@@ -1,17 +1,18 @@
 defmodule <%= inspect context.web_module %>.ChangesetJSON do
   @doc """
-  Traverses and translates changeset errors.
-
-  See `Ecto.Changeset.traverse_errors/2` and
-  `<%= inspect context.web_module %>.CoreComponents.translate_error/1` for more details.
-  """<%= if core_components? do %>) do
-  def translate_errors(changeset) do
-    Ecto.Changeset.traverse_errors(changeset, &<%= inspect context.web_module %>.CoreComponents.translate_error/1)
-  end<% else %><%= if gettext? do %>
-  def translate_errors(changeset) do
-    Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
+  Renders changeset errors.
+  """<%= if core_components? do %>
+  def error(%{changeset: changeset}) do
+    # When encoded, the changeset returns its errors
+    # as a JSON object. So we just pass it forward.
+    %{errors: Ecto.Changeset.traverse_errors(changeset, &<%= inspect context.web_module %>.CoreComponents.translate_error/1)}
+  end<% else %>
+  def error(%{changeset: changeset}) do
+    # When encoded, the changeset returns its errors
+    # as a JSON object. So we just pass it forward.
+    %{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
   end
-
+<%= if gettext? do %>
   defp translate_error({msg, opts}) do
     # set by Ecto and indicates we should also apply plural rules.
     if count = opts[:count] do
@@ -19,11 +20,8 @@ defmodule <%= inspect context.web_module %>.ChangesetJSON do
     else
       Gettext.dgettext(<%= inspect context.web_module %>.Gettext, "errors", msg, opts)
     end
-  end<% else %>
-  def translate_errors(changeset) do
-    Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
   end
-
+<% else %>
   defp translate_error({msg, opts}) do
     # You can make use of gettext to translate error messages by
     # uncommenting and adjusting the following code:
@@ -38,13 +36,4 @@ defmodule <%= inspect context.web_module %>.ChangesetJSON do
       String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
     end)
   end<% end %><% end %>
-
-  @doc """
-  Renders changeset errors.
-  """
-  def error(%{changeset: changeset}) do
-    # When encoded, the changeset returns its errors
-    # as a JSON object. So we just pass it forward.
-    %{errors: translate_errors(changeset)}
-  end
 end
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index f9264a307..4b632d353 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -147,7 +147,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
       id={@id}
       phx-mounted={@autoshow && show("##{@id}")}
-      phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("#flash")}
+      phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
       role="alert"
       class={[
         "fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
@@ -339,7 +339,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
         ]}
         {@rest}
       >
-
     <%%= @value %></textarea>
       <.error :for={msg <- @errors}><%%= msg %></.error>
     </div>
diff --git a/priv/templates/phx.gen.presence/presence.ex b/priv/templates/phx.gen.presence/presence.ex
index 204ef333c..de5d98d61 100644
--- a/priv/templates/phx.gen.presence/presence.ex
+++ b/priv/templates/phx.gen.presence/presence.ex
@@ -5,6 +5,7 @@ defmodule <%= module %> do
   See the [`Phoenix.Presence`](https://hexdocs.pm/phoenix/Phoenix.Presence.html)
   docs for more details.
   """
-  use Phoenix.Presence, otp_app: <%= inspect otp_app %>,
-                        pubsub_server: <%= inspect pubsub_server %>
+  use Phoenix.Presence,
+    otp_app: <%= inspect otp_app %>,
+    pubsub_server: <%= inspect pubsub_server %>
 end
diff --git a/priv/templates/phx.gen.socket/socket.ex b/priv/templates/phx.gen.socket/socket.ex
index 3ce1be94d..e44c2d0ea 100644
--- a/priv/templates/phx.gen.socket/socket.ex
+++ b/priv/templates/phx.gen.socket/socket.ex
@@ -30,7 +30,10 @@ defmodule <%= module %>Socket do
   #
   #     {:ok, assign(socket, :user_id, verified_user_id)}
   #
-  # To deny connection, return `:error`.
+  # To deny connection, return `:error` or `{:error, term}`. To control the
+  # response the client receives in that case, [define an error handler in the
+  # websocket
+  # configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration).
   #
   # See `Phoenix.Token` documentation for examples in
   # performing token verification on connect.


Test patch:
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index 39667c712..71b9f1e4a 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -576,11 +576,6 @@ defmodule Mix.Tasks.Phx.NewTest do
       assert_file("custom_path/mix.exs", ~r/app: :phx_blog/)
       assert_file("custom_path/lib/phx_blog_web/endpoint.ex", ~r/app: :phx_blog/)
       assert_file("custom_path/config/config.exs", ~r/namespace: PhoteuxBlog/)
-
-      assert_file(
-        "custom_path/lib/phx_blog_web.ex",
-        ~r/use Phoenix.Controller,\n.*namespace: PhoteuxBlogWeb/
-      )
     end)
   end
 
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index c7d870211..99bd4f160 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -556,11 +556,6 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
         ~r/app: :#{@app}_web/
       )
 
-      assert_file(
-        "custom_path_umbrella/apps/phx_umb_web/lib/#{@app}_web.ex",
-        ~r/use Phoenix.Controller,\n.*namespace: PhoteuxBlogWeb/
-      )
-
       assert_file(
         "custom_path_umbrella/apps/phx_umb/lib/phx_umb/application.ex",
         ~r/defmodule PhoteuxBlog.Application/
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 8ce98ab98..99594fcbc 100644
--- a/priv/templates/phx.gen.auth/reset_password_controller_test.exs
+++ b/priv/templates/phx.gen.auth/reset_password_controller_test.exs
@@ -60,7 +60,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
     test "renders reset password", %{conn: conn, token: token} do
       conn = get(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
-      assert html_response(conn, 200) =~ "Send password reset instructions"
+      assert html_response(conn, 200) =~ "Reset password"
     end
 
     test "does not render reset password with invalid token", %{conn: conn} do
diff --git a/test/fixtures/templates/layout/root.html.eex b/test/fixtures/templates/layout/root.html.eex
new file mode 100644
index 000000000..4fc56964e
--- /dev/null
+++ b/test/fixtures/templates/layout/root.html.eex
@@ -0,0 +1,1 @@
+ROOTSTART[<%= @title %>]<%= @inner_content %>ROOTEND
diff --git a/test/fixtures/views.exs b/test/fixtures/views.exs
index 835e95197..2b4d623c5 100644
--- a/test/fixtures/views.exs
+++ b/test/fixtures/views.exs
@@ -8,13 +8,6 @@ end
 
 defmodule MyApp.LayoutView do
   use Phoenix.View, root: "test/fixtures/templates"
-  import Phoenix.HTML
-
-  def render("root.html", assigns) do
-    ~E"""
-    ROOTSTART[<%= @title %>]<%= @inner_content %>ROOTEND
-    """
-  end
 
   def default_title do
     "MyApp"
diff --git a/test/mix/tasks/phx.gen.cert_test.exs b/test/mix/tasks/phx.gen.cert_test.exs
index 7e8c66090..06d5a30c9 100644
--- a/test/mix/tasks/phx.gen.cert_test.exs
+++ b/test/mix/tasks/phx.gen.cert_test.exs
@@ -56,7 +56,7 @@ defmodule Mix.Tasks.Phx.CertTest do
 
       # We don't actually verify the server cert contents, we just check that
       # the client and server are able to complete the TLS handshake
-      assert {:ok, client} = :ssl.connect('localhost', port, [], @timeout)
+      assert {:ok, client} = :ssl.connect('localhost', port, [verify: :verify_none], @timeout)
       :ssl.close(client)
       :ssl.close(server)
     end)
diff --git a/test/mix/tasks/phx.gen.embedded_test.exs b/test/mix/tasks/phx.gen.embedded_test.exs
index e0526f8d7..df47f5702 100644
--- a/test/mix/tasks/phx.gen.embedded_test.exs
+++ b/test/mix/tasks/phx.gen.embedded_test.exs
@@ -91,4 +91,18 @@ defmodule Mix.Tasks.Phx.Gen.EmbeddedTest do
       end)
     end)
   end
+
+  test "generates embedded schema with references", config do
+    in_tmp_project(config.test, fn ->
+      Gen.Embedded.run(
+        ~w(Blog.Comment comments body word_count:integer author_id:references:author)
+      )
+
+      assert_file("lib/phoenix/blog/comment.ex", fn file ->
+        assert file =~ "field :author_id, :id"
+        assert file =~ "field :body, :string"
+        assert file =~ "field :word_count, :integer"
+      end)
+    end)
+  end
 end
diff --git a/test/mix/tasks/phx.gen.json_test.exs b/test/mix/tasks/phx.gen.json_test.exs
index 023de5d61..40999e5b4 100644
--- a/test/mix/tasks/phx.gen.json_test.exs
+++ b/test/mix/tasks/phx.gen.json_test.exs
@@ -182,7 +182,7 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
       end)
 
       assert_file("lib/phoenix_web/controllers/blog/changeset_json.ex", fn file ->
-        assert file =~ ~s|Gettext.dngettext(PhoenixWeb.Gettext, "errors"|
+        assert file =~ "Ecto.Changeset.traverse_errors(changeset, &translate_error/1)"
       end)
 
       assert_receive {:mix_shell, :info,
@@ -340,7 +340,7 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
       end
       """)
 
-      Code.compile_file("lib/phoenix_web/components/core_components.ex")
+      [{module, _}] = Code.compile_file("lib/phoenix_web/components/core_components.ex")
 
       Gen.Json.run(~w(Blog Post posts title:string --web Blog))
 
@@ -348,6 +348,10 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
         assert file =~
                  "Ecto.Changeset.traverse_errors(changeset, &PhoenixWeb.CoreComponents.translate_error/1)"
       end)
+
+      # Clean up test case specific compile artifact so it doesn't leak to other test cases
+      :code.purge(module)
+      :code.delete(module)
     end)
   end
 end