_content: serve wiki pages on go.dev

The wiki pages have been copied to go.googlesource.com/wiki
and will be code reviewed using Gerrit, like other Go repos.
Unlike other Go repos, self-review will be permitted, and
there is no requirement for Googlers to be involved to make a change.
These relaxations are possible because the wiki has no production code.
The set of wiki +2'ers is a superset of the usual code +2'ers.

Fixes golang/go#61940.

Change-Id: I01823720091fbaa24e95e9c82abeaadba867a17c
Reviewed-on: https://go-review.googlesource.com/c/website/+/518297
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/_content/css/styles.css b/_content/css/styles.css
index 921c8b0..eea617d 100644
--- a/_content/css/styles.css
+++ b/_content/css/styles.css
@@ -5370,3 +5370,9 @@
     display: none;
   }
 }
+
+hr {
+  border: none;
+  border-top: 1px solid var(--color-border);
+  height: 1px;
+}
diff --git a/_content/site.tmpl b/_content/site.tmpl
index a663bf6..2057dc0 100644
--- a/_content/site.tmpl
+++ b/_content/site.tmpl
@@ -71,7 +71,7 @@
             <a href="{{if .children}}#{{else}}{{.url}}{{end}}" {{if .children}} class="js-desktop-menu-hover"{{end}} aria-label={{.name}} aria-describedby="dropdown-description">
               {{.name}} {{if .children}}<i class="material-icons" aria-hidden="true">arrow_drop_down</i>{{end}}
             </a>
-            <div class="screen-reader-only" id="dropdown-description" hidden> 
+            <div class="screen-reader-only" id="dropdown-description" hidden>
               Press Enter to activate/deactivate dropdown
             </div>
             {{- if .children}}
@@ -270,7 +270,7 @@
   <script async src="/js/copypaste.js"></script>
 </footer>
 <section class="Cookie-notice js-cookieNotice">
-  <div>go.dev uses cookies from Google to deliver and enhance the quality of its services and to 
+  <div>go.dev uses cookies from Google to deliver and enhance the quality of its services and to
   analyze traffic. <a target=_blank href="https://policies.google.com/technologies/cookies">Learn more.</a></div>
   <div><button class="go-Button">Okay</button></div>
 </section>
@@ -438,7 +438,7 @@
 {{end}}
 
 {{if .title}}
-<h1>{{.title}}</h1>
+<h1>{{if strings.HasPrefix .URL "/wiki/"}}Go Wiki: {{end}}{{.title}}</h1>
 {{else if eq .layout "error"}}
 <h1>Error</h1>
 {{else if eq .layout "dir"}}
@@ -471,6 +471,13 @@
 {{end}}
 </div>
 {{end}}
+
+{{if strings.HasPrefix .URL "/wiki/"}}
+<hr>
+<p>
+<i>This content is part of the <a href="/wiki/">Go Wiki</a>.</i>
+</p>
+{{end}}
 </article>
 
 {{end}}
diff --git a/_content/wiki/Comments.md b/_content/wiki/Comments.md
new file mode 100644
index 0000000..98ce74d
--- /dev/null
+++ b/_content/wiki/Comments.md
@@ -0,0 +1,50 @@
+---
+title: Comments
+---
+
+<!--
+This is just a placeholder page for enabling a test.
+In the deployed site it is overwritten with the content of go.googlesource.com/wiki.
+-->
+
+Every package should have a package comment. It should immediately precede the ` package ` statement in one of the files in the package. (It only needs to appear in one file.) It should begin with a single sentence that begins "Package _packagename_" and give a concise summary of the package functionality. This introductory sentence will be used in godoc's list of all packages.
+
+Subsequent sentences and/or paragraphs can give more details. Sentences should be properly punctuated.
+
+```go
+// Package superman implements methods for saving the world.
+//
+// Experience has shown that a small number of procedures can prove
+// helpful when attempting to save the world.
+package superman
+```
+
+Nearly every top-level type, const, var and func should have a comment. A comment for bar should be in the form "_bar_ floats on high o'er vales and hills.". The first letter of _bar_ should not be capitalized unless it's capitalized in the code.
+
+```go
+// enterOrbit causes Superman to fly into low Earth orbit, a position
+// that presents several possibilities for planet salvation.
+func enterOrbit() os.Error {
+  ...
+}
+```
+
+All text that you indent inside a comment, godoc will render as a pre-formatted block. This facilitates code samples.
+
+```go
+// fight can be used on any enemy and returns whether Superman won.
+//
+// Examples:
+//
+//  fight("a random potato")
+//  fight(LexLuthor{})
+//
+func fight(enemy interface{}) bool {
+	// This is testing proper escaping in the wiki.
+	for i := 0; i < 10; i++ {
+		println("fight!")
+	}
+}
+```
+
+
diff --git a/_content/wiki/default.tmpl b/_content/wiki/default.tmpl
new file mode 100644
index 0000000..d57fc0b
--- /dev/null
+++ b/_content/wiki/default.tmpl
@@ -0,0 +1,3 @@
+{{define "layout"}}
+{{doclayout .}}
+{{end}}
diff --git a/_content/wiki/index.md b/_content/wiki/index.md
new file mode 100644
index 0000000..828c4f5
--- /dev/null
+++ b/_content/wiki/index.md
@@ -0,0 +1,5 @@
+---
+title: Index
+---
+
+The Wiki is still loading...
diff --git a/cmd/golangorg/cloudbuild.yaml b/cmd/golangorg/cloudbuild.yaml
index c6edaa6..48b5296 100644
--- a/cmd/golangorg/cloudbuild.yaml
+++ b/cmd/golangorg/cloudbuild.yaml
@@ -3,6 +3,7 @@
 # Do not run directly.
 
 steps:
