gddo-server: add UI to turn on/off redirect to pkg.go.dev

This change adds UI for the option to set a cookie and have
candidate godoc.org links redirect to their equivalent pkg.go.dev
counterparts. If a user returns to godoc.org with the utm_source
form value equal to 'backtogodoc', it will display an option to
turn off the automatic redirect.

The intention is to have a link on pkg.go.dev that allows the
user to return to godoc.org and also turn off the automatic redirect
without having to know the special URL param values.

Updates golang/go#37099

Change-Id: I27bd03cbc484a1e504795ff669224c2a2a7d72b6
Reviewed-on: https://go-review.googlesource.com/c/gddo/+/222315
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
diff --git a/gddo-server/assets/site.css b/gddo-server/assets/site.css
index 71da77f..f05feb4 100644
--- a/gddo-server/assets/site.css
+++ b/gddo-server/assets/site.css
@@ -123,12 +123,17 @@
 
 .banner-action-container {
     text-align: right;
+    white-space: nowrap;
 }
 
 .banner-action {
     white-space: nowrap;
 }
 
+.banner-action + .banner-action {
+    margin-left: 24px;
+}
+
 @media (max-width: 768px) {
     .banner {
         flex-direction: column;
@@ -180,3 +185,42 @@
     text-transform: uppercase;
     font-size: 0.75em;
 }
+
+.redirect-toast {
+    align-items: center;
+    background-color: #202124;
+    border-radius: 4px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+    bottom: 20px;
+    color: #fff;
+    display: flex;
+    justify-content: space-between;
+    left: 20px;
+    padding: 10px 18px;
+    position: fixed;
+}
+
+@media (max-width: 768px) {
+    .redirect-toast {
+        width: calc(100% - 40px);
+    }
+}
+
+.redirect-toast-actions {
+    margin-left: 48px;
+    white-space: nowrap;
+}
+
+.redirect-toast-action,
+.redirect-toast-action:link,
+.redirect-toast-action:visited {
+    background: none;
+    border: 0;
+    color: #8ab4f9;
+    padding: 10px;
+}
+
+.redirect-toast-action:active,
+.redirect-toast-action:hover {
+    text-decoration: none;
+}
diff --git a/gddo-server/assets/site.js b/gddo-server/assets/site.js
index 4f52d58..04e26c3 100644
--- a/gddo-server/assets/site.js
+++ b/gddo-server/assets/site.js
@@ -103,11 +103,11 @@
         switch(e.which) {
         case 38: // up
             incrActive(-1);
-            e.preventDefault(); 
+            e.preventDefault();
             break;
         case 40: // down
             incrActive(1);
-            e.preventDefault(); 
+            e.preventDefault();
             break;
         case 13: // enter
             if (active >= 0) {
@@ -228,4 +228,14 @@
         target: '.gddo-sidebar',
         offset: 10
     });
+
+    const redirectToastEl = document.querySelector('.js-redirectToast');
+    if (!redirectToastEl) {
+        return; // element may not even be on the page
+    }
+    const redirectToastDismissEl = redirectToastEl.querySelector('.js-redirectToastDismiss');
+    redirectToastDismissEl.addEventListener('click', e => {
+        e.preventDefault();
+        redirectToastEl.style.display = 'none';
+    });
 });
diff --git a/gddo-server/assets/templates/layout.html b/gddo-server/assets/templates/layout.html
index 577203a..4bd077a 100644
--- a/gddo-server/assets/templates/layout.html
+++ b/gddo-server/assets/templates/layout.html
@@ -32,6 +32,7 @@
     Pkg.go.dev is a new destination for Go discovery & docs. Check it out at {{template "PkgGoDevLink" $}} and share your feedback.
   </div>
   <div class="banner-action-container">
+    <a class="banner-action" href="?redirect=on">Always use pkg.go.dev</a>
     <a class="banner-action" href="https://blog.golang.org/pkg.go.dev-2020">Learn more</a>
   </div>
 </div>
