Repository: phoenixframework/phoenix (elixir)

Issue:
Compilation error on apps generated with --no-html
<!--
### Precheck

* For general discussions and support, use Stack Overflow or the Elixir Forum
* For proposing and discussing new features, start a thread on the Phoenix Core mailing list
* For bugs, do a quick search and make sure the bug has not yet been reported
* Ensure that this issue is related to the Phoenix library and not one of the dependencies listed in mix.exs (Ecto, Plug, etc.)
* All checked? Be nice and have fun!
-->

### Environment

* Elixir version (elixir -v): 1.10.2
* Phoenix version (mix deps): master@9d7444f8
* NodeJS version (node -v): 10.19.0
* NPM version (npm -v): 6.13.4
* Operating system: Ubuntu 19.10

### Expected behavior

After running `mix phx.new app_with_no_html --no-html`, I should be able to run `mix test` and all of the tests should pass.

### Actual behavior

When I run `mix test` after generating a new project with `--no-html`, I get the following error.

```
$ mix test
Compiling 1 file (.ex)

== Compilation error in file lib/app_with_no_html_web/endpoint.ex ==
** (UndefinedFunctionError) function Phoenix.LiveDashboard.RequestLogger.init/1 is undefined (module Phoenix.LiveDashboard.RequestLogger is not available)
    Phoenix.LiveDashboard.RequestLogger.init([param_key: "request_logger", cookie_key: "request_logger"])
    (plug 1.10.0) lib/plug/builder.ex:304: Plug.Builder.init_module_plug/4
    (plug 1.10.0) lib/plug/builder.ex:288: anonymous fn/5 in Plug.Builder.compile/3
    (elixir 1.10.2) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
    (plug 1.10.0) lib/plug/builder.ex:286: Plug.Builder.compile/3
    (plug 1.10.0) expanding macro: Plug.Builder.__before_compile__/1
    lib/app_with_no_html_web/endpoint.ex:1: AppWithNoHtmlWeb.Endpoint (module)
```

The problem appears to be that the live dashboard code is being injected into `endpoint.ex` when its not injected elsewhere. If I comment out the following lines, in `endpoint.ex` I can get the tests to pass.

```elixir
  socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
```
and

```elixir
  plug Phoenix.LiveDashboard.RequestLogger,
    param_key: "request_logger",
    cookie_key: "request_logger"
```


Pull request description:
Fix dashboard installer, closes #3740, closes #3751
This corrects a number of issues introduced with the addition of :phoenix_live_dashboard.

- Adds `live_dashboard_path` to the `app.html.eex` dead view
- Conditions `live_dashboard` route on existence so the generated tests pass (#3740 // Thanks @aptinio! )
- Ensures the dashboard works even when `--no-html` is given (#3751 // Thanks @aaronrenner!)

When generated in a project without both html and live, we have to close the gap wrt the session. The router in such cases would look as follows:

```elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/api", DevAppWeb do
    pipe_through :api
  end

  # Enables LiveDashboard only for development
  #
  # If you want to use the LiveDashboard in production, you should put
  # it behind authentication and allow only admins to access it.
  # If your application does not have an admins-only section yet,
  # you can use Plug.BasicAuth to set up some basic authentication
  # as long as you are also using SSL (which you should anyway).
  if Mix.env() in [:dev, :test] do
    import Phoenix.LiveDashboard.Router

    scope "/" do
      pipe_through [:fetch_session, :protect_from_forgery]
      live_dashboard "/dashboard", metrics: MyAppWeb.Telemetry
    end
  end
end
```

Patch:
diff --git a/assets/package-lock.json b/assets/package-lock.json
index 6834eef02..101f71945 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -240,12 +240,6 @@
             "minimist": "^1.2.5"
           }
         },
-        "lodash": {
-          "version": "4.17.15",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-          "dev": true
-        },
         "minimist": {
           "version": "1.2.5",
           "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
@@ -329,12 +323,6 @@
             "lodash": "^4.17.13",
             "to-fast-properties": "^2.0.0"
           }
-        },
-        "lodash": {
-          "version": "4.17.15",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-          "dev": true
         }
       }
     },
@@ -377,12 +365,6 @@
             "lodash": "^4.17.13",
             "to-fast-properties": "^2.0.0"
           }
-        },
-        "lodash": {
-          "version": "4.17.15",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-          "dev": true
         }
       }
     },
@@ -568,12 +550,6 @@
           "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
           "dev": true
         },
-        "lodash": {
-          "version": "4.17.15",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-          "dev": true
-        },
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -882,12 +858,6 @@
           "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
           "dev": true
         },
-        "lodash": {
-          "version": "4.17.15",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-          "dev": true
-        },
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -1220,12 +1190,6 @@
           "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
           "dev": true
         },