+  # Clone go repo to _goroot.zip for use by uploaded app.
   - name: gcr.io/cloud-builders/git
     args: ["clone", "--branch=release-branch.go1.21", "--depth=1", "https://go.googlesource.com/go", "_gotmp"]
   - name: gcr.io/cloud-builders/git
@@ -10,14 +11,28 @@
     dir: _gotmp
   - name: golang
     args: ["rm", "-rf", "_gotmp"]
+  # Clone wiki repo into _content/wiki as initial wiki content.
+  # The server will replace this with the wiki repo when it can,
+  # but this provides a good fallback.
+  - name: gcr.io/cloud-builders/git
+    args: ["clone", "--depth=1", "https://go.googlesource.com/wiki", "_wikitmp"]
+  - name: golang
+    args: ["rm", "-rf", "_wikitmp/.git"]
+  - name: golang
+    args: ["cp", "-a", "_wikitmp/*", "_content/wiki"]
+  # Run tests.
   - name: golang
     args: ["go", "test", "./..."]
+  # Coordinate with other Cloud Build jobs to deploy only newest commit.
+  # May abort job here.
   - name: golang
     args: ["go", "run", "./cmd/locktrigger", "--project=$PROJECT_ID",
     "--build=$BUILD_ID", "--repo=https://go.googlesource.com/website"]
+  # Deploy site and redirect traffic (maybe; tests again in prod first).
   - name: gcr.io/cloud-builders/gcloud
     entrypoint: bash
     args: ["./go-app-deploy.sh", "cmd/golangorg/app.yaml"]
+  # Clean up stale versions.
   - name: golang
     args: ["go", "run", "./cmd/versionprune", "--dry_run=false", "--project=$PROJECT_ID", "--service=default"]
 
diff --git a/cmd/golangorg/server.go b/cmd/golangorg/server.go
index 7665efa..4f87330 100644
--- a/cmd/golangorg/server.go
+++ b/cmd/golangorg/server.go
@@ -59,7 +59,8 @@
 
 	runningOnAppEngine = os.Getenv("PORT") != ""
 
-	tipFlag = flag.Bool("tip", runningOnAppEngine, "load git content for tip.golang.org")
+	tipFlag  = flag.Bool("tip", runningOnAppEngine, "load git content for tip.golang.org")
+	wikiFlag = flag.Bool("wiki", runningOnAppEngine, "load git content for go.dev/wiki")
 
 	googleAnalytics string
 )
@@ -157,6 +158,19 @@
 		gorootFS = os.DirFS(goroot)
 	}
 
+	// go.dev/wiki serves content from the very latest Git commit of the wiki repo.
+	// Start with the _content/wiki directory as placeholder until Git loads.
+	var wikiFS atomicFS
+	wikiDefault, err := fs.Sub(contentFS, "wiki")
+	if err != nil {
+		log.Fatalf("loading default wiki content: %v", err)
+	}
+	wikiFS.Set(wikiDefault)
+	if *wikiFlag {
+		go watchGit(&wikiFS, "https://go.googlesource.com/wiki")
+	}
+	contentFS = &mountFS{contentFS, "wiki", &wikiFS}
+
 	// tip.golang.org serves content from the very latest Git commit
 	// of the main Go repo, instead of the one the app is bundled with.
 	var tipGoroot atomicFS
@@ -164,7 +178,7 @@
 		log.Fatalf("loading tip site: %v", err)
 	}
 	if *tipFlag {
-		go watchTip(&tipGoroot)
+		go watchGit(&tipGoroot, "https://go.googlesource.com/go")
 	}
 
 	// beta.golang.org is an old name for tip.
