cmd/golangorg: simplify local vs prod programs

There was too much duplicated code between main.go
and appinit.go and too many build-tagged-out files.

Make main.go the func main for both prod and local.
Introduce local.go, merging dl.go and play.go.
Introduce prod.go, holding the prod-specific bits of appinit.go
(the rest are in main.go).

Rename the build tag to prod instead of golangorg
(the whole program is golangorg; it's very confusing).

Fixes golang/go#41102.

Change-Id: I261ce8e9171110f01798025f8218ce9f8253af81
Reviewed-on: https://go-review.googlesource.com/c/website/+/293413
Trust: Russ Cox <rsc@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/README.md b/README.md
index 17d6276..ec3d08e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 
 To run the server to preview local content changes, use:
 
-	go run ./cmd/golangorg -a
+	go run ./cmd/golangorg
 
 The supporting programs cmd/admingolangorg and cmd/googlegolangorg
 are the servers for admin.golang.org and google.golang.org.
diff --git a/cmd/golangorg/Dockerfile.prod b/cmd/golangorg/Dockerfile.prod
index 6c22980..e60c99e 100644
--- a/cmd/golangorg/Dockerfile.prod
+++ b/cmd/golangorg/Dockerfile.prod
@@ -21,7 +21,7 @@
 
 WORKDIR /website/cmd/golangorg
 
-RUN go build -o /golangorg -tags=golangorg golang.org/x/website/cmd/golangorg
+RUN go build -o /golangorg -tags=prod golang.org/x/website/cmd/golangorg
 
 # Clean up goroot for the final image.
 RUN cd /goroot && git clean -xdf
diff --git a/cmd/golangorg/README.md b/cmd/golangorg/README.md
index 228b62f..233e605 100644
--- a/cmd/golangorg/README.md
+++ b/cmd/golangorg/README.md
@@ -16,10 +16,9 @@
   * Godoc sources inside $GOPATH
     (`go get -d golang.org/x/website/cmd/golangorg`)
 
-Build with the `golangorg` tag and run:
+Run with the `prod` tag:
 
-	go build -tags golangorg
-	./golangorg
+	go run -tags prod .
 
 In production mode it serves on localhost:8080 (not 6060).
 The port is controlled by $PORT, as in:
diff --git a/cmd/golangorg/appinit.go b/cmd/golangorg/appinit.go
deleted file mode 100644
index 9dc4076..0000000
--- a/cmd/golangorg/appinit.go
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright 2011 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.
-
-// +build go1.16
-// +build golangorg
-
-package main
-
-// This file replaces main.go when running golangorg under App Engine.
-// See README.md for details.
-
-import (
-	"context"
-	"go/build"
-	"io"
-	"log"
-	"net/http"
-	"os"
-	"regexp"
-	"runtime"
-	"strings"
-
-	"golang.org/x/tools/godoc"
-	"golang.org/x/tools/godoc/vfs"
-	"golang.org/x/tools/godoc/vfs/gatefs"
-	"golang.org/x/website"
-	"golang.org/x/website/internal/dl"
-	"golang.org/x/website/internal/proxy"
-	"golang.org/x/website/internal/redirect"
-	"golang.org/x/website/internal/short"
-
-	"cloud.google.com/go/datastore"
-	"golang.org/x/website/internal/memcache"
-)
-
-func main() {
-	log.SetFlags(log.Lshortfile | log.LstdFlags)
-
-	playEnabled = true
-
-	log.Println("initializing golang.org server ...")
-
-	fsGate := make(chan bool, 20)
-
-	rootfs := gatefs.New(vfs.OS(runtime.GOROOT()), fsGate)
-	fs.Bind("/", rootfs, "/", vfs.BindReplace)
-
-	// Try serving files in /doc from a local copy before trying the main
-	// go repository. This lets us update some documentation outside the
-	// Go release cycle. This includes root.html, which redirects to "/".
-	// See golang.org/issue/29206.
-	fs.Bind("/doc", vfs.FromFS(website.Content), "/doc", vfs.BindBefore)
-	fs.Bind("/lib/godoc", vfs.FromFS(website.Content), "/", vfs.BindReplace)
-
-	webroot := getFullPath("/src/golang.org/x/website")
-	fs.Bind("/favicon.ico", gatefs.New(vfs.OS(webroot), fsGate), "/favicon.ico", vfs.BindBefore)
-
-	corpus := godoc.NewCorpus(fs)
-	corpus.Verbose = false
-	corpus.MaxResults = 10000 // matches flag default in main.go
-	corpus.IndexEnabled = false
-	if err := corpus.Init(); err != nil {
-		log.Fatal(err)
-	}
-	corpus.InitVersionInfo()
-
-	pres = godoc.NewPresentation(corpus)
-	pres.ShowPlayground = true
-	pres.DeclLinks = true
-	pres.NotesRx = regexp.MustCompile("BUG")
-	pres.GoogleAnalytics = os.Getenv("GOLANGORG_ANALYTICS")
-
-	readTemplates(pres)
-
-	datastoreClient, memcacheClient := getClients()
-
-	// NOTE(cbro): registerHandlers registers itself against DefaultServeMux.
-	// The mux returned has host enforcement, so it's important to register
-	// against this mux and not DefaultServeMux.
-	mux := registerHandlers(pres)
-	dl.RegisterHandlers(mux, datastoreClient, memcacheClient)
-	short.RegisterHandlers(mux, datastoreClient, memcacheClient)
-
-	// Register /compile and /share handlers against the default serve mux
-	// so that other app modules can make plain HTTP requests to those
-	// hosts. (For reasons, HTTPS communication between modules is broken.)
-	proxy.RegisterHandlers(http.DefaultServeMux)
-
-	http.HandleFunc("/_ah/health", func(w http.ResponseWriter, r *http.Request) {
-		io.WriteString(w, "ok")
-	})
-
-	http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
-		io.WriteString(w, "User-agent: *\nDisallow: /search\n")
-	})
-
-	if err := redirect.LoadChangeMap("hg-git-mapping.bin"); err != nil {
-		log.Fatalf("LoadChangeMap: %v", err)
-	}
-
-	log.Println("godoc initialization complete")
-
-	// TODO(cbro): add instrumentation via opencensus.
-	port := "8080"
-	if p := os.Getenv("PORT"); p != "" { // PORT is set by GAE flex.
-		port = p
-	}
-	log.Fatal(http.ListenAndServe(":"+port, nil))
-}
-
-func getFullPath(relPath string) string {
-	gopath := os.Getenv("GOPATH")
-	if gopath == "" {
-		gopath = build.Default.GOPATH
-	}
-	return gopath + relPath
-}
-
-func getClients() (*datastore.Client, *memcache.Client) {
-	ctx := context.Background()
-
-	datastoreClient, err := datastore.NewClient(ctx, "")
-	if err != nil {
-		if strings.Contains(err.Error(), "missing project") {
-			log.Fatalf("Missing datastore project. Set the DATASTORE_PROJECT_ID env variable. Use `gcloud beta emulators datastore` to start a local datastore.")
-		}
-		log.Fatalf("datastore.NewClient: %v.", err)
-	}
-
-	redisAddr := os.Getenv("GOLANGORG_REDIS_ADDR")
-	if redisAddr == "" {
-		log.Fatalf("Missing redis server for golangorg in production mode. set GOLANGORG_REDIS_ADDR environment variable.")
-	}
-	memcacheClient := memcache.New(redisAddr)
-	return datastoreClient, memcacheClient
-}
diff --git a/cmd/golangorg/blog.go b/cmd/golangorg/blog.go
index 219fec8..e9edade 100644
--- a/cmd/golangorg/blog.go
+++ b/cmd/golangorg/blog.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 package main
 
 import (
diff --git a/cmd/golangorg/codewalk.go b/cmd/golangorg/codewalk.go
index 91c8cdc..961b008 100644
--- a/cmd/golangorg/codewalk.go
+++ b/cmd/golangorg/codewalk.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 // The /doc/codewalk/ tree is synthesized from codewalk descriptions,
 // files named $GOROOT/doc/codewalk/*.xml.
 // For an example and a description of the format, see
diff --git a/cmd/golangorg/dl.go b/cmd/golangorg/dl.go
deleted file mode 100644
index edeecb8..0000000
--- a/cmd/golangorg/dl.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2014 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.
-
-// +build !golangorg
-
-package main
-
-import "net/http"
-
-// Register a redirect handler for /dl/ to the golang.org download page.
-// This file will not be included when deploying godoc to golang.org.
-
-func init() {
-	http.Handle("/dl/", http.RedirectHandler("https://golang.org/dl/", http.StatusFound))
-}
diff --git a/cmd/golangorg/go115.go b/cmd/golangorg/go115.go
index c53a808..38174dc 100644
--- a/cmd/golangorg/go115.go
+++ b/cmd/golangorg/go115.go
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build !go1.16
 // +build !go1.16
 
 package main
diff --git a/cmd/golangorg/godoc.go b/cmd/golangorg/godoc.go
index 74a8978..54b71a3 100644
--- a/cmd/golangorg/godoc.go
+++ b/cmd/golangorg/godoc.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 package main
 
 import (
diff --git a/cmd/golangorg/godoc_test.go b/cmd/golangorg/godoc_test.go
index 6afe551..1e18436 100644
--- a/cmd/golangorg/godoc_test.go
+++ b/cmd/golangorg/godoc_test.go
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
 // +build go1.16
 
 package main_test
@@ -77,14 +78,6 @@
 		false)
 }
 