@@ -71,6 +72,16 @@
     </div>
   </div>
 </div>
+
+{{if .showPkgGoDevRedirectToast}}
+  <div class="redirect-toast js-redirectToast">
+    <div>Always use godoc.org as default</div>
+    <div class="redirect-toast-actions">
+      <a class="redirect-toast-action" href="?redirect=off">Yes</a>
+      <button class="redirect-toast-action js-redirectToastDismiss">Dismiss</button>
+    </div>
+  </div>
+{{end}}
 <script src="{{staticPath "/-/jquery-2.0.3.min.js"}}"></script>
 <script src="{{staticPath "/-/bootstrap.min.js"}}"></script>
 <script src="{{staticPath "/-/site.js"}}"></script>
diff --git a/gddo-server/main.go b/gddo-server/main.go
index f3df330..795df07 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -274,6 +274,8 @@
 		}
 	}
 
+	showPkgGoDevRedirectToast := userReturningFromPkgGoDev(req)
+
 	switch {
 	case isView(req, "imports"):
 		if pdoc.Name == "" {
@@ -284,9 +286,10 @@
 			return err
 		}
 		return s.templates.execute(resp, "imports.html", http.StatusOK, nil, map[string]interface{}{
-			"flashMessages": flashMessages,
-			"pkgs":          pkgs,
-			"pdoc":          newTDoc(s.v, pdoc),
+			"flashMessages":             flashMessages,
+			"pkgs":                      pkgs,
+			"pdoc":                      newTDoc(s.v, pdoc),
+			"showPkgGoDevRedirectToast": showPkgGoDevRedirectToast,
 		})
 	case isView(req, "tools"):
 		proto := "http"
@@ -294,9 +297,10 @@
 			proto = "https"
 		}
 		return s.templates.execute(resp, "tools.html", http.StatusOK, nil, map[string]interface{}{
-			"flashMessages": flashMessages,
-			"uri":           fmt.Sprintf("%s://%s/%s", proto, req.Host, importPath),
-			"pdoc":          newTDoc(s.v, pdoc),
+			"flashMessages":             flashMessages,
+			"uri":                       fmt.Sprintf("%s://%s/%s", proto, req.Host, importPath),
+			"pdoc":                      newTDoc(s.v, pdoc),
+			"showPkgGoDevRedirectToast": showPkgGoDevRedirectToast,
 		})
 	case isView(req, "importers"):
 		if pdoc.Name == "" {
@@ -312,9 +316,10 @@
 			template = "importers_robot.html"
 		}
 		return s.templates.execute(resp, template, http.StatusOK, nil, map[string]interface{}{
-			"flashMessages": flashMessages,
-			"pkgs":          pkgs,
-			"pdoc":          newTDoc(s.v, pdoc),
+			"flashMessages":             flashMessages,
+			"pkgs":                      pkgs,
+			"pdoc":                      newTDoc(s.v, pdoc),
+			"showPkgGoDevRedirectToast": showPkgGoDevRedirectToast,
 		})
 	case isView(req, "import-graph"):
 		if requestType == robotRequest {
@@ -339,10 +344,11 @@
 			return err
 		}
 		return s.templates.execute(resp, "graph.html", http.StatusOK, nil, map[string]interface{}{
-			"flashMessages": flashMessages,
-			"svg":           template.HTML(b),
-			"pdoc":          newTDoc(s.v, pdoc),
-			"hide":          hide,
+			"flashMessages":             flashMessages,
+			"svg":                       template.HTML(b),
+			"pdoc":                      newTDoc(s.v, pdoc),
+			"hide":                      hide,
+			"showPkgGoDevRedirectToast": showPkgGoDevRedirectToast,
 		})
 	case isView(req, "play"):
 		u, err := s.playURL(pdoc, req.Form.Get("play"), req.Header.Get("X-AppEngine-Country"))