@@ -280,32 +294,32 @@
 	return time.Parse(time.RFC3339, s)
 }
 
-// watchTip is a background goroutine that watches the main Go repo for updates.
-// When a new commit is available, watchTip downloads the new tree and calls
-// tipGoroot.Set to install the new file system.
-func watchTip(tipGoroot *atomicFS) {
+// watchGit is a background goroutine that watches a Git repo for updates.
+// When a new commit is available, watchGit downloads the new tree and calls
+// fsys.Set to install the new file system.
+func watchGit(fsys *atomicFS, repo string) {
 	for {
-		// watchTip1 runs until it panics (hopefully never).
+		// watchGit1 runs until it panics (hopefully never).
 		// If that happens, sleep 5 minutes and try again.
-		watchTip1(tipGoroot)
+		watchGit1(fsys, repo)
 		time.Sleep(5 * time.Minute)
 	}
 }
 
-// watchTip1 does the actual work of watchTip and recovers from panics.
-func watchTip1(tipGoroot *atomicFS) {
+// watchGit1 does the actual work of watchGit and recovers from panics.
+func watchGit1(afs *atomicFS, repo string) {
 	defer func() {
 		if e := recover(); e != nil {
-			log.Printf("watchTip panic: %v\n%s", e, debug.Stack())
+			log.Printf("watchGit %s panic: %v\n%s", repo, e, debug.Stack())
 		}
 	}()
 
 	var r *gitfs.Repo
 	for {
 		var err error
-		r, err = gitfs.NewRepo("https://go.googlesource.com/go")
+		r, err = gitfs.NewRepo(repo)
 		if err != nil {
-			log.Printf("tip: %v", err)
+			log.Printf("watchGit %s: %v", repo, err)
 			time.Sleep(1 * time.Minute)
 			continue
 		}
@@ -318,11 +332,11 @@
 		var err error
 		h, fsys, err = r.Clone("HEAD")
 		if err != nil {
-			log.Printf("tip: %v", err)
+			log.Printf("watchGit %s: %v", repo, err)
 			time.Sleep(1 * time.Minute)
 			continue
 		}
-		tipGoroot.Set(fsys)
+		afs.Set(fsys)
 		break
 	}
 
@@ -330,17 +344,17 @@
 		time.Sleep(5 * time.Minute)
 		h2, err := r.Resolve("HEAD")
 		if err != nil {
-			log.Printf("tip: %v", err)
+			log.Printf("watchGit %s: %v", repo, err)
 			continue
 		}
 		if h2 != h {
 			fsys, err := r.CloneHash(h2)
 			if err != nil {
-				log.Printf("tip: %v", err)
+				log.Printf("watchGit %s: %v", repo, err)
 				time.Sleep(1 * time.Minute)
 				continue
 			}
-			tipGoroot.Set(fsys)
+			afs.Set(fsys)
 			h = h2
 		}
 	}
@@ -715,6 +729,7 @@
 // README.md, and SECURITY.md. The last is particularly problematic
 // when running locally on a Mac, because it can be opened as
 // security.md, which takes priority over _content/security.html.
+// Same for wiki.
 type hideRootMDFS struct {
 	fs fs.FS
 }
@@ -723,6 +738,10 @@
 	if !strings.Contains(name, "/") && strings.HasSuffix(name, ".md") {
 		return nil, errors.New(".md file not available")
 	}
+	switch name {
+	case "wiki/README.md", "wiki/CONTRIBUTING.md", "wiki/LICENSE", "wiki/PATENTS", "wiki/codereview.cfg":
+		return nil, errors.New("wiki meta file not available")
+	}
 	return fsys.fs.Open(name)
 }
 
@@ -795,6 +814,23 @@
 	a.v.Store(&fsys)
 }
 