-func waitForSearchReady(t *testing.T, addr string) {
-	waitForServer(t,
-		fmt.Sprintf("http://%v/search?q=FALLTHROUGH", addr),
-		"The list of tokens.",
-		2*time.Minute,
-		false)
-}
-
 func waitUntilScanComplete(t *testing.T, addr string) {
 	waitForServer(t,
 		fmt.Sprintf("http://%v/pkg", addr),
diff --git a/cmd/golangorg/goroot.go b/cmd/golangorg/goroot.go
deleted file mode 100644
index 998e869..0000000
--- a/cmd/golangorg/goroot.go
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2018 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 main
-
-import (
-	"os"
-	"path/filepath"
-	"runtime"
-)
-
-// Copies of functions from src/cmd/go/internal/cfg/cfg.go for
-// finding the GOROOT.
-// Keep them in sync until support is moved to a common place, if ever.
-
-func findGOROOT() string {
-	if env := os.Getenv("GOROOT"); env != "" {
-		return filepath.Clean(env)
-	}
-	def := filepath.Clean(runtime.GOROOT())
-	if runtime.Compiler == "gccgo" {
-		// gccgo has no real GOROOT, and it certainly doesn't
-		// depend on the executable's location.
-		return def
-	}
-	exe, err := os.Executable()
-	if err == nil {
-		exe, err = filepath.Abs(exe)
-		if err == nil {
-			if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
-				// If def (runtime.GOROOT()) and dir are the same
-				// directory, prefer the spelling used in def.
-				if isSameDir(def, dir) {
-					return def
-				}
-				return dir
-			}
-			exe, err = filepath.EvalSymlinks(exe)
-			if err == nil {
-				if dir := filepath.Join(exe, "../.."); isGOROOT(dir) {
-					if isSameDir(def, dir) {
-						return def
-					}
-					return dir
-				}
-			}
-		}
-	}
-	return def
-}
-
-// isGOROOT reports whether path looks like a GOROOT.
-//
-// It does this by looking for the path/pkg/tool directory,
-// which is necessary for useful operation of the cmd/go tool,
-// and is not typically present in a GOPATH.
-func isGOROOT(path string) bool {
-	stat, err := os.Stat(filepath.Join(path, "pkg", "tool"))
-	if err != nil {
-		return false
-	}
-	return stat.IsDir()
-}
-
-// isSameDir reports whether dir1 and dir2 are the same directory.
-func isSameDir(dir1, dir2 string) bool {
-	if dir1 == dir2 {
-		return true
-	}
-	info1, err1 := os.Stat(dir1)
-	info2, err2 := os.Stat(dir2)
-	return err1 == nil && err2 == nil && os.SameFile(info1, info2)
-}
diff --git a/cmd/golangorg/handlers.go b/cmd/golangorg/handlers.go
index 1cafdcb..57c6cc5 100644
--- a/cmd/golangorg/handlers.go
+++ b/cmd/golangorg/handlers.go
@@ -2,13 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// The /doc/codewalk/ tree is synthesized from codewalk descriptions,
-// files named $GOROOT/doc/codewalk/*.xml.
-// For an example and a description of the format, see
-// http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
-// and see http://localhost:6060/doc/codewalk/codewalk .
-// That page is itself a codewalk; the source code for it is
-// $GOROOT/doc/codewalk/codewalk.xml.
+//go:build go1.16
+// +build go1.16
 
 package main
 
diff --git a/cmd/golangorg/local.go b/cmd/golangorg/local.go
new file mode 100644
index 0000000..ea32c54
--- /dev/null
+++ b/cmd/golangorg/local.go
@@ -0,0 +1,40 @@
+// Copyright 2014 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.
+
+//go:build go1.16 && !prod
+// +build go1.16,!prod
+
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"path/filepath"
+	"runtime"
+
+	// This package registers "/compile" and "/share" handlers
+	// that redirect to the golang.org playground.
+	_ "golang.org/x/tools/playground"
+)
+
+func earlySetup() {
+	_, file, _, ok := runtime.Caller(0)
+	if !ok {
+		fmt.Fprintln(os.Stderr, "runtime.Caller failed: cannot find templates for -a mode.")
+		os.Exit(2)
+	}
+	dir := filepath.Join(file, "../../../_content")
+	if _, err := os.Stat(filepath.Join(dir, "godoc.html")); err != nil {
+		log.Printf("warning: cannot find template dir; using embedded copy")
+		return
+	}
+	*templateDir = dir
+}
+
+func lateSetup(mux *http.ServeMux) {
+	// Register a redirect handler for /dl/ to the golang.org download page.
+	http.Handle("/dl/", http.RedirectHandler("https://golang.org/dl/", http.StatusFound))
+}
diff --git a/cmd/golangorg/main.go b/cmd/golangorg/main.go
index d6534fe..3faf663 100644
--- a/cmd/golangorg/main.go
+++ b/cmd/golangorg/main.go
@@ -15,22 +15,16 @@
 //				https://golang.org/pkg/compress/zlib)
 //
 