-        "lodash": {
-          "version": "4.17.15",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-          "dev": true
-        },
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -2701,7 +2665,7 @@
       "dependencies": {
         "acorn": {
           "version": "2.7.0",
-          "resolved": "http://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz",
           "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=",
           "dev": true
         }
@@ -4125,14 +4089,6 @@
             "jsesc": "^2.5.1",
             "lodash": "^4.17.13",
             "source-map": "^0.5.0"
-          },
-          "dependencies": {
-            "lodash": {
-              "version": "4.17.15",
-              "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-              "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-              "dev": true
-            }
           }
         },
         "@babel/helper-function-name": {
@@ -4222,12 +4178,6 @@
               "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz",
               "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==",
               "dev": true
-            },
-            "lodash": {
-              "version": "4.17.15",
-              "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-              "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-              "dev": true
             }
           }
         },
@@ -4240,14 +4190,6 @@
             "@babel/helper-validator-identifier": "^7.9.0",
             "lodash": "^4.17.13",
             "to-fast-properties": "^2.0.0"
-          },
-          "dependencies": {
-            "lodash": {
-              "version": "4.17.15",
-              "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-              "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-              "dev": true
-            }
           }
         },
         "debug": {
@@ -6204,7 +6146,7 @@
       "dependencies": {
         "acorn": {
           "version": "2.7.0",
-          "resolved": "http://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz",
           "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=",
           "dev": true
         }
@@ -6374,9 +6316,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.11",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
       "dev": true
     },
     "lodash.debounce": {
@@ -6938,7 +6880,7 @@
     },
     "minimist": {
       "version": "0.0.8",
-      "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
       "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
       "dev": true
     },
@@ -6973,9 +6915,9 @@
       }
     },
     "mixin-deep": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
-      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+      "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
       "dev": true,
       "requires": {
         "for-in": "^1.0.2",
@@ -8211,7 +8153,7 @@
     },
     "readable-stream": {
       "version": "2.3.6",
-      "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
       "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
       "dev": true,
       "requires": {
@@ -10878,12 +10820,6 @@
             "path-exists": "^3.0.0"
           }
         },