+// A mountFS is a root FS with a second FS mounted at a specific location.
+type mountFS struct {
+	old fs.FS  // root file system
+	dir string // mount point
+	new fs.FS  // fs mounted on dir
+}
+
+func (m *mountFS) Open(name string) (fs.File, error) {
+	if name == m.dir {
+		return m.new.Open(".")
+	}
+	if strings.HasPrefix(name, m.dir) && len(name) > len(m.dir) && name[len(m.dir)] == '/' {
+		return m.new.Open(name[len(m.dir)+1:])
+	}
+	return m.old.Open(name)
+}
+
 // Open returns fsys.Open(name) where fsys is the file system passed to the most recent call to Set.
 // If there has been no call to Set, Open returns an error with text “no file system”.
 func (a *atomicFS) Open(name string) (fs.File, error) {
diff --git a/cmd/golangorg/server_test.go b/cmd/golangorg/server_test.go
index fd5c889..a31688a 100644
--- a/cmd/golangorg/server_test.go
+++ b/cmd/golangorg/server_test.go
@@ -114,6 +114,9 @@
 
 	// Do not process these paths or path prefixes.
 	ignores := []string{
+		// Wiki is in a different repo; errors there should not block production push.
+		"/wiki/",
+
 		// Support files not meant to be served directly.
 		"/doc/articles/wiki/",
 		"/talks/2013/highperf/",
diff --git a/cmd/golangorg/testdata/godev.txt b/cmd/golangorg/testdata/godev.txt
index c071130..f33b057 100644
--- a/cmd/golangorg/testdata/godev.txt
+++ b/cmd/golangorg/testdata/godev.txt
@@ -109,3 +109,11 @@
 
 GET https://go.dev/doc/security/
 body contains Security
+
+GET https://go.dev/wiki/
+body contains Go Wiki: Index
+body contains <i>This content is part of the <a href="/wiki/">Go Wiki</a>.</i>
+
+GET https://go.dev/wiki/Comments
+body contains Go Wiki: Comments
+body contains <i>This content is part of the <a href="/wiki/">Go Wiki</a>.</i>
diff --git a/cmd/golangorg/testdata/live.txt b/cmd/golangorg/testdata/live.txt
index 28b4ae9..20470be 100644
--- a/cmd/golangorg/testdata/live.txt
+++ b/cmd/golangorg/testdata/live.txt
@@ -62,3 +62,11 @@
 body !contains The Go Playground
 body !contains About the Playground
 body contains Hello, 世界
+
+GET https://go.dev/wiki/Comments
+body contains Go Wiki: Comments
+body contains <i>This content is part of the <a href="/wiki/">Go Wiki</a>.</i>
+
+GET https://go.dev/wiki/CommonMistakes
+body contains Go Wiki: Common Mistakes
+body contains <i>This content is part of the <a href="/wiki/">Go Wiki</a>.</i>
diff --git a/internal/redirect/redirect.go b/internal/redirect/redirect.go
index f71f225..e12455b 100644
--- a/internal/redirect/redirect.go
+++ b/internal/redirect/redirect.go
@@ -111,8 +111,6 @@
 	"/doc/mem":  "/ref/mem",
 	"/doc/spec": "/ref/spec",
 
-	"/wiki": "https://github.com/golang/go/wiki",
-
 	"/doc/articles/c_go_cgo.html":                    "/blog/c-go-cgo",
 	"/doc/articles/concurrency_patterns.html":        "/blog/go-concurrency-patterns-timing-out-and",
 	"/doc/articles/defer_panic_recover.html":         "/blog/defer-panic-and-recover",
@@ -163,7 +161,6 @@
 var prefixHelpers = map[string]string{
 	"issue":  "https://github.com/golang/go/issues/",
 	"issues": "https://github.com/golang/go/issues/",
-	"wiki":   "https://github.com/golang/go/wiki/",
 }
 
 func Handler(target string) http.Handler {
diff --git a/internal/redirect/redirect_test.go b/internal/redirect/redirect_test.go
index 2e788c6..10d4de0 100644
--- a/internal/redirect/redirect_test.go
+++ b/internal/redirect/redirect_test.go
@@ -85,9 +85,6 @@
 		"/issues/new/choose":        errorResult(404),
 		"/issues/1/2/3":             errorResult(404),
 
-		"/wiki/foo":  {302, "https://github.com/golang/go/wiki/foo"},
-		"/wiki/foo/": {302, "https://github.com/golang/go/wiki/foo/"},
-
 		"/design":              {301, "https://go.googlesource.com/proposal/+/master/design"},
 		"/design/":             {302, "/design"},
 		"/design/123-foo":      {302, "https://go.googlesource.com/proposal/+/master/design/123-foo.md"},
diff --git a/internal/web/render.go b/internal/web/render.go
index bb3ef1b..a21cd04 100644
--- a/internal/web/render.go
+++ b/internal/web/render.go
@@ -127,6 +127,12 @@
 		// Either the page explicitly requested templating, or it is markdown,
 		// which is treated as a template by default.
 		isTemplate, explicit := p["template"].(bool)
+
+		// The wiki is not templated by default.
+		if !explicit && strings.HasPrefix(file, "wiki/") {
+			isTemplate, explicit = false, true
+		}
+
 		tdata := data
 		if !explicit || isTemplate {
 			// Load content as a template.
@@ -193,6 +199,7 @@
 				extension.WithLinkifyEmailRegexp(regexp.MustCompile(`[^\x00-\x{10FFFF}]`)), // impossible
 			),
 			extension.DefinitionList,
+			extension.NewTable(),
 		),
 	)
 	var buf bytes.Buffer