-// Some pages are being transitioned from $GOROOT to content/doc.
-// See golang.org/issue/29206 and golang.org/issue/33637.
-
+//go:build go1.16
 // +build go1.16
-// +build !golangorg
 
 package main
 
 import (
-	_ "expvar" // to serve /debug/vars
 	"flag"
 	"fmt"
-	"go/build"
 	"log"
 	"net/http"
-	_ "net/http/pprof" // to serve /debug/pprof/*
 	"os"
 	"path/filepath"
 	"regexp"
@@ -42,39 +36,19 @@
 	"golang.org/x/website"
 )
 
-const defaultAddr = "localhost:6060" // default webserver address
-
 var (
-	// network
-	httpAddr = flag.String("http", defaultAddr, "HTTP service address")
-
-	verbose = flag.Bool("v", false, "verbose mode")
-
-	// file system roots
-	// TODO(gri) consider the invariant that goroot always end in '/'
-	goroot = flag.String("goroot", findGOROOT(), "Go root directory")
-
-	// layout control
-	autoFlag       = flag.Bool("a", false, "update templates automatically")
+	httpAddr       = flag.String("http", "localhost:6060", "HTTP service address")
+	verbose        = flag.Bool("v", false, "verbose mode")
+	goroot         = flag.String("goroot", runtime.GOROOT(), "Go root directory")
 	showTimestamps = flag.Bool("timestamps", false, "show timestamps with directory listings")
 	templateDir    = flag.String("templates", "", "load templates/JS/CSS from disk in this directory (usually /path-to-website/content)")
-	showPlayground = flag.Bool("play", false, "enable playground")
+	showPlayground = flag.Bool("play", true, "enable playground")
 	declLinks      = flag.Bool("links", true, "link identifiers to their declarations")
-
-	// source code notes
-	notesRx = flag.String("notes", "BUG", "regular expression matching note markers to show")
+	notesRx        = flag.String("notes", "BUG", "regular expression matching note markers to show")
 )
 
