content,internal: add page descriptions to unit main pages

Adds page descriptions to the unit main pages
by using a safehtml escape hatch to build the
HTML meta tag  manually. This a workaround for
a safehtml limitation.

See: https://github.com/google/safehtml/issues/6.

Fixes golang/go#40752

Change-Id: I021e77bd2d01bb435751d353b809087ce818f57d
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/275874
Trust: Jamal Carvalho <jamal@golang.org>
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Jamal Carvalho <jamal@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/content/static/html/base.tmpl b/content/static/html/base.tmpl
index c7acbfa..f3660ac 100644
--- a/content/static/html/base.tmpl
+++ b/content/static/html/base.tmpl
@@ -11,7 +11,11 @@
 <meta charset="utf-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<meta name="Description" content="Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.">
+{{if .MetaDescription.String}}
+  {{.MetaDescription}}
+{{else}}
+  <meta name="Description" content="Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.">
+{{end}}
 <meta class="js-gtmID" data-gtmid="{{.GoogleTagManagerID}}">
 <link href="/static/css/stylesheet.css?version={{.AppVersionLabel}}" rel="stylesheet">
 <link href="/third_party/dialog-polyfill/dialog-polyfill.css?version={{.AppVersionLabel}}" rel="stylesheet">
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index 8c5a9b1..baeefa7 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -159,6 +159,7 @@
 				cmp.AllowUnexported(SearchPage{}, pagination{}),
 				cmpopts.IgnoreFields(licenses.Metadata{}, "FilePath"),
 				cmpopts.IgnoreFields(pagination{}, "Approximate"),
+				cmpopts.IgnoreFields(basePage{}, "MetaDescription"),
 			}
 			if diff := cmp.Diff(test.wantSearchPage, got, opts...); diff != "" {
 				t.Errorf("fetchSearchPage(db, %q) mismatch (-want +got):\n%s", test.query, diff)
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 219371c..7513ef4 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -17,6 +17,7 @@
 	"time"
 
 	"github.com/go-redis/redis/v8"
+	"github.com/google/safehtml"
 	"github.com/google/safehtml/template"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
@@ -237,6 +238,9 @@
 	// HTMLTitle is the value to use in the page’s <title> tag.
 	HTMLTitle string
 
+	// MetaDescription is the html used for rendering the <meta name="Description"> tag.
+	MetaDescription safehtml.HTML
+
 	// Query is the current search query (if applicable).
 	Query string
 
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index 6320f4c..9234caa 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -12,6 +12,8 @@
 	"strings"
 	"time"
 
+	"github.com/google/safehtml"
+	"github.com/google/safehtml/uncheckedconversions"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/log"
@@ -141,10 +143,28 @@
 		return err
 	}
 	page.Details = d
+	main, ok := d.(*MainDetails)
+	if ok {
+		page.MetaDescription = metaDescription(main.ImportedByCount)
+	}
 	s.servePage(ctx, w, tabSettings.TemplateName, page)
 	return nil
 }
 
+// metaDescription uses a safehtml escape hatch to build HTML used
+// to render the <meta name="Description"> for unit pages as a
+// workaround for https://github.com/google/safehtml/issues/6.
+func metaDescription(synopsis string) safehtml.HTML {
+	if synopsis == "" {
+		return safehtml.HTML{}
+	}
+	return safehtml.HTMLConcat(
+		uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(`<meta name="Description" content="`),
+		safehtml.HTMLEscaped(synopsis),
+		uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(`">`),
+	)
+}
+
 // isValidTabForUnit reports whether the tab is valid for the given unit.
 // It is assumed that tab is a key in unitTabLookup.
 func isValidTabForUnit(tab string, um *internal.UnitMeta) bool {
diff --git a/internal/frontend/unit_main.go b/internal/frontend/unit_main.go
index 6b191e9..4ad94b3 100644
--- a/internal/frontend/unit_main.go
+++ b/internal/frontend/unit_main.go
@@ -74,6 +74,10 @@
 	MobileOutline safehtml.HTML
 	IsPackage     bool
 
+	// DocSynopsis is used as the content for the <meta name="Description">
+	// tag on the main unit page.
+	DocSynopsis string
+
 	// SourceFiles contains .go files for the package.
 	SourceFiles []*File
 
@@ -135,8 +139,10 @@
 		docParts           = &dochtml.Parts{}
 		docLinks, modLinks []link
 		files              []*File
+		synopsis           string
 	)
 	if unit.Documentation != nil {
+		synopsis = unit.Documentation.Synopsis
 		end := middleware.ElapsedStat(ctx, "DecodePackage")
 		docPkg, err := godoc.DecodePackage(unit.Documentation.Source)
 		end()
@@ -192,6 +198,7 @@
 		ModuleReadmeLinks: modLinks,
 		DocOutline:        docParts.Outline,
 		DocBody:           docParts.Body,
+		DocSynopsis:       synopsis,
 		SourceFiles:       files,
 		RepositoryURL:     um.SourceInfo.RepoURL(),
 		SourceURL:         um.SourceInfo.DirectoryURL(internal.Suffix(um.Path, um.ModulePath)),
diff --git a/internal/frontend/unit_test.go b/internal/frontend/unit_test.go
index f18b026..953344e 100644
--- a/internal/frontend/unit_test.go
+++ b/internal/frontend/unit_test.go
@@ -128,3 +128,27 @@
 		}
 	}
 }
+
+func TestMetaDescription(t *testing.T) {
+	for _, test := range []struct {
+		synopsis, want string
+	}{
+		{
+			synopsis: "",
+			want:     "",
+		},
+		{
+			synopsis: "Hello, world.",
+			want:     `<meta name="Description" content="Hello, world.">`,
+		},
+		{
+			synopsis: `"><script>alert();</script><br`,
+			want:     `<meta name="Description" content="&#34;&gt;&lt;script&gt;alert();&lt;/script&gt;&lt;br">`,
+		},
+	} {
+		got := metaDescription(test.synopsis).String()
+		if got != test.want {
+			t.Errorf("metaDescription(%q) = %q, want %q", test.synopsis, got, test.want)
+		}
+	}
+}