Repository: phoenixframework/phoenix
Pull request #5430 (2023-04-20 23:40:12)

Reintroduce the File Uploads guide
If you search for `elixir phoenix upload` your first result will be a page that is about six years old

<img width="609" alt="Screenshot 2023-04-20 at 4 35 47 PM" src="https://user-images.githubusercontent.com/168677/233508206-a233dc13-d050-4d28-b50a-bc8835ce7f8b.png">

If you are very unlucky, you might find a link on the forums to a guide on `phoenixframework.org` and it will 404

This PR reintroduces the File Uploads guide, updated to work with the current Contexts guide.



Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 22f49c5a1..3a0c4a57d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,7 +14,7 @@ Use the issues tracker for:
 Please **do not** use the issue tracker for personal support requests nor feature requests. Support requests should be sent to:
 
 * The [phoenix-talk mailing list](https://groups.google.com/group/phoenix-talk) (closed in favor of forum, but archive is still online)
-* [The Phoenix subforum on the Elixir forum](https://elixirforum.com/c/phoenix-forum)
+* [The Phoenix subforum on the Elixir Forum](https://elixirforum.com/c/phoenix-forum)
 * **[#elixir](irc://irc.libera.chat/elixir)** on [Libera](https://libera.chat/) IRC
 
 Development issues can be discussed on the [phoenix-core mailing list](https://groups.google.com/group/phoenix-core).
diff --git a/README.md b/README.md
index db46c1536..06e3b1ff3 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,7 @@ npm install
 
   [1]: https://web.libera.chat/?channels=#elixir
   [2]: https://libera.chat/
-  [3]: https://elixir-slackin.herokuapp.com/
+  [3]: https://elixir-lang.slack.com/join/shared_invite/zt-1f13hz7mb-N4KGjF523ONLCcHfb8jYgA#/shared-invite/email
   [4]: https://github.com/phoenixframework/phoenix/issues
   [5]: https://elixirforum.com/c/phoenix-forum
   [6]: https://groups.google.com/group/phoenix-core
diff --git a/guides/contexts.md b/guides/contexts.md
index 53b413a3b..7ce19df4e 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -38,10 +38,10 @@ description:string price:decimal views:integer
 
 * creating lib/hello_web/controllers/product_controller.ex
 * creating lib/hello_web/controllers/product_html/edit.html.heex
-* creating lib/hello_web/controllers/product_html/product_form.html.heex
 * creating lib/hello_web/controllers/product_html/index.html.heex
 * creating lib/hello_web/controllers/product_html/new.html.heex
 * creating lib/hello_web/controllers/product_html/show.html.heex
+* creating lib/hello_web/controllers/product_html/product_form.html.heex
 * creating lib/hello_web/controllers/product_html.ex
 * creating test/hello_web/controllers/product_controller_test.exs
 * creating lib/hello/catalog/product.ex
@@ -490,35 +490,26 @@ First, we added `Repo.preload` to preload our categories when we fetch a product
 Next, let's expose our new feature to the web by adding the category input to our product form. To keep our form template tidy, let's write a new function to wrap up the details of rendering a category select input for our product. Open up your `ProductHTML` view in `lib/hello_web/controllers/product_html.ex` and key this in:
 
 ```elixir
-defmodule HelloWeb.ProductHTML do
-  use HelloWeb, :html
-
-  import Phoenix.HTML.Form
-
-  def category_select(f, changeset) do
+  def category_opts(changeset) do
     existing_ids =
       changeset
       |> Ecto.Changeset.get_change(:categories, [])
       |> Enum.map(& &1.data.id)
 
-    category_opts =
-      for cat <- Hello.Catalog.list_categories(),
-          do: [key: cat.title, value: cat.id, selected: cat.id in existing_ids]
-
-    multiple_select(f, :category_ids, category_opts)
+    for cat <- Hello.Catalog.list_categories(),
+        do: [key: cat.title, value: cat.id, selected: cat.id in existing_ids]
   end
-end
 ```
 
-We added a new `category_select/2` function which uses `Phoenix.HTML.Form`'s `multiple_select/3` to generate a multiple select tag. We calculated the existing category IDs from our changeset, then used those values when we generate the select options for the input tag. We did this by enumerating over all of our categories and returning the appropriate `key`, `value`, and `selected` values. We marked an option as selected if the category ID was found in those category IDs in our changeset.
+We added a new `category_opts/1` function which generates the select options for a multiple select tag we will add soon. We calculated the existing category IDs from our changeset, then used those values when we generate the select options for the input tag. We did this by enumerating over all of our categories and returning the appropriate `key`, `value`, and `selected` values. We marked an option as selected if the category ID was found in those category IDs in our changeset.
 
-With our `category_select` function in place, we can open up `lib/hello_web/controllers/product_html/product_form.html.heex` and add:
+With our `category_opts` function in place, we can open up `lib/hello_web/controllers/product_html/product_form.html.heex` and add:
 
 ```diff
   ...
-  <.input type="number" field={f[:views]} label="Views" />
+  <.input field={f[:views]} type="number" label="Views" />
 
-+ <%= category_select f, @changeset %>
++ <.input field={f[:category_ids]} type="select" multiple={true} options={category_opts(@changeset)} />
 
   <:actions>
     <.button>Save Product</.button>
@@ -685,10 +676,10 @@ Now that our cart is associated to the items we place in it, let's set up the ca
 
 ```elixir
   schema "cart_items" do
--   field :cart_id, :id
--   field :product_id, :id
     field :price_when_carted, :decimal
     field :quantity, :integer
+-   field :cart_id, :id
+-   field :product_id, :id
 
 +   belongs_to :cart, Hello.ShoppingCart.Cart
 +   belongs_to :product, Hello.Catalog.Product
@@ -705,7 +696,7 @@ Now that our cart is associated to the items we place in it, let's set up the ca
   end
 ```
 
-First, we replaced the `cart_id` field with a standard `belongs_to` pointing at our `ShoppingCart.Cart` schema. Next, we replaced our `product_id` field by adding our first cross-context data dependency with a `belongs_to` for the `Catalog.Product` schema. Here, we intentionally coupled the data boundaries because it provides exactly what we need. An isolated context API with the bare minimum knowledge necessary to reference a product in our system. Next, we added a new validation to our changeset. With `validate_number/3`, we ensure any quantity provided by user input is between 0 and 100.
+First, we replaced the `cart_id` field with a standard `belongs_to` pointing at our `ShoppingCart.Cart` schema. Next, we replaced our `product_id` field by adding our first cross-context data dependency with a `belongs_to` for the `Catalog.Product` schema. Here, we intentionally coupled the data boundaries because it provides exactly what we need: an isolated context API with the bare minimum knowledge necessary to reference a product in our system. Next, we added a new validation to our changeset. With `validate_number/3`, we ensure any quantity provided by user input is between 0 and 100.
 
 With our schemas in place, we can start integrating the new data structures and `ShoppingCart` context APIs into our web-facing features.
 
@@ -720,7 +711,7 @@ We won't focus on a real user authentication system at this point, but by the ti
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.LayoutView, :root}
+    plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
 +   plug :fetch_current_user
@@ -741,7 +732,7 @@ We won't focus on a real user authentication system at this point, but by the ti
 
 + alias Hello.ShoppingCart
 +
-+ def fetch_current_cart(conn, _opts) do
++ defp fetch_current_cart(conn, _opts) do
 +   if cart = ShoppingCart.get_cart_by_user_uuid(conn.assigns.current_uuid) do
 +     assign(conn, :cart, cart)
 +   else
@@ -777,7 +768,7 @@ With our routes in place, let's add the ability to add an item to our cart from
 defmodule HelloWeb.CartItemController do
   use HelloWeb, :controller
 
-  alias Hello.{ShoppingCart, Catalog}
+  alias Hello.ShoppingCart
 
   def create(conn, %{"product_id" => product_id}) do
     case ShoppingCart.add_item_to_cart(conn.assigns.cart, product_id) do
@@ -805,8 +796,9 @@ We defined a new `CartItemController` with the create and delete actions that we
 Let's implement the new interface for the `ShoppingCart` context API in `lib/hello/shopping_cart.ex`:
 
 ```elixir
-  alias Hello.Catalog
-  alias Hello.ShoppingCart.{Cart, CartItem}
++  alias Hello.Catalog
+-  alias Hello.ShoppingCart.Cart
++  alias Hello.ShoppingCart.{Cart, CartItem}
 
   def get_cart_by_user_uuid(user_uuid) do
     Repo.one(
@@ -833,32 +825,32 @@ Let's implement the new interface for the `ShoppingCart` context API in `lib/hel
 +   end
   end
 
-  defp reload_cart(%Cart{} = cart), do: get_cart_by_user_uuid(cart.user_uuid)
-
-  def add_item_to_cart(%Cart{} = cart, product_id) do
-    product = Catalog.get_product!(product_id)
-
-    %CartItem{quantity: 1, price_when_carted: product.price}
-    |> CartItem.changeset(%{})
-    |> Ecto.Changeset.put_assoc(:cart, cart)
-    |> Ecto.Changeset.put_assoc(:product, product)
-    |> Repo.insert(
-      on_conflict: [inc: [quantity: 1]],
-      conflict_target: [:cart_id, :product_id]
-    )
-  end
-
-  def remove_item_from_cart(%Cart{} = cart, product_id) do
-    {1, _} =
-      Repo.delete_all(
-        from(i in CartItem,
-          where: i.cart_id == ^cart.id,
-          where: i.product_id == ^product_id
-        )
-      )
-
-    {:ok, reload_cart(cart)}
-  end
++  defp reload_cart(%Cart{} = cart), do: get_cart_by_user_uuid(cart.user_uuid)
++
++  def add_item_to_cart(%Cart{} = cart, product_id) do
++    product = Catalog.get_product!(product_id)
++
++    %CartItem{quantity: 1, price_when_carted: product.price}
++    |> CartItem.changeset(%{})
++    |> Ecto.Changeset.put_assoc(:cart, cart)
++    |> Ecto.Changeset.put_assoc(:product, product)
++    |> Repo.insert(
++      on_conflict: [inc: [quantity: 1]],
++      conflict_target: [:cart_id, :product_id]
++    )
++  end
++
++  def remove_item_from_cart(%Cart{} = cart, product_id) do
++    {1, _} =
++      Repo.delete_all(
++        from(i in CartItem,
++          where: i.cart_id == ^cart.id,
++          where: i.product_id == ^product_id
++        )
++      )
++
++    {:ok, reload_cart(cart)}
++  end
 ```
 
 We started by implementing  `get_cart_by_user_uuid/1` which fetches our cart and joins the cart items, and their products so that we have the full cart populated with all preloaded data. Next, we modified our `create_cart` function to accept a user UUID instead of attributes, which we used to populate the `user_uuid` field. If the insert is successful, we reload the cart contents by calling a private `reload_cart/1` function, which simply calls `get_cart_by_user_uuid/1` to refetch data.
@@ -870,9 +862,13 @@ Finally, we implemented `remove_item_from_cart/2` where we simply issue a `Repo.
 With our new cart functions in place, we can now expose the "Add to cart" button on the product catalog show page. Open up your template in `lib/hello_web/controllers/product_html/show.html.heex` and make the following changes:
 
 ```diff
-<h1>Show Product</h1>
-
-+<.link href={~p"/cart_items?product_id=#{@product.id}"} method="post">Add to cart</.link>
+...
+     <.link href={~p"/products/#{@product}/edit"}>
+       <.button>Edit product</.button>
+     </.link>
++    <.link href={~p"/cart_items?product_id=#{@product.id}"} method="post">
++      <.button>Add to cart</.button>
++    </.link>
 ...
 ```
 
@@ -927,9 +923,7 @@ defmodule HelloWeb.CartHTML do
   use HelloWeb, :html
 
   alias Hello.ShoppingCart
-
-  import Phoenix.HTML.Form
-
+  
   embed_templates "cart_html/*"
 
   def currency_to_str(%Decimal{} = val), do: "$#{Decimal.round(val, 2)}"
@@ -941,31 +935,34 @@ We created a view to render our `show.html` template and aliased our `ShoppingCa
 Next we can create the template at `lib/hello_web/controllers/cart_html/show.html.heex`:
 
 ```heex
-<h1>My Cart</h1>
-
 <%= if @cart.items == [] do %>
-  Your cart is empty
+<.header>
+  My Cart
+  <:subtitle>Your cart is empty</:subtitle>
+</.header>
 <% else %>
-  <.form :let={f} for={@changeset} action={~p"/cart"}>
-    <ul>
-      <%= for item_form <- inputs_for(f, :items), item = item_form.data do %>
-        <li>
-          <%= hidden_inputs_for(item_form) %>
-          <%= item.product.title %>
-          <%= number_input item_form, :quantity %>
-          <%= currency_to_str(ShoppingCart.total_item_price(item)) %>
-        </li>
-      <% end %>
-    </ul>
-
-    <%= submit "update cart" %>
-  </.form>
+  <.header>
+    My Cart
+  </.header>
+
+  <.simple_form :let={f} for={@changeset} action={~p"/cart"}>
+    <.inputs_for :let={item_form} field={f[:items]}>
+	<% item = item_form.data %>
+      <.input field={item_form[:quantity]} type="number" label={item.product.title} />
+      <%= currency_to_str(ShoppingCart.total_item_price(item)) %>
+    </.inputs_for>
+    <:actions>
+      <.button>Update cart</.button>
+    </:actions>
+  </.simple_form>
 
   <b>Total</b>: <%= currency_to_str(ShoppingCart.total_cart_price(@cart)) %>
 <% end %>
+
+<.back navigate={~p"/products"}>Back to products</.back>
 ```
 
-We started by showing the empty cart message if our preloaded `cart.items` is empty. If we have items, we use the [`form`] component provided by (`Phoenix.Component`) to take our cart changeset that we assigned in the `CartController.show/2` action and create a form which maps to our cart controller `update/2` action. Within the form, we use `Phoenix.HTML.Form.inputs_for/2` to render inputs for the nested cart items. For each item form input, we use [`hidden_inputs_for/1`](`Phoenix.HTML.Form.hidden_inputs_for/1`) which will render out the item ID as a hidden input tag. This will allow us to map item inputs back together when the form is submitted. Next, we display the product title for the item in the cart, followed by a number input for the item quantity. We finish the item form by converting the item price to string. We haven't written the `ShoppingCart.total_item_price/1` function yet, but again we employed the idea of clear, descriptive public interfaces for our contexts. After rendering inputs for all the cart items, we show an "update cart" submit button, along with the total price of the entire cart. This is accomplished with another new `ShoppingCart.total_cart_price/1` function which we'll implement in a moment.
+We started by showing the empty cart message if our preloaded `cart.items` is empty. If we have items, we use the `simple_form` component provided by our `HelloWeb.CoreComponents` to take our cart changeset that we assigned in the `CartController.show/2` action and create a form which maps to our cart controller `update/2` action. Within the form, we use the [`inputs_for`](`Phoenix.Component.inputs_for/1`) component to render inputs for the nested cart items. This will allow us to map item inputs back together when the form is submitted. Next, we display a number input input for the item quantity and label it with the product title. We finish the item form by converting the item price to string. We haven't written the `ShoppingCart.total_item_price/1` function yet, but again we employed the idea of clear, descriptive public interfaces for our contexts. After rendering inputs for all the cart items, we show an "update cart" submit button, along with the total price of the entire cart. This is accomplished with another new `ShoppingCart.total_cart_price/1` function which we'll implement in a moment. Finally, we added a `back` component to go back to our products page.
 
 We're almost ready to try out our cart page, but first we need to implement our new currency calculation functions. Open up your shopping cart context at `lib/hello/shopping_cart.ex` and add these new functions:
 
@@ -1035,7 +1032,7 @@ Head back over to your shopping cart context in `lib/hello/shopping_cart.ex` and
   end
 ```
 
-We started much like how our out-of-the-box code started – we take the cart struct and cast the user input to a cart changeset, except this time we use `Ecto.Changeset.cast_assoc/3` to cast the nested item data into `CartItem` changesets. Remember the [`hidden_inputs_for/1`](`Phoenix.HTML.Form.hidden_inputs_for/1`) call in our cart form template? That hidden ID data is what allows Ecto's `cast_assoc` to map item data back to existing item associations in the cart. Next we use `Ecto.Multi.new/0`, which you may not have seen before. Ecto's `Multi` is a feature that allows lazily defining a chain of named operations to eventually execute inside a database transaction. Each operation in the multi chain receives the values from the previous steps and executes until a failed step is encountered. When an operation fails, the transaction is rolled back and an error is returned, otherwise the transaction is committed.
+We started much like how our out-of-the-box code started – we take the cart struct and cast the user input to a cart changeset, except this time we use `Ecto.Changeset.cast_assoc/3` to cast the nested item data into `CartItem` changesets. Remember the [`<.inputs_for />`](`Phoenix.Component.inputs_for/1`) call in our cart form template? That hidden ID data is what allows Ecto's `cast_assoc` to map item data back to existing item associations in the cart. Next we use `Ecto.Multi.new/0`, which you may not have seen before. Ecto's `Multi` is a feature that allows lazily defining a chain of named operations to eventually execute inside a database transaction. Each operation in the multi chain receives the values from the previous steps and executes until a failed step is encountered. When an operation fails, the transaction is rolled back and an error is returned, otherwise the transaction is committed.
 
 For our multi operations, we start by issuing an update of our cart, which we named `:cart`. After the cart update is issued, we perform a multi `delete_all` operation, which takes the updated cart and applies our zero-quantity logic. We prune any items in the cart with zero quantity by returning an ecto query that finds all cart items for this cart with an empty quantity. Calling `Repo.transaction/1` with our multi will execute the operations in a new transaction and we return the success or failure result to the caller just like the original function.
 
@@ -1043,23 +1040,15 @@ Let's head back to the browser and try it out. Add a few products to your cart,
 
 ## Adding an Orders context
 
-With our `Catalog` and `ShoppingCart` contexts, we're seeing first-hand how our well-considered modules and function names are yielding clear and maintainable code. Our last order of business is to allow the user to initiate the checkout process. We won't go as far as integrating payment processing or order fulfillment, but we'll get you started in that direction. Like before, we need to decide where code for completing an order should live. Is it part of the catalog? Clearly not, but what about the shopping cart? Shopping carts are related to orders – after all the user has to add items in order to purchase any products, but should the order checkout process be grouped here?
+With our `Catalog` and `ShoppingCart` contexts, we're seeing first-hand how our well-considered modules and function names are yielding clear and maintainable code. Our last order of business is to allow the user to initiate the checkout process. We won't go as far as integrating payment processing or order fulfillment, but we'll get you started in that direction. Like before, we need to decide where code for completing an order should live. Is it part of the catalog? Clearly not, but what about the shopping cart? Shopping carts are related to orders – after all, the user has to add items in order to purchase any products – but should the order checkout process be grouped here?
 
-If we stop and consider the order process, we'll see that orders involve related, but distinctly different data from the cart contents. Also, business rules around the checkout process are much different than carting. For example, we may allow a user to add a back-ordered item to their cart, but we could not allow an order with no inventory to be completed. Additionally, we need to capture point-in-time product information when an order is completed, such as the price of the items *at payment transaction time*. This is essential because a product price may change in the future, but the line items in our order must always record and display what we charged at time of purchase. For these reasons, we can start to see ordering can reasonably stand on its own with its own data concerns and business rules.
+If we stop and consider the order process, we'll see that orders involve related, but distinctly different data from the cart contents. Also, business rules around the checkout process are much different than carting. For example, we may allow a user to add a back-ordered item to their cart, but we could not allow an order with no inventory to be completed. Additionally, we need to capture point-in-time product information when an order is completed, such as the price of the items *at payment transaction time*. This is essential because a product price may change in the future, but the line items in our order must always record and display what we charged at time of purchase. For these reasons, we can start to see that ordering can stand on its own with its own data concerns and business rules.
 
 Naming wise, `Orders` clearly defines the scope of our context, so let's get started by again taking advantage of the context generators. Run the following command in your console:
 
 ```console
-$ mix phx.gen.html Orders Order orders user_uuid:uuid total_price:decimal
-
-* creating lib/hello_web/controllers/order_controller.ex
-* creating lib/hello_web/controllers/order_html/edit.html.heex
-* creating lib/hello_web/controllers/order_html/order_form.html.heex
-* creating lib/hello_web/controllers/order_html/index.html.heex
-* creating lib/hello_web/controllers/order_html/new.html.heex
-* creating lib/hello_web/controllers/order_html/show.html.heex
-* creating lib/hello_web/controllers/order_html.ex
-* creating test/hello_web/controllers/order_controller_test.exs
+$ mix phx.gen.context Orders Order orders user_uuid:uuid total_price:decimal
+
 * creating lib/hello/orders/order.ex
 * creating priv/repo/migrations/20210209214612_create_orders.exs
 * creating lib/hello/orders.ex
@@ -1069,17 +1058,12 @@ $ mix phx.gen.html Orders Order orders user_uuid:uuid total_price:decimal
 * creating test/support/fixtures/orders_fixtures.ex
 * injecting test/support/fixtures/orders_fixtures.ex
 
-Add the resource to your browser scope in lib/hello_web/router.ex:
-
-    resources "/orders", OrderController
-
-
 Remember to update your repository by running migrations:
 
     $ mix ecto.migrate
 ```
 
-We generated a `Orders` context along with HTML controllers, views, etc. We added a `user_uuid` field to associate our placeholder current user to an order, along with a `total_price` column. With our starting point in place, let's open up the newly created migration in `priv/repo/migrations/*_create_orders.exs` and make the following changes:
+We generated an `Orders` context. We added a `user_uuid` field to associate our placeholder current user to an order, along with a `total_price` column. With our starting point in place, let's open up the newly created migration in `priv/repo/migrations/*_create_orders.exs` and make the following changes:
 
 ```elixir
   def change do
@@ -1103,6 +1087,7 @@ price:decimal quantity:integer \
 order_id:references:orders product_id:references:products
 
 You are generating into an existing context.
+...
 Would you like to proceed? [Yn] y
 * creating lib/hello/orders/line_item.ex
 * creating priv/repo/migrations/20210209215050_create_order_line_items.exs
@@ -1209,9 +1194,14 @@ Before we render information about our orders, we need to ensure our order data
 
 We rewrote the function to accept a user UUID and query our repo for an order matching the user's ID for a given order ID. Then we populated the order by preloading our line item and product associations.
 
-To complete an order, our cart page can issue a POST to the `OrderController.create` action, but we need to implement the operations and logic to actually complete an order. Like before, we'll start at the web interface by rewriting the create function in `lib/hello_web/controllers/order_controller.ex`:
+To complete an order, our cart page can issue a POST to the `OrderController.create` action, but we need to implement the operations and logic to actually complete an order. Like before, we'll start at the web interface. Create a new file at `lib/hello_web/controllers/order_controller.ex` and key this in:
 
 ```elixir
+defmodule HelloWeb.OrderController do
+  use HelloWeb, :controller
+
+  alias Hello.Orders
+
   def create(conn, _) do
     case Orders.complete_order(conn.assigns.cart) do
       {:ok, order} ->
@@ -1225,11 +1215,12 @@ To complete an order, our cart page can issue a POST to the `OrderController.cre
         |> redirect(to: ~p"/cart")
     end
   end
+end
 ```
 
-We rewrote the `create` action to call an as-yet-implemented `Orders.complete_order/1` function. The code that phoenix generated had a generic `Orders.create_order/1` call. Our code is technically "creating" an order, but it's important to step back and consider the naming of your interfaces. The act of *completing* an order is extremely important in our system. Money changes hands in a transaction, physical goods could be automatically shipped, etc. Such an operation deserves a better, more obvious function name, such as `complete_order`. If the order is completed successfully we redirect to the show page, otherwise a flash error is shown as we redirect back to the cart page.
+We wrote the `create` action to call an as-yet-implemented `Orders.complete_order/1` function. Our code is technically "creating" an order, but it's important to step back and consider the naming of your interfaces. The act of *completing* an order is extremely important in our system. Money changes hands in a transaction, physical goods could be automatically shipped, etc. Such an operation deserves a better, more obvious function name, such as `complete_order`. If the order is completed successfully we redirect to the show page, otherwise a flash error is shown as we redirect back to the cart page.
 
-Here is also a good opportunity to highlight that contexts can naturally work with data defined by other contexts too. This will be specially common with data that is used throughout the application, such as the cart here (but it can also be the current user or the current project, and so forth, depending on your project).
+Here is also a good opportunity to highlight that contexts can naturally work with data defined by other contexts too. This will be especially common with data that is used throughout the application, such as the cart here (but it can also be the current user or the current project, and so forth, depending on your project).
 
 Now we can implement our `Orders.complete_order/1` function. To complete an order, our job will require a few operations:
 
@@ -1242,8 +1233,8 @@ Now we can implement our `Orders.complete_order/1` function. To complete an orde
 From our requirements alone, we can start to see why a generic `create_order` function doesn't cut it. Let's implement this new function in `lib/hello/orders.ex`:
 
 ```elixir
-  alias Hello.ShoppingCart
   alias Hello.Orders.LineItem
+  alias Hello.ShoppingCart
 
   def complete_order(%ShoppingCart.Cart{} = cart) do
     line_items =
@@ -1271,7 +1262,7 @@ From our requirements alone, we can start to see why a generic `create_order` fu
   end
 ```
 
-We started by mapping the `%ShoppingCart.CartItem{}`'s in our shopping cart into a map of order line items structs. The job of the order line item record is to capture the price of the product *at payment transaction time*, so we reference the product's price here. Next, we create a bare order changeset with `Ecto.Changeset.change/2` and associate our user UUID, set our total price calculation, and place our order line items in the changeset. With a fresh order changeset ready to be inserted, we can again make use of `Ecto.Multi` to execute our operations in a database transaction. We start by inserting the order, followed by a `run` operation. The `Ecto.Multi.run/3` function allows us to run any code in the function which must either succeed with `{:ok, result}` or error, which halts and rolls back the transaction. Here, we simply can call into our shopping cart context and ask it to prune all items in a cart. Running the transaction will execute the multi as before and we return the result to the caller.
+We started by mapping the `%ShoppingCart.CartItem{}`'s in our shopping cart into a map of order line items structs. The job of the order line item record is to capture the price of the product *at payment transaction time*, so we reference the product's price here. Next, we create a bare order changeset with `Ecto.Changeset.change/2` and associate our user UUID, set our total price calculation, and place our order line items in the changeset. With a fresh order changeset ready to be inserted, we can again make use of `Ecto.Multi` to execute our operations in a database transaction. We start by inserting the order, followed by a `run` operation. The `Ecto.Multi.run/3` function allows us to run any code in the function which must either succeed with `{:ok, result}` or error, which halts and rolls back the transaction. Here, we simply call into our shopping cart context and ask it to prune all items in a cart. Running the transaction will execute the multi as before and we return the result to the caller.
 
 To close out our order completion, we need to implement the `ShoppingCart.prune_cart_items/1` function in `lib/hello/shopping_cart.ex`:
 
@@ -1282,51 +1273,61 @@ To close out our order completion, we need to implement the `ShoppingCart.prune_
   end
 ```
 
-Our new function accepts the cart struct and issues a `Repo.delete_all` which accepts a query of all items for the provided cart. We return a success result by simply reloading the pruned cart to the caller. With our context complete, we now need to show the user their completed order. Head back to your order controller and modify the `show/2` action:
+Our new function accepts the cart struct and issues a `Repo.delete_all` which accepts a query of all items for the provided cart. We return a success result by simply reloading the pruned cart to the caller. With our context complete, we now need to show the user their completed order. Head back to your order controller and add the `show/2` action:
 
 ```elixir
   def show(conn, %{"id" => id}) do
--   order = Orders.get_order!(id)
-+   order = Orders.get_order!(conn.assigns.current_uuid, id)
+    order = Orders.get_order!(conn.assigns.current_uuid, id)
     render(conn, :show, order: order)
   end
 ```
 
-We tweaked the show action to pass our `conn.assigns.current_uuid` to `get_order!` which authorizes orders to be viewable only by the owner of the order. Next, we can replace the order show template in `lib/hello_web/controllers/order_html/show.html.heex`:
-
-```heex
-<h1>Thank you for your order!</h1>
-
-<ul>
-  <li>
-    <strong>User uuid:</strong>
-    <%= @order.user_uuid %>
-  </li>
+We added the show action to pass our `conn.assigns.current_uuid` to `get_order!` which authorizes orders to be viewable only by the owner of the order. Next, we can implement the view and template. Create a new view file at `lib/hello_web/controllers/order_html.ex` with the following content:
 
-  <li :for={item <- @order.line_items}>
-    <%= item.product.title %>
-    (<%= item.quantity %>) - <%= HelloWeb.CartHTML.currency_to_str(item.price) %>
-  </li>
-
-  <li>
-    <strong>Total price:</strong>
-    <%= HelloWeb.CartHTML.currency_to_str(@order.total_price) %>
-  </li>
+```elixir
+defmodule HelloWeb.OrderHTML do
+  use HelloWeb, :html
 
-</ul>
+  embed_templates "order_html/*"
+end
+```
+Next we can create the template at `lib/hello_web/controllers/order_html/show.html.heex`:
 
-<span><.link href={~p"/cart"}>Back</.link></span>
+```heex
+<.header>
+  Thank you for your order!
+  <:subtitle>
+     <strong>User uuid: </strong><%= @order.user_uuid %>
+  </:subtitle>
+</.header>
+
+<.table id="items" rows={@order.line_items}>
+  <:col :let={item} label="Title"><%= item.product.title %></:col>
+  <:col :let={item} label="Quantity"><%= item.quantity %></:col>
+  <:col :let={item} label="Price">
+    <%= HelloWeb.CartHTML.currency_to_str(item.price) %>
+  </:col>
+</.table>
+
+<strong>Total price:</strong>
+<%= HelloWeb.CartHTML.currency_to_str(@order.total_price) %>
+
+<.back navigate={~p"/products"}>Back to products</.back>
 ```
 
 To show our completed order, we displayed the order's user, followed by the line item listing with product title, quantity, and the price we "transacted" when completing the order, along with the total price.
 
-Our last addition will be to add the "complete order" button to our cart page to allow completing an order. Add the following button to the bottom of the cart show template in `lib/hello_web/controllers/cart_html/show.html.heex`:
+Our last addition will be to add the "complete order" button to our cart page to allow completing an order. Add the following button to the <.header> of the cart show template in `lib/hello_web/controllers/cart_html/show.html.heex`:
 
 ```diff
-  <b>Total</b>: <%= currency_to_str(ShoppingCart.total_cart_price(@cart)) %>
-
-+ <.link href={~p"/orders"} method="post">complete order</.link>
-<% end %>
+  <.header>
+    My Cart
++    <:actions>
++      <.link href={~p"/orders"} method="post">
++        <.button>Complete order</.button>
++      </.link>
++    </:actions>
+  </.header>
 ```
 
 We added a link with `method="post"` to send a POST request to our `OrderController.create` action. If we head back to our cart page at [`http://localhost:4000/cart`](http://localhost:4000/cart) and complete an order, we'll be greeted by our rendered template:
@@ -1335,7 +1336,9 @@ We added a link with `method="post"` to send a POST request to our `OrderControl
 Thank you for your order!
 
 User uuid: 08964c7c-908c-4a55-bcd3-9811ad8b0b9d
-Metaprogramming Elixir (2) - $15.00
+Title                   Quantity Price
+Metaprogramming Elixir  2        $15.00
+
 Total price: $30.00
 ```
 
@@ -1353,6 +1356,6 @@ As we explored the context API, you might have wondered:
 
 Although Changesets are part of Ecto, they are not tied to the database, and they can be used to map data from and to any source, which makes it a general and useful data structure for tracking field changes, perform validations, and generate error messages.
 
-For those reasons, `%Ecto.Changeset{}` is a good choice to model the data changes between your contexts and your web layer. Regardless if you are talking to an API or the database.
+For those reasons, `%Ecto.Changeset{}` is a good choice to model the data changes between your contexts and your web layer - regardless if you are talking to an API or the database.
 
 Finally, note that your controllers and views are not hardcoded to work exclusively with Ecto either. Instead, Phoenix defines protocols such as `Phoenix.Param` and `Phoenix.HTML.FormData`, which allow any library to extend how Phoenix generates URL parameters or renders forms. Conveniently for us, the `phoenix_ecto` project implements those protocols, but you could as well bring your own data structures and implement them yourself.
diff --git a/guides/controllers.md b/guides/controllers.md
index 85df2c9a9..6ca334642 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -216,7 +216,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html", "json"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.LayoutView, :root}
+    plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
@@ -380,7 +380,7 @@ The flash functionality is handy when mixed with redirects. Perhaps you want to
   def home(conn, _params) do
     conn
     |> put_flash(:error, "Let's pretend we have an error.")
-    |> redirect(to: ~p"/redirect_test"))
+    |> redirect(to: ~p"/redirect_test")
   end
 ```
 
diff --git a/guides/deployment/gigalixir.md b/guides/deployment/gigalixir.md
index 5cdf45d50..48667955d 100644
--- a/guides/deployment/gigalixir.md
+++ b/guides/deployment/gigalixir.md
@@ -189,4 +189,4 @@ For custom domains, scaling, jobs and other features, see the [Gigalixir Documen
 
 See [Troubleshooting](https://gigalixir.readthedocs.io/en/latest/troubleshooting.html)
 
-Also, don't hesitate to email [help@gigalixir.com](mailto:help@gigalixir.com) or [request an invitation](https://elixir-slackin.herokuapp.com/) and join the #gigalixir channel on [Slack](https://elixir-lang.slack.com).
+Also, don't hesitate to email [help@gigalixir.com](mailto:help@gigalixir.com) or [request an invitation](https://elixir-lang.slack.com/join/shared_invite/zt-1f13hz7mb-N4KGjF523ONLCcHfb8jYgA#/shared-invite/email) and join the #gigalixir channel on [Slack](https://elixir-lang.slack.com).
diff --git a/guides/howto/file_uploads.md b/guides/howto/file_uploads.md
new file mode 100644
index 000000000..c7d78d1b5
--- /dev/null
+++ b/guides/howto/file_uploads.md
@@ -0,0 +1,158 @@
+# File Uploads
+
+One common task for web applications is uploading files. These files might be images, videos, PDFs, or files of any other type. In order to upload files through an HTML interface, we need a `file` input tag in a multipart form.
+
+> #### Looking for the LiveView Uploads guide? {: .neutral}
+>
+> This guide explains multipart HTTP file uploads via `Plug.Upload`.
+> For more information about LiveView file uploads, including direct-to-cloud external uploads on
+> the client, refer to the [LiveView Uploads guide](https://hexdocs.pm/phoenix_live_view/uploads.html).
+
+Plug provides a `Plug.Upload` struct to hold the data from the `file` input. A `Plug.Upload` struct will automatically appear in your request parameters if a user has selected a file when they submit the form.
+
+In this guide you will do the following:
+
+  1.  Configure a multipart form
+
+  2. Add a file input element to the form
+
+  3. Verify your upload params
+
+  4. Manage your uploaded files
+
+In the [`Contexts guide`](contexts.md), we generated an HTML resource for products. We can reuse the form we generated there in order to demonstrate how file uploads work in Phoenix. Please refer to that guide for instructions on generating the product resource you will be using here.
+
+### Configure a multipart form
+
+The first thing you need to do is change your form into a multipart form. The `HelloWeb.CoreComponents` `simple_form/1` component accepts a `multipart` attribute where you can specify this.
+
+Here is the form from `lib/hello_web/controllers/product_html/product_form.html.heex` with that change in place:
+
+```heex
+<.simple_form :let={f} for={@changeset} action={@action} multipart>
+. . .
+```
+
+### Add a file input
+
+Once you have a multipart form, you need a `file` input. Here's how you would do that, also in `product_form.html.heex`:
+
+```heex
+. . .
+  <.input field={f[:photo]} type="file" label="Photo" />
+
+  <:actions>
+    <.button>Save Product</.button>
+  </:actions>
+</.simple_form>
+```
+
+When rendered, here is the HTML for the default `HelloWeb.CoreComponents` `input/1` component:
+
+```html
+<div phx-feedback-for="product[photo]">
+  <label for="product_photo" class="block text-sm...">Photo</label>
+  <input type="file" name="product[photo]" id="product_photo" class="mt-2 block w-full...">
+</div>
+```
+
+Note the `name` attribute of your `file` input. This will create the `"photo"` key in the `product_params` map which will be available in your controller action.
+
+This is all from the form side. Now when users submit the form, a `POST` request will route to your `HelloWeb.ProductController` `create/2` action.
+
+> #### Should I add photo to my Ecto schema? {: .neutral}
+>
+> The photo input does not need to be part of your schema for it to come across in the `product_params`. If you want to persist any properties of the photo in a database, however, you would need to add it to your `Hello.Product` schema.
+
+### Verify your upload params
+
+Since you generated an HTML resource, you can now start your server with `mix phx.server`, visit [http://localhost:4000/products/new](http://localhost:4000/products/new), and create a new product with a photo.
+
+Before you begin, add `IO.inspect product_params` to the top of your `ProductController.create/2` action in `lib/hello_web/controllers/product_controller.ex`. This will show the `product_params` in your development log so you can get a better sense of what's happening.
+
+```elixir
+. . .
+  def create(conn, %{"product" => product_params}) do
+    IO.inspect product_params
+. . .
+```
+
+When you do that, this is what your `product_params` will output in the log:
+
+```elixir
+%{"title" => "Metaprogramming Elixir", "description" => "Write Less Code, Get More Done (and Have Fun!)", "price" => "15.000000", "views" => "0",
+"photo" => %Plug.Upload{content_type: "image/png", filename: "meta-cover.png", path: "/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/multipart-558399-917557-1"}}
+```
+
+You have a `"photo"` key which maps to the pre-populated `Plug.Upload` struct representing your uploaded photo.
+
+To make this easier to read, focus on the struct itself:
+
+```elixir
+%Plug.Upload{content_type: "image/png", filename: "meta-cover.png", path: "/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/multipart-558399-917557-1"}
+```
+
+`Plug.Upload` provides the file's content type, original filename, and path to the temporary file which Plug created for you. In this case, `"/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/"` is the directory created by Plug in which to put uploaded files. The directory will persist across requests. `"multipart-558399-917557-1"` is the name Plug gave to your uploaded file. If you had multiple `file` inputs and if the user selected photos for all of them, you would have multiple files scattered in temporary directories. Plug will make sure all the filenames are unique.
+
+> #### Plug.Upload files are temporary {: .info}
+>
+> Plug removes uploads from its directory as the request completes. If you need to do anything with this file, you need to do it before then (or [give it away](`Plug.Upload.give_away/3`), but that is outside the scope of this guide).
+
+### Manage your uploaded files
+
+Once you have the `Plug.Upload` struct available in your controller, you can perform any operation on it you want. For example, you may want to do one or more of the following:
+
+* Check to make sure the file exists with `File.exists?/1`
+
+* Copy the file somewhere else on the filesystem with `File.cp/2`
+
+* Give the file away to another Elixir process with `Plug.Upload.give_away/3`
+
+* Send it to S3 with an external library
+
+* Send it back to the client with `Plug.Conn.send_file/5`
+
+In a production system, you may want to copy the file to a root directory, such as `/media`. When doing so, it is important to guarantee the names are unique. For instance, if you are allowing users to upload product cover images, you could use the product id to generate a unique name:
+
+```elixir
+if upload = product_params["photo"] do
+  extension = Path.extname(upload.filename)
+  File.cp(upload.path, "/media/#{product.id}-cover#{extension}")
+end
+```
+
+Then a `Plug.Static` plug could be added in your `lib/my_app_web/endpoint.ex` to serve the files at `"/media"`:
+
+```elixir
+plug Plug.Static, at: "/uploads", from: "/media"
+```
+
+The uploaded file can now be accessed from your browsers using a path such as `"/uploads/1-cover.jpg"`. In practice, there are other concerns you want to handle when uploading files, such validating extensions, encoding names, and so on. Many times, using a library that already handles such cases is preferred.
+
+Finally, notice that when there is no data from the `file` input, you get neither the `"photo"` key nor a `Plug.Upload` struct. Here are the `product_params` from the log.
+
+```elixir
+%{"title" => "Metaprogramming Elixir", "description" => "Write Less Code, Get More Done (and Have Fun!)", "price" => "15.000000", "views" => "0"}
+```
+
+## Configuring upload limits
+
+The conversion from the data being sent by the form to an actual `Plug.Upload` is done by the `Plug.Parsers` plug which you can find inside `HelloWeb.Endpoint`:
+
+```elixir
+# lib/hello_web/endpoint.ex
+plug Plug.Parsers,
+  parsers: [:urlencoded, :multipart, :json],
+  pass: ["*/*"],
+  json_decoder: Phoenix.json_library()
+```
+
+Besides the options above, `Plug.Parsers` accepts other options to control data upload:
+
+  * `:length` - sets the max body length to read, defaults to `8_000_000` bytes
+  * `:read_length` - set the amount of bytes to read at one time, defaults to `1_000_000` bytes
+  * `:read_timeout` - set the timeout for each chunk received, defaults to `15_000` ms
+
+The first option configures the maximum data allowed. The remaining ones configure how much data we expect to read and its frequency. If the client cannot push data fast enough, the connection will be terminated. Phoenix ships with reasonable defaults but you may want to customize it under special circumstances, for example, if you are expecting really slow clients to send large chunks of data.
+
+It is also worth pointing out those limits are important as a security mechanism. For example, if you don't set a limit for data upload, attackers could open up thousands of connections to your application and send one byte every 2 minutes, which would take very long to complete while using up all connections to your server. The limits above expect at least a reasonable amount of progress, making attackers' lives a bit harder.
diff --git a/guides/howto/using_ssl.md b/guides/howto/using_ssl.md
index 93183c75c..8af2f7456 100644
--- a/guides/howto/using_ssl.md
+++ b/guides/howto/using_ssl.md
@@ -4,7 +4,7 @@ To prepare an application to serve requests over SSL, we need to add a little bi
 
 The configuration consists of a new `https:` key for our endpoint whose value is a keyword list of port, path to the key file, and path to the cert (PEM) file. If we add the `otp_app:` key whose value is the name of our application, Plug will begin to look for them at the root of our application. We can then put those files in our `priv` directory and set the paths to `priv/our_keyfile.key` and `priv/our_cert.crt`.
 
-Here's an example configuration from `config/prod.exs`.
+Here's an example configuration from `config/runtime.exs`.
 
 ```elixir
 import Config
diff --git a/guides/howto/writing_a_channels_client.md b/guides/howto/writing_a_channels_client.md
new file mode 100644
index 000000000..32a1f5348
--- /dev/null
+++ b/guides/howto/writing_a_channels_client.md
@@ -0,0 +1,81 @@
+# Writing a Channels Client
+
+Client libraries for Phoenix Channels already exist in [several languages](https://hexdocs.pm/phoenix/channels.html#client-libraries), but if you want to write your own, this guide should get you started.
+It may also be useful as a guide for manual testing with a WebSocket client.
+
+## Overview
+
+Because WebSockets are bidirectional, messages can flow in either direction at any time.
+For this reason, clients typically use callbacks to handle incoming messages whenever they come.
+
+A client must join at least one topic to begin sending and receiving messages, and may join any number of topics using the same connection.
+
+## Connecting
+
+To establish a WebSocket connection to Phoenix Channels, first make note of the `socket` declaration in the application's `Endpoint` module.
+For example, if you see: `socket "/mobile", MyAppWeb.MobileSocket`, the path for the initial HTTP request is:
+
+    [host]:[port]/mobile/websocket?vsn=2.0.0
+
+Passing `&vsn=2.0.0` specifies `Phoenix.Socket.V2.JSONSerializer`, which is built into Phoenix, and which expects and returns messages in the form of lists.
+
+You also need to include [the standard header fields for upgrading an HTTP request to a WebSocket connection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) or use an HTTP library that handles this for you; in Elixir, [mint_web_socket](https://hex.pm/packages/mint_web_socket) is an example.
+
+Other parameters or headers may be expected or required by the specific `connect/3` function in the application's socket module (in the example above, `MyAppWeb.MobileSocket.connect/3`).
+
+## Message Format
+
+The message format is determined by the serializer configured for the application.
+For these examples, `Phoenix.Socket.V2.JSONSerializer` is assumed.
+
+The general format for messages a client sends to a Phoenix Channel is as follows:
+
+```
+[join_reference, message_reference, topic_name, event_name, payload]
+```
+
+- The `message_reference` is chosen by the client and should be a unique value. The server includes it in its reply so that the client knows which message the reply is for.
+- The `join_reference` is also chosen by the client and should also be a unique value. It only needs to be sent for a `"phx_join"` event; for other messages it can be `null`. It is used as a message reference for `push` messages from the server, meaning those that are not replies to a specific client message. For example, imagine something like "a new user just joined the chat room".
+- The `topic_name` must be a known topic for the socket endpoint, and a client must join that topic before sending any messages on it.
+- The `event_name` must match the first argument of a `handle_in` function on the server channel module.
+- The `payload` should be a map and is passed as the second argument to that `handle_in` function.
+
+There are three events that are understood by every Phoenix application.
+
+First, `phx_join` is used join a channel. For example, to join the `miami:weather` channel:
+
+```json
+["0", "0", "miami:weather", "phx_join", {"some": "param"}]
+```
+
+Second, `phx_leave` is used to leave a channel. For example, to leave the `miami:weather` channel:
+
+```json
+[null, "1", "miami:weather", "phx_leave", {}]
+```
+
+Third, `heartbeat` is used to maintain the WebSocket connection. For example:
+
+
+```json
+[null, "2", "phoenix", "heartbeat", {}]
+```
+
+The `heartbeat` message is only needed when no other messages are being sent and prevents Phoenix from closing the connection; the exact `:timeout` is configured in the application's `Endpoint` module.
+
+Other allowed messages depend on the Phoenix application.
+
+For example, if the Channel serving the `miami:weather` can handle a `report_emergency` event:
+
+```elixir
+def handle_in("report_emergency", payload, socket) do
+  MyApp.Emergencies.report(payload) # or whatever
+  {:reply, :ok, socket}
+end
+```
+
+...a client could send:
+
+```json
+[null, "3", "miami:weather", "report_emergency", {"category": "sharknado"}]
+```
diff --git a/guides/introduction/community.md b/guides/introduction/community.md
index 9cc142369..64f01a4d6 100644
--- a/guides/introduction/community.md
+++ b/guides/introduction/community.md
@@ -6,7 +6,7 @@ There are a number of places to connect with community members at all experience
 
   * We're on Libera IRC in the [\#elixir](https://web.libera.chat/?channels=#elixir) channel.
 
-  * [Request an invitation](https://elixir-slackin.herokuapp.com/) and join the #phoenix channel on [Slack](https://elixir-lang.slack.com).
+  * [Request an invitation](https://elixir-lang.slack.com/join/shared_invite/zt-1f13hz7mb-N4KGjF523ONLCcHfb8jYgA#/shared-invite/email) and join the #phoenix channel on [Slack](https://elixir-lang.slack.com).
 
   * Feel free to join and check out the #phoenix channel on [Discord](https://discord.gg/elixir).
 
@@ -30,7 +30,7 @@ There are a number of places to connect with community members at all experience
 
   * [Phoenix in Action (by Geoffrey Lessel - 2019)](https://manning.com/books/phoenix-in-action)
 
-  * [Phoenix Inside Out - Book Series (by Shankar Dhanasekaran - 2017)](https://shankardevy.com/phoenix-book/)
+  * [Phoenix Inside Out - Book Series (by Shankar Dhanasekaran - 2017)](https://shankardevy.com/phoenix-book/). First book of the series Mastering Phoenix Framework is [free to read online](https://shankardevy.com/phoenix-inside-out-mpf/)
 
   * [Functional Web Development with Elixir, OTP, and Phoenix Rethink the Modern Web App (by Lance Halvorsen - 2017)](https://pragprog.com/titles/lhelph/functional-web-development-with-elixir-otp-and-phoenix/)
 
diff --git a/guides/plug.md b/guides/plug.md
index 469a9b0e5..97d525a9e 100644
--- a/guides/plug.md
+++ b/guides/plug.md
@@ -189,7 +189,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.LayoutView, :root}
+    plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
     plug HelloWeb.Plugs.Locale, "en"
diff --git a/guides/real_time/channels.md b/guides/real_time/channels.md
index fed1a0d5f..f3d7a01b4 100644
--- a/guides/real_time/channels.md
+++ b/guides/real_time/channels.md
@@ -165,7 +165,7 @@ If your deployment environment does not support distributed Elixir or direct com
 ### Client Libraries
 
 Any networked device can connect to Phoenix Channels as long as it has a client library.
-The following libraries exist today, and new ones are always welcome.
+The following libraries exist today, and new ones are always welcome; to write your own, see our how-to guide "Writing a Channels Client".
 
 #### Official
 
@@ -183,6 +183,7 @@ Phoenix ships with a JavaScript client that is available when generating a new P
   - [PhoenixSharp](https://github.com/Mazyod/PhoenixSharp)
 + Elixir
   - [phoenix_gen_socket_client](https://github.com/Aircloak/phoenix_gen_socket_client)
+  - [slipstream](https://hexdocs.pm/slipstream/Slipstream.html)
 + GDScript (Godot Game Engine)
   - [GodotPhoenixChannels](https://github.com/alfredbaudisch/GodotPhoenixChannels)
 
diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md
index 90d5d1a80..79a1e88a7 100644
--- a/guides/request_lifecycle.md
+++ b/guides/request_lifecycle.md
@@ -46,7 +46,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.Layouts, :root}
+    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
diff --git a/guides/routing.md b/guides/routing.md
index 34bb02ef2..856982a3c 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -16,7 +16,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.Layouts, :root}
+    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
@@ -236,14 +236,14 @@ When building paths for nested routes, we will need to interpolate the IDs where
 ```elixir
 user_id = 42
 post_id = 17
-~p"/users/#{user_id}/#{post_id}"
+~p"/users/#{user_id}/posts/#{post_id}"
 "/users/42/posts/17"
 ```
 
 Verified routes also support the `Phoenix.Param` protocol, but we don't need to concern ourselves with elixir protocols just yet. Just know that once we start building our application with structs like `%User{}` and `%Post{}`, we'll be able to interpolate those data structures directly into our `~p` paths and phoenix will pluck out the correct fields to use in the route.
 
 ```elixir
-~p"/users/#{user}/#{post}"
+~p"/users/#{user}/posts/#{post}"
 "/users/42/posts/17"
 ```
 
@@ -457,7 +457,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.Layouts, :root}
+    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
@@ -502,7 +502,7 @@ defmodule HelloWeb.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {HelloWeb.Layouts, :root}
+    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
@@ -519,7 +519,7 @@ defmodule HelloWeb.Router do
 end
 ```
 
-The above assumes there is a plug called `MyApp.Authentication` that performs authentication and is now part of the `:auth` pipeline.
+The above assumes there is a plug called `HelloWeb.Authentication` that performs authentication and is now part of the `:auth` pipeline.
 
 Note that pipelines themselves are plugs, so we can plug a pipeline inside another pipeline. For example, we could rewrite the `review_checks` pipeline above to automatically invoke `browser`, simplifying the downstream pipeline call:
 
diff --git a/installer/templates/phx_assets/tailwind.config.js b/installer/templates/phx_assets/tailwind.config.js
index 06601557f..856af57c6 100644
--- a/installer/templates/phx_assets/tailwind.config.js
+++ b/installer/templates/phx_assets/tailwind.config.js
@@ -54,6 +54,7 @@ module.exports = {
             [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
             "-webkit-mask": `var(--hero-${name})`,
             "mask": `var(--hero-${name})`,
+            "mask-repeat": "no-repeat",
             "background-color": "currentColor",
             "vertical-align": "middle",
             "display": "inline-block",
diff --git a/installer/templates/phx_single/config/prod.exs b/installer/templates/phx_single/config/prod.exs
index f6e8c82ed..a9c197346 100644
--- a/installer/templates/phx_single/config/prod.exs
+++ b/installer/templates/phx_single/config/prod.exs
@@ -1,8 +1,4 @@
-import Config
-
-# For production, don't forget to configure the url host
-# to something meaningful, Phoenix uses this information
-# when generating URLs.<%= if @javascript or @css do %>
+import Config<%= if @javascript or @css do %>
 
 # Note we also include the path to a cache manifest
 # containing the digested version of static files. This
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index b01f2474e..9311244fd 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -42,9 +42,9 @@ defmodule <%= @app_module %>.MixProject do
       {<%= inspect @adapter_app %>, ">= 0.0.0"},<% end %><%= if @html do %>
       {:phoenix_html, "~> 3.3"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},
-      {:phoenix_live_view, "~> 0.18.16"},
+      {:phoenix_live_view, "~> 0.19.0"},
       {:floki, ">= 0.30.0", only: :test},<% end %><%= if @dashboard do %>
-      {:phoenix_live_dashboard, "~> 0.7.2"},<% end %><%= if @javascript do %>
+      {:phoenix_live_dashboard, "~> 0.8.0"},<% end %><%= if @javascript do %>
       {:esbuild, "~> 0.7", runtime: Mix.env() == :dev},<% end %><%= if @css do %>
       {:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},<% end %><%= if @mailer do %>
       {:swoosh, "~> 1.3"},
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
index a3369183b..4acae1147 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
@@ -1,7 +1,3 @@
-# For production, don't forget to configure the url host
-# to something meaningful, Phoenix uses this information
-# when generating URLs.
-#
 # Note we also include the path to a cache manifest
 # containing the digested version of static files. This
 # manifest is generated by the `mix phx.digest` task,
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 ba3af5b23..dfc770ea7 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -40,9 +40,9 @@ defmodule <%= @web_namespace %>.MixProject do
       {:phoenix_ecto, "~> 4.4"},<% end %><%= if @html do %>
       {:phoenix_html, "~> 3.3"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},
-      {:phoenix_live_view, "~> 0.18.16"},
+      {:phoenix_live_view, "~> 0.19.0"},
       {:floki, ">= 0.30.0", only: :test},<% end %><%= if @dashboard do %>
-      {:phoenix_live_dashboard, "~> 0.7.2"},<% end %><%= if @javascript do %>
+      {:phoenix_live_dashboard, "~> 0.8.0"},<% end %><%= if @javascript do %>
       {:esbuild, "~> 0.7", runtime: Mix.env() == :dev},<% end %><%= if @css do %>
       {:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},<% end %>
       {:telemetry_metrics, "~> 0.6"},
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index f59a798f3..258322004 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -175,7 +175,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
   attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
 
   attr :rest, :global,
-    include: ~w(autocomplete name rel action enctype method novalidate target),
+    include: ~w(autocomplete name rel action enctype method novalidate target multipart),
     doc: "the arbitrary HTML attributes to apply to the form tag"
 
   slot :inner_block, required: true
@@ -269,8 +269,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
   attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
 
   attr :rest, :global,
-    include: ~w(autocomplete cols disabled form list max maxlength min minlength
-                pattern placeholder readonly required rows size step)
+    include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
+                multiple pattern placeholder readonly required rows size step)
 
   slot :inner_block
 
@@ -314,7 +314,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       <select
         id={@id}
         name={@name}
-        class="mt-1 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-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
         multiple={@multiple}
         {@rest}
       >
@@ -335,8 +335,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
         name={@name}
         class={[
           "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
-          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
-          "min-h-[6rem] border-zinc-300 focus:border-zinc-400",
+          "min-h-[6rem] phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
+          @errors == [] && "border-zinc-300 focus:border-zinc-400",
           @errors != [] && "border-rose-400 focus:border-rose-400"
         ]}
         {@rest}
diff --git a/installer/templates/phx_web/components/layouts/root.html.heex b/installer/templates/phx_web/components/layouts/root.html.heex
index aa2e9961b..b9926899e 100644
--- a/installer/templates/phx_web/components/layouts/root.html.heex
+++ b/installer/templates/phx_web/components/layouts/root.html.heex
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en" style="scrollbar-gutter: stable;">
+<html lang="en" class="[scrollbar-gutter:stable]">
   <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
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 3c8152790..3a5917f9f 100644
--- a/installer/templates/phx_web/controllers/page_html/home.html.heex
+++ b/installer/templates/phx_web/controllers/page_html/home.html.heex
@@ -159,22 +159,7 @@
               >
                 <path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
               </svg>
-              Discuss on the Elixir forum
-            </a>
-          </div>
-          <div>
-            <a
-              href="https://elixir-slackin.herokuapp.com"
-              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.95 9.85a1.47 1.47 0 1 1-2.94 0 1.47 1.47 0 0 1 1.47-1.472h1.47v1.471Zm.735 0a1.47 1.47 0 1 1 2.94 0v3.678a1.47 1.47 0 1 1-2.94 0V9.85ZM6.156 3.942a1.47 1.47 0 0 1-1.47-1.472 1.47 1.47 0 1 1 2.94 0v1.472h-1.47Zm0 .747c.813 0 1.47.658 1.47 1.471a1.47 1.47 0 0 1-1.47 1.472H2.47A1.47 1.47 0 0 1 1 6.16 1.47 1.47 0 0 1 2.47 4.69h3.686ZM12.048 6.16a1.47 1.47 0 1 1 2.94 0 1.47 1.47 0 0 1-1.47 1.472h-1.47V6.16Zm-.735 0a1.47 1.47 0 1 1-2.94 0V2.47a1.47 1.47 0 1 1 2.94 0v3.69ZM9.843 12.057c.813 0 1.47.657 1.47 1.471a1.47 1.47 0 1 1-2.94 0v-1.471h1.47Zm0-.736a1.47 1.47 0 0 1-1.47-1.472 1.47 1.47 0 0 1 1.47-1.471h3.686c.813 0 1.47.658 1.47 1.471a1.47 1.47 0 0 1-1.47 1.472H9.843Z" />
-              </svg>
-              Join our Slack channel
+              Discuss on the Elixir Forum
             </a>
           </div>
           <div>
diff --git a/installer/templates/phx_web/router.ex b/installer/templates/phx_web/router.ex
index 2cc4b1e6b..9d49c3d86 100644
--- a/installer/templates/phx_web/router.ex
+++ b/installer/templates/phx_web/router.ex
@@ -5,7 +5,7 @@ defmodule <%= @web_namespace %>.Router do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_live_flash
-    plug :put_root_layout, {<%= @web_namespace %>.Layouts, :root}
+    plug :put_root_layout, html: {<%= @web_namespace %>.Layouts, :root}
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end<% end %>
diff --git a/lib/phoenix/code_reloader.ex b/lib/phoenix/code_reloader.ex
index 9aeb034fe..931a7ccc5 100644
--- a/lib/phoenix/code_reloader.ex
+++ b/lib/phoenix/code_reloader.ex
@@ -30,17 +30,22 @@ defmodule Phoenix.CodeReloader do
   applications from library dependencies.
 
   This function is a no-op and returns `:ok` if Mix is not available.
+
+  ## Options
+
+    * `:reloadable_args` - additional CLI args to pass to the compiler tasks
+
   """
-  @spec reload(module) :: :ok | {:error, binary()}
-  def reload(endpoint) do
-    if Code.ensure_loaded?(Mix.Project), do: reload!(endpoint), else: :ok
+  @spec reload(module, keyword) :: :ok | {:error, binary()}
+  def reload(endpoint, opts \\ []) do
+    if Code.ensure_loaded?(Mix.Project), do: reload!(endpoint, opts), else: :ok
   end
 
   @doc """
   Same as `reload/1` but it will raise if Mix is not available.
   """
-  @spec reload!(module) :: :ok | {:error, binary()}
-  defdelegate reload!(endpoint), to: Phoenix.CodeReloader.Server
+  @spec reload!(module, keyword) :: :ok | {:error, binary()}
+  defdelegate reload!(endpoint, opts), to: Phoenix.CodeReloader.Server
 
   @doc """
   Synchronizes with the code server if it is alive.
diff --git a/lib/phoenix/code_reloader/proxy.ex b/lib/phoenix/code_reloader/proxy.ex
index fc203606f..2ada872bf 100644
--- a/lib/phoenix/code_reloader/proxy.ex
+++ b/lib/phoenix/code_reloader/proxy.ex
@@ -8,18 +8,26 @@ defmodule Phoenix.CodeReloader.Proxy do
     GenServer.start(__MODULE__, :ok)
   end
 
+  def diagnostics(proxy, diagnostics) do
+    GenServer.cast(proxy, {:diagnostics, diagnostics})
+  end
+
   def stop(proxy) do
-    GenServer.call(proxy, :stop)
+    GenServer.call(proxy, :stop, :infinity)
   end
 
   ## Callbacks
 
   def init(:ok) do
-    {:ok, ""}
+    {:ok, []}
+  end
+
+  def handle_cast({:diagnostics, diagnostics}, output) do
+    {:noreply, diagnostics |> Enum.map(&diagnostic_to_chars/1) |> Enum.reverse(output)}
   end
 
   def handle_call(:stop, _from, output) do
-    {:stop, :normal, output, output}
+    {:stop, :normal, Enum.reverse(output), output}
   end
 
   def handle_info(msg, output) do
@@ -47,6 +55,18 @@ defmodule Phoenix.CodeReloader.Proxy do
 
   defp put_chars(from, reply, chars, output) do
     send(Process.group_leader(), {:io_request, from, reply, {:put_chars, chars}})
-    {:noreply, output <> IO.chardata_to_string(chars)}
+    {:noreply, [chars | output]}
   end
+
+  defp diagnostic_to_chars(%{severity: :error, message: "**" <> _ = message}) do
+    "\n#{message}\n"
+  end
+
+  defp diagnostic_to_chars(%{severity: severity, message: message, file: file, position: position}) do
+    "\n#{severity}: #{message}\n  #{Path.relative_to_cwd(file)}#{position(position)}\n"
+  end
+
+  defp position({line, col}), do: ":#{line}:#{col}"
+  defp position(line) when is_integer(line) and line > 0, do: ":#{line}"
+  defp position(_), do: ""
 end
diff --git a/lib/phoenix/code_reloader/server.ex b/lib/phoenix/code_reloader/server.ex
index 64f467b46..e1146c4a1 100644
--- a/lib/phoenix/code_reloader/server.ex
+++ b/lib/phoenix/code_reloader/server.ex
@@ -13,8 +13,8 @@ defmodule Phoenix.CodeReloader.Server do
     GenServer.call(__MODULE__, :check_symlinks, :infinity)
   end
 
-  def reload!(endpoint) do
-    GenServer.call(__MODULE__, {:reload!, endpoint}, :infinity)
+  def reload!(endpoint, opts) do
+    GenServer.call(__MODULE__, {:reload!, endpoint, opts}, :infinity)
   end
 
   def sync do
@@ -58,9 +58,10 @@ defmodule Phoenix.CodeReloader.Server do
     {:reply, :ok, %{state | check_symlinks: false}}
   end
 
-  def handle_call({:reload!, endpoint}, from, state) do
+  def handle_call({:reload!, endpoint, opts}, from, state) do
     compilers = endpoint.config(:reloadable_compilers)
-    reloadable_apps = endpoint.config(:reloadable_apps) || default_reloadable_apps()
+    apps = endpoint.config(:reloadable_apps) || default_reloadable_apps()
+    args = Keyword.get(opts, :reloadable_args, [])
 
     # We do a backup of the endpoint in case compilation fails.
     # If so we can bring it back to finish the request handling.
@@ -70,7 +71,7 @@ defmodule Phoenix.CodeReloader.Server do
     {res, out} =
       proxy_io(fn ->
         try do
-          mix_compile(Code.ensure_loaded(Mix.Task), compilers, reloadable_apps, state.timestamp)
+          mix_compile(Code.ensure_loaded(Mix.Task), compilers, apps, args, state.timestamp)
         catch
           :exit, {:shutdown, 1} ->
             :error
@@ -88,7 +89,7 @@ defmodule Phoenix.CodeReloader.Server do
 
         :error ->
           write_backup(backup)
-          {:error, out}
+          {:error, IO.iodata_to_binary(out)}
       end
 
     Enum.each(froms, &GenServer.reply(&1, reply))
@@ -159,7 +160,7 @@ defmodule Phoenix.CodeReloader.Server do
 
   defp all_waiting(acc, endpoint) do
     receive do
-      {:"$gen_call", from, {:reload!, ^endpoint}} -> all_waiting([from | acc], endpoint)
+      {:"$gen_call", from, {:reload!, ^endpoint, _}} -> all_waiting([from | acc], endpoint)
     after
       0 -> acc
     end
@@ -174,7 +175,7 @@ defmodule Phoenix.CodeReloader.Server do
     defp purge_protocols(_path), do: :ok
   end
 
-  defp mix_compile({:module, Mix.Task}, compilers, apps_to_reload, timestamp) do
+  defp mix_compile({:module, Mix.Task}, compilers, apps_to_reload, compile_args, timestamp) do
     config = Mix.Project.config()
     path = Mix.Project.consolidation_path(config)
 
@@ -183,8 +184,8 @@ defmodule Phoenix.CodeReloader.Server do
       purge_protocols(path)
     end
 
-    mix_compile_deps(Mix.Dep.cached(), apps_to_reload, compilers, timestamp, path)
-    mix_compile_project(config[:app], apps_to_reload, compilers, timestamp, path)
+    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)
 
     if config[:consolidate_protocols] do
       Code.prepend_path(path)
@@ -193,30 +194,30 @@ 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, compilers, timestamp, path) do
+  defp mix_compile_deps(deps, apps_to_reload, compile_args, compilers, timestamp, path) do
     for dep <- deps, dep.app in apps_to_reload do
       Mix.Dep.in_dependency(dep, fn _ ->
-        mix_compile_unless_stale_config(compilers, timestamp, path)
+        mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
       end)
     end
   end
 
-  defp mix_compile_project(nil, _, _, _, _), do: :ok
+  defp mix_compile_project(nil, _, _, _, _, _), do: :ok
 
-  defp mix_compile_project(app, apps_to_reload, compilers, timestamp, path) do
+  defp mix_compile_project(app, apps_to_reload, compile_args, compilers, timestamp, path) do
     if app in apps_to_reload do
-      mix_compile_unless_stale_config(compilers, timestamp, path)
+      mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
     end
   end
 
-  defp mix_compile_unless_stale_config(compilers, timestamp, path) do
+  defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path) do
     manifests = Mix.Tasks.Compile.Elixir.manifests()
     configs = Mix.Project.config_files()
     config = Mix.Project.config()
@@ -229,7 +230,7 @@ defmodule Phoenix.CodeReloader.Server do
           purge_modules(Path.join(Mix.Project.app_path(config), "ebin"))
         end
 
-        mix_compile(compilers, config, path)
+        mix_compile(compilers, compile_args, config, path)
 
       files ->
         raise """
@@ -243,7 +244,7 @@ defmodule Phoenix.CodeReloader.Server do
     end
   end
 
-  defp mix_compile(compilers, config, consolidation_path) do
+  defp mix_compile(compilers, compile_args, config, consolidation_path) do
     all = config[:compilers] || Mix.compilers()
 
     compilers =
@@ -255,7 +256,7 @@ defmodule Phoenix.CodeReloader.Server do
     # We call build_structure mostly for Windows so new
     # assets in priv are copied to the build directory.
     Mix.Project.build_structure(config)
-    args = ["--purge-consolidation-path-if-stale", consolidation_path]
+    args = ["--purge-consolidation-path-if-stale", consolidation_path | compile_args]
     result = run_compilers(compilers, args, [])
 
     cond do
@@ -301,8 +302,8 @@ defmodule Phoenix.CodeReloader.Server do
   defp run_compilers([compiler | compilers], args, acc) do
     with {status, diagnostics} <- Mix.Task.run("compile.#{compiler}", args) do
       # Diagnostics are written to stderr and therefore not captured,
-      # so we print them to the group leader here
-      Enum.each(diagnostics, &print_diagnostic/1)
+      # so we send them to the group leader here
+      Proxy.diagnostics(Process.group_leader(), diagnostics)
       {status, diagnostics}
     end
     |> case do
@@ -319,16 +320,4 @@ defmodule Phoenix.CodeReloader.Server do
       :noop
     end
   end
-
-  defp print_diagnostic(%{severity: :error, message: "**" <> _ = message}) do
-    IO.write("\n#{message}\n")
-  end
-
-  defp print_diagnostic(%{severity: severity, message: message, file: file, position: position}) do
-    IO.write("\n#{severity}: #{message}\n  #{Path.relative_to_cwd(file)}#{position(position)}\n")
-  end
-
-  defp position({line, col}), do: ":#{line}:#{col}"
-  defp position(line) when is_integer(line) and line > 0, do: ":#{line}"
-  defp position(_), do: ""
 end
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 5cc2d1315..259123215 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -241,8 +241,6 @@ defmodule Phoenix.Controller do
 
     quote bind_quoted: [opts: opts] do
       import Phoenix.Controller
-
-      # TODO v2: No longer automatically import dependencies
       import Plug.Conn
 
       use Phoenix.Controller.Pipeline
@@ -307,7 +305,7 @@ defmodule Phoenix.Controller do
 
         def call(conn, {:error, :unauthorized}) do
           conn
-          |> put_status(403)
+          |> put_status(:forbidden)
           |> put_view(MyErrorView)
           |> render(:"403")
         end
diff --git a/lib/phoenix/endpoint/render_errors.ex b/lib/phoenix/endpoint/render_errors.ex
index f7dd9d93b..b4241dd8e 100644
--- a/lib/phoenix/endpoint/render_errors.ex
+++ b/lib/phoenix/endpoint/render_errors.ex
@@ -53,16 +53,18 @@ defmodule Phoenix.Endpoint.RenderErrors do
 
   @doc false
   def __catch__(conn, kind, reason, stack, opts) do
-    receive do
-      @already_sent ->
-        send(self(), @already_sent)
-        %Plug.Conn{conn | state: :sent}
-    after
-      0 ->
-        instrument_render_and_send(conn, kind, reason, stack, opts)
-    end
+    conn =
+      receive do
+        @already_sent ->
+          send(self(), @already_sent)
+          %Plug.Conn{conn | state: :sent}
+      after
+        0 ->
+          instrument_render_and_send(conn, kind, reason, stack, opts)
+      end
 
-    :erlang.raise(kind, reason, stack)
+    maybe_raise(kind, reason, stack)
+    conn
   end
 
   defp instrument_render_and_send(conn, kind, reason, stack, opts) do
@@ -91,6 +93,9 @@ defmodule Phoenix.Endpoint.RenderErrors do
   defp error_conn(_conn, :error, %NoRouteError{conn: conn}), do: conn
   defp error_conn(conn, _kind, _reason), do: conn
 
+  defp maybe_raise(:error, %NoRouteError{}, _stack), do: :ok
+  defp maybe_raise(kind, reason, stack), do: :erlang.raise(kind, reason, stack)
+
   ## Rendering
 
   @doc false
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 91174dca4..0039ed72f 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -453,7 +453,7 @@ defmodule Phoenix.Router do
   end
 
   defp match_dispatch() do
-    quote location: :keep do
+    quote location: :keep, generated: true do
       @behaviour Plug
 
       @doc """
@@ -506,10 +506,15 @@ defmodule Phoenix.Router do
     {matches, {pipelines, _}} =
       Enum.map_reduce(routes_with_exprs, {[], %{}}, &build_match/2)
 
-    verifies =
+    routes_per_path =
       routes_with_exprs
       |> Enum.group_by(&elem(&1, 1).path, &elem(&1, 0))
-      |> Enum.map(&build_verify/1)
+
+    verifies =
+      routes_with_exprs
+      |> Enum.map(&elem(&1, 1).path)
+      |> Enum.uniq()
+      |> Enum.map(&build_verify(&1, routes_per_path))
 
     verify_catch_all =
       quote generated: true do
@@ -574,7 +579,9 @@ defmodule Phoenix.Router do
     end
   end
 
-  defp build_verify({path, routes}) do
+  defp build_verify(path, routes_per_path) do
+    routes = Map.get(routes_per_path, path)
+
     forward_plug =
       Enum.find_value(routes, fn
         %{kind: :forward, plug: plug} -> plug
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index e5a99a7ce..d2e8595bb 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -291,8 +291,12 @@ defmodule Phoenix.Socket.Transport do
   Runs the code reloader if enabled.
   """
   def code_reload(conn, endpoint, opts) do
-    reload? = Keyword.get(opts, :code_reloader, endpoint.config(:code_reloader))
-    reload? && Phoenix.CodeReloader.reload(endpoint)
+    if Keyword.get(opts, :code_reloader, endpoint.config(:code_reloader)) do
+      # If the WebSocket reconnects, then often the page has already been reloaded
+      # and we don't want to print warnings twice, so we disable all warnings.
+      Phoenix.CodeReloader.reload(endpoint, reloadable_args: ~w(--no-all-warnings))
+    end
+
     conn
   end
 
diff --git a/lib/phoenix/token.ex b/lib/phoenix/token.ex
index 46a692fae..932ef6c58 100644
--- a/lib/phoenix/token.ex
+++ b/lib/phoenix/token.ex
@@ -115,9 +115,11 @@ defmodule Phoenix.Token do
       when generating the encryption and signing keys. Defaults to `:sha256`
     * `:signed_at` - set the timestamp of the token in seconds.
       Defaults to `System.system_time(:second)`
+    * `:max_age` - the default maximum age of the token. Defaults to
+      86400 seconds (1 day) and it may be overridden on verify/4.
 
   """
-  @spec sign(context, binary, term, [shared_opt | signed_at_opt]) :: binary
+  @spec sign(context, binary, term, [shared_opt | max_age_opt | signed_at_opt]) :: binary
   def sign(context, salt, data, opts \\ []) when is_binary(salt) do
     context
     |> get_key_base()
@@ -137,9 +139,11 @@ defmodule Phoenix.Token do
       when generating the encryption and signing keys. Defaults to `:sha256`
     * `:signed_at` - set the timestamp of the token in seconds.
       Defaults to `System.system_time(:second)`
+    * `:max_age` - the default maximum age of the token. Defaults to
+      86400 seconds (1 day) and it may be overridden on verify/4.
 
   """
-  @spec encrypt(context, binary, term, [shared_opt | signed_at_opt]) :: binary
+  @spec encrypt(context, binary, term, [shared_opt | max_age_opt | signed_at_opt]) :: binary
   def encrypt(context, secret, data, opts \\ []) when is_binary(secret) do
     context
     |> get_key_base()
diff --git a/mix.exs b/mix.exs
index 171f5cd73..d4f7bcf42 100644
--- a/mix.exs
+++ b/mix.exs
@@ -167,7 +167,9 @@ defmodule Phoenix.MixProject do
       "guides/deployment/fly.md",
       "guides/deployment/heroku.md",
       "guides/howto/custom_error_pages.md",
+      "guides/howto/file_uploads.md",
       "guides/howto/using_ssl.md",
+      "guides/howto/writing_a_channels_client.md",
       "CHANGELOG.md"
     ]
   end
diff --git a/priv/templates/phx.gen.auth/auth.ex b/priv/templates/phx.gen.auth/auth.ex
index 9aa6a753e..b32758a75 100644
--- a/priv/templates/phx.gen.auth/auth.ex
+++ b/priv/templates/phx.gen.auth/auth.ex
@@ -144,11 +144,11 @@ defmodule <%= inspect auth_module %> do
       end
   """
   def on_mount(:mount_current_<%= schema.singular %>, _params, session, socket) do
-    {:cont, mount_current_<%= schema.singular %>(session, socket)}
+    {:cont, mount_current_<%= schema.singular %>(socket, session)}
   end
 
   def on_mount(:ensure_authenticated, _params, session, socket) do
-    socket = mount_current_<%= schema.singular %>(session, socket)
+    socket = mount_current_<%= schema.singular %>(socket, session)
 
     if socket.assigns.current_<%= schema.singular %> do
       {:cont, socket}
@@ -163,7 +163,7 @@ defmodule <%= inspect auth_module %> do
   end
 
   def on_mount(:redirect_if_<%= schema.singular %>_is_authenticated, _params, session, socket) do
-    socket = mount_current_<%= schema.singular %>(session, socket)
+    socket = mount_current_<%= schema.singular %>(socket, session)
 
     if socket.assigns.current_<%= schema.singular %> do
       {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
@@ -172,7 +172,7 @@ defmodule <%= inspect auth_module %> do
     end
   end
 
-  defp mount_current_<%= schema.singular %>(session, socket) do
+  defp mount_current_<%= schema.singular %>(socket, session) do
     Phoenix.Component.assign_new(socket, :current_<%= schema.singular %>, fn ->
       if <%= schema.singular %>_token = session["<%= schema.singular %>_token"] do
         <%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(<%= schema.singular %>_token)
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index f59a798f3..258322004 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -175,7 +175,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
   attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
 
   attr :rest, :global,
-    include: ~w(autocomplete name rel action enctype method novalidate target),
+    include: ~w(autocomplete name rel action enctype method novalidate target multipart),
     doc: "the arbitrary HTML attributes to apply to the form tag"
 
   slot :inner_block, required: true
@@ -269,8 +269,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
   attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
 
   attr :rest, :global,
-    include: ~w(autocomplete cols disabled form list max maxlength min minlength
-                pattern placeholder readonly required rows size step)
+    include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
+                multiple pattern placeholder readonly required rows size step)
 
   slot :inner_block
 
@@ -314,7 +314,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
       <select
         id={@id}
         name={@name}
-        class="mt-1 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-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
         multiple={@multiple}
         {@rest}
       >
@@ -335,8 +335,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
         name={@name}
         class={[
           "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
-          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
-          "min-h-[6rem] border-zinc-300 focus:border-zinc-400",
+          "min-h-[6rem] phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
+          @errors == [] && "border-zinc-300 focus:border-zinc-400",
           @errors != [] && "border-rose-400 focus:border-rose-400"
         ]}
         {@rest}


Test patch:
diff --git a/guides/testing/testing_channels.md b/guides/testing/testing_channels.md
index 592b88b69..1692e3fba 100644
--- a/guides/testing/testing_channels.md
+++ b/guides/testing/testing_channels.md
@@ -85,7 +85,7 @@ setup do
   {:ok, _, socket} =
     HelloWeb.UserSocket
     |> socket("user_id", %{some: :assign})
-    |> subscribe_and_join(RoomChannel, "room:lobby")
+    |> subscribe_and_join(HelloWeb.RoomChannel, "room:lobby")
 
   %{socket: socket}
 end
@@ -106,7 +106,7 @@ test "ping replies with status ok", %{socket: socket} do
 end
 ```
 
-This tests the following code in our `MyAppWeb.RoomChannel`:
+This tests the following code in our `HelloWeb.RoomChannel`:
 
 ```elixir
 # Channels can be used in a request/response fashion
@@ -124,7 +124,7 @@ We emulate the client pushing a message to the channel with `push/3`. In the lin
 
 ### Testing a Broadcast
 
-It is common to receive messages from the client and broadcast to everyone subscribed to a current topic. This common pattern is simple to express in Phoenix and is one of the generated `handle_in/3` callbacks in our `MyAppWeb.RoomChannel`.
+It is common to receive messages from the client and broadcast to everyone subscribed to a current topic. This common pattern is simple to express in Phoenix and is one of the generated `handle_in/3` callbacks in our `HelloWeb.RoomChannel`.
 
 ```elixir
 def handle_in("shout", payload, socket) do
@@ -150,17 +150,10 @@ Since the `handle_in/3` callback for the `"shout"` event just broadcasts the sam
 
 ### Testing an asynchronous push from the server
 
-The last test in our `MyAppWeb.RoomChannelTest` verifies that broadcasts from the server are pushed to the client. Unlike the previous tests discussed, we are indirectly testing that our channel's `handle_out/3` callback is triggered. This `handle_out/3` is defined in our `MyApp.RoomChannel` as:
-
-```elixir
-def handle_out(event, payload, socket) do
-  push(socket, event, payload)
-  {:noreply, socket}
-end
-```
+The last test in our `HelloWeb.RoomChannelTest` verifies that broadcasts from the server are pushed to the client. Unlike the previous tests discussed, we are indirectly testing that the channel's `handle_out/3` callback is triggered. By default, `handle_out/3` is implemented for us and simply pushes the message on to the client.
 
 Since the `handle_out/3` event is only triggered when we call `broadcast/3` from our channel, we will need to emulate that in our test. We do that by calling `broadcast_from` or `broadcast_from!`. Both serve the same purpose with the only difference of `broadcast_from!` raising an error when broadcast fails.
 
-The line `broadcast_from!(socket, "broadcast", %{"some" => "data"})` will trigger our `handle_out/3` callback above which pushes the same event and payload back to the client. To test this, we do `assert_push "broadcast", %{"some" => "data"}`.
+The line `broadcast_from!(socket, "broadcast", %{"some" => "data"})` will trigger the `handle_out/3` callback which pushes the same event and payload back to the client. To test this, we do `assert_push "broadcast", %{"some" => "data"}`.
 
 That's it. Now you are ready to develop and fully test real-time applications. To learn more about other functionality provided when testing channels, check out the documentation for [`Phoenix.ChannelTest`](https://hexdocs.pm/phoenix/Phoenix.ChannelTest.html).
diff --git a/guides/testing/testing_contexts.md b/guides/testing/testing_contexts.md
index 76082b779..c2b330c21 100644
--- a/guides/testing/testing_contexts.md
+++ b/guides/testing/testing_contexts.md
@@ -21,12 +21,12 @@ $ mix test
 ................
 
 Finished in 0.6 seconds
-19 tests, 0 failures
+21 tests, 0 failures
 
 Randomized with seed 638414
 ```
 
-Great. We've got nineteen tests and they are all passing!
+Great. We've got twenty-one tests and they are all passing!
 
 ## Testing posts
 
@@ -41,18 +41,9 @@ defmodule Hello.BlogTest do
   describe "posts" do
     alias Hello.Blog.Post
 
-    @valid_attrs %{body: "some body", title: "some title"}
-    @update_attrs %{body: "some updated body", title: "some updated title"}
-    @invalid_attrs %{body: nil, title: nil}
-
-    def post_fixture(attrs \\ %{}) do
-      {:ok, post} =
-        attrs
-        |> Enum.into(@valid_attrs)
-        |> Blog.create_post()
+    import Hello.BlogFixtures
 
-      post
-    end
+    @invalid_attrs %{body: nil, title: nil}
 
     test "list_posts/0 returns all posts" do
       post = post_fixture()
@@ -78,7 +69,9 @@ The tests defined for our context are very straight-forward. They call the funct
 
 ```elixir
 test "create_post/1 with valid data creates a post" do
-  assert {:ok, %Post{} = post} = Blog.create_post(@valid_attrs)
+  valid_attrs = %{body: "some body", title: "some title"}
+
+  assert {:ok, %Post{} = post} = Blog.create_post(valid_attrs)
   assert post.body == "some body"
   assert post.title == "some title"
 end
@@ -106,11 +99,15 @@ defmodule Hello.DataCase do
   end
 
   setup tags do
-    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Demo.Repo, shared: not tags[:async])
-    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+    Hello.DataCase.setup_sandbox(tags)
     :ok
   end
 
+  def setup_sandbox(tags) do
+    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Hello.Repo, shared: not tags[:async])
+    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+  end
+
   def errors_on(changeset) do
     ...
   end
diff --git a/guides/testing/testing_controllers.md b/guides/testing/testing_controllers.md
index 1303513f4..b406dd1be 100644
--- a/guides/testing/testing_controllers.md
+++ b/guides/testing/testing_controllers.md
@@ -20,15 +20,17 @@ If you open up `test/hello_web/controllers/post_controller_test.exs`, you will f
 defmodule HelloWeb.PostControllerTest do
   use HelloWeb.ConnCase
 
-  alias Hello.Blog
+  import Hello.BlogFixtures
 
   @create_attrs %{body: "some body", title: "some title"}
   @update_attrs %{body: "some updated body", title: "some updated title"}
   @invalid_attrs %{body: nil, title: nil}
-
-  def fixture(:post) do
-    {:ok, post} = Blog.create_post(@create_attrs)
-    post
+  
+  describe "index" do
+    test "lists all posts", %{conn: conn} do
+      conn = get(conn, ~p"/posts")
+      assert html_response(conn, 200) =~ "Listing Posts"
+    end
   end
 
   ...
@@ -43,7 +45,7 @@ The first describe block is for the `index` action. The action itself is impleme
 ```elixir
 def index(conn, _params) do
   posts = Blog.list_posts()
-  render(conn, "index.html", posts: posts)
+  render(conn, :index, posts: posts)
 end
 ```
 
@@ -75,7 +77,7 @@ def create(conn, %{"post" => post_params}) do
       |> redirect(to: ~p"/posts/#{post}")
 
     {:error, %Ecto.Changeset{} = changeset} ->
-      render(conn, "new.html", changeset: changeset)
+      render(conn, :new, changeset: changeset)
   end
 end
 ```
@@ -91,7 +93,7 @@ describe "create post" do
     assert redirected_to(conn) == ~p"/posts/#{id}"
 
     conn = get(conn, ~p"/posts/#{id}")
-    assert html_response(conn, 200) =~ "Show Post"
+    assert html_response(conn, 200) =~ "Post #{id}"
   end
 
   test "renders errors when data is invalid", %{conn: conn} do
@@ -154,6 +156,7 @@ The test is written like this:
     test "deletes chosen post", %{conn: conn, post: post} do
       conn = delete(conn, ~p"/posts/#{post}")
       assert redirected_to(conn) == ~p"/posts"
+
       assert_error_sent 404, fn ->
         get(conn, ~p"/posts/#{post}")
       end
@@ -161,7 +164,7 @@ The test is written like this:
   end
 
   defp create_post(_) do
-    post = fixture(:post)
+    post = post_fixture()
     %{post: post}
   end
 ```
@@ -190,7 +193,7 @@ This pretty much mimics how Phoenix handles exceptions. For example, when we acc
 ```elixir
 def show(conn, %{"id" => id}) do
   post = Blog.get_post!(id)
-  render(conn, "show.html", post: post)
+  render(conn, :show, post: post)
 end
 ```
 
@@ -220,19 +223,19 @@ $ mix phx.gen.json News Article articles title body
 
 We chose a very similar concept to the Blog context <-> Post schema, except we are using a different name, so we can study these concepts in isolation.
 
-After you run the command above, do not forget to follow the final steps output by the generator. Once all is done, we should run `mix test` and now have 33 passing tests:
+After you run the command above, do not forget to follow the final steps output by the generator. Once all is done, we should run `mix test` and now have 35 passing tests:
 
 ```console
 $ mix test
 ................
 
 Finished in 0.6 seconds
-33 tests, 0 failures
+35 tests, 0 failures
 
 Randomized with seed 618478
 ```
 
-You may have noticed that this time the scaffold controller has generated fewer tests. Previously it generated 16 (we went from 3 to 19) and now it generated 14 (we went from 19 to 33). That's because JSON APIs do not need to expose the `new` and `edit` actions. We can see this is the case in the resource we have added to the router at the end of the `mix phx.gen.json` command:
+You may have noticed that this time the scaffold controller has generated fewer tests. Previously it generated 16 (we went from 5 to 21) and now it generated 14 (we went from 21 to 35). That's because JSON APIs do not need to expose the `new` and `edit` actions. We can see this is the case in the resource we have added to the router at the end of the `mix phx.gen.json` command:
 
 ```elixir
 resources "/articles", ArticleController, except: [:new, :edit]
@@ -259,16 +262,17 @@ The action gets all articles and renders the index template. Since we are talkin
 
 ```elixir
 defmodule HelloWeb.ArticleJSON do
+  alias Hello.News.Article
 
   def index(%{articles: articles}) do
-    %{data: for(article <- article, do: data(article))}
+    %{data: for(article <- articles, do: data(article))}
   end
 
   def show(%{article: article}) do
-    %{data: data(article)
+    %{data: data(article)}
   end
 
-  defp data(article) do
+  defp data(%Article{} = article) do
     %{
       id: article.id,
       title: article.title,
@@ -285,7 +289,7 @@ Let's take a look at the test for the `index` action then:
 ```elixir
 describe "index" do
   test "lists all articles", %{conn: conn} do
-    conn = get(conn, ~p"/articles")
+    conn = get(conn, ~p"/api/articles")
     assert json_response(conn, 200)["data"] == []
   end
 end
@@ -304,7 +308,7 @@ def create(conn, %{"article" => article_params}) do
   with {:ok, %Article{} = article} <- News.create_article(article_params) do
     conn
     |> put_status(:created)
-    |> put_resp_header("location", ~p"/articles/#{article}")
+    |> put_resp_header("location", ~p"/api/articles/#{article}")
     |> render(:show, article: article)
   end
 end
@@ -315,15 +319,15 @@ As we can see, it checks if an article could be created. If so, it sets the stat
 This is precisely what the first test for the `create` action verifies:
 
 ```elixir
-describe "create" do
+describe "create article" do
   test "renders article when data is valid", %{conn: conn} do
     conn = post(conn, ~p"/articles", article: @create_attrs)
     assert %{"id" => id} = json_response(conn, 201)["data"]
 
-    conn = get(conn, ~p"/articles/#{id}")
+    conn = get(conn, ~p"/api/articles/#{id}")
 
     assert %{
-             "id" => id,
+             "id" => ^id,
              "body" => "some body",
              "title" => "some title"
            } = json_response(conn, 200)["data"]
@@ -332,7 +336,7 @@ describe "create" do
 
 The test uses `post/2` to create a new article and then we verify that the article returned a JSON response, with status 201, and that it had a "data" key in it. We pattern match the "data" on `%{"id" => id}`, which allows us to extract the ID of the new article. Then we perform a `get/2` request on the `show` route and verify that the article was successfully created.
 
-Inside `describe "create"`, we will find another test, which handles the failure scenario. Can you spot the failure scenario in the `create` action? Let's recap it:
+Inside `describe "create article"`, we will find another test, which handles the failure scenario. Can you spot the failure scenario in the `create` action? Let's recap it:
 
 ```elixir
 def create(conn, %{"article" => article_params}) do
@@ -363,7 +367,7 @@ defmodule HelloWeb.FallbackController do
   def call(conn, {:error, :not_found}) do
     conn
     |> put_status(:not_found)
-    |> put_view(json: HelloWeb.ErrorJSON)
+    |> put_view(html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON)
     |> render(:"404")
   end
 end
@@ -375,7 +379,7 @@ With this in mind, let's look at our second test for `create`:
 
 ```elixir
 test "renders errors when data is invalid", %{conn: conn} do
-  conn = post(conn, ~p"/articles", article: @invalid_attrs)
+  conn = post(conn, ~p"/api/articles", article: @invalid_attrs)
   assert json_response(conn, 422)["errors"] != %{}
 end
 ```
@@ -407,17 +411,17 @@ describe "delete article" do
   setup [:create_article]
 
   test "deletes chosen article", %{conn: conn, article: article} do
-    conn = delete(conn, ~p"/articles/#{article}")
+    conn = delete(conn, ~p"/api/articles/#{article}")
     assert response(conn, 204)
 
     assert_error_sent 404, fn ->
-      get(conn, ~p"/articles/#{article}")
+      get(conn, ~p"/api/articles/#{article}")
     end
   end
 end
 
 defp create_article(_) do
-  article = fixture(:article)
+  article = article_fixture()
   %{article: article}
 end
 ```
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 2e86d82b9..684c9c735 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -266,7 +266,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file(web_path(@app, "lib/phx_umb_web/router.ex"), fn file ->
         assert file =~ ~s[plug :fetch_live_flash]
-        assert file =~ ~s[plug :put_root_layout, {PhxUmbWeb.Layouts, :root}]
+        assert file =~ ~s[plug :put_root_layout, html: {PhxUmbWeb.Layouts, :root}]
         assert file =~ ~s[get "/", PageController]
       end)
 
diff --git a/integration_test/mix.exs b/integration_test/mix.exs
index 46f8ac2b9..fe5ea4d29 100644
--- a/integration_test/mix.exs
+++ b/integration_test/mix.exs
@@ -41,10 +41,10 @@ defmodule Phoenix.Integration.MixProject do
       {:tds, ">= 0.0.0"},
       {:ecto_sqlite3, ">= 0.0.0"},
       {:phoenix_html, "~> 3.3"},
-      {:phoenix_live_view, "~> 0.18.16"},
+      {:phoenix_live_view, "~> 0.19.0"},
       {:floki, ">= 0.30.0"},
       {:phoenix_live_reload, "~> 1.2"},
-      {:phoenix_live_dashboard, "~> 0.7.2"},
+      {:phoenix_live_dashboard, "~> 0.8.0"},
       {:telemetry_metrics, "~> 0.6"},
       {:telemetry_poller, "~> 1.0"},
       {:gettext, "~> 0.20"},
diff --git a/integration_test/mix.lock b/integration_test/mix.lock
index 4c75afd13..64606030c 100644
--- a/integration_test/mix.lock
+++ b/integration_test/mix.lock
@@ -9,8 +9,8 @@
   "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
   "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
   "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
-  "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
-  "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 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", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
+  "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
+  "ecto": {:hex, :ecto, "3.9.5", "9f0aa7ae44a1577b651c98791c6988cd1b69b21bc724e3fd67090b97f7604263", [:mix], [{:decimal, "~> 1.6 or ~> 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", "d4f3115d8cbacdc0bfa4b742865459fb1371d0715515842a1fb17fe31920b74c"},
   "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 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", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
   "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.9.1", "6cb740afcb0af882cf9dfbac7ff8db863600d3bb24454c840d506e05b1508d6e", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "cc7664d9210c232370e67e89331056cf0a35e74862cd57e893e7035eacbd4afc"},
   "elixir_make": {:hex, :elixir_make, "0.7.5", "784cc00f5fa24239067cc04d449437dcc5f59353c44eb08f188b2b146568738a", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "c3d63e8d5c92fa3880d89ecd41de59473fa2e83eeb68148155e25e8b95aa2887"},
@@ -31,12 +31,12 @@
   "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "2.1.0", "ce2f75056d43281df044a6da902c933f73789dcea560e25c24c9ede80b8365cc", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "bf8aa304bd2b47ed74de6e5eb4c6b7dc766b936a0a86d643ada89657c715f525"},
   "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
   "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
-  "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [: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]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
+  "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [: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", [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.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"},
   "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
-  "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{: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", [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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
+  "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.0", "de5643d03e3cdf5ff19cd45b5d14543a3b1ad8551d529f6b24246e88a6c6f1b8", [:mix], [{: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", [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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a650b6f814c4f386314b98f1aebf92f8652649166612f84ef2e60a20894addfa"},
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
   "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
-  "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
+  "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
   "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
   "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
   "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 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", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
diff --git a/test/phoenix/code_reloader_test.exs b/test/phoenix/code_reloader_test.exs
index fc80efc2d..70b5d0cfe 100644
--- a/test/phoenix/code_reloader_test.exs
+++ b/test/phoenix/code_reloader_test.exs
@@ -3,13 +3,8 @@ defmodule Phoenix.CodeReloaderTest do
   use RouterHelper
 
   defmodule Endpoint do
-    def config(:reloadable_compilers) do
-      [:unknown_compiler, :elixir]
-    end
-
-    def config(:reloadable_apps) do
-      nil
-    end
+    def config(:reloadable_compilers), do: [:unknown_compiler, :elixir]
+    def config(:reloadable_apps), do: nil
   end
 
   def reload(_) do
@@ -48,7 +43,7 @@ defmodule Phoenix.CodeReloaderTest do
            |> Phoenix.CodeReloader.call(opts)
     assert conn.state == :unset
 
-    assert_receive {:trace, ^pid, :receive, {_, _, {:reload!, Endpoint}}}
+    assert_receive {:trace, ^pid, :receive, {_, _, {:reload!, Endpoint, _}}}
   end
 
   test "renders compilation error on failure" do
diff --git a/test/phoenix/endpoint/render_errors_test.exs b/test/phoenix/endpoint/render_errors_test.exs
index ef2014ff6..cfd24fcb3 100644
--- a/test/phoenix/endpoint/render_errors_test.exs
+++ b/test/phoenix/endpoint/render_errors_test.exs
@@ -96,33 +96,21 @@ defmodule Phoenix.Endpoint.RenderErrorsTest do
     assert_received {:plug_conn, :sent}
   end
 
-  test "call/2 is overridden with no route match as HTML" do
-    assert_raise Phoenix.Router.NoRouteError,
-                 "no route found for GET /unknown (Phoenix.Endpoint.RenderErrorsTest.Router)",
-                 fn ->
-                   call(Router, :get, "/unknown")
-                 end
+  test "call/2 is overridden with no route match as HTML and does not reraise" do
+    call(Router, :get, "/unknown")
 
     assert_received {:plug_conn, :sent}
   end
 
-  test "call/2 is overridden with no route match as JSON" do
-    assert_raise Phoenix.Router.NoRouteError,
-                 "no route found for GET /unknown (Phoenix.Endpoint.RenderErrorsTest.Router)",
-                 fn ->
-                   call(Router, :get, "/unknown?_format=json")
-                 end
+  test "call/2 is overridden with no route match as JSON and does not reraise" do
+    call(Router, :get, "/unknown?_format=json")
 
     assert_received {:plug_conn, :sent}
   end
 
   @tag :capture_log
-  test "call/2 is overridden with no route match while malformed format" do
-    assert_raise Phoenix.Router.NoRouteError,
-                 "no route found for GET /unknown (Phoenix.Endpoint.RenderErrorsTest.Router)",
-                 fn ->
-                   call(Router, :get, "/unknown?_format=unknown")
-                 end
+  test "call/2 is overridden with no route match while malformed format and does not reraise" do
+    call(Router, :get, "/unknown?_format=unknown")
 
     assert_received {:plug_conn, :sent}
   end
@@ -321,12 +309,8 @@ defmodule Phoenix.Endpoint.RenderErrorsTest do
            end) =~ "Could not render errors due to unknown format \"unknown\""
   end
 
-  test "exception page for NoRouteError with plug_status 404" do
-    body =
-      assert_render(404, conn(:get, "/"), [], fn ->
-        raise Phoenix.Router.NoRouteError, conn: conn(:get, "/"), router: nil, plug_status: 404
-      end)
-
-    assert body == "Got 404 from error with GET"
+  test "exception page for NoRouteError with plug_status 404 renders and does not reraise" do
+    conn = call(Router, :get, "/unknown")
+    assert Phoenix.ConnTest.response(conn, 404) =~ "Got 404 from error with GET"
   end
 end
diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs
index 095c0c9b7..c67d7c040 100644
--- a/test/phoenix/verified_routes_test.exs
+++ b/test/phoenix/verified_routes_test.exs
@@ -49,6 +49,14 @@ defmodule Phoenix.VerifiedRoutesTest do
     forward "/plug_forward", UserController
   end
 
+  defmodule CatchAllWarningRouter do
+    use Phoenix.Router
+    alias Phoenix.VerifiedRoutesTest.PostController
+
+    get "/", PostController, :root
+    get "/*path", PostController, :root, warn_on_verify: true
+  end
+
   # Emulate regular endpoint functions
 
   defmodule Endpoint do
@@ -528,6 +536,19 @@ defmodule Phoenix.VerifiedRoutesTest do
         assert warnings =~
                  ~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/should-warn/foobar"|
       end
+
+      test "~p does not warn if route without warn_on_verify: true matches first" do
+        warnings =
+          ExUnit.CaptureIO.capture_io(:stderr, fn ->
+            defmodule VerifyFalse do
+              use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: CatchAllWarningRouter
+
+              def test, do: ~p"/"
+            end
+          end)
+
+        assert warnings == ""
+      end
     end
   end
 end