content,internal/frontend: add redirect banner for alternative modules

When a request is redirected from an alternative module path, a banner
is added which displays that result.

Fixes golang/go#43518

Change-Id: Ifed01073561b5a55a053c626691cfc176d936440
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/281678
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/content/lib/css/unit_header.css b/content/lib/css/unit_header.css
index fd2dcf7..271c96e 100644
--- a/content/lib/css/unit_header.css
+++ b/content/lib/css/unit_header.css
@@ -100,6 +100,8 @@
 .DetailsHeader-badge--unknown {
   display: none;
 }
+
+.UnitHeader-flashBanner,
 .UnitHeader-majorVersionBanner {
   background-color: var(--gray-10);
   display: flex;
diff --git a/content/static/css/unit_header.css b/content/static/css/unit_header.css
index 7bf59a1..05a3b90 100644
--- a/content/static/css/unit_header.css
+++ b/content/static/css/unit_header.css
@@ -2,5 +2,5 @@
  * Copyright 2020 The Go Authors. All rights reserved.
  * Use of this source code is governed by a BSD-style
  * license that can be found in the LICENSE file.
- */.UnitHeader{border-bottom:.0625rem solid var(--gray-8);padding:.5rem 1rem}@media only screen and (min-width:57.7rem){.UnitHeader{border-bottom:.0625rem solid var(--gray-8);padding:.5rem 1.5rem}}.UnitHeader-container{margin:auto;max-width:95rem}.UnitHeader-breadcrumb{margin-top:.5rem}.UnitHeader-breadcrumbItem{color:var(--gray-4);display:inline-flex;font-size:.875rem;line-height:1.5rem}.UnitHeader-breadcrumbItem:not(:last-child):after{content:">";padding:0 .5rem}.UnitHeader-heading{display:flex;align-items:center}.UnitHeader-title{font-size:1.75rem;line-height:2.25rem;margin-bottom:1rem;margin-top:.625rem}.UnitHeader-detail{align-items:center;display:flex;flex-wrap:wrap;white-space:nowrap}@media only screen and (min-width:52rem){.UnitHeader-detail{margin-bottom:.5rem}}.UnitHeader-detailItem{color:var(--gray-4);font-size:.875rem;margin-bottom:.5rem}.UnitHeader-detailItem img{margin-right:.5rem;vertical-align:middle}.UnitHeader-detailItem a>span{color:var(--gray-4)}.UnitHeader-detailItem:not(:first-of-type):before{content:"|";padding:1rem}@media only screen and (max-width:52rem){.UnitHeader-detailItem:not(:last-of-type):before{padding:.5rem}}.DetailsHeader-badge,.UnitHeader-versionBadge{border-radius:unset;color:#fff;font-size:.7rem;margin:-1rem 0 -1rem .5rem;padding:.25rem .5rem;text-transform:uppercase;top:-.0625rem}.DetailsHeader-badge--latest,.UnitHeader-versionBadge--latest{background:var(--turq-dark)}.DetailsHeader-badge--goToLatest,.UnitHeader-versionBadge--goToLatest{background:var(--pink)}.DetailsHeader-badge--unknown,.UnitHeader-versionBadge--unknown{display:none}.UnitHeader-majorVersionBanner{background-color:var(--gray-10);display:flex;margin:-.5rem 0 1rem;padding:.75rem 0}.DetailsHeader-banner--latest,.UnitHeader-majorVersionBanner--latest{display:none}.UnitHeader-detailIcon{color:var(--gray-3);flex-shrink:0;margin-left:1rem;margin-right:.7rem;width:1rem}a.UnitHeader-backLink{color:#000;display:block;font-size:1rem}.UnitHeader-backLink img{height:.8125rem;margin-right:.5rem;position:relative;top:-.0625rem;width:auto}.UnitHeader-badge{border:.0625rem solid var(--gray-4);border-radius:.125rem;font-size:.6875rem;font-weight:500;line-height:1rem;margin-left:.5rem;padding:0 .35rem;text-align:center}
+ */.UnitHeader{border-bottom:.0625rem solid var(--gray-8);padding:.5rem 1rem}@media only screen and (min-width:57.7rem){.UnitHeader{border-bottom:.0625rem solid var(--gray-8);padding:.5rem 1.5rem}}.UnitHeader-container{margin:auto;max-width:95rem}.UnitHeader-breadcrumb{margin-top:.5rem}.UnitHeader-breadcrumbItem{color:var(--gray-4);display:inline-flex;font-size:.875rem;line-height:1.5rem}.UnitHeader-breadcrumbItem:not(:last-child):after{content:">";padding:0 .5rem}.UnitHeader-heading{display:flex;align-items:center}.UnitHeader-title{font-size:1.75rem;line-height:2.25rem;margin-bottom:1rem;margin-top:.625rem}.UnitHeader-detail{align-items:center;display:flex;flex-wrap:wrap;white-space:nowrap}@media only screen and (min-width:52rem){.UnitHeader-detail{margin-bottom:.5rem}}.UnitHeader-detailItem{color:var(--gray-4);font-size:.875rem;margin-bottom:.5rem}.UnitHeader-detailItem img{margin-right:.5rem;vertical-align:middle}.UnitHeader-detailItem a>span{color:var(--gray-4)}.UnitHeader-detailItem:not(:first-of-type):before{content:"|";padding:1rem}@media only screen and (max-width:52rem){.UnitHeader-detailItem:not(:last-of-type):before{padding:.5rem}}.DetailsHeader-badge,.UnitHeader-versionBadge{border-radius:unset;color:#fff;font-size:.7rem;margin:-1rem 0 -1rem .5rem;padding:.25rem .5rem;text-transform:uppercase;top:-.0625rem}.DetailsHeader-badge--latest,.UnitHeader-versionBadge--latest{background:var(--turq-dark)}.DetailsHeader-badge--goToLatest,.UnitHeader-versionBadge--goToLatest{background:var(--pink)}.DetailsHeader-badge--unknown,.UnitHeader-versionBadge--unknown{display:none}.UnitHeader-flashBanner,.UnitHeader-majorVersionBanner{background-color:var(--gray-10);display:flex;margin:-.5rem 0 1rem;padding:.75rem 0}.DetailsHeader-banner--latest,.UnitHeader-majorVersionBanner--latest{display:none}.UnitHeader-detailIcon{color:var(--gray-3);flex-shrink:0;margin-left:1rem;margin-right:.7rem;width:1rem}a.UnitHeader-backLink{color:#000;display:block;font-size:1rem}.UnitHeader-backLink img{height:.8125rem;margin-right:.5rem;position:relative;top:-.0625rem;width:auto}.UnitHeader-badge{border:.0625rem solid var(--gray-4);border-radius:.125rem;font-size:.6875rem;font-weight:500;line-height:1rem;margin-left:.5rem;padding:0 .35rem;text-align:center}
 /*# sourceMappingURL=unit_header.css.map */
\ No newline at end of file
diff --git a/content/static/css/unit_header.css.map b/content/static/css/unit_header.css.map
index 601879f..8d1c5cb 100644
--- a/content/static/css/unit_header.css.map
+++ b/content/static/css/unit_header.css.map
@@ -1 +1 @@
-{"version":3,"sources":["../../lib/css/unit_header.css"],"names":[],"mappings":"AAAA;;;;EAIE,CAEF,YACE,0CAA4C,CAC5C,kBACF,CACA,2CACE,YACE,0CAA4C,CAC5C,oBACF,CACF,CACA,sBACE,WAAY,CACZ,eACF,CACA,uBACE,gBACF,CACA,2BACE,mBAAoB,CACpB,mBAAoB,CACpB,iBAAmB,CACnB,kBACF,CACA,kDACE,WAAY,CACZ,eACF,CACA,oBACE,YAAa,CACb,kBACF,CACA,kBACE,iBAAkB,CAClB,mBAAoB,CACpB,kBAAmB,CACnB,kBACF,CACA,mBACE,kBAAmB,CACnB,YAAa,CACb,cAAe,CACf,kBACF,CACA,yCACE,mBACE,mBACF,CACF,CACA,uBACE,mBAAoB,CACpB,iBAAmB,CACnB,mBACF,CACA,2BACE,kBAAoB,CACpB,qBACF,CACA,8BACE,mBACF,CACA,kDACE,WAAY,CACZ,YACF,CACA,yCACE,iDACE,aACF,CACF,CAMA,8CAEE,mBAAoB,CACpB,UAAY,CACZ,eAAiB,CACjB,0BAA4B,CAC5B,oBAAuB,CACvB,wBAAyB,CACzB,aACF,CACA,8DAEE,2BACF,CACA,sEAEE,sBACF,CACA,gEAEE,YACF,CACA,+BACE,+BAAgC,CAChC,YAAa,CACb,oBAAwB,CACxB,gBACF,CAMA,qEAEE,YACF,CACA,uBACE,mBAAoB,CACpB,aAAc,CACd,gBAAiB,CACjB,kBAAoB,CACpB,UACF,CACA,sBACE,UAAY,CACZ,aAAc,CACd,cACF,CACA,yBACE,eAAiB,CACjB,kBAAoB,CACpB,iBAAkB,CAClB,aAAe,CACf,UACF,CACA,kBACE,mCAAqC,CACrC,qBAAuB,CACvB,kBAAoB,CACpB,eAAgB,CAChB,gBAAiB,CACjB,iBAAmB,CACnB,gBAAkB,CAClB,iBACF","file":"unit_header.css","sourcesContent":["/*!\n * Copyright 2020 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.UnitHeader {\n  border-bottom: 0.0625rem solid var(--gray-8);\n  padding: 0.5rem 1rem;\n}\n@media only screen and (min-width: 57.7rem) {\n  .UnitHeader {\n    border-bottom: 0.0625rem solid var(--gray-8);\n    padding: 0.5rem 1.5rem;\n  }\n}\n.UnitHeader-container {\n  margin: auto;\n  max-width: 95rem;\n}\n.UnitHeader-breadcrumb {\n  margin-top: 0.5rem;\n}\n.UnitHeader-breadcrumbItem {\n  color: var(--gray-4);\n  display: inline-flex;\n  font-size: 0.875rem;\n  line-height: 1.5rem;\n}\n.UnitHeader-breadcrumbItem:not(:last-child)::after {\n  content: '>';\n  padding: 0 0.5rem;\n}\n.UnitHeader-heading {\n  display: flex;\n  align-items: center;\n}\n.UnitHeader-title {\n  font-size: 1.75rem;\n  line-height: 2.25rem;\n  margin-bottom: 1rem;\n  margin-top: 0.625rem;\n}\n.UnitHeader-detail {\n  align-items: center;\n  display: flex;\n  flex-wrap: wrap;\n  white-space: nowrap;\n}\n@media only screen and (min-width: 52rem) {\n  .UnitHeader-detail {\n    margin-bottom: 0.5rem;\n  }\n}\n.UnitHeader-detailItem {\n  color: var(--gray-4);\n  font-size: 0.875rem;\n  margin-bottom: 0.5rem;\n}\n.UnitHeader-detailItem img {\n  margin-right: 0.5rem;\n  vertical-align: middle;\n}\n.UnitHeader-detailItem a > span {\n  color: var(--gray-4);\n}\n.UnitHeader-detailItem:not(:first-of-type)::before {\n  content: '|';\n  padding: 1rem;\n}\n@media only screen and (max-width: 52rem) {\n  .UnitHeader-detailItem:not(:last-of-type)::before {\n    padding: 0.5rem;\n  }\n}\n\n/*\n * TODO: Replace DetailsHeader-badge with UnitHeader-versionBadge in\n * middleware/latestversion.go after unit page is launched.\n */\n.UnitHeader-versionBadge,\n.DetailsHeader-badge {\n  border-radius: unset;\n  color: white;\n  font-size: 0.7rem;\n  margin: -1rem 0 -1rem 0.5rem;\n  padding: 0.25rem 0.5rem;\n  text-transform: uppercase;\n  top: -0.0625rem;\n}\n.UnitHeader-versionBadge--latest,\n.DetailsHeader-badge--latest {\n  background: var(--turq-dark);\n}\n.UnitHeader-versionBadge--goToLatest,\n.DetailsHeader-badge--goToLatest {\n  background: var(--pink);\n}\n.UnitHeader-versionBadge--unknown,\n.DetailsHeader-badge--unknown {\n  display: none;\n}\n.UnitHeader-majorVersionBanner {\n  background-color: var(--gray-10);\n  display: flex;\n  margin: -0.5rem 0 1rem 0;\n  padding: 0.75rem 0;\n}\n\n/*\n * TODO: Replace DetailsHeader-banner with UnitHeader-majorVersionBanner in\n * middleware/latestversion.go after unit page is launched.\n */\n.UnitHeader-majorVersionBanner--latest,\n.DetailsHeader-banner--latest {\n  display: none;\n}\n.UnitHeader-detailIcon {\n  color: var(--gray-3);\n  flex-shrink: 0;\n  margin-left: 1rem;\n  margin-right: 0.7rem;\n  width: 1rem;\n}\na.UnitHeader-backLink {\n  color: black;\n  display: block;\n  font-size: 1rem;\n}\n.UnitHeader-backLink img {\n  height: 0.8125rem;\n  margin-right: 0.5rem;\n  position: relative;\n  top: -0.0625rem;\n  width: auto;\n}\n.UnitHeader-badge {\n  border: 0.0625rem solid var(--gray-4);\n  border-radius: 0.125rem;\n  font-size: 0.6875rem;\n  font-weight: 500;\n  line-height: 1rem;\n  margin-left: 0.5rem;\n  padding: 0 0.35rem;\n  text-align: center;\n}\n"]}
\ No newline at end of file
+{"version":3,"sources":["../../lib/css/unit_header.css"],"names":[],"mappings":"AAAA;;;;EAIE,CAEF,YACE,0CAA4C,CAC5C,kBACF,CACA,2CACE,YACE,0CAA4C,CAC5C,oBACF,CACF,CACA,sBACE,WAAY,CACZ,eACF,CACA,uBACE,gBACF,CACA,2BACE,mBAAoB,CACpB,mBAAoB,CACpB,iBAAmB,CACnB,kBACF,CACA,kDACE,WAAY,CACZ,eACF,CACA,oBACE,YAAa,CACb,kBACF,CACA,kBACE,iBAAkB,CAClB,mBAAoB,CACpB,kBAAmB,CACnB,kBACF,CACA,mBACE,kBAAmB,CACnB,YAAa,CACb,cAAe,CACf,kBACF,CACA,yCACE,mBACE,mBACF,CACF,CACA,uBACE,mBAAoB,CACpB,iBAAmB,CACnB,mBACF,CACA,2BACE,kBAAoB,CACpB,qBACF,CACA,8BACE,mBACF,CACA,kDACE,WAAY,CACZ,YACF,CACA,yCACE,iDACE,aACF,CACF,CAMA,8CAEE,mBAAoB,CACpB,UAAY,CACZ,eAAiB,CACjB,0BAA4B,CAC5B,oBAAuB,CACvB,wBAAyB,CACzB,aACF,CACA,8DAEE,2BACF,CACA,sEAEE,sBACF,CACA,gEAEE,YACF,CAEA,uDAEE,+BAAgC,CAChC,YAAa,CACb,oBAAwB,CACxB,gBACF,CAMA,qEAEE,YACF,CACA,uBACE,mBAAoB,CACpB,aAAc,CACd,gBAAiB,CACjB,kBAAoB,CACpB,UACF,CACA,sBACE,UAAY,CACZ,aAAc,CACd,cACF,CACA,yBACE,eAAiB,CACjB,kBAAoB,CACpB,iBAAkB,CAClB,aAAe,CACf,UACF,CACA,kBACE,mCAAqC,CACrC,qBAAuB,CACvB,kBAAoB,CACpB,eAAgB,CAChB,gBAAiB,CACjB,iBAAmB,CACnB,gBAAkB,CAClB,iBACF","file":"unit_header.css","sourcesContent":["/*!\n * Copyright 2020 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.UnitHeader {\n  border-bottom: 0.0625rem solid var(--gray-8);\n  padding: 0.5rem 1rem;\n}\n@media only screen and (min-width: 57.7rem) {\n  .UnitHeader {\n    border-bottom: 0.0625rem solid var(--gray-8);\n    padding: 0.5rem 1.5rem;\n  }\n}\n.UnitHeader-container {\n  margin: auto;\n  max-width: 95rem;\n}\n.UnitHeader-breadcrumb {\n  margin-top: 0.5rem;\n}\n.UnitHeader-breadcrumbItem {\n  color: var(--gray-4);\n  display: inline-flex;\n  font-size: 0.875rem;\n  line-height: 1.5rem;\n}\n.UnitHeader-breadcrumbItem:not(:last-child)::after {\n  content: '>';\n  padding: 0 0.5rem;\n}\n.UnitHeader-heading {\n  display: flex;\n  align-items: center;\n}\n.UnitHeader-title {\n  font-size: 1.75rem;\n  line-height: 2.25rem;\n  margin-bottom: 1rem;\n  margin-top: 0.625rem;\n}\n.UnitHeader-detail {\n  align-items: center;\n  display: flex;\n  flex-wrap: wrap;\n  white-space: nowrap;\n}\n@media only screen and (min-width: 52rem) {\n  .UnitHeader-detail {\n    margin-bottom: 0.5rem;\n  }\n}\n.UnitHeader-detailItem {\n  color: var(--gray-4);\n  font-size: 0.875rem;\n  margin-bottom: 0.5rem;\n}\n.UnitHeader-detailItem img {\n  margin-right: 0.5rem;\n  vertical-align: middle;\n}\n.UnitHeader-detailItem a > span {\n  color: var(--gray-4);\n}\n.UnitHeader-detailItem:not(:first-of-type)::before {\n  content: '|';\n  padding: 1rem;\n}\n@media only screen and (max-width: 52rem) {\n  .UnitHeader-detailItem:not(:last-of-type)::before {\n    padding: 0.5rem;\n  }\n}\n\n/*\n * TODO: Replace DetailsHeader-badge with UnitHeader-versionBadge in\n * middleware/latestversion.go after unit page is launched.\n */\n.UnitHeader-versionBadge,\n.DetailsHeader-badge {\n  border-radius: unset;\n  color: white;\n  font-size: 0.7rem;\n  margin: -1rem 0 -1rem 0.5rem;\n  padding: 0.25rem 0.5rem;\n  text-transform: uppercase;\n  top: -0.0625rem;\n}\n.UnitHeader-versionBadge--latest,\n.DetailsHeader-badge--latest {\n  background: var(--turq-dark);\n}\n.UnitHeader-versionBadge--goToLatest,\n.DetailsHeader-badge--goToLatest {\n  background: var(--pink);\n}\n.UnitHeader-versionBadge--unknown,\n.DetailsHeader-badge--unknown {\n  display: none;\n}\n\n.UnitHeader-flashBanner,\n.UnitHeader-majorVersionBanner {\n  background-color: var(--gray-10);\n  display: flex;\n  margin: -0.5rem 0 1rem 0;\n  padding: 0.75rem 0;\n}\n\n/*\n * TODO: Replace DetailsHeader-banner with UnitHeader-majorVersionBanner in\n * middleware/latestversion.go after unit page is launched.\n */\n.UnitHeader-majorVersionBanner--latest,\n.DetailsHeader-banner--latest {\n  display: none;\n}\n.UnitHeader-detailIcon {\n  color: var(--gray-3);\n  flex-shrink: 0;\n  margin-left: 1rem;\n  margin-right: 0.7rem;\n  width: 1rem;\n}\na.UnitHeader-backLink {\n  color: black;\n  display: block;\n  font-size: 1rem;\n}\n.UnitHeader-backLink img {\n  height: 0.8125rem;\n  margin-right: 0.5rem;\n  position: relative;\n  top: -0.0625rem;\n  width: auto;\n}\n.UnitHeader-badge {\n  border: 0.0625rem solid var(--gray-4);\n  border-radius: 0.125rem;\n  font-size: 0.6875rem;\n  font-weight: 500;\n  line-height: 1rem;\n  margin-left: 0.5rem;\n  padding: 0 0.35rem;\n  text-align: center;\n}\n"]}
\ No newline at end of file
diff --git a/content/static/html/helpers/_unit_header.tmpl b/content/static/html/helpers/_unit_header.tmpl
index 71f04a5..c868a22 100644
--- a/content/static/html/helpers/_unit_header.tmpl
+++ b/content/static/html/helpers/_unit_header.tmpl
@@ -36,13 +36,20 @@
           <span class="UnitHeader-badge">{{.}}</span>
         {{end}}
       </div>
+      {{if .RedirectedFrom}}
+        <div class="UnitHeader-flashBanner">
+          <img height="19px" width="16px" class="UnitHeader-detailIcon" src="/static/img/pkg-icon-info_19x16.svg" alt="">
+          <span>
+            Redirected from {{.RedirectedFrom}}.
+          </span>
+        </div>
+      {{end}}
       <div class="UnitHeader-majorVersionBanner $$GODISCOVERY_LATESTMAJORCLASS$$" data-test-id="UnitHeader-majorVersionBanner">
         <img height="19px" width="16px" class="UnitHeader-detailIcon" src="/static/img/pkg-icon-info_19x16.svg" alt="">
         <span>
           The highest tagged major version is <a href="/$$GODISCOVERY_LATESTMAJORVERSIONURL$$">$$GODISCOVERY_LATESTMAJORVERSION$$</a>.
         </span>
       </div>
-
       <div class="js-fixedHeaderSentinel"></div>
       {{if (eq .SelectedTab.Name "")}}
         <div class="UnitHeader-detail">
diff --git a/internal/frontend/404.go b/internal/frontend/404.go
index ed8ab80..5bdb964 100644
--- a/internal/frontend/404.go
+++ b/internal/frontend/404.go
@@ -64,7 +64,9 @@
 		return pathNotFoundError(fullPath, requestedVersion)
 	}
 	if fr.goModPath != fr.modulePath && fr.status == derrors.ToStatus(derrors.AlternativeModule) {
-		http.Redirect(w, r, constructUnitURL(fr.goModPath, fr.goModPath, internal.LatestVersion), http.StatusFound)
+		u := constructUnitURL(fr.goModPath, fr.goModPath, internal.LatestVersion)
+		setFlashMessage(w, alternativeModuleFlash, fullPath, u)
+		http.Redirect(w, r, u, http.StatusFound)
 		return
 	}
 	return &serverError{
diff --git a/internal/frontend/flash.go b/internal/frontend/flash.go
new file mode 100644
index 0000000..a31c734
--- /dev/null
+++ b/internal/frontend/flash.go
@@ -0,0 +1,43 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package frontend
+
+import (
+	"encoding/base64"
+	"fmt"
+	"net/http"
+	"time"
+
+	"golang.org/x/pkgsite/internal/derrors"
+)
+
+// alternativeModuleFlash indicates the alternative module path that
+// a request was redirected from.
+const alternativeModuleFlash = "tmp-redirected-from-alternative-module"
+
+func getFlashMessage(w http.ResponseWriter, r *http.Request, name string) (_ string, err error) {
+	defer derrors.Wrap(&err, "getFlashMessage")
+	c, err := r.Cookie(name)
+	if err != nil && err != http.ErrNoCookie {
+		return "", fmt.Errorf("r.Cookie(%q): %v", name, err)
+	}
+	if c == nil {
+		return "", nil
+	}
+	val, err := base64.URLEncoding.DecodeString(c.Value)
+	if err != nil {
+		return "", nil
+	}
+	http.SetCookie(w, &http.Cookie{
+		Name:    name,
+		Expires: time.Now().Add(-1 * time.Minute),
+	})
+	return string(val), nil
+}
+
+func setFlashMessage(w http.ResponseWriter, name, val, urlPath string) {
+	value := base64.URLEncoding.EncodeToString([]byte(val))
+	http.SetCookie(w, &http.Cookie{Name: name, Value: value, Path: urlPath})
+}
diff --git a/internal/frontend/flash_test.go b/internal/frontend/flash_test.go
new file mode 100644
index 0000000..9ab30bc
--- /dev/null
+++ b/internal/frontend/flash_test.go
@@ -0,0 +1,27 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package frontend
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+func TestFlashMessages(t *testing.T) {
+	w := httptest.NewRecorder()
+
+	testVal := "value"
+	setFlashMessage(w, alternativeModuleFlash, testVal, "/")
+	r := &http.Request{Header: http.Header{"Cookie": w.Header()["Set-Cookie"]}}
+
+	got, err := getFlashMessage(w, r, alternativeModuleFlash)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if got != testVal {
+		t.Errorf("got %q, want %q", got, testVal)
+	}
+}
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index 30c3b11..f226840 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -1162,12 +1162,12 @@
 	_, handler, _ := newTestServer(t, nil)
 
 	for _, test := range []struct {
-		name, path string
-		wantCode   int
+		name, path, flash string
+		wantCode          int
 	}{
-		{"not found", "/invalid-page", http.StatusNotFound},
-		{"bad request", "/gocloud.dev/@latest/blob", http.StatusBadRequest},
-		{"alternative module", "/" + alternativeModule.ModulePath, http.StatusFound},
+		{"not found", "/invalid-page", "", http.StatusNotFound},
+		{"bad request", "/gocloud.dev/@latest/blob", "", http.StatusBadRequest},
+		{"alternative", "/" + alternativeModule.ModulePath, alternativeModule.ModulePath, http.StatusFound},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			w := httptest.NewRecorder()
@@ -1175,6 +1175,14 @@
 			if w.Code != test.wantCode {
 				t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.wantCode)
 			}
+			r := &http.Request{Header: http.Header{"Cookie": w.Header()["Set-Cookie"]}}
+			got, err := getFlashMessage(w, r, alternativeModuleFlash)
+			if err != nil {
+				t.Fatalf("getFlashMessage(%q): %v", alternativeModuleFlash, err)
+			}
+			if got != test.flash {
+				t.Fatalf("got %q; want %q", got, test.flash)
+			}
 		})
 	}
 }
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index e69fe38..0c6c3d4 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -67,6 +67,10 @@
 	// Settings contains settings for the selected tab.
 	SelectedTab TabSettings
 
+	// RedirectedFrom is the unit path that this request was redirected from.
+	// It is set as a flash cookie.
+	RedirectedFrom string
+
 	// Details contains data specific to the type of page being rendered.
 	Details interface{}
 }
@@ -147,6 +151,13 @@
 	if ok {
 		page.MetaDescription = metaDescription(main.ImportedByCount)
 	}
+
+	msg, err := getFlashMessage(w, r, alternativeModuleFlash)
+	if err != nil {
+		log.Errorf(ctx, "serveUnitPage(ctx, w, r, ds, %v): %v", info, err)
+	} else if msg != "" {
+		page.RedirectedFrom = msg
+	}
 	s.servePage(ctx, w, tabSettings.TemplateName, page)
 	return nil
 }