-        "lodash": {
-          "version": "4.17.15",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-          "dev": true
-        },
         "p-limit": {
           "version": "2.2.2",
           "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
diff --git a/guides/contexts.md b/guides/contexts.md
index 61378c81a..46475dc23 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -24,7 +24,7 @@ Let's use these ideas to build out our web application. Our goal is to build a u
 
 User accounts are often wide-reaching across a platform so it's important to think upfront about writing a well-defined interface. With that in mind, our goal is to build an accounts API that handles creating, updating, and deleting user accounts, as well as authenticating user credentials. We'll start off with basic features, but as we add authentication later, we'll see how starting with a solid foundation allows us to grow our application naturally as we add functionality.
 
-Phoenix includes the `phx.gen.html`, `phx.gen.json`, and `phx.gen.context` generators that apply the ideas of isolating functionality in our applications into contexts. These generators are a great way to hit the ground running while Phoenix nudges you in the right direction to grow your application. Let's put these tools to use for our new user accounts context.
+Phoenix includes the `phx.gen.html`, `phx.gen.json`, `phx.gen.live`, and `phx.gen.context` generators that apply the ideas of isolating functionality in our applications into contexts. These generators are a great way to hit the ground running while Phoenix nudges you in the right direction to grow your application. Let's put these tools to use for our new user accounts context.
 
 In order to run the context generators, we need to come up with a module name that groups the related functionality that we're building. In the [Ecto guide](ecto.html), we saw how we can use Changesets and Repos to validate and persist user schemas, but we didn't integrate this with our application at large. In fact, we didn't think about where a "user" in our application should live at all. Let's take a step back and think about the different parts of our system. We know that we'll have users of our product. Along with users comes things like account login credentials and user registration. An `Accounts` context in our system is a natural place for our user functionality to live.
 
diff --git a/guides/howto/custom_error_pages.md b/guides/howto/custom_error_pages.md
index d40ef8ec3..4bb24de34 100644
--- a/guides/howto/custom_error_pages.md
+++ b/guides/howto/custom_error_pages.md
@@ -91,7 +91,7 @@ Phoenix generates an `ErrorView` for us, but it doesn't give us a `lib/hello_web
       </div>
 
       <div class="footer">
-        <p><a href="http://phoenixframework.org">phoenixframework.org</a></p>
+        <p><a href="https://phoenixframework.org">phoenixframework.org</a></p>
       </div>
 
     </div> <!-- /container -->
diff --git a/guides/realtime/channels.md b/guides/realtime/channels.md
index 19b185ba6..222778381 100644
--- a/guides/realtime/channels.md
+++ b/guides/realtime/channels.md
@@ -38,7 +38,7 @@ Currently, that's either Websockets or long polling, but other transports may be
 
 Unlike stateless HTTP connections, Channels support long-lived connections, each backed by a lightweight BEAM process, working in parallel and maintaining its own state.
 
-This architecture scales well; Phoenix Channels [can support millions of subscribers with reasonable latency on a single box](http://phoenixframework.org/blog/the-road-to-2-million-websocket-connections), passing hundreds of thousands of messages per second.
+This architecture scales well; Phoenix Channels [can support millions of subscribers with reasonable latency on a single box](https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections), passing hundreds of thousands of messages per second.
 And that capacity can be multiplied by adding more nodes to the cluster.
 
 ## The Moving Parts
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index f49e85636..4ccdde4e4 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -52,8 +52,8 @@ defmodule Mix.Tasks.Phx.New do
     * `--verbose` - use verbose output
 
   When passing the `--no-ecto` flag, Phoenix generators such as
-  `phx.gen.html`, `phx.gen.json` and `phx.gen.context` may no
-  longer work as expected as they generate context files that rely
+  `phx.gen.html`, `phx.gen.json`, `phx.gen.live`, and `phx.gen.context`
+  may no longer work as expected as they generate context files that rely
   on Ecto for the database access. In those cases, you can pass the
   `--no-context` flag to generate most of the HTML and JSON files
   but skip the context, allowing you to fill in the blanks as desired.
diff --git a/installer/templates/phx_live/templates/layout/root.html.leex b/installer/templates/phx_live/templates/layout/root.html.leex
index 1ab53388e..adad0063e 100644
--- a/installer/templates/phx_live/templates/layout/root.html.leex
+++ b/installer/templates/phx_live/templates/layout/root.html.leex
@@ -4,17 +4,20 @@
     <meta charset="utf-8"/>
     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <%%= csrf_meta_tag() %>
     <%%= live_title_tag assigns[:page_title] || "<%= app_module %>", suffix: " · Phoenix Framework" %>
     <link rel="stylesheet" href="<%%= Routes.static_path(@conn, "/css/app.css") %>"/>
-    <%%= csrf_meta_tag() %>
+    <script defer type="text/javascript" src="<%%= Routes.static_path(@conn, "/js/app.js") %>"></script>
   </head>
   <body>
     <header>
       <section class="container">
         <nav role="navigation">
           <ul>
-            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li><%= if live and dashboard do %>
-            <li><%%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li><% end %>
+            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
+            <%= if dashboard do %><%%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
+              <li><%%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
+            <%% end %><% end %>
           </ul>
         </nav>
         <a href="https://phoenixframework.org/" class="phx-logo">
@@ -23,6 +26,5 @@
       </section>
     </header>
     <%%= @inner_content %>
-    <script type="text/javascript" src="<%%= Routes.static_path(@conn, "/js/app.js") %>"></script>
   </body>
 </html>
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index 1d4d2565a..4cca83a2b 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -42,10 +42,10 @@ defmodule <%= app_module %>.MixProject do
       {:ecto_sql, "~> 3.4"},
       {<%= inspect adapter_app %>, ">= 0.0.0"},<% end %><%= if html do %><%= if live do %>
       {:phoenix_live_view, "~> 0.11.0"},
-      {:floki, ">= 0.0.0", only: :test},<% end %><%= if dashboard do %>
-      {:phoenix_live_dashboard, github: "phoenixframework/phoenix_live_dashboard"},<% end %>
+      {:floki, ">= 0.0.0", only: :test},<% end %>
       {:phoenix_html, "~> 2.11"},
-      {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %>
+      {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if dashboard do %>
+      {:phoenix_live_dashboard, github: "phoenixframework/phoenix_live_dashboard"},<% end %>
       {:telemetry_metrics, "~> 0.4"},
       {:telemetry_poller, "~> 0.4"},<%= if gettext do %>
       {:gettext, "~> 0.11"},<% end %>
diff --git a/installer/templates/phx_web/router.ex b/installer/templates/phx_web/router.ex
index e4a9605c7..9471cbb67 100644
--- a/installer/templates/phx_web/router.ex
+++ b/installer/templates/phx_web/router.ex
@@ -19,7 +19,16 @@ defmodule <%= web_namespace %>.Router do
     pipe_through :browser
 
     <%= if live do %>live "/", PageLive, :index<% else %>get "/", PageController, :index<% end %>
-  end<%= if dashboard do %>
+  end
+
+  # Other scopes may use custom stacks.
+  # scope "/api", <%= web_namespace %> do
+  #   pipe_through :api
+  # end<% else %>
+
+  scope "/api", <%= web_namespace %> do
+    pipe_through :api
+  end<% end %><%= if dashboard do %>
 
   # Enables LiveDashboard only for development
   #
@@ -28,21 +37,13 @@ defmodule <%= web_namespace %>.Router do
   # If your application does not have an admins-only section yet,
   # you can use Plug.BasicAuth to set up some basic authentication
   # as long as you are also using SSL (which you should anyway).
-  if Mix.env() == :dev do
+  if Mix.env() in [:dev, :test] do
     import Phoenix.LiveDashboard.Router
 
-    scope "/" do
-      pipe_through :browser
+    scope "/" do<%= if html do %>
+      pipe_through :browser<% else %>
+      pipe_through [:fetch_session, :protect_from_forgery]<% end %>
       live_dashboard "/dashboard", metrics: <%= web_namespace %>.Telemetry
     end
   end<% end %>
-
-  # Other scopes may use custom stacks.
-  # scope "/api", <%= web_namespace %> do
-  #   pipe_through :api
-  # end<% else %>
-
-  scope "/api", <%= web_namespace %> do
-    pipe_through :api
-  end<% end %>
 end
diff --git a/installer/templates/phx_web/templates/layout/app.html.eex b/installer/templates/phx_web/templates/layout/app.html.eex
index 010e4c7f9..6cddabbaa 100644
--- a/installer/templates/phx_web/templates/layout/app.html.eex
+++ b/installer/templates/phx_web/templates/layout/app.html.eex
@@ -6,7 +6,6 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <title><%= app_module %> · Phoenix Framework</title>
     <link rel="stylesheet" href="<%%= Routes.static_path(@conn, "/css/app.css") %>"/>
-    <%%= csrf_meta_tag() %>
     <script defer type="text/javascript" src="<%%= Routes.static_path(@conn, "/js/app.js") %>"></script>
   </head>
   <body>
@@ -15,6 +14,9 @@
         <nav role="navigation">
           <ul>
             <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
+            <%= if dashboard do %><%%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
+              <li><%%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
+            <%% end %><% end %>
           </ul>
         </nav>
         <a href="https://phoenixframework.org/" class="phx-logo">
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 19aea6c47..9f57e2cd7 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -100,11 +100,11 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   end
 
   @doc false
-  def build(args) do
+  def build(args, help \\ __MODULE__) do
     {opts, parsed, _} = parse_opts(args)
-    [context_name, schema_name, plural | schema_args] = validate_args!(parsed)
+    [context_name, schema_name, plural | schema_args] = validate_args!(parsed, help)
     schema_module = inspect(Module.concat(context_name, schema_name))
-    schema = Gen.Schema.build([schema_module, plural | schema_args], opts, __MODULE__)
+    schema = Gen.Schema.build([schema_module, plural | schema_args], opts, help)
     context = Context.new(context_name, schema, opts)
     {context, schema}
   end
@@ -200,25 +200,25 @@ defmodule Mix.Tasks.Phx.Gen.Context do
     end
   end
 
-  defp validate_args!([context, schema, _plural | _] = args) do
+  defp validate_args!([context, schema, _plural | _] = args, help) do
     cond do
       not Context.valid?(context) ->
-        raise_with_help "Expected the context, #{inspect context}, to be a valid module name"
+        help.raise_with_help "Expected the context, #{inspect context}, to be a valid module name"
       not Schema.valid?(schema) ->
-        raise_with_help "Expected the schema, #{inspect schema}, to be a valid module name"
+        help.raise_with_help "Expected the schema, #{inspect schema}, to be a valid module name"
       context == schema ->
-        raise_with_help "The context and schema should have different names"
+        help.raise_with_help "The context and schema should have different names"
       context == Mix.Phoenix.base() ->
-        raise_with_help "Cannot generate context #{context} because it has the same name as the application"
+        help.raise_with_help "Cannot generate context #{context} because it has the same name as the application"
       schema == Mix.Phoenix.base() ->
-        raise_with_help "Cannot generate schema #{schema} because it has the same name as the application"
+        help.raise_with_help "Cannot generate schema #{schema} because it has the same name as the application"
       true ->
         args
     end
   end
 
-  defp validate_args!(_) do
-    raise_with_help "Invalid arguments"
+  defp validate_args!(_, help) do
+    help.raise_with_help "Invalid arguments"
   end
 
   @doc false
@@ -227,13 +227,14 @@ defmodule Mix.Tasks.Phx.Gen.Context do
     Mix.raise """
     #{msg}
 
-    mix phx.gen.html, phx.gen.json and phx.gen.context expect a
-    context module name, followed by singular and plural names of
-    the generated resource, ending with any number of attributes.
+    mix phx.gen.html, phx.gen.json, phx.gen.live, and phx.gen.context
+    expect a context module name, followed by singular and plural names
+    of the generated resource, ending with any number of attributes.
     For example:
 
         mix phx.gen.html Accounts User users name:string
         mix phx.gen.json Accounts User users name:string
+        mix phx.gen.live Accounts User users name:string
         mix phx.gen.context Accounts User users name:string
 
     The context serves as the API boundary for the given resource.
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 4058319c2..d7618c056 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -1402,7 +1402,7 @@ defmodule Phoenix.Controller do
   end
 
   @doc """
-  Returns a message from flash by `key`.
+  Returns a message from flash by `key` (or `nil` if no message is available for `key`).
 
   ## Examples
 
diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex
index c8ac0bc9d..12be1a03a 100644
--- a/lib/phoenix/logger.ex
+++ b/lib/phoenix/logger.ex
@@ -12,14 +12,14 @@ defmodule Phoenix.Logger do
       * Measurement: `%{time: System.monotonic_time}`
       * Metadata: `%{conn: Plug.Conn.t, options: Keyword.t}`
       * Options: `%{log: Logger.level | false}`
-      * Disable logging: In your endpoint `plug Plug.Telemetry, ..., options: [log: false]`
+      * Disable logging: In your endpoint `plug Plug.Telemetry, ..., log: Logger.level | false`
 
     * `[:phoenix, :endpoint, :stop]` - dispatched by `Plug.Telemetry` in your
       endpoint whenever the response is sent
       * Measurement: `%{duration: native_time}`
       * Metadata: `%{conn: Plug.Conn.t, options: Keyword.t}`
       * Options: `%{log: Logger.level | false}`
-      * Disable logging: In your endpoint `plug Plug.Telemetry, ..., options: [log: false]`
+      * Disable logging: In your endpoint `plug Plug.Telemetry, ..., log: Logger.level | false`
 
     * `[:phoenix, :router_dispatch, :start]` - dispatched by `Phoenix.Router`
       before dispatching to a matched route
@@ -168,22 +168,30 @@ defmodule Phoenix.Logger do
   ## Event: [:phoenix, :endpoint, *]
 
   defp phoenix_endpoint_start(_, _, %{conn: conn} = metadata, _) do
-    level = metadata[:options][:log] || :info
-
-    Logger.log(level, fn ->
-      %{method: method, request_path: request_path} = conn
-      [method, ?\s, request_path]
-    end)
+    case metadata[:options][:log] do
+      false ->
+        :ok
+
+      level ->
+        Logger.log(level || :info, fn ->
+          %{method: method, request_path: request_path} = conn
+          [method, ?\s, request_path]
+        end)
+    end
   end
 
   defp phoenix_endpoint_stop(_, %{duration: duration}, %{conn: conn} = metadata, _) do
-    level = metadata[:options][:log] || :info
-
-    Logger.log(level, fn ->
-      %{status: status, state: state} = conn
-      status = Integer.to_string(status)
-      [connection_type(state), ?\s, status, " in ", duration(duration)]
-    end)
+    case metadata[:options][:log] do
+      false ->
+        :ok
+
+      level ->
+        Logger.log(level || :info, fn ->
+          %{status: status, state: state} = conn
+          status = Integer.to_string(status)
+          [connection_type(state), ?\s, status, " in ", duration(duration)]
+        end)
+    end
   end
 
   defp connection_type(:set_chunked), do: "Chunked"
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index 5f3d0d616..02432320d 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -212,9 +212,10 @@ defmodule Phoenix.Socket.Transport do
     do: module.default_config()
 
   def load_config(config, module),
-    do: module.default_config() |> Keyword.merge(config) |> validate_config()
+    do: module.default_config() |> Keyword.merge(config) |> load_config()
 
-  defp validate_config(config) do
+  @doc false
+  def load_config(config) do
     {connect_info, config} = Keyword.pop(config, :connect_info, [])
 
     connect_info =
@@ -244,16 +245,8 @@ defmodule Phoenix.Socket.Transport do
     {key, store, init}
   end
 
-  defp init_session({module, function, arguments})  do
-    case apply(module, function, arguments) do
-      session_config when is_list(session_config) ->
-        init_session(session_config)
-
-      session_config ->
-        raise ArgumentError,
-          "invalid MFA session_config return #{inspect session_config}. " <>
-            "When session_config is a MFA, it's expected that it will return a keyword list same as the argument given to `Plug.Session`"
-    end
+  defp init_session({_, _, _} = mfa)  do
+    {:mfa, mfa}
   end
 
   @doc """
@@ -426,19 +419,8 @@ defmodule Phoenix.Socket.Transport do
         :uri ->
           {:uri, fetch_uri(conn)}
 
-        {:session, {key, store, store_config}} ->
-          conn = Plug.Conn.fetch_cookies(conn)
-
-          with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"],
-               cookie when is_binary(cookie) <- conn.cookies[key],
-               conn = put_in(conn.secret_key_base, endpoint.config(:secret_key_base)),
-               {_, session} <- store.get(conn, cookie, store_config),
-               csrf_state when is_binary(csrf_state) <- Plug.CSRFProtection.dump_state_from_session(session["_csrf_token"]),
-               true <- Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) do
-            {:session, session}
-          else
-            _ -> {:session, nil}
-          end
+        {:session, session} ->
+          {:session, connect_session(conn, endpoint, session)}
 
         {key, val} ->
           {key, val}
@@ -446,6 +428,32 @@ defmodule Phoenix.Socket.Transport do
     end
   end
 
+  defp connect_session(conn, endpoint, {key, store, store_config}) do
+    conn = Plug.Conn.fetch_cookies(conn)
+
+    with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"],
+         cookie when is_binary(cookie) <- conn.cookies[key],
+         conn = put_in(conn.secret_key_base, endpoint.config(:secret_key_base)),
+         {_, session} <- store.get(conn, cookie, store_config),
+         csrf_state when is_binary(csrf_state) <- Plug.CSRFProtection.dump_state_from_session(session["_csrf_token"]),
+         true <- Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) do
+      session
+    else
+      _ -> nil
+    end
+  end
+
+  defp connect_session(conn, endpoint, {:mfa, {module, function, args}}) do
+    case apply(module, function, args) do
+      session_config when is_list(session_config) ->
+        connect_session(conn, endpoint, init_session(session_config))
+
+      other ->
+        raise ArgumentError,
+          "the MFA given to `session_config` must return a keyword list, got: #{inspect other}"
+    end
+  end
+
   defp subprotocols_error_response(conn, subprotocols) do
     import Plug.Conn
     request_headers = get_req_header(conn, "sec-websocket-protocol")
diff --git a/lib/phoenix/token.ex b/lib/phoenix/token.ex
index ff8705186..1ad231d96 100644
--- a/lib/phoenix/token.ex
+++ b/lib/phoenix/token.ex
@@ -15,8 +15,8 @@ defmodule Phoenix.Token do
   the id from a database. For example:
 
       iex> user_id = 1
-      iex> token = Phoenix.Token.sign(MyApp.Endpoint, "user salt", user_id)
-      iex> Phoenix.Token.verify(MyApp.Endpoint, "user salt", token, max_age: 86400)
+      iex> token = Phoenix.Token.sign(MyApp.Endpoint, "user auth", user_id)
+      iex> Phoenix.Token.verify(MyApp.Endpoint, "user auth", token, max_age: 86400)
       {:ok, 1}
 
   In that example we have a user's id, we generate a token and
@@ -38,8 +38,9 @@ defmodule Phoenix.Token do
 
   The second argument is a [cryptographic salt](https://en.wikipedia.org/wiki/Salt_(cryptography))
   which must be the same in both calls to `sign/4` and `verify/4`.
-  For instance, it may be called "user auth" when generating a token
-  that will be used to authenticate users on channels or on your APIs.
+  For instance, it may be called "user auth" and treated as namespace
+  when generating a token that will be used to authenticate users on
+  channels or on your APIs.
 
   The third argument can be any term (string, int, list, etc.)
   that you wish to codify into the token. Upon valid verification,
@@ -52,14 +53,14 @@ defmodule Phoenix.Token do
   One is via the meta tag:
 
       <%= tag :meta, name: "channel_token",
-                     content: Phoenix.Token.sign(@conn, "user salt", @current_user.id) %>
+                     content: Phoenix.Token.sign(@conn, "user auth", @current_user.id) %>
 
   Or an endpoint that returns it:
 
       def create(conn, params) do
         user = User.create(params)
         render(conn, "user.json",
-               %{token: Phoenix.Token.sign(conn, "user salt", user.id), user: user})
+               %{token: Phoenix.Token.sign(conn, "user auth", user.id), user: user})
       end
 
   Once the token is sent, the client may now send it back to the server
@@ -70,7 +71,7 @@ defmodule Phoenix.Token do
         use Phoenix.Socket
 
         def connect(%{"token" => token}, socket, _connect_info) do
-          case Phoenix.Token.verify(socket, "user salt", token, max_age: 86400) do
+          case Phoenix.Token.verify(socket, "user auth", token, max_age: 86400) do
             {:ok, user_id} ->
               socket = assign(socket, :user, Repo.get!(User, user_id))
               {:ok, socket}
@@ -145,8 +146,8 @@ defmodule Phoenix.Token do
 
       iex> user_id    = 99
       iex> secret     = "kjoy3o1zeidquwy1398juxzldjlksahdk3"
-      iex> user_salt  = "user salt"
-      iex> token      = Phoenix.Token.sign(secret, user_salt, user_id)
+      iex> namespace  = "user auth"
+      iex> token      = Phoenix.Token.sign(secret, namespace, user_id)
 
   The mechanism for passing the token to the client is typically through a
   cookie, a JSON response body, or HTTP header. For now, assume the client has
@@ -155,7 +156,7 @@ defmodule Phoenix.Token do
   When the server receives a request, it can use `verify/4` to determine if it
   should provide the requested resources to the client:
 
-      iex> Phoenix.Token.verify(secret, user_salt, token, max_age: 86400)
+      iex> Phoenix.Token.verify(secret, namespace, token, max_age: 86400)
       {:ok, 99}
 
   In this example, we know the client sent a valid token because `verify/4`
@@ -165,10 +166,10 @@ defmodule Phoenix.Token do
   However, if the client had sent an expired or otherwise invalid token
   `verify/4` would have returned an error instead:
 
-      iex> Phoenix.Token.verify(secret, user_salt, expired, max_age: 86400)
+      iex> Phoenix.Token.verify(secret, namespace, expired, max_age: 86400)
       {:error, :expired}
 
-      iex> Phoenix.Token.verify(secret, user_salt, invalid, max_age: 86400)
+      iex> Phoenix.Token.verify(secret, namespace, invalid, max_age: 86400)
       {:error, :invalid}
 
   ## Options
diff --git a/mix.exs b/mix.exs
index 7139a8cd1..2dead2d67 100644
--- a/mix.exs
+++ b/mix.exs
@@ -34,7 +34,7 @@ defmodule Phoenix.MixProject do
       docs: docs(),
       aliases: aliases(),
       source_url: "https://github.com/phoenixframework/phoenix",
-      homepage_url: "http://www.phoenixframework.org",
+      homepage_url: "https://www.phoenixframework.org",
       description: """
       Productive. Reliable. Fast. A productive web framework that
       does not compromise speed or maintainability.


Test patch:
diff --git a/installer/templates/phx_test/live/page_live_test.exs b/installer/templates/phx_test/live/page_live_test.exs
index 08df863c7..75cf111d4 100644
--- a/installer/templates/phx_test/live/page_live_test.exs
+++ b/installer/templates/phx_test/live/page_live_test.exs
@@ -4,8 +4,8 @@ defmodule <%= web_namespace %>.PageLiveTest do
   import Phoenix.LiveViewTest
 
   test "disconnected and connected render", %{conn: conn} do
-    {:ok, page_live, disconneted_html} = live(conn, "/")
-    assert disconneted_html =~ "Welcome to Phoenix!"
+    {:ok, page_live, disconnected_html} = live(conn, "/")
+    assert disconnected_html =~ "Welcome to Phoenix!"
     assert render(page_live) =~ "Welcome to Phoenix!"
   end
 end
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index cda0cd336..4783922c3 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -305,6 +305,46 @@ defmodule Mix.Tasks.Phx.NewTest do
     end
   end
 
+  test "new with no_dashboard" do
+    in_tmp "new with no_dashboard", fn ->
+      Mix.Tasks.Phx.New.run([@app_name, "--no-dashboard"])
+
+      assert_file "phx_blog/mix.exs", &refute(&1 =~ ~r":phoenix_live_dashboard")
+
+      assert_file "phx_blog/lib/phx_blog_web/templates/layout/app.html.eex", fn file ->
+        refute file =~ ~s|<%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home)|
+      end
+
+      assert_file "phx_blog/lib/phx_blog_web/endpoint.ex", fn file ->
+        assert file =~ ~s|defmodule PhxBlogWeb.Endpoint|
+        refute file =~ ~s|socket "/live"|
+        refute file =~ ~s|plug Phoenix.LiveDashboard.RequestLogger|
+      end
+    end
+  end
+
+  test "new with no_html" do
+    in_tmp "new with no_html", fn ->
+      Mix.Tasks.Phx.New.run([@app_name, "--no-html"])
+
+      assert_file "phx_blog/mix.exs", fn file ->
+        refute file =~ ~s|:phoenix_live_view|
+        assert file =~ ~s|:phoenix_live_dashboard|
+      end
+
+      assert_file "phx_blog/lib/phx_blog_web/endpoint.ex", fn file ->
+        assert file =~ ~s|defmodule PhxBlogWeb.Endpoint|
+        assert file =~ ~s|socket "/live"|
+        assert file =~ ~s|plug Phoenix.LiveDashboard.RequestLogger|
+      end
+
+      assert_file "phx_blog/lib/phx_blog_web/router.ex", fn file ->
+        refute file =~ ~s|pipeline :browser|
+        assert file =~ ~s|pipe_through [:fetch_session, :protect_from_forgery]|
+      end
+    end
+  end
+
   test "new with no_webpack" do
     in_tmp "new with no_webpack", fn ->
       Mix.Tasks.Phx.New.run([@app_name, "--no-webpack"])
@@ -382,16 +422,21 @@ defmodule Mix.Tasks.Phx.NewTest do
     end
   end
 
-  test "new with live without dashboard" do
-    in_tmp "new with live without dashboard", fn ->
+  test "new with live no_dashboard" do
+    in_tmp "new with live no_dashboard", fn ->
       Mix.Tasks.Phx.New.run([@app_name, "--live", "--no-dashboard"])
 
       assert_file "phx_blog/mix.exs", &refute(&1 =~ ~r":phoenix_live_dashboard")
 
-      # No Dashboard
       assert_file "phx_blog/lib/phx_blog_web/templates/layout/root.html.leex", fn file ->
         refute file =~ ~s|<%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home)|
       end
+
+      assert_file "phx_blog/lib/phx_blog_web/endpoint.ex", fn file ->
+        assert file =~ ~s|defmodule PhxBlogWeb.Endpoint|
+        assert file =~ ~s|socket "/live"|
+        refute file =~ ~s|plug Phoenix.LiveDashboard.RequestLogger|
+      end
     end
   end
 
diff --git a/test/mix/tasks/phx_test.exs b/test/mix/tasks/phx_test.exs
index 399aae542..8829b70e0 100644
--- a/test/mix/tasks/phx_test.exs
+++ b/test/mix/tasks/phx_test.exs
@@ -11,6 +11,7 @@ defmodule Mix.Tasks.Phx.Test do
     assert_received {:mix_shell, :info, ["mix phx.gen.embedded" <> _]}
     assert_received {:mix_shell, :info, ["mix phx.gen.html" <> _]}
     assert_received {:mix_shell, :info, ["mix phx.gen.json" <> _]}
+    assert_received {:mix_shell, :info, ["mix phx.gen.live" <> _]}
   end
 
   test "expects no arguments" do
diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs
index d47436c73..c4af2c7ea 100644
--- a/test/phoenix/socket/transport_test.exs
+++ b/test/phoenix/socket/transport_test.exs
@@ -8,13 +8,35 @@ defmodule Phoenix.Socket.TransportTest do
 
   alias Phoenix.Socket.Transport
 
+  @secret_key_base String.duplicate("abcdefgh", 8)
+
   Application.put_env :phoenix, __MODULE__.Endpoint,
     force_ssl: [],
     url: [host: {:system, "TRANSPORT_TEST_HOST"}],
-    check_origin: ["//endpoint.com"]
+    check_origin: ["//endpoint.com"],
+    secret_key_base: @secret_key_base
 
   defmodule Endpoint do
     use Phoenix.Endpoint, otp_app: :phoenix
+
+    @session_config [
+      store: :cookie,
+      key: "_hello_key",
+      signing_salt: "change_me"
+    ]
+
+    def session_config, do: @session_config
+
+    plug Plug.Session, @session_config
+    plug :fetch_session
+    plug Plug.CSRFProtection
+    plug :put_session
+
+    defp put_session(conn, _) do
+      conn
+      |> put_session(:from_session, "123")
+      |> send_resp(200, Plug.CSRFProtection.get_csrf_token())
+    end
   end
 
   setup_all do
@@ -238,73 +260,24 @@ defmodule Phoenix.Socket.TransportTest do
     end
   end
 
-  describe "load_config/2" do
-    defmodule DummyTransport do
-      def default_config() do
-        [
-          window_ms: 10_000,
-          path: "/longpoll",
-          pubsub_timeout_ms: 2_000,
-          serializer: [{V1.JSONSerializer, "~> 1.0.0"}, {V2.JSONSerializer, "~> 2.0.0"}],
-          transport_log: false,
-          crypto: [max_age: 1_209_600]
-        ]
-      end
-
-      def get_correct_session_config() do
-        send(self(), :get_correct_session_config)
-        [
-          store: :cookie,
-          encryption_salt: "encryption_salt",
-          key: "_session_key",
-          signing_salt: "signing_salt",
-        ]
-      end
-
-      def get_incorrect_session_config() do
-        %{}
-      end
+  describe "connect_info/3" do
+    defp load_connect_info(connect_info) do
+      [connect_info: connect_info] = Transport.load_config(connect_info: connect_info)
+      connect_info
     end
 
-    test "return default_config from Transport module provided when true is given as config argument" do
-      assert DummyTransport.default_config() == Transport.load_config(true, DummyTransport)
-    end
+    test "loads the session from MFA" do
+      conn = conn(:get, "https://foo.com/") |> Endpoint.call([])
+      csrf_token = conn.resp_body
+      session_cookie = conn.cookies["_hello_key"]
 
-    test "config passed will orverride default config from Transport module provided" do
-      window_ms = 5_000
-      config = Transport.load_config([window_ms: window_ms], DummyTransport)
-      assert Keyword.get(config, :window_ms) == window_ms
-    end
-
-    test "support MFA session config" do
-      config = Transport.load_config([
-          connect_info: [
-            session: {DummyTransport, :get_correct_session_config, []}
-          ]
-        ], DummyTransport)
-
-      %{
-        encryption_salt: encryption_salt,
-        key: session_key,
-        signing_salt: signing_salt,
-      } = DummyTransport.get_correct_session_config() |> Enum.into(%{})
-
-      assert {^session_key, Plug.Session.COOKIE, %{
-        encryption_salt: ^encryption_salt,
-        signing_salt: ^signing_salt,
-      }} = get_in(config, [:connect_info, :session])
-
-      assert_received :get_correct_session_config
-    end
+      connect_info = load_connect_info(session: {Endpoint, :session_config, []})
 
-    test "raise exception if MFA is given as session config but it returns anything other than list" do
-      assert_raise ArgumentError, fn ->
-        Transport.load_config([
-          connect_info: [
-            session: {DummyTransport, :get_incorrect_session_config, []}
-          ]
-        ], DummyTransport)
-      end
+      assert %{session: %{"from_session" => "123"}} =
+               conn(:get, "https://foo.com/", _csrf_token: csrf_token)
+               |> put_req_cookie("_hello_key", session_cookie)
+               |> fetch_query_params()
+               |> Transport.connect_info(Endpoint, connect_info)
     end
   end
 end