-func getFullPath(relPath string) string {
-	gopath := os.Getenv("GOPATH")
-	if gopath == "" {
-		gopath = build.Default.GOPATH
-	}
-	return gopath + relPath
-}
-
 func usage() {
-	fmt.Fprintf(os.Stderr, "usage: golangorg -http="+defaultAddr+"\n")
+	fmt.Fprintf(os.Stderr, "usage: golangorg\n")
 	flag.PrintDefaults()
 	os.Exit(2)
 }
@@ -86,37 +60,12 @@
 	})
 }
 
-func initCorpus(corpus *godoc.Corpus) {
-	err := corpus.Init()
-	if err != nil {
-		log.Fatal(err)
-	}
-}
-
 func main() {
+	earlySetup()
+
 	flag.Usage = usage
 	flag.Parse()
 
-	// Find templates in -a mode.
-	if *autoFlag {
-		if *templateDir != "" {
-			fmt.Fprintln(os.Stderr, "Cannot use -a and -templates together.")
-			usage()
-		}
-		_, file, _, ok := runtime.Caller(0)
-		if !ok {
-			fmt.Fprintln(os.Stderr, "runtime.Caller failed: cannot find templates for -a mode.")
-			os.Exit(2)
-		}
-		dir := filepath.Join(file, "../../../_content")
-		if _, err := os.Stat(filepath.Join(dir, "godoc.html")); err != nil {
-			fmt.Fprintln(os.Stderr, err)
-			fmt.Fprintln(os.Stderr, "Cannot find templates for -a mode.")
-			os.Exit(2)
-		}
-		*templateDir = dir
-	}
-
 	playEnabled = *showPlayground
 
 	// Check usage.
@@ -129,14 +78,12 @@
 		usage()
 	}
 
