Repository: phoenixframework/phoenix
Pull request #4969 (2022-09-21 19:17:31)

bump tailwind config to version 3.1.8
per https://tailwindcss.com/docs/guides/phoenix


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bc66afe19..6c1657dd0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,7 +25,7 @@ jobs:
 
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Set up Elixir
       uses: erlef/setup-beam@v1
@@ -34,7 +34,7 @@ jobs:
         otp-version: ${{ matrix.otp }}
 
     - name: Restore deps and _build cache
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: |
           deps
@@ -67,10 +67,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: Restore deps and _build cache
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: |
             deps
@@ -80,12 +80,12 @@ jobs:
             deps-${{ runner.os }}-npm
 
       - name: Set up Node.js 12.x
-        uses: actions/setup-node@v1
+        uses: actions/setup-node@v3
         with:
           node-version: 12.x
 
       - name: Restore npm cache
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: ~/.npm
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7f11a90b4..7696930c4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,7 +26,7 @@ making it easier for developers to [contribute to Phoenix](#pull-requests).
 ## Bug reports
 
 A bug is either a _demonstrable problem_ that is caused by the code in the repository,
-or indicate missing, unclear, or misleading documentation. Good bug reports are extremely 
+or indicate missing, unclear, or misleading documentation. Good bug reports are extremely
 helpful - thank you!
 
 Guidelines for bug reports:
@@ -134,8 +134,10 @@ in order to craft an excellent pull request:
    ```bash
    # Clone your fork of the repo into the current directory
    git clone https://github.com/<your-username>/phoenix
+
    # Navigate to the newly cloned directory
    cd phoenix
+
    # Assign the original repo to a remote called "upstream"
    git remote add upstream https://github.com/phoenixframework/phoenix
    ```
@@ -216,9 +218,11 @@ complete, the better.
 
 For formatting the guides:
 
-- We use the "elixir" code fence for all module code.
-- We use the "console" code fence for iex and shell commands.
-- We use the "html" code fence for html templates, even if there is elixir code
+- We use the `elixir` code fence for all module code.
+- We use the `iex` for IEx sessions.
+- We use the `console` code fence for shell commands.
+- We use the `html` code fence for html templates, even if there is elixir code
   in the template.
 - We use backticks for filenames and directory paths.
 - We use backticks for module names, function names, and variable names.
+- Documentation line length should hard wrapped at around 100 characters if possible.
diff --git a/README.md b/README.md
index bff6a5a5f..1f21739a4 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,20 @@
 ![phoenix logo](https://raw.githubusercontent.com/phoenixframework/phoenix/master/priv/static/phoenix.png)
+
 > Peace of mind from prototype to production.
 
 [![Build Status](https://github.com/phoenixframework/phoenix/workflows/CI/badge.svg)](https://github.com/phoenixframework/phoenix/actions/workflows/ci.yml) [![Hex.pm](https://img.shields.io/hexpm/v/phoenix.svg)](https://hex.pm/packages/phoenix) [![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/phoenix)
 
 ## Getting started
 
-See the official site at https://www.phoenixframework.org/
+See the official site at <https://www.phoenixframework.org/>.
 
-Install the latest version of Phoenix by following the instructions at https://hexdocs.pm/phoenix/installation.html#phoenix
+Install the latest version of Phoenix by following the instructions at <https://hexdocs.pm/phoenix/installation.html#phoenix>.
 
 ## Documentation
 
-API documentation is available at [https://hexdocs.pm/phoenix](https://hexdocs.pm/phoenix)
+API documentation is available at <https://hexdocs.pm/phoenix>.
 
-Phoenix.js documentation is available at [https://hexdocs.pm/phoenix/js](https://hexdocs.pm/phoenix/js)
+Phoenix.js documentation is available at <https://hexdocs.pm/phoenix/js>.
 
 ## Contributing
 
@@ -28,8 +29,8 @@ You can create a new project using the latest Phoenix source installer (the `phx
 3. Run the `phx.new` Mix task from within the `installer` directory, for example:
 
 ```bash
-$ cd phoenix/installer
-$ mix phx.new dev_app --dev
+cd phoenix/installer
+mix phx.new dev_app --dev
 ```
 
 The `--dev` flag will configure your new project's `:phoenix` dep as a relative path dependency, pointing to your local Phoenix checkout:
@@ -44,30 +45,30 @@ To create projects outside of the `installer/` directory, add the latest archive
 To build the documentation from source:
 
 ```bash
-$ npm install --prefix assets
-$ MIX_ENV=docs mix docs
+npm install --prefix assets
+MIX_ENV=docs mix docs
 ```
 
 To build Phoenix from source:
 
 ```bash
-$ mix deps.get
-$ mix compile
+mix deps.get
+mix compile
 ```
 
 To build the Phoenix installer from source:
 
 ```bash
-$ mix deps.get
-$ mix compile
-$ mix archive.build
+mix deps.get
+mix compile
+mix archive.build
 ```
 
 ### Building phoenix.js
 
 ```bash
-$ cd assets
-$ npm install
+cd assets
+npm install
 ```
 
 ## Important links
diff --git a/guides/asset_management.md b/guides/asset_management.md
index 953c1ac8b..da03aeae2 100644
--- a/guides/asset_management.md
+++ b/guides/asset_management.md
@@ -14,13 +14,17 @@ Finally, all other assets, that usually don't have to be preprocessed, go direct
 
 If you want to import JavaScript dependencies, you have two options to add them to your application:
 
-  1. Vendor those dependencies inside your project and import them in your "assets/js/app.js" using a relative path:
+1. Vendor those dependencies inside your project and import them in your "assets/js/app.js" using a relative path:
 
-         import topbar from "../vendor/topbar"
+   ```js
+   import topbar from "../vendor/topbar"
+   ```
 
-  2. Call `npm install topbar --save` inside your assets directory and `esbuild` will be able to automatically pick them up:
+2. Call `npm install topbar --save` inside your assets directory and `esbuild` will be able to automatically pick them up:
 
-         import topbar from "topbar"
+   ```js
+   import topbar from "topbar"
+   ```
 
 ## CSS
 
diff --git a/guides/contexts.md b/guides/contexts.md
index 5782021e0..24477adac 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -105,13 +105,13 @@ $ mix ecto.migrate
 
 Before we jump into the generated code, let's start the server with `mix phx.server` and visit [http://localhost:4000/products](http://localhost:4000/products). Let's follow the "New Product" link and click the "Save" button without providing any input. We should be greeted with the following output:
 
-```
+```text
 Oops, something went wrong! Please check the errors below.
 ```
 
 When we submit the form, we can see all the validation errors inline with the inputs. Nice! Out of the box, the context generator included the schema fields in our form template and we can see our default validations for required inputs are in effect. Let's enter some example product data and resubmit the form:
 
-```
+```text
 Product created successfully.
 
 Title: Metaprogramming Elixir
@@ -312,7 +312,7 @@ We modified our `show` action to pipe our fetched product into `Catalog.inc_page
 
 We can also see our atomic update in action in the ecto debug logs:
 
-```
+```text
 [debug] QUERY OK source="products" db=0.5ms idle=834.5ms
 UPDATE "products" AS p0 SET "views" = p0."views" + $1 WHERE (p0."id" = $2) RETURNING p0."views" [1, 1]
 ```
@@ -427,7 +427,6 @@ INSERT INTO "categories" ("title","inserted_at","updated_at") VALUES ($1,$2,$3)
 
 Perfect. Before we integrate categories in the web layer, we need to let our context know how to associate products and categories. First, open up `lib/hello/catalog/product.ex` and add the following association:
 
-
 ```diff
 + alias Hello.Catalog.Category
 
@@ -448,7 +447,6 @@ We used `Ecto.Schema`'s `many_to_many` macro to let Ecto know how to associate o
 
 With our schema associations set up, we can implement the selection of categories in our product form. To do so, we need to translate the user input of catalog IDs from the front-end to our many-to-many association. Fortunately Ecto makes this a breeze now that our schema is set up. Open up your catalog context and make the following changes:
 
-
 ```diff
 + alias Hello.Catalog.Category
 
@@ -543,7 +541,7 @@ We added a `category_select` above our save button. Now let's try it out. Next,
 
 Now if we start the server with `mix phx.server` and visit [http://localhost:4000/products/new](http://localhost:4000/products/new), we'll see the new category multiple select input. Enter some valid product details, select a category or two, and click save.
 
-```
+```text
 Title: Elixir Flashcards
 Description: Flash card set for the Elixir programming language
 Price: 5.000000
@@ -882,7 +880,7 @@ The `link` function component from `Phoenix.LiveView.Helpers` accepts a `:method
 
 Let's try it out. Start your server with `mix phx.server` and visit a product page. If we try clicking the add to cart link, we'll be greeted by an error page with the following logs in the console:
 
-```
+```text
 [info] POST /cart_items
 [debug] Processing with HelloWeb.CartItemController.create/2
   Parameters: %{"_method" => "post", "product_id" => "1", ...}
@@ -987,7 +985,7 @@ Now that we can calculate price totals, let's try it out! Visit [`http://localho
 
 Our cart page is almost complete, but submitting the form will yield yet another error.
 
-```
+```text
 Request: POST /cart
 ** (exit) an exception was raised:
     ** (UndefinedFunctionError) function HelloWeb.CartController.update/2 is undefined or private
@@ -1197,7 +1195,6 @@ $ mix ecto.migrate
 
 Before we render information about our orders, we need to ensure our order data is fully populated and can be looked up by a current user. Open up your orders context in `lib/hello/orders.ex` and replace your `get_order!/1` function by a new `get_order!/2` definition:
 
-
 ```elixir
   def get_order!(user_uuid, id) do
     Order
diff --git a/guides/controllers.md b/guides/controllers.md
index 06969bc75..a30accf3a 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -274,7 +274,6 @@ Reloading [http://localhost:4000](http://localhost:4000) should show us a comple
 
 To be specific about the content type, we can use [`put_resp_content_type/2`] in conjunction with [`send_resp/3`].
 
-
 ```elixir
 def index(conn, _params) do
   conn
@@ -499,7 +498,6 @@ end
 
 Whenever the `with` conditions do not match, `HelloWeb.MyFallbackController` will receive the original `conn` as well as the result of the action and respond accordingly.
 
-
 [`/hello/Frank`]:  http://localhost:4000/hello/Frank
 [`assign/3`]: `Plug.Conn.assign/3`
 [`clear_flash/1`]: `Phoenix.Controller.clear_flash/1`
diff --git a/guides/deployment/fly.md b/guides/deployment/fly.md
index fe1174bde..cb14aada5 100644
--- a/guides/deployment/fly.md
+++ b/guides/deployment/fly.md
@@ -6,7 +6,7 @@ The only thing we'll need for this guide is a working Phoenix application. For t
 
 You can just:
 
-```
+```console
 $ mix phx.new my_app
 ```
 
@@ -208,18 +208,17 @@ Our next step is to add the `topologies` configuration to `config/runtime.exs`.
 
 This configures `libcluster` to use the `DNSPoll` strategy and look for other deployed apps using the `$FLY_APP_NAME` on the `.internal` private network.
 
-
 #### Controlling the name for our node
 
 We need to control the naming of our Elixir nodes. To help them connect up, we'll name them using this pattern: `your-fly-app-name@the.ipv6.address.on.fly`. To do this, we'll generate the release config.
 
-```
+```console
 $ mix release.init
 ```
 
 Then edit the generated `rel/env.sh.eex` file and add the following lines:
 
-```
+```console
 ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
 export RELEASE_DISTRIBUTION=name
 export RELEASE_NODE=$FLY_APP_NAME@$ip
@@ -227,7 +226,7 @@ export RELEASE_NODE=$FLY_APP_NAME@$ip
 
 After making the change, deploy your app!
 
-```
+```console
 $ fly deploy
 ```
 
diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md
index 751d48b14..990251386 100644
--- a/guides/deployment/heroku.md
+++ b/guides/deployment/heroku.md
@@ -95,7 +95,7 @@ https://mysterious-meadow-6277.herokuapp.com/ | https://git.heroku.com/mysteriou
 
 The buildpack uses a predefined Elixir and Erlang version, but to avoid surprises when deploying, it is best to explicitly list the Elixir and Erlang version we want in production to be the same we are using during development or in your continuous integration servers. This is done by creating a config file named `elixir_buildpack.config` in the root directory of your project with your target version of Elixir and Erlang:
 
-```
+```console
 # Elixir version
 elixir_version=1.14.0
 
@@ -110,7 +110,7 @@ hook_post_compile="eval mix assets.deploy && rm -f _build/esbuild*"
 
 Finally, let's tell the build pack how to start our webserver. Create a file named `Procfile` at the root of your project:
 
-```
+```console
 web: mix phx.server
 ```
 
@@ -139,7 +139,7 @@ When using this buildpack, you want to delegate all asset bundling to `npm`. So
 
 The Phoenix Static buildpack uses a predefined Node.js version, but to avoid surprises when deploying, it is best to explicitly list the Node.js version we want in production to be the same we are using during development or in your continuous integration servers. This is done by creating a config file named `phoenix_static_buildpack.config` in the root directory of your project with your target version of Node.js:
 
-```
+```text
 # Node.js version
 node_version=10.20.1
 ```
@@ -374,9 +374,11 @@ $ heroku run "POOL_SIZE=2 mix ecto.migrate"
 Heroku gives you the ability to connect to your dyno with an IEx shell which allows running Elixir code such as database queries.
 
 - Modify the `web` process in your Procfile to run a named node:
-  ```
+
+  ```text
   web: elixir --sname server -S mix phx.server
   ```
+
 - Redeploy to Heroku
 - Connect to the dyno with `heroku ps:exec` (if you have several applications on the same repository you will need to specify the app name or the remote name with `--app APP_NAME` or `--remote REMOTE_NAME`)
 - Launch an iex session with `iex --sname console --remsh server`
@@ -409,7 +411,7 @@ To https://git.heroku.com/mysterious-meadow-6277.git
 
 This has to do with stale dependencies which are not getting recompiled properly. It's possible to force Heroku to recompile all dependencies on each deploy, which should fix this problem. The way to do it is to add a new file called `elixir_buildpack.config` at the root of the application. The file should contain this line:
 
-```
+```text
 always_rebuild=true
 ```
 
diff --git a/guides/directory_structure.md b/guides/directory_structure.md
index d30f1242d..b1dde82cc 100644
--- a/guides/directory_structure.md
+++ b/guides/directory_structure.md
@@ -68,7 +68,6 @@ You can learn more about applications in [Elixir's official docs for Application
 
 The `lib/hello/mailer.ex` file holds the `Hello.Mailer` module, which defines the main interface to deliver e-mails:
 
-
 ```elixir
 defmodule Hello.Mailer do
   use Swoosh.Mailer, otp_app: :hello
diff --git a/guides/ecto.md b/guides/ecto.md
index 1c96cebd9..c2846bccc 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -307,7 +307,7 @@ iex> changeset = User.changeset(%User{}, params)
   errors: [],
   data: #Hello.User<>,
   valid?: true
-> 
+>
 ```
 
 Our new changeset is valid.
diff --git a/guides/howto/custom_error_pages.md b/guides/howto/custom_error_pages.md
index 0448942ee..6a583e0b3 100644
--- a/guides/howto/custom_error_pages.md
+++ b/guides/howto/custom_error_pages.md
@@ -149,6 +149,7 @@ However, implementing the `Plug.Exception` protocol by hand can be convenient in
 Exception actions are functions that can be triggered by the error page, and they're basically a list of maps defining a `label` and a `handler` to be executed.
 
 They are rendered in the error page as a collection of buttons and follow the format of:
+
 ```elixir
 [
   %{
diff --git a/guides/introduction/community.md b/guides/introduction/community.md
index fb5ef58cf..10fef641f 100644
--- a/guides/introduction/community.md
+++ b/guides/introduction/community.md
@@ -7,7 +7,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).
-  
+
   * Feel free to join and check out the #phoenix channel on [Discord](https://discord.gg/elixir).
 
   * Read about [bug reports](https://github.com/phoenixframework/phoenix/blob/master/CONTRIBUTING.md#bug-reports) or open an issue in the Phoenix [issue tracker](https://github.com/phoenixframework/phoenix/issues).
@@ -21,6 +21,7 @@ There are a number of places to connect with community members at all experience
   * The source for these guides is [on GitHub](https://github.com/phoenixframework/phoenix/tree/master/guides). To help improve the guides, please report an [issue](https://github.com/phoenixframework/phoenix/issues) or open a [pull request](https://github.com/phoenixframework/phoenix/pulls).
 
 ## Books
+
   * [Programming Phoenix LiveView - Interactive Elixir Web Programming Without Writing Any JavaScript - 2021 (by Bruce Tate and Sophie DeBenedetto)](https://pragprog.com/titles/liveview/programming-phoenix-liveview/)
 
   * [Real-Time Phoenix - Build Highly Scalable Systems with Channels (by Stephen Bussey - 2020)](https://pragprog.com/book/sbsockets/real-time-phoenix)
@@ -38,7 +39,7 @@ There are a number of places to connect with community members at all experience
   * [Phoenix LiveView Free Course (by The Pragmatic Studio - 2020)](https://pragmaticstudio.com/courses/phoenix-liveview)
 
   * [Groxio LiveView: Self Study Program (by Bruce Tate - 2020)](https://grox.io/language/liveview/course)
-  
+
   * [Alchemist Camp: Learn Elixir and Phoenix by building (2018-2022)](https://alchemist.camp/episodes)
 
   * [The Complete Elixir and Phoenix Bootcamp Master Functional Programming Techniques with Elixir and Phoenix while Learning to Build Compelling Web Applications (by Stephen Grider - 2017)](https://www.udemy.com/the-complete-elixir-and-phoenix-bootcamp-and-tutorial/)
diff --git a/guides/mix_tasks.md b/guides/mix_tasks.md
index 8f6020247..fce0afae9 100644
--- a/guides/mix_tasks.md
+++ b/guides/mix_tasks.md
@@ -362,6 +362,7 @@ If we don't specify a router for this task, it will default to the router Phoeni
 $ mix phx.routes
 GET  /  TaskTester.PageController.index/2
 ```
+
 We can also specify an individual router if we have more than one for our application.
 
 ```console
@@ -377,12 +378,14 @@ This is the task we use to get our application running. It takes no arguments at
 $ mix phx.server
 [info] Running TaskTesterWeb.Endpoint with Cowboy on port 4000 (http)
 ```
+
 It will silently ignore our `DoesNotExist` argument:
 
 ```console
 $ mix phx.server DoesNotExist
 [info] Running TaskTesterWeb.Endpoint with Cowboy on port 4000 (http)
 ```
+
 If we would like to start our application and also have an `IEx` session open to it, we can run the Mix task within `iex` like this, `iex -S mix phx.server`.
 
 ```console
diff --git a/guides/plug.md b/guides/plug.md
index 0278bfe40..0578969e2 100644
--- a/guides/plug.md
+++ b/guides/plug.md
@@ -13,6 +13,7 @@ At the simplest level, the Plug specification comes in two flavors: *function pl
 ## Function plugs
 
 In order to act as a plug, a function needs to:
+
 1. accept a connection struct (`%Plug.Conn{}`) as its first argument, and connection options as its second one;
 2. return a connection struct.
 
@@ -146,7 +147,7 @@ The default endpoint plugs do quite a lot of work. Here they are in order:
 
 - `Plug.Static` - serves static assets. Since this plug comes before the logger, requests for static assets are not logged.
 
-- `Phoenix.LiveDashboard.RequestLogger` - sets up the _Request Logger_ for Phoenix LiveDashboard, this will allow you to have the option to either pass a query parameter to stream requests logs or to enable/disable a cookie that streams requests logs from your dashboard.
+- `Phoenix.LiveDashboard.RequestLogger` - sets up the *Request Logger* for Phoenix LiveDashboard, this will allow you to have the option to either pass a query parameter to stream requests logs or to enable/disable a cookie that streams requests logs from your dashboard.
 
 - `Plug.RequestId` - generates a unique request ID for each request.
 
@@ -172,6 +173,7 @@ In the middle of the endpoint, there is also a conditional block:
 ```
 
 This block is only executed in development. It enables:
+
 * live reloading - if you change a CSS file, they are updated in-browser without refreshing the page;
 * [code reloading](`Phoenix.CodeReloader`) - so we can see changes to our application without restarting the server;
 * check repo status - which makes sure our database is up to date, raising a readable and actionable error otherwise.
@@ -303,7 +305,6 @@ At the end of the day, by replacing the nested blocks of code with a flattened s
 
 To learn more about plugs, see the documentation for the [Plug project](`Plug`), which provides many built-in plugs and functionalities.
 
-
 [`init/1`]: `c:Plug.init/1`
 [`call/2`]: `c:Plug.call/2`
 [`assign/3`]: `Plug.Conn.assign/3`
diff --git a/guides/real_time/channels.md b/guides/real_time/channels.md
index 714ec142f..6a1494ddb 100644
--- a/guides/real_time/channels.md
+++ b/guides/real_time/channels.md
@@ -145,6 +145,7 @@ Topics are string identifiers - names that the various layers use in order to ma
 ### Messages
 
 The `Phoenix.Socket.Message` module defines a struct with the following keys which denotes a valid message. From the [Phoenix.Socket.Message docs](https://hexdocs.pm/phoenix/Phoenix.Socket.Message.html).
+
 - `topic` - The string topic or `"topic:subtopic"` pair namespace, such as `"messages"` or `"messages:123"`
 - `event` - The string event name, for example `"phx_join"`
 - `payload` - The message payload
@@ -383,7 +384,7 @@ That's all there is to our basic chat app. Fire up multiple browser tabs and you
 
 When we connect, we'll often need to authenticate the client. Fortunately, this is a 4-step process with [Phoenix.Token](https://hexdocs.pm/phoenix/Phoenix.Token.html).
 
-**Step 1 - Assign a Token in the Connection**
+### Step 1 - Assign a Token in the Connection
 
 Let's say we have an authentication plug in our app called `OurAuth`. When `OurAuth` authenticates a user, it sets a value for the `:current_user` key in `conn.assigns`. Since the `current_user` exists, we can simply assign the user's token in the connection for use in the layout. We can wrap that behavior up in a private function plug, `put_user_token/2`. This could also be put in its own module as well. To make this all work, we just add `OurAuth` and `put_user_token/2` to the browser pipeline.
 
@@ -406,7 +407,7 @@ end
 
 Now our `conn.assigns` contains the `current_user` and `user_token`.
 
-**Step 2 - Pass the Token to the JavaScript**
+### Step 2 - Pass the Token to the JavaScript
 
 Next, we need to pass this token to JavaScript. We can do so inside a script tag in `web/templates/layout/app.html.heex` right above the app.js script, as follows:
 
@@ -415,7 +416,7 @@ Next, we need to pass this token to JavaScript. We can do so inside a script tag
 <script src={~p"/assets/app.js"}></script>
 ```
 
-**Step 3 - Pass the Token to the Socket Constructor and Verify**
+### Step 3 - Pass the Token to the Socket Constructor and Verify
 
 We also need to pass the `:params` to the socket constructor and verify the user token in the `connect/3` function. To do so, edit `web/channels/user_socket.ex`, as follows:
 
@@ -439,7 +440,7 @@ let socket = new Socket("/socket", {params: {token: window.userToken}})
 
 We used `Phoenix.Token.verify/4` to verify the user token provided by the client. `Phoenix.Token.verify/4` returns either `{:ok, user_id}` or `{:error, reason}`. We can pattern match on that return in a `case` statement. With a verified token, we set the user's id as the value to `:current_user` in the socket. Otherwise, we return `:error`.
 
-**Step 4 - Connect to the socket in JavaScript**
+### Step 4 - Connect to the socket in JavaScript
 
 With authentication set up, we can connect to sockets and channels from JavaScript.
 
diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md
index 55c2dbeb2..8aacd7f5c 100644
--- a/guides/request_lifecycle.md
+++ b/guides/request_lifecycle.md
@@ -10,11 +10,11 @@ Let's get on with our first new Phoenix page!
 
 When your browser accesses [http://localhost:4000/](http://localhost:4000/), it sends a HTTP request to whatever service is running on that address, in this case our Phoenix application. The HTTP request is made of a verb and a path. For example, the following browser requests translate into:
 
-| Browser address bar                | Verb | Path          |
-|:-----------------------------------|:-----|:--------------|
-| http://localhost:4000/             | GET  | /             |
-| http://localhost:4000/hello        | GET  | /hello        |
-| http://localhost:4000/hello/world  | GET  | /hello/world  |
+| Browser address bar                 | Verb | Path          |
+|:------------------------------------|:-----|:--------------|
+| <http://localhost:4000/>            | GET  | /             |
+| <http://localhost:4000/hello>       | GET  | /hello        |
+| <http://localhost:4000/hello/world> | GET  | /hello/world  |
 
 There are other HTTP verbs. For example, submitting a form typically uses the POST verb.
 
diff --git a/guides/routing.md b/guides/routing.md
index 90dc7ffe8..05d892c0b 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -119,7 +119,7 @@ resources "/posts", PostController, only: [:index, :show]
 
 Running `mix phx.routes` shows that we now only have the routes to the index and show actions defined.
 
-```
+```console
 GET     /posts      HelloWeb.PostController :index
 GET     /posts/:id  HelloWeb.PostController :show
 ```
@@ -213,6 +213,7 @@ resources "/users", UserController do
   resources "/posts", PostController
 end
 ```
+
 When we run `mix phx.routes` now, in addition to the routes we saw for `users` above, we get the following set of routes:
 
 ```elixir
diff --git a/guides/telemetry.md b/guides/telemetry.md
index 585d8c244..f8f1c4731 100644
--- a/guides/telemetry.md
+++ b/guides/telemetry.md
@@ -449,7 +449,6 @@ Hopefully, this gives you some inspiration on how to use the `:tag_values`
 option. Just remember to keep this function fast since it is called on every
 event.
 
-
 ## Periodic measurements
 
 You might want to periodically measure key-value pairs within
@@ -530,7 +529,7 @@ their own (in alphabetical order, please):
 
 If you need custom metrics and instrumentation in your
 application, you can utilize the `:telemetry` package
-(https://hexdocs.pm/telemetry) just like your favorite
+(<https://hexdocs.pm/telemetry>) just like your favorite
 frameworks and libraries.
 
 Here is an example of a simple GenServer that emits telemetry
diff --git a/guides/views.md b/guides/views.md
index 49aebdae6..316fbf13e 100644
--- a/guides/views.md
+++ b/guides/views.md
@@ -388,7 +388,6 @@ end
 
 Phoenix has a view called `ErrorView` which lives in `lib/hello_web/views/error_view.ex`. The purpose of `ErrorView` is to handle errors in a general way, from one centralized location.  Similar to the views we built in this guide, error views can return both HTML and JSON responses. See the [Custom Error Pages How-To](custom_error_pages.html) for more information.
 
-
 [welcome page]: http://localhost:4000
 [`render/3`]: `Phoenix.View.render/3`
 [`render_many/3`]: `Phoenix.View.render_many/3`
diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs
index 5cb8832aa..db8a2f09f 100644
--- a/installer/templates/phx_single/config/config.exs
+++ b/installer/templates/phx_single/config/config.exs
@@ -40,7 +40,7 @@ config :esbuild,
 
 # Configure tailwind (the version is required)
 config :tailwind,
-  version: "3.1.0",
+  version: "3.1.8",
   default: [
     args: ~w(
       --config=tailwind.config.js
diff --git a/installer/templates/phx_web/components.ex b/installer/templates/phx_web/components.ex
deleted file mode 120000
index c0ebe39a1..000000000
--- a/installer/templates/phx_web/components.ex
+++ /dev/null
@@ -1,1 +0,0 @@
-../../../priv/templates/phx.gen.live/components.ex
\ No newline at end of file
diff --git a/installer/templates/phx_web/components.ex b/installer/templates/phx_web/components.ex
new file mode 100644
index 000000000..31138930b
--- /dev/null
+++ b/installer/templates/phx_web/components.ex
@@ -0,0 +1,607 @@
+defmodule <%= @web_namespace %>.Components do
+  @moduledoc """
+  Provides core UI components.
+
+  The components in this module use Tailwind CSS, a utility-first CSS framework.
+  See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to
+  customize the generated components in this module.
+
+  Icons are provided by [heroicons](https://heroicons.com), using the
+  [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
+  """
+  use Phoenix.Component
+
+  <%= if @gettext do %>import <%= @web_namespace %>.Gettext, warn: false
+  <% end %>
+  alias Phoenix.LiveView.JS
+
+  @doc """
+  Renders a modal.
+
+  ## Examples
+
+      <.modal id="confirm-modal">
+        Are you sure?
+        <:confirm>OK</:confirm>
+        <:cancel>Cancel</:cancel>
+      </.modal>
+
+  JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
+  for the caller to reactor to each button press, for example:
+
+      <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
+        Are you sure you?
+        <:confirm>OK</:confirm>
+        <:cancel>Cancel</:cancel>
+      </.modal>
+  """
+  attr :id, :string, required: true
+  attr :show, :boolean, default: false
+  attr :on_cancel, JS, default: %JS{}
+  attr :on_confirm, JS, default: %JS{}
+
+  slot :inner_block, required: true
+  slot :title
+  slot :subtitle
+  slot :confirm
+  slot :cancel
+
+  def modal(assigns) do
+    ~H"""
+    <div id={@id} phx-mounted={@show && show_modal(@id)} class="relative z-50 hidden">
+      <div id={"#{@id}-bg"} class="fixed inset-0 bg-zinc-50/90 transition-opacity" aria-hidden="true" />
+      <div
+        class="fixed inset-0 overflow-y-auto"
+        aria-labelledby={"#{@id}-title"}
+        aria-describedby={"#{@id}-description"}
+        role="dialog"
+        aria-modal="true"
+        tabindex="0"
+      >
+        <div class="flex min-h-full items-center justify-center">
+          <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
+            <.focus_wrap
+              id={"#{@id}-container"}
+              phx-mounted={@show && show_modal(@id)}
+              phx-window-keydown={hide_modal(@on_cancel, @id)}
+              phx-key="escape"
+              phx-click-away={hide_modal(@on_cancel, @id)}
+              class="hidden relative rounded-2xl bg-white p-14 shadow-lg shadow-zinc-700/10 ring-1 ring-zinc-700/10 transition"
+            >
+              <div class="absolute top-6 right-5">
+                <button
+                  phx-click={hide_modal(@on_cancel, @id)}
+                  type="button"
+                  class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
+                  aria-label="Close"
+                >
+                  <Heroicons.x_mark solid class="h-5 w-5 stroke-current" />
+                </button>
+              </div>
+              <div id={"#{@id}-content"}>
+                <header :if={@title != []}>
+                  <h1 id={"#{@id}-title"} class="text-lg font-semibold leading-8 text-zinc-800">
+                    <%%= render_slot(@title) %>
+                  </h1>
+                  <p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
+                    <%%= render_slot(@subtitle) %>
+                  </p>
+                </header>
+                <%%= render_slot(@inner_block) %>
+                <div :if={@confirm != [] or @cancel != []} class="ml-6 mb-4 flex items-center gap-5">
+                  <.button
+                    :for={confirm <- @confirm}
+                    id={"#{@id}-confirm"}
+                    phx-click={@on_confirm}
+                    phx-disable-with
+                    class="py-2 px-3"
+                  >
+                    <%%= render_slot(confirm) %>
+                  </.button>
+                  <.link
+                    :for={cancel <- @cancel}
+                    phx-click={hide_modal(@on_cancel, @id)}
+                    class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
+                  >
+                    <%%= render_slot(cancel) %>
+                  </.link>
+                </div>
+              </div>
+            </.focus_wrap>
+          </div>
+        </div>
+      </div>
+    </div>
+    """
+  end
+
+  @doc """
+  Renders flash notices.
+
+  ## Examples
+
+      <.flash kind={:info} flash={@flash} />
+      <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
+  """
+  attr :id, :string, default: "flash", doc: "the optional id of flash container"
+  attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
+  attr :title, :string, default: nil
+  attr :kind, :atom, values: [:info, :error], doc: "Used for styling and flash lookup"
+  attr :autoshow, :boolean, default: true, doc: "whether to auto show the flash on mount"
+  attr :close, :boolean, default: true, doc: "whether the flash can be closed"
+  attr :rest, :global, doc: "the aribtrary HTML attributes to add to the flash container"
+
+  slot :inner_block, doc: "the optional inner block that renders the flash message"
+
+  def flash(assigns) do
+    ~H"""
+    <div
+      :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
+      id={@id}
+      phx-mounted={@autoshow && show("##{@id}")}
+      phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("#flash")}
+      role="alert"
+      class={[
+        "fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
+        @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
+        @kind == :error && "bg-rose-50 p-3 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
+      ]}
+      {@rest}
+    >
+      <p :if={@title} class="flex items-center gap-1.5 text-[0.8125rem] font-semibold leading-6">
+        <Heroicons.information_circle :if={@kind == :info} mini class="h-4 w-4" />
+        <Heroicons.exclamation_circle :if={@kind == :error} mini class="h-4 w-4" />
+        <%%= @title %>
+      </p>
+      <p class="mt-2 text-[0.8125rem] leading-5"><%%= msg %></p>
+      <button :if={@close} type="button" class="group absolute top-2 right-1 p-2" aria-label="Close">
+        <Heroicons.x_mark solid class="h-5 w-5 stroke-current opacity-40 group-hover:opacity-70" />
+      </button>
+    </div>
+    """
+  end
+
+  @doc """
+  Renders a simple form.
+
+  ## Examples
+
+      <.simple_form :let={f} for={:user} phx-change="validate" phx-submit="save">
+        <.input field={{f, :email}} label="Email"/>
+        <.input field={{f, :username}} label="Username" />
+        <:actions>
+          <.button>Save</.button>
+        </:actions>
+      </.simple_form>
+  """
+  attr :for, :any, default: nil, doc: "the datastructure for the form"
+  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),
+    doc: "the arbitrary HTML attributes to apply to the form tag"
+
+  slot :inner_block, required: true
+  slot :actions, doc: "the slot for form actions, such as a submit button"
+
+  def simple_form(assigns) do
+    ~H"""
+    <.form :let={f} for={@for} as={@as} {@rest}>
+      <div class="space-y-8 bg-white mt-10">
+        <%%= render_slot(@inner_block, f) %>
+        <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
+          <%%= render_slot(action, f) %>
+        </div>
+      </div>
+    </.form>
+    """
+  end
+
+  @doc """
+  Renders a button.
+
+  ## Examples
+
+      <.button>Send!</.button>
+      <.button phx-click="go" class="ml-2">Send!</.button>
+  """
+  attr :type, :string, default: nil
+  attr :class, :string, default: nil
+  attr :rest, :global, include: ~w(disabled form name value)
+
+  slot :inner_block, required: true
+
+  def button(assigns) do
+    ~H"""
+    <button
+      type={@type}
+      class={[
+        "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
+        "text-sm font-semibold leading-6 text-white active:text-white/80",
+        @class
+      ]}
+      {@rest}
+    >
+      <%%= render_slot(@inner_block) %>
+    </button>
+    """
+  end
+
+  @doc """
+  Renders an input with label and error messages.
+
+  A `%Phoenix.HTML.Form{}` and field name may be passed to the input
+  to build input names and error messages, or all the attributes and
+  errors may be passed explicitly.
+
+  ## Examples
+
+      <.input field={{f, :email}} type="email" />
+      <.input name="my-input" errors={["oh no!"]} />
+  """
+  attr :id, :any
+  attr :name, :any
+  attr :label, :string, default: nil
+
+  attr :type, :string,
+    default: "text",
+    values: ~w(checkbox color date datetime-local email file hidden month number password
+               range radio search tel text time url week)
+
+  attr :value, :any
+  attr :field, :any, doc: "a %Phoenix.HTML.Form{}/field name tuple, for example: {f, :email}"
+  attr :errors, :list
+  attr :rest, :global, include: ~w(autocomplete checked disabled form max maxlength min minlength
+                                   multiple pattern placeholder readonly required size step)
+  slot :inner_block
+  slot :option, doc: "the slot for select input options"
+
+  def input(%{field: {f, field}} = assigns) do
+    assigns
+    |> assign(field: nil)
+    |> assign_new(:name, fn -> Phoenix.HTML.Form.input_name(f, field) end)
+    |> assign_new(:id, fn -> Phoenix.HTML.Form.input_id(f, field) end)
+    |> assign_new(:value, fn -> Phoenix.HTML.Form.input_value(f, field) end)
+    |> assign_new(:errors, fn -> translate_errors(f.errors || [], field) end)
+    |> input()
+  end
+
+  def input(%{type: "checkbox"} = assigns) do
+    ~H"""
+    <label phx-feedback-for={@name} class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
+      <input
+        type="checkbox"
+        id={@id || @name}
+        name={@name}
+        class="rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
+      />
+      <%%= @label %>
+    </label>
+    """
+  end
+
+  def input(%{type: "select"} = assigns) do
+    ~H"""
+    <div phx-feedback-for={@name}>
+      <.label for={@id}><%%= @label %></.label>
+      <select
+        id={@id}
+        name={@name}
+        class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-zinc-500 focus:border-zinc-500 sm:text-sm"
+        {@rest}
+      >
+        <option :for={opt <- @option} {assigns_to_attributes(opt)}><%%= render_slot(opt) %></option>
+      </select>
+      <.error :for={msg <- @errors} message={msg} />
+    </div>
+    """
+  end
+
+  def input(%{type: "textarea"} = assigns) do
+    ~H"""
+    <div phx-feedback-for={@name}>
+      <.label for={@id}><%%= @label %></.label>
+      <textarea
+        id={@id || @name}
+        name={@name}
+        class={[
+          input_border(@errors),
+          "mt-2 block min-h-[6rem] w-full rounded-lg border-zinc-300 py-[calc(theme(spacing.2)-1px)] px-[calc(theme(spacing.3)-1px)]",
+          "text-zinc-900 focus:border-zinc-400 focus:outline-none focus:ring-4 focus:ring-zinc-800/5 sm:text-sm sm:leading-6",
+          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5"
+        ]}
+        {@rest}
+      ><%%= @value %></textarea>
+      <.error :for={msg <- @errors} message={msg} />
+    </div>
+    """
+  end
+
+  def input(assigns) do
+    ~H"""
+    <div phx-feedback-for={@name}>
+      <.label for={@id}><%%= @label %></.label>
+      <input
+        type={@type}
+        name={@name}
+        id={@id || @name}
+        value={@value}
+        class={[
+          input_border(@errors),
+          "mt-2 block w-full rounded-lg border-zinc-300 py-[calc(theme(spacing.2)-1px)] px-[calc(theme(spacing.3)-1px)]",
+          "text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
+          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5"
+        ]}
+        {@rest}
+      />
+      <.error :for={msg <- @errors} message={msg} />
+    </div>
+    """
+  end
+
+  defp input_border([] = _errors),
+    do: "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5"
+
+  defp input_border([_ | _] = _errors),
+    do: "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
+
+  @doc """
+  Renders a label.
+  """
+  attr :for, :string, default: nil
+  slot :inner_block, required: true
+
+  def label(assigns) do
+    ~H"""
+    <label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
+      <%%= render_slot(@inner_block) %>
+    </label>
+    """
+  end
+
+  @doc """
+  Generates a generic error message.
+  """
+  attr :message, :string, required: true
+
+  def error(assigns) do
+    ~H"""
+    <p class="phx-no-feedback:hidden mt-3 flex gap-3 text-sm leading-6 text-rose-600">
+      <Heroicons.exclamation_circle mini class="mt-0.5 h-5 w-5 flex-none fill-rose-500" />
+      <%%= @message %>
+    </p>
+    """
+  end
+
+  @doc """
+  Renders a header with title.
+  """
+  attr :class, :string, default: nil
+
+  slot :inner_block, required: true
+  slot :subtitle
+  slot :actions
+
+  def header(assigns) do
+    ~H"""
+    <header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
+      <div>
+        <h1 class="text-lg font-semibold leading-8 text-zinc-800">
+          <%%= render_slot(@inner_block) %>
+        </h1>
+        <p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
+          <%%= render_slot(@subtitle) %>
+        </p>
+      </div>
+      <div class="flex-none"><%%= render_slot(@actions) %></div>
+    </header>
+    """
+  end
+
+  @doc ~S"""
+  Renders a table with generic styling.
+
+  ## Examples
+
+      <.table rows={@users}>
+        <:col :let={user} label="id"><%%= user.id %></:col>
+        <:col :let={user} label="username"><%%= user.username %></:col>
+      </.table>
+  """
+  attr :id, :string, required: true
+  attr :row_click, JS, default: nil
+  attr :rows, :list, required: true
+
+  slot :col, required: true do
+    attr :label, :string
+  end
+
+  slot :action, doc: "the slot for showing user actions in the last table column"
+
+  def table(assigns) do
+    ~H"""
+    <div id={@id} class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
+      <table class="mt-11 w-[40rem] sm:w-full">
+        <thead class="text-left text-[0.8125rem] leading-6 text-zinc-500">
+          <tr>
+            <th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%%= col[:label] %></th>
+            <th class="relative p-0 pb-4"><span class="sr-only">Actions</span></th>
+          </tr>
+        </thead>
+        <tbody class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700">
+          <tr
+            :for={row <- @rows}
+            id={"#{@id}-#{Phoenix.Param.to_param(row)}"}
+            class="group hover:bg-zinc-50"
+          >
+            <td
+              :for={{col, i} <- Enum.with_index(@col)}
+              phx-click={@row_click && @row_click.(row)}
+              class={["relative p-0", @row_click && "hover:cursor-pointer"]}
+            >
+              <div class="block py-4 pr-6">
+                <span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
+                <span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
+                  <%%= render_slot(col, row) %>
+                </span>
+              </div>
+            </td>
+            <td :if={@action != []} class="relative p-0 w-14">
+              <div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
+                <span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
+                <span
+                  :for={action <- @action}
+                  class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
+                >
+                  <%%= render_slot(action, row) %>
+                </span>
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    """
+  end
+
+  @doc """
+  Renders a data list.
+
+  ## Examples
+
+      <.list>
+        <:item title="Title"><%%= @post.title %></:item>
+        <:item title="Views"><%%= @post.views %></:item>
+      </.list>
+  """
+  slot :item, required: true do
+    attr :title, :string, required: true
+  end
+
+  def list(assigns) do
+    ~H"""
+    <div class="mt-14">
+      <dl class="-my-4 divide-y divide-zinc-100">
+        <div :for={item <- @item} class="flex gap-4 py-4 sm:gap-8">
+          <dt class="w-1/4 flex-none text-[0.8125rem] leading-6 text-zinc-500"><%%= item.title %></dt>
+          <dd class="text-sm leading-6 text-zinc-700"><%%= render_slot(item) %></dd>
+        </div>
+      </dl>
+    </div>
+    """
+  end
+
+  @doc """
+  Renders a back navigation link.
+
+  ## Examples
+
+      <.back navigate={~p"/posts"}>Back to posts</.back>
+  """
+  attr :navigate, :any, required: true
+  slot :inner_block, required: true
+
+  def back(assigns) do
+    ~H"""
+    <div class="mt-16">
+      <.link
+        navigate={@navigate}
+        class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
+      >
+        <Heroicons.arrow_left solid class="w-3 h-3 stroke-current inline" />
+        <%%= render_slot(@inner_block) %>
+      </.link>
+    </div>
+    """
+  end
+
+  ## JS Commands
+
+  def show(js \\ %JS{}, selector) do
+    JS.show(js,
+      to: selector,
+      transition:
+        {"transition-all transform ease-out duration-300",
+         "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
+         "opacity-100 translate-y-0 sm:scale-100"}
+    )
+  end
+
+  def hide(js \\ %JS{}, selector) do
+    JS.hide(js,
+      to: selector,
+      time: 200,
+      transition:
+        {"transition-all transform ease-in duration-200",
+         "opacity-100 translate-y-0 sm:scale-100",
+         "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
+    )
+  end
+
+  def show_modal(js \\ %JS{}, id) when is_binary(id) do
+    js
+    |> JS.show(to: "##{id}")
+    |> JS.show(
+      to: "##{id}-bg",
+      transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
+    )
+    |> show("##{id}-container")
+    |> JS.focus_first(to: "##{id}-content")
+  end
+
+  def hide_modal(js \\ %JS{}, id) do
+    js
+    |> JS.hide(
+      to: "##{id}-bg",
+      transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
+    )
+    |> hide("##{id}-container")
+    |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
+    |> JS.pop_focus()
+  end<%= if @gettext do %>
+
+  @doc """
+  Translates an error message using gettext.
+  """
+  def translate_error({msg, opts}) do
+    # When using gettext, we typically pass the strings we want
+    # to translate as a static argument:
+    #
+    #     # Translate "is invalid" in the "errors" domain
+    #     dgettext("errors", "is invalid")
+    #
+    #     # Translate the number of files with plural rules
+    #     dngettext("errors", "1 file", "%{count} files", count)
+    #
+    # Because the error messages we show in our forms and APIs
+    # are defined inside Ecto, we need to translate them dynamically.
+    # This requires us to call the Gettext module passing our gettext
+    # backend as first argument.
+    #
+    # Note we use the "errors" domain, which means translations
+    # should be written to the errors.po file. The :count option is
+    # set by Ecto and indicates we should also apply plural rules.
+    if count = opts[:count] do
+      Gettext.dngettext(<%= @web_namespace %>.Gettext, "errors", msg, msg, count, opts)
+    else
+      Gettext.dgettext(<%= @web_namespace %>.Gettext, "errors", msg, opts)
+    end
+  end<% else %>
+
+  @doc """
+  Translates an error message.
+  """
+  def translate_error({msg, opts}) do
+    # Because the error messages we show in our forms and APIs
+    # are defined inside Ecto, we need to translate them dynamically.
+    Enum.reduce(opts, msg, fn {key, value}, acc ->
+      String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
+    end)
+  end<% end %>
+
+  @doc """
+  Translates the errors for a field from a keyword list of errors.
+  """
+  def translate_errors(errors, field) when is_list(errors) do
+    for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
+  end
+end
diff --git a/lib/phoenix.ex b/lib/phoenix.ex
index 5038f8ba3..112ef52af 100644
--- a/lib/phoenix.ex
+++ b/lib/phoenix.ex
@@ -26,12 +26,8 @@ defmodule Phoenix do
     * [Phoenix HTML](https://hexdocs.pm/phoenix_html) - conveniences for
       working with HTML in Phoenix
 
-    * [Phoenix View](https://hexdocs.pm/phoenix_view) - a set of functions
-      for building `Phoenix.View` and working with template languages such
-      as Elixir's own `EEx`
-
     * [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view) - rich,
-      real-time user experiences with server-rendered HTML
+      real-time user experiences with server-rendered HTML and components
 
     * [Phoenix LiveDashboard](https://hexdocs.pm/phoenix_live_dashboard) -
       real-time performance monitoring and debugging tools for Phoenix
@@ -41,7 +37,7 @@ defmodule Phoenix do
       for composable modules in between web applications
 
     * [Swoosh](https://hexdocs.pm/swoosh) - a library for composing,
-    delivering and testing emails, also used by `mix phx.gen.auth`
+      delivering and testing emails, also used by `mix phx.gen.auth`
 
     * [Telemetry Metrics](https://hexdocs.pm/telemetry_metrics) - common
       interface for defining metrics based on Telemetry events
diff --git a/lib/phoenix/endpoint/cowboy2_handler.ex b/lib/phoenix/endpoint/cowboy2_handler.ex
index cbedb17a8..727f944f2 100644
--- a/lib/phoenix/endpoint/cowboy2_handler.ex
+++ b/lib/phoenix/endpoint/cowboy2_handler.ex
@@ -24,25 +24,16 @@ defmodule Phoenix.Endpoint.Cowboy2Handler do
               cowboy_opts =
                 opts
                 |> Enum.flat_map(fn
-                  {:timeout, timeout} ->
-                    [idle_timeout: timeout]
-
-                  {:compress, _} = opt ->
-                    [opt]
-
-                  {:max_frame_size, _} = opt ->
-                    [opt]
-
-                  {:fullsweep_after, value} ->
-                    :erlang.process_flag(:fullsweep_after, value)
-                    []
-
-                  _other ->
-                    []
+                  {:timeout, timeout} -> [idle_timeout: timeout]
+                  {:compress, _} = opt -> [opt]
+                  {:max_frame_size, _} = opt -> [opt]
+                  _other -> []
                 end)
                 |> Map.new()
 
-              {:cowboy_websocket, copy_resp_headers(conn, req), [handler | state], cowboy_opts}
+              handler_opts = Keyword.take(opts, [:fullsweep_after])
+              triplet = {handler, handler_opts, state}
+              {:cowboy_websocket, copy_resp_headers(conn, req), triplet, cowboy_opts}
 
             {:error, %Plug.Conn{adapter: {@connection, req}} = conn} ->
               {:ok, copy_resp_headers(conn, req), {handler, opts}}
@@ -136,7 +127,11 @@ defmodule Phoenix.Endpoint.Cowboy2Handler do
 
   ## Websocket callbacks
 
-  def websocket_init([handler | state]) do
+  def websocket_init({handler, process_flags, state}) do
+    for {key, value} <- process_flags do
+      :erlang.process_flag(key, value)
+    end
+
     {:ok, state} = handler.init(state)
     {:ok, [handler | state]}
   end
diff --git a/lib/phoenix/router/helpers.ex b/lib/phoenix/router/helpers.ex
index 97bd2cec9..f7a8f6c4f 100644
--- a/lib/phoenix/router/helpers.ex
+++ b/lib/phoenix/router/helpers.ex
@@ -278,7 +278,7 @@ defmodule Phoenix.Router.Helpers do
       |> Enum.map(fn {_, bindings} -> length(bindings) end)
       |> Enum.uniq()
 
-    # Each helper defines catch alls like this:
+    # Each helper defines catch all like this:
     #
     #     def helper_path(context, action, ...binding)
     #     def helper_path(context, action, ...binding, params)
@@ -299,7 +299,7 @@ defmodule Phoenix.Router.Helpers do
   end
 
   @doc """
-  Callback for generate router catch alls.
+  Callback for generate router catch all.
   """
   def raise_route_error(mod, fun, arity, action, routes, params) do
     cond do
diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex
index 9af3117ba..65d2138c0 100644
--- a/lib/phoenix/verified_routes.ex
+++ b/lib/phoenix/verified_routes.ex
@@ -188,7 +188,7 @@ defmodule Phoenix.VerifiedRoutes do
       redirect(to: ~p"/users/#{@user}")
 
       ~H"""
-      <.link to={~p"/users??page=#{@page}"}>profile</.link>
+      <.link to={~p"/users?page=#{@page}"}>profile</.link>
 
       <.link to={~p"/users?#{@params}"}>profile</.link>
       """
@@ -642,7 +642,7 @@ defmodule Phoenix.VerifiedRoutes do
   end
 
   @doc false
-  def __encode_query__(dict) when is_list(dict) or is_map(dict) do
+  def __encode_query__(dict) when is_list(dict) or (is_map(dict) and not is_struct(dict)) do
     case Plug.Conn.Query.encode(dict, &to_param/1) do
       "" -> ""
       query_str -> query_str
diff --git a/priv/templates/phx.gen.auth/confirmation_edit.html.heex b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
index ef263479e..9cd20070e 100644
--- a/priv/templates/phx.gen.auth/confirmation_edit.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
@@ -1,6 +1,6 @@
 <.header>Confirm account</.header>
 
-<.simple_form :let={_f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirmation/#{@token}"}>
+<.simple_form :let={_f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm/#{@token}"}>
   <:actions>
     <.button>Confirm my account</.button>
   </:actions>
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index 544d9d3df..f9924f2db 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -32,7 +32,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       {:ok, _} ->
         {:noreply,
          socket
-         |> put_flash(:info, "User confirmed successfully.")
+         |> put_flash(:info, "<%= inspect schema.alias %> confirmed successfully.")
          |> redirect(to: ~p"/")}
 
       :error ->
@@ -47,7 +47,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           %{} ->
             {:noreply,
              socket
-             |> put_flash(:error, "User confirmation link is invalid or it has expired.")
+             |> put_flash(:error, "<%= inspect schema.alias %> confirmation link is invalid or it has expired.")
              |> redirect(to: ~p"/")}
         end
     end
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index 76c559127..605f7272b 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -18,9 +18,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       <.simple_form
         :let={f}
         id="login_form"
-        for={:user}
+        for={:<%= schema.singular %>}
         action={~p"<%= schema.route_prefix %>/log_in"}
-        as={:user}
+        as={:<%= schema.singular %>}
         phx-update="ignore"
       >
         <.input field={{f, :email}} type="email" label="Email" required />
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index b23e4e103..423d00511 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -27,7 +27,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         phx-trigger-action={@trigger_submit}
         action={~p"<%= schema.route_prefix %>/log_in?_action=registered"}
         method="post"
-        as={:user}
+        as={:<%= schema.singular %>}
       >
         <%%= if @changeset.action == :insert do %>
           <.error message="Oops, something went wrong! Please check the errors below." />
diff --git a/priv/templates/phx.gen.live/components.ex b/priv/templates/phx.gen.live/components.ex
index 25e7dca99..056b3edd0 100644
--- a/priv/templates/phx.gen.live/components.ex
+++ b/priv/templates/phx.gen.live/components.ex
@@ -24,7 +24,7 @@ defmodule <%= @web_namespace %>.Components do
         Are you sure?
         <:confirm>OK</:confirm>
         <:cancel>Cancel</:cancel>
-      <.modal>
+      </.modal>
 
   JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
   for the caller to reactor to each button press, for example:
@@ -32,8 +32,8 @@ defmodule <%= @web_namespace %>.Components do
       <.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
         Are you sure you?
         <:confirm>OK</:confirm>
-        <:cancel>Cancel</:confirm>
-      <.modal>
+        <:cancel>Cancel</:cancel>
+      </.modal>
   """
   attr :id, :string, required: true
   attr :show, :boolean, default: false
@@ -128,7 +128,7 @@ defmodule <%= @web_namespace %>.Components do
   attr :title, :string, default: nil
   attr :rest, :global
   attr :kind, :atom, doc: "one of :info, :error used for styling and flash lookup"
-  attr :autoshow, :boolean, default: true, doc: "wether to auto show the flash on mount"
+  attr :autoshow, :boolean, default: true, doc: "whether to auto show the flash on mount"
   attr :close, :boolean, default: true, doc: "whether the flash can be closed"
 
   slot :inner_block, doc: "the optional inner block that renders the flash message"
@@ -140,6 +140,7 @@ defmodule <%= @web_namespace %>.Components do
       id={@id}
       phx-mounted={@autoshow && show("##{@id}")}
       phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("#flash")}
+      role="alert"
       class={[
         "fixed hidden top-2 right-2 w-96 z-50 rounded-lg p-3 shadow-md shadow-zinc-900/5 ring-1",
         @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
@@ -147,15 +148,15 @@ defmodule <%= @web_namespace %>.Components do
       ]}
       {@rest}
     >
-      <button :if={@close} type="button" class="group absolute top-2 right-1 p-2" aria-label="Close">
-        <Heroicons.x_mark solid class="h-5 w-5 stroke-current opacity-40 group-hover:opacity-70" />
-      </button>
       <p :if={@title} class="flex items-center gap-1.5 text-[0.8125rem] font-semibold leading-6">
         <Heroicons.information_circle :if={@kind == :info} mini class="h-4 w-4" />
         <Heroicons.exclamation_circle :if={@kind == :error} mini class="h-4 w-4" />
         <%%= @title %>
       </p>
       <p class="mt-2 text-[0.8125rem] leading-5"><%%= msg %></p>
+      <button :if={@close} type="button" class="group absolute top-2 right-1 p-2" aria-label="Close">
+        <Heroicons.x_mark solid class="h-5 w-5 stroke-current opacity-40 group-hover:opacity-70" />
+      </button>
     </div>
     """
   end
@@ -170,12 +171,12 @@ defmodule <%= @web_namespace %>.Components do
         <.input field={{f, :username}} label="Username" />
         <:actions>
           <.button>Save</.button>
-        <:actions>
+        </:actions>
       </.simple_form>
   """
   attr :for, :any, default: nil, doc: "the datastructure for the form"
   attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
-  attr :rest, :global, doc: "the arbitraty HTML attributes to apply to the form tag"
+  attr :rest, :global, doc: "the arbitrary HTML attributes to apply to the form tag"
 
   slot :inner_block, required: true
   slot :actions, doc: "the slot for form actions, such as a submit button"
@@ -203,7 +204,7 @@ defmodule <%= @web_namespace %>.Components do
   """
   attr :type, :string, default: nil
   attr :class, :string, default: nil
-  attr :rest, :global, doc: "the arbitraty HTML attributes to apply to the button tag"
+  attr :rest, :global, doc: "the arbitrary HTML attributes to apply to the button tag"
 
   slot :inner_block, required: true
 
@@ -542,7 +543,7 @@ defmodule <%= @web_namespace %>.Components do
       transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
     )
     |> show("##{id}-container")
-    |> JS.focus_first(to: "##{id}-container")
+    |> JS.focus_first(to: "##{id}-content")
   end
 
   def hide_modal(js \\ %JS{}, id) do


Test patch:
diff --git a/guides/testing/testing.md b/guides/testing/testing.md
index f10464a3f..59405afcc 100644
--- a/guides/testing/testing.md
+++ b/guides/testing/testing.md
@@ -116,7 +116,6 @@ end
 
 There is a lot to unpack here.
 
-
 The second line says this is a case template. This is a ExUnit feature that allows developers to replace the built-in `use ExUnit.Case` by their own case. This line is pretty much what allows us to write `use HelloWeb.ConnCase` at the top of our controller tests.
 
 Now that we have made this module a case template, we can define callbacks that are invoked on certain occasions. The `using` callback defines code to be injected on every module that calls `use HelloWeb.ConnCase`. In this case, it starts by setting the `@endpoint` module attribute with the name of our endpoint.
@@ -493,10 +492,12 @@ database: "hello_test#{System.get_env("MIX_TEST_PARTITION")}",
 
 By default, the `MIX_TEST_PARTITION` environment variable has no value, and therefore it has no effect. But in your CI server, you can, for example, split your test suite across machines by using four distinct commands:
 
-    $ MIX_TEST_PARTITION=1 mix test --partitions 4
-    $ MIX_TEST_PARTITION=2 mix test --partitions 4
-    $ MIX_TEST_PARTITION=3 mix test --partitions 4
-    $ MIX_TEST_PARTITION=4 mix test --partitions 4
+```console
+$ MIX_TEST_PARTITION=1 mix test --partitions 4
+$ MIX_TEST_PARTITION=2 mix test --partitions 4
+$ MIX_TEST_PARTITION=3 mix test --partitions 4
+$ MIX_TEST_PARTITION=4 mix test --partitions 4
+```
 
 That's all you need to do and ExUnit and Phoenix will take care of all rest, including setting up the database for each distinct partition with a distinct name.
 
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index 40f7afbfb..483c79650 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -14,13 +14,18 @@ defmodule Mix.Tasks.Phx.NewTest do
     :ok
   end
 
-  test "assets are in sync with installer" do
+  test "assets are in sync with priv" do
     for file <- ~w(favicon.ico phoenix.png) do
       assert File.read!("../priv/static/#{file}") ==
         File.read!("templates/phx_static/#{file}")
     end
   end
 
+  test "components are in sync with priv" do
+    assert File.read!("../priv/templates/phx.gen.live/components.ex") ==
+            File.read!("templates/phx_web/components.ex")
+  end
+
   test "returns the version" do
     Mix.Tasks.Phx.New.run(["-v"])
     assert_received {:mix_shell, :info, ["Phoenix installer v" <> _]}
diff --git a/priv/templates/phx.gen.auth/confirmation_live_test.exs b/priv/templates/phx.gen.auth/confirmation_live_test.exs
index 75b907e14..22abeb500 100644
--- a/priv/templates/phx.gen.auth/confirmation_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_live_test.exs
@@ -33,7 +33,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert {:ok, conn} = result
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-        "User confirmed successfully"
+        "<%= inspect schema.alias %> confirmed successfully"
 
       assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
       refute get_session(conn, :<%= schema.singular %>_token)
@@ -50,7 +50,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert {:ok, conn} = result
       assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-        "User confirmation link is invalid or it has expired"
+        "<%= inspect schema.alias %> confirmation link is invalid or it has expired"
 
 
       # when logged in
@@ -81,7 +81,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         |> follow_redirect(conn, ~p"/")
 
       assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-        "User confirmation link is invalid or it has expired"
+        "<%= inspect schema.alias %> confirmation link is invalid or it has expired"
 
       refute <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
     end
diff --git a/test/phoenix/endpoint/supervisor_test.exs b/test/phoenix/endpoint/supervisor_test.exs
index 3d3c03432..ede199d1a 100644
--- a/test/phoenix/endpoint/supervisor_test.exs
+++ b/test/phoenix/endpoint/supervisor_test.exs
@@ -96,7 +96,7 @@ defmodule Phoenix.Endpoint.SupervisorTest do
              end)
     end
 
-    test "init/1 doesnt start watchers when `:server` config is false" do
+    test "init/1 doesn't start watchers when `:server` config is false" do
       Application.put_env(:phoenix, WatchersEndpoint, server: false, watchers: @watchers)
       {:ok, {_, children}} = Supervisor.init({:phoenix, WatchersEndpoint, []})
 
diff --git a/test/phoenix/router/pipeline_test.exs b/test/phoenix/router/pipeline_test.exs
index 2838c2b2c..c3fd75b4c 100644
--- a/test/phoenix/router/pipeline_test.exs
+++ b/test/phoenix/router/pipeline_test.exs
@@ -160,7 +160,7 @@ defmodule Phoenix.Router.PipelineTest do
   end
 
   test "pipeline raises on conflict" do
-    assert_raise ArgumentError, ~r{there is an import from Kernel with the same nam}, fn ->
+    assert_raise ArgumentError, ~r{there is an import from Kernel with the same name}, fn ->
       defmodule ConflictingPipeline do
         use Phoenix.Router, otp_app: :phoenix
         pipeline :raise do
diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs
index 4d10a41ac..b2d29e060 100644
--- a/test/phoenix/verified_routes_test.exs
+++ b/test/phoenix/verified_routes_test.exs
@@ -125,6 +125,10 @@ defmodule Phoenix.VerifiedRoutesTest do
 
     assert path(@endpoint, @router, ~p"/posts/bottom/#{dir}/#{id}?foo=bar") ==
              "/posts/bottom/asc/123?foo=bar"
+
+    # dynamic query params
+    assert ~p"/posts/1?other_post=#{id}" == "/posts/1?other_post=123"
+    assert ~p"/posts/1?other_post=#{struct}" == "/posts/1?other_post=post-123"
   end
 
   test "~p with dynamic string and static query params" do