@@ -410,10 +416,11 @@
 		template += templateExt(req)
 
 		return s.templates.execute(resp, template, status, http.Header{"Etag": {etag}}, map[string]interface{}{
-			"flashMessages": flashMessages,
-			"pkgs":          pkgs,
-			"pdoc":          newTDoc(s.v, pdoc),
-			"importerCount": importerCount,
+			"flashMessages":             flashMessages,
+			"pkgs":                      pkgs,
+			"pdoc":                      newTDoc(s.v, pdoc),
+			"importerCount":             importerCount,
+			"showPkgGoDevRedirectToast": showPkgGoDevRedirectToast,
 		})
 	}
 }
@@ -544,7 +551,11 @@
 		}
 
 		return s.templates.execute(resp, "home"+templateExt(req), http.StatusOK, nil,
-			map[string]interface{}{"Popular": pkgs})
+			map[string]interface{}{
+				"Popular": pkgs,
+
+				"showPkgGoDevRedirectToast": userReturningFromPkgGoDev(req),
+			})
 	}
 
 	if path, ok := isBrowseURL(q); ok {
@@ -577,12 +588,21 @@
 	}
 
 	return s.templates.execute(resp, "results"+templateExt(req), http.StatusOK, nil,
-		map[string]interface{}{"q": q, "pkgs": pkgs})
+		map[string]interface{}{
+			"q":    q,
+			"pkgs": pkgs,
+
+			"showPkgGoDevRedirectToast": userReturningFromPkgGoDev(req),
+		})
 }
 
 func (s *server) serveAbout(resp http.ResponseWriter, req *http.Request) error {
 	return s.templates.execute(resp, "about.html", http.StatusOK, nil,
-		map[string]interface{}{"Host": req.Host})
+		map[string]interface{}{
+			"Host": req.Host,
+
+			"showPkgGoDevRedirectToast": userReturningFromPkgGoDev(req),
+		})
 }
 
 func (s *server) serveBot(resp http.ResponseWriter, req *http.Request) error {
@@ -1071,6 +1091,10 @@
 	})
 }
 
+func userReturningFromPkgGoDev(req *http.Request) bool {
+	return req.FormValue("utm_source") == "backtogodoc"
+}
+
 var gddoToPkgGoDevRequest = map[string]string{
 	"/-/about": "/about",
 	"/-/go":    "/std",
@@ -1083,6 +1107,10 @@
 // if not redirecting to the same path that was used for the godoc.org request.
 func pkgGoDevRedirectHandler(f func(http.ResponseWriter, *http.Request) error) func(http.ResponseWriter, *http.Request) error {
 	return func(w http.ResponseWriter, r *http.Request) error {
+		if userReturningFromPkgGoDev(r) {
+			return f(w, r)
+		}
+
 		redirectParam := r.FormValue(pkgGoDevRedirectParam)
 
 		if redirectParam == pkgGoDevRedirectOn {
diff --git a/gddo-server/main_test.go b/gddo-server/main_test.go
index ba76b64..269030a 100644
--- a/gddo-server/main_test.go
+++ b/gddo-server/main_test.go
@@ -85,6 +85,12 @@
 			wantSetCookieHeader: "",
 			wantStatusCode:      http.StatusFound,
 		},
+		{
+			name:           "do not redirect if user is returning from pkg.go.dev",
+			url:            "http://godoc.org/net/http?utm_source=backtogodoc",
+			cookie:         &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
+			wantStatusCode: http.StatusOK,
+		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			req := httptest.NewRequest("GET", test.url, nil)
@@ -108,7 +114,7 @@
 			}
 
 			if got, want := resp.StatusCode, test.wantStatusCode; got != want {
-				t.Errorf("Status code mismatch: got %q; want %q", got, want)
+				t.Errorf("Status code mismatch: got %d; want %d", got, want)
 			}
 		})
 	}