-	// Set the resolved goroot.
-	vfs.GOROOT = *goroot
-
 	fsGate := make(chan bool, 20)
 
 	// Determine file system to use.
 	rootfs := gatefs.New(vfs.OS(*goroot), fsGate)
 	fs.Bind("/", rootfs, "/", vfs.BindReplace)
+
 	// Try serving files in /doc from a local copy before trying the main
 	// go repository. This lets us update some documentation outside the
 	// Go release cycle. This includes root.html, which redirects to "/".
@@ -144,25 +91,22 @@
 	if *templateDir != "" {
 		fs.Bind("/doc", vfs.OS(*templateDir), "/doc", vfs.BindBefore)
 		fs.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore)
+		root := filepath.Join(*templateDir, "..")
+		fs.Bind("/robots.txt", vfs.OS(root), "/robots.txt", vfs.BindBefore)
+		fs.Bind("/favicon.ico", vfs.OS(root), "/favicon.ico", vfs.BindBefore)
 	} else {
 		fs.Bind("/doc", vfs.FromFS(website.Content), "/doc", vfs.BindBefore)
 		fs.Bind("/lib/godoc", vfs.FromFS(website.Content), "/", vfs.BindReplace)
+		fs.Bind("/robots.txt", vfs.FromFS(website.Root), "/robots.txt", vfs.BindBefore)
+		fs.Bind("/favicon.ico", vfs.FromFS(website.Root), "/favicon.ico", vfs.BindBefore)
 	}
 
-	// Bind $GOPATH trees into Go root.
-	for _, p := range filepath.SplitList(build.Default.GOPATH) {
-		fs.Bind("/src", gatefs.New(vfs.OS(p), fsGate), "/src", vfs.BindAfter)
-	}
-
-	webroot := getFullPath("/src/golang.org/x/website")
-	fs.Bind("/robots.txt", gatefs.New(vfs.OS(webroot), fsGate), "/robots.txt", vfs.BindBefore)
-	fs.Bind("/favicon.ico", gatefs.New(vfs.OS(webroot), fsGate), "/favicon.ico", vfs.BindBefore)
-
 	corpus := godoc.NewCorpus(fs)
 	corpus.Verbose = *verbose
-
-	go initCorpus(corpus)
-
+	corpus.IndexEnabled = false
+	if err := corpus.Init(); err != nil {
+		log.Fatal(err)
+	}
 	// Initialize the version info before readTemplates, which saves
 	// the map value in a method value.
 	corpus.InitVersionInfo()
@@ -176,7 +120,8 @@
 	}
 
 	readTemplates(pres)
-	registerHandlers(pres)
+	mux := registerHandlers(pres)
+	lateSetup(mux)
 
 	var handler http.Handler = http.DefaultServeMux
 	if *verbose {
diff --git a/cmd/golangorg/play.go b/cmd/golangorg/play.go
deleted file mode 100644
index f44a3cc..0000000
--- a/cmd/golangorg/play.go
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright 2012 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.
-
-// +build !golangorg
-
-package main
-
-// This package registers "/compile" and "/share" handlers
-// that redirect to the golang.org playground.
-import _ "golang.org/x/tools/playground"
diff --git a/cmd/golangorg/prod.go b/cmd/golangorg/prod.go
new file mode 100644
index 0000000..e2b8328
--- /dev/null
+++ b/cmd/golangorg/prod.go
@@ -0,0 +1,83 @@
+// Copyright 2011 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.
+
+//go:build go1.16 && prod
+// +build go1.16,prod
+
+package main
+
+import (
+	"context"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"strings"
+
+	"golang.org/x/website/internal/dl"
+	"golang.org/x/website/internal/proxy"
+	"golang.org/x/website/internal/redirect"
+	"golang.org/x/website/internal/short"
+
+	"cloud.google.com/go/datastore"
+	"golang.org/x/website/internal/memcache"
+)
+
+func earlySetup() {
+	log.SetFlags(log.Lshortfile | log.LstdFlags)
+	log.Println("initializing golang.org server ...")
+
+	port := "8080"
+	if p := os.Getenv("PORT"); p != "" {
+		port = p
+	}
+	*httpAddr = ":" + port
+}
+
+func lateSetup(mux *http.ServeMux) {
+	pres.GoogleAnalytics = os.Getenv("GOLANGORG_ANALYTICS")
+
+	datastoreClient, memcacheClient := getClients()
+
+	dl.RegisterHandlers(mux, datastoreClient, memcacheClient)
+	short.RegisterHandlers(mux, datastoreClient, memcacheClient)
+
+	// Register /compile and /share handlers against the default serve mux
+	// so that other app modules can make plain HTTP requests to those
+	// hosts. (For reasons, HTTPS communication between modules is broken.)
+	proxy.RegisterHandlers(http.DefaultServeMux)
+
+	http.HandleFunc("/_ah/health", func(w http.ResponseWriter, r *http.Request) {
+		io.WriteString(w, "ok")
+	})
+
+	http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
+		io.WriteString(w, "User-agent: *\nDisallow: /search\n")
+	})
+
+	if err := redirect.LoadChangeMap("hg-git-mapping.bin"); err != nil {
+		log.Fatalf("LoadChangeMap: %v", err)
+	}
+
+	log.Println("godoc initialization complete")
+}
+
+func getClients() (*datastore.Client, *memcache.Client) {
+	ctx := context.Background()
+
+	datastoreClient, err := datastore.NewClient(ctx, "")
+	if err != nil {
+		if strings.Contains(err.Error(), "missing project") {
+			log.Fatalf("Missing datastore project. Set the DATASTORE_PROJECT_ID env variable. Use `gcloud beta emulators datastore` to start a local datastore.")
+		}
+		log.Fatalf("datastore.NewClient: %v.", err)
+	}
+
+	redisAddr := os.Getenv("GOLANGORG_REDIS_ADDR")
+	if redisAddr == "" {
+		log.Fatalf("Missing redis server for golangorg in production mode. set GOLANGORG_REDIS_ADDR environment variable.")
+	}
+	memcacheClient := memcache.New(redisAddr)
+	return datastoreClient, memcacheClient
+}
diff --git a/cmd/golangorg/project.go b/cmd/golangorg/project.go
index dd5e8d3..ca57382 100644
--- a/cmd/golangorg/project.go
+++ b/cmd/golangorg/project.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 package main
 
 import (
diff --git a/cmd/golangorg/regtest_test.go b/cmd/golangorg/regtest_test.go
index 84e83a5..28c40f1 100644
--- a/cmd/golangorg/regtest_test.go
+++ b/cmd/golangorg/regtest_test.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 // Regression tests to run against a production instance of godoc.
 
 package main_test
diff --git a/cmd/golangorg/release.go b/cmd/golangorg/release.go
index ea62317..0c5d9d1 100644
--- a/cmd/golangorg/release.go
+++ b/cmd/golangorg/release.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 package main
 
 import (
diff --git a/cmd/golangorg/x.go b/cmd/golangorg/x.go
index 1b3de3f..af2a068 100644
--- a/cmd/golangorg/x.go
+++ b/cmd/golangorg/x.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 // This file contains the handlers that serve go-import redirects for Go
 // sub-repositories. It specifies the mapping from import paths like
 // "golang.org/x/tools" to the actual repository locations.
diff --git a/cmd/golangorg/x_test.go b/cmd/golangorg/x_test.go
index a97dda9..1cd5afb 100644
--- a/cmd/golangorg/x_test.go
+++ b/cmd/golangorg/x_test.go
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
+// +build go1.16
+
 package main
 
 import (
diff --git a/content.go b/content.go
index b3b7245..58646fb 100644
--- a/content.go
+++ b/content.go
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.16
 // +build go1.16
 
 // Package website exports the static content as an embed.FS.
@@ -25,3 +26,7 @@
 	}
 	return s
 }
+
+// Root is the website root files: favicon.ico and robots.txt.
+//go:embed favicon.ico robots.txt
+var Root embed.FS