internal/web: merge with go.dev/cmd/internal/site
internal/web was the framework left serving golang.org.
go.dev/cmd/internal/site was the framework serving go.dev.
This CL merges the two into a coherent, simple site serving
framework that works for both sites, a step toward merging
the sites themselves.
The CL is difficult to break up, so it's a bit larger than would be ideal.
The best place to start is the doc comment in internal/web/site.go
and then the other changes in that directory.
The rest of the CL is just minor adjustments to the repo to match.
Change-Id: I927dea29396104a817bd81b6bf25fa43f996968f
Reviewed-on: https://go-review.googlesource.com/c/website/+/339403
Trust: Russ Cox <rsc@golang.org>
Website-Publish: Russ Cox <rsc@golang.org>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/_content/lib/godoc/codewalk.html b/_content/codewalk.tmpl
similarity index 97%
rename from _content/lib/godoc/codewalk.html
rename to _content/codewalk.tmpl
index 89360bf..511a095 100644
--- a/_content/lib/godoc/codewalk.html
+++ b/_content/codewalk.tmpl
@@ -4,7 +4,8 @@
license that can be found in the LICENSE file.
-->
-{{with .Data}}
+{{define "layout"}}
+{{with .codewalk}}
<style type='text/css'>@import "/doc/codewalk/codewalk.css";</style>
<script type="text/javascript" src="/doc/codewalk/codewalk.js"></script>
@@ -56,3 +57,4 @@
</div>
</div>
{{end}}
+{{end}}
diff --git a/_content/lib/godoc/codewalkdir.html b/_content/codewalkdir.tmpl
similarity index 86%
rename from _content/lib/godoc/codewalkdir.html
rename to _content/codewalkdir.tmpl
index 5479429..5912e19 100644
--- a/_content/lib/godoc/codewalkdir.html
+++ b/_content/codewalkdir.tmpl
@@ -4,8 +4,9 @@
license that can be found in the LICENSE file.
-->
+{{define "layout"}}
<table class="layout">
-{{range .Data}}
+{{range .dirs}}
<tr>
<td><a href="{{.Name}}">{{.Name}}</a></td>
<td width="25"> </td>
@@ -13,3 +14,4 @@
</tr>
{{end}}
</table>
+{{end}}
diff --git a/_content/lib/godoc/dirlist.html b/_content/dir.tmpl
similarity index 86%
rename from _content/lib/godoc/dirlist.html
rename to _content/dir.tmpl
index 6f32403..51db1c5 100644
--- a/_content/lib/godoc/dirlist.html
+++ b/_content/dir.tmpl
@@ -4,7 +4,7 @@
license that can be found in the LICENSE file.
-->
-{{with .Data}}
+{{define "layout"}}
<p>
<table class="layout">
<tr>
@@ -15,10 +15,11 @@
<tr>
<td><a href="../">../</a></td>
</tr>
-{{range .}}{{if .IsDir}}
+
+{{range .dir}}{{if .IsDir}}
<tr><td align="left"><a href="{{.Name}}/">{{.Name}}/</a><td></tr>
{{end}}{{end}}
-{{range .}}{{if not .IsDir}}
+{{range .dir}}{{if not .IsDir}}
<tr><td align="left"><a href="{{.Name}}">{{.Name}}</a><td align="right">{{.Size}}</tr>
{{end}}{{end}}
diff --git a/_content/lib/godoc/dl.html b/_content/dl.tmpl
similarity index 98%
rename from _content/lib/godoc/dl.html
rename to _content/dl.tmpl
index 11a893a..4cbc922 100644
--- a/_content/lib/godoc/dl.html
+++ b/_content/dl.tmpl
@@ -1,4 +1,5 @@
-{{with .Data}}
+{{define "layout"}}
+{{with .dl}}
<p>
After downloading a binary release suitable for your system,
please follow the <a href="/doc/install">installation instructions</a>.
@@ -157,3 +158,4 @@
</div>
</a>
{{end}}
+{{end}}
diff --git a/_content/doc/code.html b/_content/doc/code.html
index 1f020c9..ffa9b08 100644
--- a/_content/doc/code.html
+++ b/_content/doc/code.html
@@ -438,7 +438,7 @@
</p>
<p>
-Take {{if $.GoogleCN}}
+Take {{if googleCN}}
A Tour of Go
{{else}}
<a href="//tour.golang.org/">A Tour of Go</a>
diff --git a/_content/doc/index.html b/_content/doc/index.html
index a55c828..7daf45b 100644
--- a/_content/doc/index.html
+++ b/_content/doc/index.html
@@ -58,7 +58,7 @@
<img class="gopher" src="/doc/gopher/doc.png" alt=""/>
<h3 id="go_tour">
- {{if $.GoogleCN}}
+ {{if googleCN}}
A Tour of Go
{{else}}
<a href="//tour.golang.org/">A Tour of Go</a>
@@ -69,7 +69,7 @@
The first section covers basic syntax and data structures; the second discusses
methods and interfaces; and the third introduces Go's concurrency primitives.
Each section concludes with a few exercises so you can practice what you've
-learned. You can {{if not $.GoogleCN}}<a href="//tour.golang.org/">take the tour
+learned. You can {{if not googleCN}}<a href="//tour.golang.org/">take the tour
online</a> or{{end}} install it locally with:
</p>
<pre>
@@ -254,7 +254,7 @@
<li><a href="/doc/codewalk/sharemem">Share Memory by Communicating</a></li>
</ul>
-{{if not $.GoogleCN}}
+{{if not googleCN}}
<h2 id="blog">From the Go Blog</h2>
<p>The <a href="//blog.golang.org/">official blog of the Go project</a>, featuring news and in-depth articles by
the Go team and guests.</p>
@@ -296,7 +296,7 @@
<li><a href="/doc/gdb">Debugging Go Code with GDB</a></li>
<li><a href="/doc/articles/race_detector.html">Data Race Detector</a> - a manual for the data race detector.</li>
<li><a href="/doc/asm">A Quick Guide to Go's Assembler</a> - an introduction to the assembler used by Go.</li>
-{{if not $.GoogleCN}}
+{{if not googleCN}}
<li><a href="/blog/c-go-cgo">C? Go? Cgo!</a> - linking against C code with <a href="/cmd/cgo/">cgo</a>.</li>
<li><a href="/blog/godoc-documenting-go-code">Godoc: documenting Go code</a> - writing good documentation for <a href="/cmd/godoc/">godoc</a>.</li>
<li><a href="/blog/profiling-go-programs">Profiling Go Programs</a></li>
@@ -314,7 +314,7 @@
for more Go learning resources.
</p>
-{{if not $.GoogleCN}}
+{{if not googleCN}}
<h2 id="talks">Talks</h2>
<img class="gopher" src="/doc/gopher/talks.png" alt=""/>
diff --git a/_content/lib/godoc/error.html b/_content/error.tmpl
similarity index 68%
rename from _content/lib/godoc/error.html
rename to _content/error.tmpl
index cb5325e..30fe79d 100644
--- a/_content/lib/godoc/error.html
+++ b/_content/error.tmpl
@@ -4,8 +4,8 @@
license that can be found in the LICENSE file.
-->
-{{with .Data}}
+{{define "layout"}}
<p>
-<span class="alert" style="font-size:120%">{{.}}</span>
+<span class="alert" style="font-size:120%">{{.error}}</span>
</p>
{{end}}
diff --git a/_content/help.html b/_content/help.html
index e6da3ba..2b53e44 100644
--- a/_content/help.html
+++ b/_content/help.html
@@ -9,7 +9,7 @@
<img class="gopher" src="/doc/gopher/help.png" alt=""/>
-{{if not $.GoogleCN}}
+{{if not googleCN}}
<h3 id="mailinglist"><a href="https://groups.google.com/group/golang-nuts">Go Nuts Mailing List</a></h3>
<p>
Get help from Go users, and share your work on the official mailing list.
@@ -42,7 +42,7 @@
<h3 id="faq"><a href="/doc/faq">Frequently Asked Questions (FAQ)</a></h3>
<p>Answers to common questions about Go.</p>
-{{if not $.GoogleCN}}
+{{if not googleCN}}
<h2 id="inform">Stay informed</h2>
<h3 id="announce"><a href="https://groups.google.com/group/golang-announce">Go Announcements Mailing List</a></h3>
@@ -79,7 +79,7 @@
meet to talk about Go. Find a chapter near you.
</p>
-{{if not $.GoogleCN}}
+{{if not googleCN}}
<h3 id="playground"><a href="/play">Go Playground</a></h3>
<p>A place to write, run, and share Go code.</p>
diff --git a/_content/index.html b/_content/index.html
index 51cb389..ae08edd 100644
--- a/_content/index.html
+++ b/_content/index.html
@@ -22,7 +22,7 @@
<section class="HomeSection Playground">
<div class="Playground-headerContainer">
<h2 class="HomeSection-header">Try Go</h2>
- {{if not $.GoogleCN}}
+ {{if not googleCN}}
<a class="Playground-popout js-playgroundShareEl">Open in Playground</a>
{{end}}
</div>
@@ -55,7 +55,7 @@
<div class="Playground-buttons">
<button class="Button Button--primary js-playgroundRunEl" title="Run this code [shift-enter]">Run</button>
<div class="Playground-secondaryButtons">
- {{if not $.GoogleCN}}
+ {{if not googleCN}}
<button class="Button js-playgroundShareEl" title="Share this code">Share</button>
<a class="Button tour" href="https://tour.golang.org/" title="Playground Go from your browser">Tour</a>
{{end}}
@@ -64,7 +64,7 @@
</div>
</section>
- {{if not $.GoogleCN}}
+ {{if not googleCN}}
<section class="HomeSection Blog js-blogContainerEl">
<h2 class="HomeSection-header">Featured articles</h2>
<div class="Blog-footer js-blogFooterEl"><a class="Button Button--primary" href="https://blog.golang.org/">Read more ></a></div>
@@ -101,7 +101,7 @@
}
});
- {{if not $.GoogleCN}}
+ {{if not googleCN}}
function readableTime(t) {
var m = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
@@ -175,6 +175,6 @@
var v = videos[Math.floor(Math.random()*videos.length)];
$(".js-videoContainer iframe").attr("src", v.s).attr("title", v.title);
});
- {{end}} {{/* if not .GoogleCN */}}
+ {{end}} {{/* if not googleCN */}}
})();
</script>
diff --git a/_content/lib/godoc/package.html b/_content/pkg.tmpl
similarity index 92%
rename from _content/lib/godoc/package.html
rename to _content/pkg.tmpl
index e2cbd4d..9ffbb0a 100644
--- a/_content/lib/godoc/package.html
+++ b/_content/pkg.tmpl
@@ -9,7 +9,9 @@
them to conflict with generated attributes (some of which
correspond to Go identifiers).
-->
-{{$pkg := .Data}}
+{{define "layout"}}
+{{$canShare := not googleCN}}
+{{$pkg := .pkg}}
{{with $pkg.PDoc}}
{{if $pkg.IsMain}}
{{/* command documentation */}}
@@ -39,7 +41,7 @@
<div class="expanded">
<h2 class="toggleButton" title="Click to hide Overview section">Overview ▾</h2>
{{$pkg.Comment .Doc}}
- {{range $pkg.FmtExamples ""}}{{template "example" .}}{{end}}
+ {{range $pkg.FmtExamples ""}}{{example . $canShare}}{{end}}
</div>
</div>
@@ -95,7 +97,7 @@
<p>
<span style="font-size:90%">
{{range .}}
- <a href="/{{.}}">{{basename .}}</a>
+ <a href="/{{.}}">{{path.Base .}}</a>
{{end}}
</span>
</p>
@@ -126,7 +128,7 @@
</h2>
<pre>{{$pkg.Node .Decl}}</pre>
{{$pkg.Comment .Doc}}
- {{range $pkg.FmtExamples .Name}}{{template "example" .}}{{end}}
+ {{range $pkg.FmtExamples .Name}}{{example . $canShare}}{{end}}
{{end}}
{{range .Types}}
{{$typeName := .Name}}
@@ -148,7 +150,7 @@
<pre>{{$pkg.Node .Decl}}</pre>
{{end}}
- {{range $pkg.FmtExamples .Name}}{{template "example" .}}{{end}}
+ {{range $pkg.FmtExamples .Name}}{{example . $canShare}}{{end}}
{{range .Funcs}}
<h3 id="{{.Name}}">func <a href="{{$pkg.SrcPosLink .Decl}}">{{.Name}}</a>
@@ -158,7 +160,7 @@
</h3>
<pre>{{$pkg.Node .Decl}}</pre>
{{$pkg.Comment .Doc}}
- {{range $pkg.FmtExamples .Name}}{{template "example" .}}{{end}}
+ {{range $pkg.FmtExamples .Name}}{{example . $canShare}}{{end}}
{{end}}
{{range .Methods}}
@@ -169,7 +171,7 @@
</h3>
<pre>{{$pkg.Node .Decl}}</pre>
{{$pkg.Comment .Doc}}
- {{range $pkg.FmtExamples (printf "%s_%s" $typeName .Name)}}{{template "example" .}}{{end}}
+ {{range $pkg.FmtExamples (printf "%s_%s" $typeName .Name)}}{{example . $canShare}}{{end}}
{{end}}
{{end}}
{{end}}
@@ -223,8 +225,11 @@
</table>
</div>
{{end}}
+{{end}}
-{{define "example"}}
+{{define "example ex canShare"}}
+{{$canShare := .canShare}}
+{{with .ex}}
<div id="example_{{.Name}}" class="toggle">
<div class="collapsed">
<p class="exampleHeading toggleButton">▹ <span class="text">Example{{.Page.ExampleSuffix .Name}}</span></p>
@@ -240,7 +245,7 @@
<div class="buttons">
<button class="Button Button--primary run" title="Run this code [shift-enter]">Run</button>
<button class="Button fmt" title="Format this code">Format</button>
- {{if not $.Page.Web.GoogleCN}}
+ {{if $canShare}}
<button class="Button share" title="Share this code">Share</button>
{{end}}
</div>
@@ -256,3 +261,4 @@
</div>
</div>
{{end}}
+{{end}}
diff --git a/_content/lib/godoc/packageroot.html b/_content/pkgroot.tmpl
similarity index 98%
rename from _content/lib/godoc/packageroot.html
rename to _content/pkgroot.tmpl
index aaa2c53..b972cd9 100644
--- a/_content/lib/godoc/packageroot.html
+++ b/_content/pkgroot.tmpl
@@ -9,7 +9,8 @@
them to conflict with generated attributes (some of which
correspond to Go identifiers).
-->
-{{$pkg := .Data}}
+{{define "layout"}}
+{{$pkg := .pkg}}
{{with $pkg.Dirs}}
{{/* DirList entries are numbers and strings - no need for FSet */}}
@@ -99,3 +100,4 @@
<li><a href="/wiki/Projects">Projects at the Go Wiki</a> - a curated list of Go projects.</li>
</ul>
{{end}}
+{{end}}
diff --git a/_content/lib/godoc/site.html b/_content/site.tmpl
similarity index 72%
rename from _content/lib/godoc/site.html
rename to _content/site.tmpl
index 09624be..129d5b7 100644
--- a/_content/lib/godoc/site.html
+++ b/_content/site.tmpl
@@ -4,7 +4,7 @@
<meta name="description" content="Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#00ADD8">
-{{with .TabTitle}}
+{{with (or .tabTitle .title (strings.Trim .URL "/"))}}
<title>{{.}} - The Go Programming Language</title>
{{else}}
<title>The Go Programming Language</title>
@@ -13,7 +13,7 @@
<link href="https://fonts.googleapis.com/css?family=Product+Sans&text=Supported%20by%20Google&display=swap" rel="stylesheet">
<link type="text/css" rel="stylesheet" href="/lib/godoc/style.css">
<script>window.initFuncs = [];</script>
-{{with .GoogleAnalytics}}
+{{with googleAnalytics}}
<script>
var _gaq = _gaq || [];
_gaq.push(["_setAccount", "{{.}}"]);
@@ -29,7 +29,7 @@
<script src="/lib/godoc/jquery.js" defer></script>
<script src="/lib/godoc/playground.js" defer></script>
-{{with .Version}}<script>var goVersion = {{printf "%q" .}};</script>{{end}}
+{{with version}}<script>var goVersion = {{printf "%q" .}};</script>{{end}}
<script src="/lib/godoc/godocs.js" defer></script>
<body class="Site">
@@ -40,7 +40,7 @@
target="_blank"
rel="noopener">Support the Equal Justice Initiative.</a>
</div>
- <nav class="Header-nav {{if .Title}}Header-nav--wide{{end}}">
+ <nav class="Header-nav {{if .title}}Header-nav--wide{{end}}">
<a href="/"><img class="Header-logo" src="/lib/godoc/images/go-logo-blue.svg" alt="Go"></a>
<button class="Header-menuButton js-headerMenuButton" aria-label="Main menu" aria-expanded="false">
<div class="Header-menuButtonInner"></div>
@@ -50,7 +50,7 @@
<li class="Header-menuItem"><a href="/pkg/">Packages</a></li>
<li class="Header-menuItem"><a href="/project/">The Project</a></li>
<li class="Header-menuItem"><a href="/help/">Help</a></li>
- {{if not .GoogleCN}}
+ {{if not googleCN}}
<li class="Header-menuItem"><a href="/blog/">Blog</a></li>
<li class="Header-menuItem"><a href="https://play.golang.org/">Play</a></li>
{{end}}
@@ -58,38 +58,42 @@
</nav>
</header>
-<main id="page" class="Site-content{{if .Title}} wide{{end}}">
+<main id="page" class="Site-content{{if .title}} wide{{end}}">
<div class="container">
-{{define "srcBreadcrumb"}}
- {{$elems := split . "/"}}
- {{$prefix := slice $elems 0 (sub (len $elems) 1)}}
- {{if hasSuffix . "/"}}
- {{$prefix = slice $elems 0 (sub (len $elems) 2)}}
- {{end}}
- {{range $i, $elem := $prefix -}}
- <a href="/{{join (slice $prefix 0 (add $i 1)) "/"}}">{{$elem}}</a>/
- {{- end -}}
- <span class="text-muted">{{join (slice $elems (len $prefix) (len $elems)) "/"}} {{len $prefix}} {{len $elems}}</span>
+{{define "breadcrumb"}}
+ {{$elems := strings.Split (strings.Trim . "/") "/"}}
+ {{$prefix := slice $elems 0 (sub (len $elems) 1)}}
+ {{if strings.HasSuffix . "/"}}
+ {{$prefix = slice $elems 0 (sub (len $elems) 2)}}
+ {{end}}
+ {{range $i, $elem := $prefix -}}
+ <a href="/{{strings.Join (slice $prefix 0 (add $i 1)) "/"}}">{{$elem}}</a>/
+ {{- end -}}
+ <span class="text-muted">{{strings.Join (slice $elems (len $prefix) (len $elems)) "/"}}</span>
{{end}}
-{{if or .Title .SrcPath}}
- <h1>
- {{.Title}}
- {{template "srcBreadcrumb" .SrcPath}}
- </h1>
+{{if .title}}
+ <h1>{{.title}}</h1>
+{{else if eq .layout "error"}}
+ <h1>Error</h1>
+{{else if eq .layout "dir"}}
+ <h1>Directory {{breadcrumb .URL}}</h1>
+{{else if and (eq .layout "texthtml") (strings.HasSuffix .URL ".go")}}
+ <h1>Source file {{breadcrumb .URL}}</h1>
+{{else if eq .layout "texthtml"}}
+ <h1>Text file {{breadcrumb .URL}}</h1>
{{end}}
-{{with .Subtitle}}
+{{with .subtitle}}
<h2>{{.}}</h2>
{{end}}
-{{if hasPrefix .SrcPath "src/"}}
+{{if strings.HasPrefix .URL "/src/"}}
<h2>
Documentation:
- {{$path := trimPrefix .SrcPath "src/"}}
- {{if $path}}
- <a href="/pkg/{{$path}}">{{$path}}</a>
+ {{with strings.TrimPrefix .URL "/src/"}}
+ <a href="/pkg/{{.}}">{{.}}</a>
{{else}}
<a href="/pkg">Index</a>
{{end}}
@@ -100,12 +104,12 @@
Do not delete this <div>. */}}
<div id="nav"></div>
-{{.HTML}}
+{{block "layout" .}}{{.Content}}{{end}}
</div><!-- .container -->
</main><!-- #page -->
<footer>
- <div class="Footer {{if .Title}}Footer--wide{{end}}">
+ <div class="Footer {{if .title}}Footer--wide{{end}}">
<img class="Footer-gopher" src="/lib/godoc/images/footer-gopher.jpg" alt="The Go Gopher">
<ul class="Footer-links">
<li class="Footer-link"><a href="/doc/copyright.html">Copyright</a></li>
@@ -116,7 +120,7 @@
<a class="Footer-supportedBy" href="https://google.com">Supported by Google</a>
</div>
</footer>
-{{if .GoogleAnalytics}}
+{{if googleAnalytics}}
<script>
(function() {
var ga = document.createElement("script"); ga.type = "text/javascript"; ga.async = true;
diff --git a/go.dev/_content/default.tmpl b/_content/texthtml.tmpl
similarity index 66%
rename from go.dev/_content/default.tmpl
rename to _content/texthtml.tmpl
index ae82f43..c7cd1af 100644
--- a/go.dev/_content/default.tmpl
+++ b/_content/texthtml.tmpl
@@ -1,3 +1,3 @@
{{define "layout"}}
-{{.Content}}
+{{.texthtml}}
{{end}}
diff --git a/internal/web/googlecn.go b/cmd/golangorg/googlecn.go
similarity index 71%
rename from internal/web/googlecn.go
rename to cmd/golangorg/googlecn.go
index 82d658b..d1dbb8c 100644
--- a/internal/web/googlecn.go
+++ b/cmd/golangorg/googlecn.go
@@ -2,18 +2,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package web
+package main
import (
"net/http"
"strings"
)
-// GoogleCN reports whether request r is considered to be arriving from China.
+// googleCN reports whether request r is considered to be arriving from China.
// Typically that means the request is for host golang.google.cn,
// but we also report true for requests that set googlecn=1 as a query parameter
-// and requests that App Engine geolocates in China or in “unknown country.”
-func GoogleCN(r *http.Request) bool {
+// and for requests that App Engine geolocates in China.
+func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
@@ -21,7 +21,7 @@
return true
}
switch r.Header.Get("X-Appengine-Country") {
- case "ZZ", "CN":
+ case "CN":
return true
}
return false
diff --git a/cmd/golangorg/server.go b/cmd/golangorg/server.go
index 8924abe..a707371 100644
--- a/cmd/golangorg/server.go
+++ b/cmd/golangorg/server.go
@@ -17,6 +17,7 @@
"log"
"net/http"
"os"
+ "path"
"path/filepath"
"runtime"
"runtime/debug"
@@ -36,6 +37,7 @@
"golang.org/x/website/internal/dl"
"golang.org/x/website/internal/env"
"golang.org/x/website/internal/gitfs"
+ "golang.org/x/website/internal/history"
"golang.org/x/website/internal/memcache"
"golang.org/x/website/internal/pkgdoc"
"golang.org/x/website/internal/proxy"
@@ -52,6 +54,8 @@
contentDir = flag.String("content", "", "path to _content directory")
runningOnAppEngine = os.Getenv("PORT") != ""
+
+ googleAnalytics string
)
func usage() {
@@ -145,6 +149,9 @@
if err != nil {
log.Fatalf("newSite: %v", err)
}
+ if _, err := newSite(mux, "golang.google.cn", content, gorootFS); err != nil {
+ log.Fatalf("newSite: %v", err)
+ }
// 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.
@@ -198,12 +205,15 @@
// and registers it in mux to handle requests for host.
// If host is the empty string, the registrations are for the wildcard host.
func newSite(mux *http.ServeMux, host string, content, goroot fs.FS) (*web.Site, error) {
- fsys := unionFS{content, goroot}
- site, err := web.NewSite(fsys)
- if err != nil {
- return nil, err
- }
- docs, err := pkgdoc.NewServer(fsys, site)
+ fsys := unionFS{content, &fixSpecsFS{goroot}}
+ site := web.NewSite(fsys)
+ site.Funcs(template.FuncMap{
+ "googleAnalytics": func() string { return googleAnalytics },
+ "googleCN": func() bool { return host == "golang.google.cn" },
+ "releases": func() []*history.Major { return history.Majors },
+ "version": func() string { return runtime.Version() },
+ })
+ docs, err := pkgdoc.NewServer(fsys, site, googleCN)
if err != nil {
return nil, err
}
@@ -282,7 +292,7 @@
}
func appEngineSetup(site *web.Site, mux *http.ServeMux) {
- site.GoogleAnalytics = os.Getenv("GOLANGORG_ANALYTICS")
+ googleAnalytics = os.Getenv("GOLANGORG_ANALYTICS")
ctx := context.Background()
@@ -302,7 +312,7 @@
dl.RegisterHandlers(mux, site, datastoreClient, memcacheClient)
short.RegisterHandlers(mux, datastoreClient, memcacheClient)
- proxy.RegisterHandlers(mux)
+ proxy.RegisterHandlers(mux, googleCN)
log.Println("AppEngine initialization complete")
}
@@ -339,22 +349,30 @@
}
// hostEnforcerHandler redirects http://foo.golang.org/bar to https://golang.org/bar.
-// It permits golang.google.cn as an alias for golang.org, for use in China.
+// It also forces all requests coming from China to use golang.google.cn.
func hostEnforcerHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
isHTTPS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" || r.URL.Scheme == "https"
+ defaultHost := "golang.org"
isValidHost := validHosts[strings.ToLower(r.Host)]
+ if googleCN(r) {
+ // golang.google.cn is the only web site in China.
+ defaultHost = "golang.google.cn"
+ isValidHost = strings.ToLower(r.Host) == defaultHost
+ }
+
if !isHTTPS || !isValidHost {
r.URL.Scheme = "https"
if isValidHost {
r.URL.Host = r.Host
} else {
- r.URL.Host = "golang.org"
+ r.URL.Host = defaultHost
}
http.Redirect(w, r, r.URL.String(), http.StatusFound)
return
}
+
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
h.ServeHTTP(w, r)
})
@@ -543,6 +561,33 @@
return nil, errOut
}
+// A fixSpecsFS is an FS mapping /ref/mem.html and /ref/spec.html to
+// /doc/go_mem.html and /doc/go_spec.html.
+var _ fs.FS = &fixSpecsFS{}
+
+type fixSpecsFS struct {
+ fs fs.FS
+}
+
+func (fsys fixSpecsFS) Open(name string) (fs.File, error) {
+ switch name {
+ case "ref/mem.html", "ref/spec.html":
+ if f, err := fsys.fs.Open(name); err == nil {
+ // Let Go distribution win if they move.
+ return f, nil
+ }
+ // Otherwise fall back to doc/go_*.html
+ name = "doc/go_" + strings.TrimPrefix(name, "ref/")
+ return fsys.fs.Open(name)
+
+ case "doc/go_mem.html", "doc/go_spec.html":
+ data := []byte("<!--{\n\t\"Redirect\": \"/ref/" + strings.TrimPrefix(strings.TrimSuffix(name, ".html"), "doc/go_") + "\"\n}-->\n")
+ return &memFile{path.Base(name), bytes.NewReader(data)}, nil
+ }
+
+ return fsys.fs.Open(name)
+}
+
// A seekableFS is an FS wrapper that makes every file seekable
// by reading it entirely into memory when it is opened and then
// serving read operations (including seek) from the memory copy.
@@ -587,6 +632,20 @@
return f.Reader.Read(b)
}
+// A memFile is an fs.File implementation backed by in-memory data.
+type memFile struct {
+ name string
+ *bytes.Reader
+}
+
+func (f *memFile) Stat() (fs.FileInfo, error) { return f, nil }
+func (f *memFile) Name() string { return f.name }
+func (*memFile) Mode() fs.FileMode { return 0444 }
+func (*memFile) ModTime() time.Time { return time.Time{} }
+func (*memFile) IsDir() bool { return false }
+func (*memFile) Sys() interface{} { return nil }
+func (*memFile) Close() error { return nil }
+
// An atomicFS is an fs.FS value safe for reading from multiple goroutines
// as well as updating (assigning a different fs.FS to use in future read requests).
type atomicFS struct {
diff --git a/cmd/golangorg/testdata/web.txt b/cmd/golangorg/testdata/web.txt
index 76e919e..0bd66f9 100644
--- a/cmd/golangorg/testdata/web.txt
+++ b/cmd/golangorg/testdata/web.txt
@@ -88,9 +88,11 @@
GET https://golang.org/pkg/fmt/?m=old
body contains Package fmt implements formatted I/O
+body contains Share this code
GET https://golang.google.cn/pkg/fmt/
body contains Package fmt implements formatted I/O
+body !contains Share this code
GET https://golang.org/pkg
redirect == /pkg/
@@ -203,3 +205,18 @@
GET https://m.golang.org/anything
redirect == https://mail.google.com/a/golang.org/
+
+GET https://golang.org/doc/effective_go
+body contains KB ByteSize
+
+GET https://golang.org/doc/codewalk/codewalk/
+body contains Codewalk: How to Write a Codewalk
+body contains A codewalk is a guided tour
+
+GET https://golang.org/doc/codewalk/
+body contains Codewalks
+body contains <td>How to Write a Codewalk</td>
+
+GET https://golang.org/asdf
+code == 404
+body ~ <span class="alert" style="font-size:120%">open ../../_content/asdf: (.*)</span>
diff --git a/go.dev/_content/article.tmpl b/go.dev/_content/article.tmpl
index 6b6d7a3..15c4401 100644
--- a/go.dev/_content/article.tmpl
+++ b/go.dev/_content/article.tmpl
@@ -1,5 +1,5 @@
{{define "layout"}}
- <article class="Article {{if ne .Section "/"}}Article--{{trim .Section "/"}}{{end -}}">
+ <article class="Article {{with section .}}Article--{{strings.Trim . "/"}}{{end}}">
<h1>{{.title}}</h1>
{{.Content}}
</article>
diff --git a/_content/lib/godoc/error.html b/go.dev/_content/error.tmpl
similarity index 68%
copy from _content/lib/godoc/error.html
copy to go.dev/_content/error.tmpl
index cb5325e..30fe79d 100644
--- a/_content/lib/godoc/error.html
+++ b/go.dev/_content/error.tmpl
@@ -4,8 +4,8 @@
license that can be found in the LICENSE file.
-->
-{{with .Data}}
+{{define "layout"}}
<p>
-<span class="alert" style="font-size:120%">{{.}}</span>
+<span class="alert" style="font-size:120%">{{.error}}</span>
</p>
{{end}}
diff --git a/go.dev/_content/index.md b/go.dev/_content/index.md
index 72d429c..db1c048 100644
--- a/go.dev/_content/index.md
+++ b/go.dev/_content/index.md
@@ -79,7 +79,7 @@
</div>
<div class="WhoUsesCaseStudyList">
<ul class="WhoUsesCaseStudyList-gridContainer">
- {{- range newest (pages "solutions/*")}}{{if eq .series "Case Studies"}}
+ {{- range newest (pages "/solutions/*")}}{{if eq .series "Case Studies"}}
{{- if .link }}
{{- if .inLandingPageGrid }}
<li class="WhoUsesCaseStudyList-caseStudy">
@@ -97,7 +97,7 @@
{{- end}}
{{- else}}
<li class="WhoUsesCaseStudyList-caseStudy">
- <a href="{{.Path}}" class="WhoUsesCaseStudyList-caseStudyLink">
+ <a href="{{.URL}}" class="WhoUsesCaseStudyList-caseStudyLink">
<img
loading="lazy"
height="48"
@@ -119,7 +119,7 @@
<div class="GoCarousel-controlsContainer">
<div class="GoCarousel-wrapper">
<ul class="js-testimonialsGoQuotes TestimonialsGo-quotes">
- {{- range $index, $element := data "testimonials"}}
+ {{- range $index, $element := data "/testimonials.yaml"}}
<li class="TestimonialsGo-quoteGroup GoCarousel-slide" id="quote_slide{{$index}}">
<div class="TestimonialsGo-quoteSingleItem">
<div class="TestimonialsGo-quoteSection">
@@ -153,7 +153,7 @@
</h4>
</div>
<ul class="WhyGo-reasons">
- {{- range first 4 (data "resources")}}
+ {{- range first 4 (data "/resources.yaml")}}
<li class="WhyGo-reason">
<div class="WhyGo-reasonDetails">
<div class="WhyGo-reasonIcon" role="presentation">
@@ -188,7 +188,7 @@
</div>
</li>
{{- end}}
- {{- if gt (len (data "resources")) 3}}
+ {{- if gt (len (data "resources.yaml")) 3}}
<li class="WhyGo-reason">
<div class="WhyGo-reasonShowMore">
<div class="WhyGo-reasonShowMoreImgWrapper">
@@ -216,7 +216,7 @@
<div class="GoCarousel-controlsContainer">
<div class="GoCarousel-eventsWrapper">
<ul class="js-goCarouselEventsSlides GoCarousel-eventsSlides">
- {{- range $index, $element := (data "events").all}}
+ {{- range $index, $element := (data "/events.yaml").all}}
<li
class="GoCarousel-eventGroup"
id="event_slide{{$index}}">
@@ -312,7 +312,7 @@
<li class="GettingStartedGo-resourcesHeader">
In-Person Trainings
</li>
- {{- range first 4 (data "learn/training")}}
+ {{- range first 4 (data "/learn/training.yaml")}}
<li class="GettingStartedGo-resourceItem">
<a href="{{.url}}" class="GettingStartedGo-resourceItemTitle">
{{.title}}
diff --git a/go.dev/_content/learn/index.md b/go.dev/_content/learn/index.md
index fe84ce3..816d669 100644
--- a/go.dev/_content/learn/index.md
+++ b/go.dev/_content/learn/index.md
@@ -45,7 +45,7 @@
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-quickstarts Learn-cardList">
- {{- range first 3 (data "learn/quickstart")}}
+ {{- range first 3 (data "quickstart.yaml")}}
<li class="Learn-quickstart Learn-card">
{{- template "learn-card" .}}
</li>
@@ -66,7 +66,7 @@
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-cardList">
- {{- range first 4 (data "learn/guided")}}
+ {{- range first 4 (data "guided.yaml")}}
<li class="Learn-card">
{{- template "learn-card" .}}
</li>
@@ -83,7 +83,7 @@
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-cardList">
- {{- range first 4 (data "learn/courses") }}
+ {{- range first 4 (data "courses.yaml") }}
<li class="Learn-card">
{{- template "learn-card" .}}
</li>
@@ -100,7 +100,7 @@
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-cardList">
- {{- range first 4 (data "learn/cloud")}}
+ {{- range first 4 (data "cloud.yaml")}}
<li class="Learn-card">
{{- template "learn-self-paced-card" .}}
</li>
@@ -118,7 +118,7 @@
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-cardList Learn-bookList">
- {{- range first 5 (data "learn/books")}}
+ {{- range first 5 (data "books.yaml")}}
<li class="Learn-card Learn-book">
{{template "learn-book" .}}
</li>
@@ -135,7 +135,7 @@
</div>
<div class="LearnGo-gridContainer">
<ul class="Learn-inPersonList">
- {{- range first 4 (data "learn/training")}}
+ {{- range first 4 (data "training.yaml")}}
<li class="Learn-inPerson">
<p class="Learn-inPersonTitle">
<a href="{{.url}}">{{.title}} </a>
@@ -157,7 +157,7 @@
</p>
</div>
<ul class="Learn-events">
- {{- range first 3 (data "events").all}}
+ {{- range first 3 (data "/events.yaml").all}}
<li class="Learn-eventItem">
<div
class="Learn-eventThumbnail {{if not .photourl}}Learn-eventThumbnail--noimage{{end}}"
diff --git a/go.dev/_content/site.tmpl b/go.dev/_content/site.tmpl
index 12271ff..c77e6d3 100644
--- a/go.dev/_content/site.tmpl
+++ b/go.dev/_content/site.tmpl
@@ -23,7 +23,7 @@
})(window,document,'script','dataLayer','GTM-W8MVQXG');</script>
<!-- End Google Tag Manager -->
<script src="/js/site.js"></script>
-<title>{{.title}}{{if .Parent}} - go.dev{{end}}</title>
+<title>{{.title}}{{if ne .URL "/"}} - go.dev{{end}}</title>
{{if .link -}}
<meta http-equiv="refresh" content="0; url={{.link}}">
{{end -}}
@@ -34,7 +34,7 @@
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
-{{$menus := data "menus"}}
+{{$menus := data "/menus.yaml"}}
<header class="Site-header js-siteHeader">
<div class="Banner">
<div class="Banner-inner">
@@ -93,7 +93,7 @@
</div>
<ul class="NavigationDrawer-list">
{{- range $menus.main}}
- <li class="NavigationDrawer-listItem {{if eq .url $currentPage.Section}} NavigationDrawer-listItem--active{{end}}">
+ <li class="NavigationDrawer-listItem {{if eq .url (section $currentPage)}} NavigationDrawer-listItem--active{{end}}">
<a href="{{.url}}">{{.name}}</a>
</li>
{{- end}}
@@ -102,7 +102,7 @@
</aside>
<div class="NavigationDrawer-scrim js-scrim" role="presentation"></div>
<main class="SiteContent SiteContent--default">
- {{- block "layout" . -}}{{- end -}}
+ {{block "layout" .}}{{.Content}}{{end}}
</main>
<footer class="Site-footer">
<div class="Footer">
@@ -173,13 +173,13 @@
</body>
</html>
-{{define "breadcrumbnav"}}
-{{- if .p1.Parent}}
- {{- template "breadcrumbnav" (dict "p1" (page .p1.Parent) "p2" .p2 )}}
+{{define "breadcrumbnav p1 p2"}}
+{{- if ne .p1.URL "/"}}
+ {{- breadcrumbnav (page (path.Dir (strings.TrimRight .p1.URL "/"))) .p2}}
{{- end}}
{{- if not (eq .p1.title "go.dev")}}
-<li class="BreadcrumbNav-li {{if eq .p1.Path .p2.Path}}active{{end}}">
- <a class="BreadcrumbNav-link" href="{{.p1.Path}}">
+<li class="BreadcrumbNav-li {{if eq .p1.URL .p2.URL}}active{{end}}">
+ <a class="BreadcrumbNav-link" href="{{.p1.URL}}">
{{or .p1.company .p1.title}}
</a>
</li>
@@ -189,7 +189,7 @@
{{define "breadcrumbs"}}
<div class="BreadcrumbNav">
<ol class="BreadcrumbNav-inner">
- {{template "breadcrumbnav" (dict "p1" . "p2" .)}}
+ {{breadcrumbnav . .}}
</ol>
</div>
{{- end}}
diff --git a/go.dev/_content/site.yaml b/go.dev/_content/site.yaml
deleted file mode 100644
index 6878875..0000000
--- a/go.dev/_content/site.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-url: https://go.dev/
-title: go.dev
diff --git a/go.dev/_content/solutions/default.tmpl b/go.dev/_content/solutions/default.tmpl
index 79372a2..a6cb0c7 100644
--- a/go.dev/_content/solutions/default.tmpl
+++ b/go.dev/_content/solutions/default.tmpl
@@ -10,7 +10,7 @@
<div class="Article-author">{{.}}</div>
{{end}}
{{if .date}}
- <div class="Article-date">{{.Date.Format "2 January 2006"}}</div>
+ <div class="Article-date">{{.date.Format "2 January 2006"}}</div>
{{end}}
</div>
{{if .company}}
@@ -22,7 +22,7 @@
</div>
</div>
- <article class="Article {{if ne .Section "/"}}Article--{{trim .Section "/"}}{{end -}}">
+ <article class="Article {{if ne (section .) "/"}}Article--{{strings.Trim (section .) "/"}}{{end -}}">
{{if (eq .series "Case Studies") }}
<div class="CaseStudy-content">
<div class="CaseStudy-contentBody">
@@ -73,7 +73,7 @@
</span>
</div>
{{ end }}
- {{rawhtml (replace .Content ` .sectionHeading">` `" class="sectionHeading">`)}}
+ {{.Content}}
</div>
</div>
{{end}}
diff --git a/go.dev/_content/solutions/index.md b/go.dev/_content/solutions/index.md
index e4ea287..db07513 100644
--- a/go.dev/_content/solutions/index.md
+++ b/go.dev/_content/solutions/index.md
@@ -1,8 +1,9 @@
---
title: Why Go
+layout: none
---
-{{$solutions := pages "solutions/*"}}
+{{$solutions := pages "/solutions/*"}}
<section class="Solutions-headline">
<div class="GoCarousel" id="SolutionsHeroCarousel-carousel">
<div class="GoCarousel-controlsContainer">
@@ -17,17 +18,17 @@
<div class="Solutions-headlineImg">
<img
src="/images/{{.carouselImgSrc}}"
- alt="{{.linkTitle}}"
+ alt="{{(or .linkTitle .title)}}"
/>
</div>
<div class="Solutions-headlineText">
<p class="Solutions-headlineNotification">RECENTLY UPDATED</p>
<h2>
- {{.linkTitle}}
+ {{(or .linkTitle .title)}}
</h2>
<p class="Solutions-headlineBody">
{{with .quote}}{{.}}{{end}}
- <a href="{{.Path}}"
+ <a href="{{.URL}}"
>Learn more
<i class="material-icons Solutions-forwardArrowIcon"
>arrow_forward</i
@@ -106,7 +107,7 @@
/>
</div>
<div class="Solutions-useCaseBody">
- <h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
+ <h3 class="Solutions-useCaseTitle">{{or .linkTitle .title}}</h3>
<p class="Solutions-useCaseDescription">
{{.description}}
</p>
@@ -117,7 +118,7 @@
</p>
</a>
{{- else}}
- <a href="{{.Path}}" class="Solutions-useCaseLink">
+ <a href="{{.URL}}" class="Solutions-useCaseLink">
<div class="Solutions-useCaseLogo">
<img
loading="lazy"
@@ -126,7 +127,7 @@
/>
</div>
<div class="Solutions-useCaseBody">
- <h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
+ <h3 class="Solutions-useCaseTitle">{{or .linkTitle .title}}</h3>
<p class="Solutions-useCaseDescription">
{{with .quote}}{{.}}{{end}}
</p>
@@ -149,19 +150,19 @@
>
{{- range newest $solutions}}{{if eq .series "Use Cases"}}
<li class="Solutions-card">
- <a href="{{.Path}}" class="Solutions-useCaseLink">
+ <a href="{{.URL}}" class="Solutions-useCaseLink">
<div class="Solutions-useCaseLogo">
{{- $icon := .icon}}
{{- if $icon}}
<img
loading="lazy"
alt="{{$icon.alt}}"
- src="{{.Dir}}/{{$icon.file}}"
+ src="{{path.Dir .URL}}/{{$icon.file}}"
/>
{{- end}}
</div>
<div class="Solutions-useCaseBody">
- <h3 class="Solutions-useCaseTitle">{{.linkTitle}}</h3>
+ <h3 class="Solutions-useCaseTitle">{{or .linkTitle .title}}</h3>
<p class="Solutions-useCaseDescription">
{{.description}}
</p>
diff --git a/go.dev/cmd/internal/site/site_test.go b/go.dev/cmd/frontend/golden_test.go
similarity index 79%
rename from go.dev/cmd/internal/site/site_test.go
rename to go.dev/cmd/frontend/golden_test.go
index f19b95c..04b6435 100644
--- a/go.dev/cmd/internal/site/site_test.go
+++ b/go.dev/cmd/frontend/golden_test.go
@@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package site
+package main
import (
"bytes"
"io/ioutil"
+ "net/http/httptest"
"os"
"path"
"path/filepath"
@@ -19,14 +20,14 @@
func TestGolden(t *testing.T) {
start := time.Now()
- site, err := Load("../../..")
+ h, err := NewHandler("../../_content")
if err != nil {
t.Fatal(err)
}
total := time.Since(start)
t.Logf("Load %v\n", total)
- root := "../../../testdata/golden"
+ root := "../../testdata/golden"
err = filepath.Walk(root, func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
@@ -55,20 +56,29 @@
return nil
}
- want, err := ioutil.ReadFile(site.file("testdata/golden/" + name))
+ want, err := ioutil.ReadFile(filepath.Join(root, name))
if err != nil {
t.Fatal(err)
}
start := time.Now()
- f, err := site.Open(name)
- if err != nil {
- t.Fatal(err)
+ r := httptest.NewRequest("GET", "/"+name, nil)
+ resp := httptest.NewRecorder()
+ resp.Body = new(bytes.Buffer)
+ h.ServeHTTP(resp, r)
+ for nredir := 0; resp.Code/10 == 30; nredir++ {
+ if nredir > 10 {
+ t.Fatalf("%s <- redirect loop!", name)
+ }
+ r.URL.Path = resp.Result().Header.Get("Location")
+ resp = httptest.NewRecorder()
+ resp.Body = new(bytes.Buffer)
+ h.ServeHTTP(resp, r)
}
- have, err := ioutil.ReadAll(f)
- if err != nil {
- t.Fatalf("%v: %v", name, err)
+ if resp.Code != 200 {
+ t.Fatalf("GET %s <- %d, want 200", r.URL, resp.Code)
}
+ have := resp.Body.Bytes()
total += time.Since(start)
if path.Ext(name) == ".html" {
diff --git a/go.dev/cmd/frontend/main.go b/go.dev/cmd/frontend/main.go
index db67169..effd257 100644
--- a/go.dev/cmd/frontend/main.go
+++ b/go.dev/cmd/frontend/main.go
@@ -11,9 +11,13 @@
"net/url"
"os"
"path/filepath"
+ "sort"
"strings"
+ "time"
- "golang.org/x/website/go.dev/cmd/internal/site"
+ "golang.org/x/website/internal/backport/html/template"
+ "golang.org/x/website/internal/backport/osfs"
+ "golang.org/x/website/internal/web"
"golang.org/x/website/internal/webtest"
)
@@ -24,10 +28,10 @@
}
func main() {
- dir := "../.."
+ dir := "../../_content"
if _, err := os.Stat("go.dev/_content/events.yaml"); err == nil {
// Running in repo root.
- dir = "go.dev"
+ dir = "go.dev/_content"
}
h, err := NewHandler(dir)
@@ -52,17 +56,63 @@
}
func NewHandler(dir string) (http.Handler, error) {
- godev, err := site.Load(dir)
- if err != nil {
- return nil, err
- }
+ godev := web.NewSite(osfs.DirFS(dir))
+ godev.Funcs(template.FuncMap{
+ "newest": newest,
+ "section": section,
+ })
mux := http.NewServeMux()
- mux.Handle("/", addCSP(http.FileServer(godev)))
+ mux.Handle("/", addCSP(godev))
mux.Handle("/explore/", http.StripPrefix("/explore/", redirectHosts(discoveryHosts)))
mux.Handle("learn.go.dev/", http.HandlerFunc(redirectLearn))
return mux, nil
}
+// newest returns the pages sorted newest first,
+// breaking ties by .linkTitle or else .title.
+func newest(pages []web.Page) []web.Page {
+ out := make([]web.Page, len(pages))
+ copy(out, pages)
+
+ sort.Slice(out, func(i, j int) bool {
+ pi := out[i]
+ pj := out[j]
+ di, _ := pi["date"].(time.Time)
+ dj, _ := pj["date"].(time.Time)
+ if !di.Equal(dj) {
+ return di.After(dj)
+ }
+ ti, _ := pi["linkTitle"].(string)
+ if ti == "" {
+ ti, _ = pi["title"].(string)
+ }
+ tj, _ := pj["linkTitle"].(string)
+ if tj == "" {
+ tj, _ = pj["title"].(string)
+ }
+ if ti != tj {
+ return ti < tj
+ }
+ return false
+ })
+ return out
+}
+
+// section returns the site section for the given Page,
+// defined as the first path element, or else an empty string.
+// For example if p's URL is /x/y/z then section is "x".
+func section(p web.Page) string {
+ u, _ := p["URL"].(string)
+ if !strings.HasPrefix(u, "/") {
+ return ""
+ }
+ i := strings.Index(u[1:], "/")
+ if i < 0 {
+ return ""
+ }
+ return u[:1+i+1]
+}
+
func redirectLearn(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://go.dev/learn/"+strings.TrimPrefix(r.URL.Path, "/"), http.StatusMovedPermanently)
}
diff --git a/go.dev/cmd/frontend/main_test.go b/go.dev/cmd/frontend/main_test.go
index 61c7d0f..05a60d8 100644
--- a/go.dev/cmd/frontend/main_test.go
+++ b/go.dev/cmd/frontend/main_test.go
@@ -10,8 +10,6 @@
"net/http/httptest"
"strings"
"testing"
-
- "golang.org/x/website/go.dev/cmd/internal/site"
)
var testHosts = map[string]string{
@@ -79,11 +77,11 @@
}{
{"/", []string{"Go is an open source programming language supported by Google"}},
{"/solutions/", []string{"Using Go at Google"}},
- {"/solutions/dropbox/", []string{"About Dropbox"}},
+ {"/solutions/dropbox", []string{"About Dropbox"}},
}
func TestSite(t *testing.T) {
- godev, err := site.Load("../..")
+ h, err := NewHandler("../../_content")
if err != nil {
t.Fatal(err)
}
@@ -93,7 +91,7 @@
r := httptest.NewRequest("GET", tt.target, nil)
resp := httptest.NewRecorder()
resp.Body = new(bytes.Buffer)
- http.FileServer(godev).ServeHTTP(resp, r)
+ h.ServeHTTP(resp, r)
if resp.Code != 200 {
t.Fatalf("Code = %d, want 200", resp.Code)
}
diff --git a/go.dev/cmd/frontend/server_test.go b/go.dev/cmd/frontend/server_test.go
index 831838a..4b0d4ec 100644
--- a/go.dev/cmd/frontend/server_test.go
+++ b/go.dev/cmd/frontend/server_test.go
@@ -11,7 +11,7 @@
)
func TestWeb(t *testing.T) {
- h, err := NewHandler("../..")
+ h, err := NewHandler("../../_content")
if err != nil {
t.Fatal(err)
}
diff --git a/go.dev/cmd/frontend/testdata/godev.txt b/go.dev/cmd/frontend/testdata/godev.txt
index 950b385..3785608 100644
--- a/go.dev/cmd/frontend/testdata/godev.txt
+++ b/go.dev/cmd/frontend/testdata/godev.txt
@@ -3,3 +3,19 @@
GET https://go.dev/solutions/google/
body ~ it\s+has\s+powered\s+many\s+projects\s+at\s+Google.
+
+GET /solutions/chrome
+redirect == /solutions/google/chrome
+
+GET /solutions/coredata
+redirect == /solutions/google/coredata
+
+GET /solutions/firebase
+redirect == /solutions/google/firebase
+
+GET /solutions/sitereliability
+redirect == /solutions/google/sitereliability
+
+GET /solutions/americanexpress
+body contains <div class="Article-date">19 December 2019</div>
+
diff --git a/go.dev/cmd/internal/site/md.go b/go.dev/cmd/internal/site/md.go
deleted file mode 100644
index a90f57f..0000000
--- a/go.dev/cmd/internal/site/md.go
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2021 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 site
-
-import (
- "bytes"
- "regexp"
- "strings"
-
- "github.com/yuin/goldmark"
- "github.com/yuin/goldmark/ast"
- "github.com/yuin/goldmark/extension"
- "github.com/yuin/goldmark/parser"
- "github.com/yuin/goldmark/renderer/html"
- "github.com/yuin/goldmark/text"
- "github.com/yuin/goldmark/util"
- "golang.org/x/website/internal/backport/html/template"
-)
-
-// markdownToHTML converts markdown to HTML using the renderer and settings that Hugo uses.
-func markdownToHTML(markdown string) (template.HTML, error) {
- // parser.WithHeadingAttribute allows custom ids on headings.
- // html.WithUnsafe allows use of raw HTML, which we need for tables.
- md := goldmark.New(
- goldmark.WithParserOptions(
- parser.WithHeadingAttribute(),
- parser.WithAutoHeadingID(),
- parser.WithASTTransformers(util.Prioritized(mdTransformFunc(mdLink), 1)),
- ),
- goldmark.WithRendererOptions(html.WithUnsafe()),
- goldmark.WithExtensions(
- extension.NewTypographer(),
- extension.NewLinkify(
- extension.WithLinkifyAllowedProtocols([][]byte{[]byte("http"), []byte("https")}),
- extension.WithLinkifyEmailRegexp(regexp.MustCompile(`[^\x00-\x{10FFFF}]`)), // impossible
- ),
- ),
- )
- var buf bytes.Buffer
- if err := md.Convert([]byte(markdown), &buf); err != nil {
- return "", err
- }
- return template.HTML(buf.Bytes()), nil
-}
-
-// mdTransformFunc is a func implementing parser.ASTTransformer.
-type mdTransformFunc func(*ast.Document, text.Reader, parser.Context)
-
-func (f mdTransformFunc) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
- f(node, reader, pc)
-}
-
-// mdLink walks doc, adding rel=noreferrer target=_blank to non-relative links.
-func mdLink(doc *ast.Document, _ text.Reader, _ parser.Context) {
- mdLinkWalk(doc)
-}
-
-func mdLinkWalk(n ast.Node) {
- switch n := n.(type) {
- case *ast.Link:
- dest := string(n.Destination)
- if strings.HasPrefix(dest, "https://") || strings.HasPrefix(dest, "http://") {
- n.SetAttributeString("rel", []byte("noreferrer"))
- n.SetAttributeString("target", []byte("_blank"))
- }
- return
- case *ast.AutoLink:
- // All autolinks are non-relative.
- n.SetAttributeString("rel", []byte("noreferrer"))
- n.SetAttributeString("target", []byte("_blank"))
- return
- }
-
- for child := n.FirstChild(); child != nil; child = child.NextSibling() {
- mdLinkWalk(child)
- }
-}
diff --git a/go.dev/cmd/internal/site/page.go b/go.dev/cmd/internal/site/page.go
deleted file mode 100644
index dea5439..0000000
--- a/go.dev/cmd/internal/site/page.go
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright 2021 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 site
-
-import (
- "bytes"
- "fmt"
- "io/ioutil"
- "os"
- "path"
- "path/filepath"
- "strings"
- "time"
-
- "golang.org/x/website/internal/tmplfunc"
- "gopkg.in/yaml.v3"
-)
-
-// A page is a single web page.
-// It corresponds to some .md file in the content tree.
-type page struct {
- id string // page ID (url path excluding site.BaseURL and trailing slash)
- file string // .md file for page
- data []byte // page data (markdown)
- html []byte // rendered page (HTML)
- params tPage // parameters passed to templates
-}
-
-// A tPage is the template form of the page, the data passed to rendering templates.
-type tPage map[string]interface{}
-
-// loadPage loads the site's page from the given file.
-// It returns the page but also adds the page to site.pages and site.pagesByID.
-func (site *Site) loadPage(file string) (*page, error) {
- file = filepath.ToSlash(file)
- id := strings.TrimPrefix(file, "_content/")
- if id == "index.md" {
- id = ""
- } else if strings.HasSuffix(id, "/index.md") {
- id = strings.TrimSuffix(id, "/index.md")
- } else {
- id = strings.TrimSuffix(id, ".md")
- }
-
- p := site.newPage(id)
- p.file = file
-
- urlPath := "/" + p.id
- if strings.HasSuffix(p.file, "/index.md") && p.id != "" {
- urlPath += "/"
- }
-
- // Load content, including leading yaml.
- data, err := ioutil.ReadFile(site.file(file))
- if err != nil {
- return nil, err
- }
- if bytes.HasPrefix(data, []byte("---\n")) {
- i := bytes.Index(data, []byte("\n---\n"))
- if i < 0 {
- if bytes.HasSuffix(data, []byte("\n---")) {
- i = len(data) - 4
- }
- }
- if i >= 0 {
- meta := data[4 : i+1]
- err := yaml.Unmarshal(meta, p.params)
- if err != nil {
- return nil, fmt.Errorf("load %s: %v", file, err)
- }
-
- // Drop YAML but insert the right number of newlines to keep line numbers correct in template errors.
- nl := 0
- for _, c := range data[:i+4] {
- if c == '\n' {
- nl++
- }
- }
- i += 4
- for ; nl > 0; nl-- {
- i--
- data[i] = '\n'
- }
- data = data[i:]
- }
- }
- p.data = data
-
- // Default linkTitle to title
- if _, ok := p.params["linkTitle"]; !ok {
- p.params["linkTitle"] = p.params["title"]
- }
-
- // Parse date to Date.
- // Note that YAML parser may have done it for us (!)
- p.params["Date"] = time.Time{}
- if d, ok := p.params["date"].(string); ok {
- t, err := parseDate(d)
- if err != nil {
- return nil, err
- }
- p.params["Date"] = t
- } else if d, ok := p.params["date"].(time.Time); ok {
- p.params["Date"] = d
- }
-
- // Path, Dir, URL
- p.params["Path"] = urlPath
- p.params["Dir"] = path.Dir(urlPath)
- p.params["URL"] = strings.TrimRight(site.URL, "/") + urlPath
-
- // Parent
- if p.id != "" {
- parent := path.Dir("/" + p.id)
- if parent != "/" {
- parent += "/"
- }
- p.params["Parent"] = parent
- }
-
- // Section
- section := "/"
- if i := strings.Index(p.id, "/"); i >= 0 {
- section = "/" + p.id[:i+1]
- } else if strings.HasSuffix(p.file, "/index.md") {
- section = "/" + p.id + "/"
- }
- p.params["Section"] = section
-
- return p, nil
-}
-
-// renderHTML renders the HTML for the page, leaving it in p.html.
-func (site *Site) renderHTML(p *page) error {
- // Load base template.
- base, err := ioutil.ReadFile(site.file("_content/site.tmpl"))
- if err != nil {
- return err
- }
- t := site.clone().New("_content/site.tmpl")
- if err := tmplfunc.Parse(t, string(base)); err != nil {
- return err
- }
-
- // Load page-specific layout template.
- layout, _ := p.params["layout"].(string)
- if layout == "" {
- // Determine nearest default.tmpl in current or parent directory.
- // In the case of index.md, the current directory's default.tmpl
- // is ignored, under the assumption that it's for the other pages
- // in the directory but not the index page.
- dir := path.Dir(p.file)
- rel := ""
- if strings.HasSuffix(p.file, "/index.md") && p.id != "" {
- dir = path.Dir(dir)
- rel = "../"
- }
- for {
- name := site.file(path.Join(dir, "default.tmpl"))
- if _, err := os.Stat(name); err == nil {
- layout = rel + "default"
- break
- }
- if dir == "." {
- break
- }
- dir = path.Dir(dir)
- rel += "../"
- }
- if layout == "" {
- return fmt.Errorf("%s: cannot find default template", p.id)
- }
- }
-
- layout = path.Join(path.Dir(p.file), layout+".tmpl")
- data, err := ioutil.ReadFile(site.file(layout))
- if err != nil {
- return err
- }
- if err := tmplfunc.Parse(t.New(layout), string(data)); err != nil {
- return err
- }
-
- // Load actual Markdown content (also a template).
- tf := t.New(p.file)
- if err := tmplfunc.Parse(tf, string(p.data)); err != nil {
- return err
- }
- var buf bytes.Buffer
- if err := tf.Execute(&buf, p.params); err != nil {
- return err
- }
- html, err := markdownToHTML(buf.String())
- if err != nil {
- return err
- }
- p.params["Content"] = html
-
- buf.Reset()
- if err := t.Execute(&buf, p.params); err != nil {
- return err
- }
- p.html = buf.Bytes()
- return nil
-}
-
-var dateFormats = []string{
- "2006-01-02",
- time.RFC3339,
-}
-
-func parseDate(d string) (time.Time, error) {
- for _, f := range dateFormats {
- if tt, err := time.Parse(f, d); err == nil {
- return tt, nil
- }
- }
- return time.Time{}, fmt.Errorf("invalid date: %s", d)
-}
diff --git a/go.dev/cmd/internal/site/site.go b/go.dev/cmd/internal/site/site.go
deleted file mode 100644
index 7ac0a19..0000000
--- a/go.dev/cmd/internal/site/site.go
+++ /dev/null
@@ -1,251 +0,0 @@
-// Copyright 2021 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 site implements generation of content for serving from go.dev.
-// It is meant to support a transition from being a Hugo-based web site
-// to being a site compatible with x/website.
-package site
-
-import (
- "bytes"
- "fmt"
- "io"
- "io/ioutil"
- "net/http"
- "os"
- "path"
- "path/filepath"
- "sort"
- "strings"
- "time"
-
- "golang.org/x/website/internal/backport/html/template"
- "gopkg.in/yaml.v3"
-)
-
-// A Site holds metadata about the entire site.
-type Site struct {
- URL string
- Title string
-
- pagesByID map[string]*page
- dir string
- base *template.Template
-}
-
-// Load loads and returns the site in the directory rooted at dir.
-func Load(dir string) (*Site, error) {
- dir, err := filepath.Abs(dir)
- if err != nil {
- return nil, err
- }
- site := &Site{
- dir: dir,
- pagesByID: make(map[string]*page),
- }
- if err := site.initTemplate(); err != nil {
- return nil, err
- }
-
- // Read site config.
- data, err := ioutil.ReadFile(site.file("_content/site.yaml"))
- if err != nil {
- return nil, err
- }
- if err := yaml.Unmarshal(data, &site); err != nil {
- return nil, fmt.Errorf("parsing _content/site.yaml: %v", err)
- }
-
- // Load site pages from md files.
- err = filepath.Walk(site.file("_content"), func(name string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- if strings.HasSuffix(name, ".md") {
- _, err := site.loadPage(name[len(site.file("."))+1:])
- return err
- }
- return nil
- })
- if err != nil {
- return nil, fmt.Errorf("loading pages: %v", err)
- }
-
- // Now that all pages are loaded and set up, can render all.
- // (Pages can refer to other pages.)
- for _, p := range site.pagesByID {
- if err := site.renderHTML(p); err != nil {
- return nil, err
- }
- }
-
- return site, nil
-}
-
-// file returns the full path to the named file within the site.
-func (site *Site) file(name string) string { return filepath.Join(site.dir, name) }
-
-// newPage returns a new page belonging to site.
-func (site *Site) newPage(short string) *page {
- p := &page{
- id: short,
- params: make(tPage),
- }
- site.pagesByID[p.id] = p
- return p
-}
-
-// data parses the named yaml file and returns its structured data.
-func (site *Site) data(name string) (interface{}, error) {
- data, err := ioutil.ReadFile(site.file("_content/" + name + ".yaml"))
- if err != nil {
- return nil, err
- }
- var d interface{}
- if err := yaml.Unmarshal(data, &d); err != nil {
- return nil, err
- }
- return d, nil
-}
-
-// pageByID returns the page with a given path.
-func (site *Site) pageByPath(path string) (tPage, error) {
- p := site.pagesByID[strings.Trim(path, "/")]
- if p == nil {
- return nil, fmt.Errorf("no such page with path %q", path)
- }
- return p.params, nil
-}
-
-// pagesGlob returns the pages with IDs matching glob.
-func (site *Site) pagesGlob(glob string) ([]tPage, error) {
- _, err := path.Match(glob, "")
- if err != nil {
- return nil, err
- }
- glob = strings.Trim(glob, "/")
- var out []tPage
- for _, p := range site.pagesByID {
- if ok, _ := path.Match(glob, p.id); ok {
- out = append(out, p.params)
- }
- }
-
- sort.Slice(out, func(i, j int) bool {
- return out[i]["Path"].(string) < out[j]["Path"].(string)
- })
- return out, nil
-}
-
-// newest returns the pages sorted newest first,
-// breaking ties by .linkTitle or else .title.
-func newest(pages []tPage) []tPage {
- out := make([]tPage, len(pages))
- copy(out, pages)
-
- sort.Slice(out, func(i, j int) bool {
- pi := out[i]
- pj := out[j]
- di, _ := pi["Date"].(time.Time)
- dj, _ := pj["Date"].(time.Time)
- if !di.Equal(dj) {
- return di.After(dj)
- }
- ti, _ := pi["linkTitle"].(string)
- tj, _ := pj["linkTitle"].(string)
- if ti != tj {
- return ti < tj
- }
- return false
- })
- return out
-}
-
-// Open returns the content to serve at the given path.
-// This function makes Site an http.FileServer, for easy HTTP serving.
-func (site *Site) Open(name string) (http.File, error) {
- name = strings.TrimPrefix(name, "/")
- switch ext := path.Ext(name); ext {
- case ".css", ".jpeg", ".jpg", ".js", ".png", ".svg", ".txt":
- if f, err := os.Open(site.file("_content/" + name)); err == nil {
- return f, nil
- }
-
- case ".html":
- id := strings.TrimSuffix(name, "/index.html")
- if name == "index.html" {
- id = ""
- }
- if p := site.pagesByID[id]; p != nil {
- if redir, ok := p.params["redirect"].(string); ok {
- s := fmt.Sprintf(redirectFmt, redir)
- return &httpFile{strings.NewReader(s), int64(len(s))}, nil
- }
- return &httpFile{bytes.NewReader(p.html), int64(len(p.html))}, nil
- }
- }
-
- if !strings.HasSuffix(name, ".html") {
- if f, err := site.Open(name + "/index.html"); err == nil {
- size, err := f.Seek(0, io.SeekEnd)
- f.Close()
- if err == nil {
- return &httpDir{httpFileInfo{"index.html", size, false}, 0}, nil
- }
- }
- }
-
- return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
-}
-
-type httpFile struct {
- io.ReadSeeker
- size int64
-}
-
-func (*httpFile) Close() error { return nil }
-func (f *httpFile) Stat() (os.FileInfo, error) { return &httpFileInfo{".", f.size, false}, nil }
-func (*httpFile) Readdir(count int) ([]os.FileInfo, error) {
- return nil, fmt.Errorf("readdir not available")
-}
-
-const redirectFmt = `<!DOCTYPE html><html><head><title>%s</title><link rel="canonical" href="%[1]s"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=%[1]s" /></head></html>`
-
-type httpDir struct {
- info httpFileInfo
- off int // 0 or 1
-}
-
-func (*httpDir) Close() error { return nil }
-func (*httpDir) Read([]byte) (int, error) { return 0, fmt.Errorf("read not available") }
-func (*httpDir) Seek(int64, int) (int64, error) { return 0, fmt.Errorf("seek not available") }
-func (*httpDir) Stat() (os.FileInfo, error) { return &httpFileInfo{".", 0, true}, nil }
-func (d *httpDir) Readdir(count int) ([]os.FileInfo, error) {
- if count == 0 {
- return nil, nil
- }
- if d.off > 0 {
- return nil, io.EOF
- }
- d.off = 1
- return []os.FileInfo{&d.info}, nil
-}
-
-type httpFileInfo struct {
- name string
- size int64
- dir bool
-}
-
-func (info *httpFileInfo) Name() string { return info.name }
-func (info *httpFileInfo) Size() int64 { return info.size }
-func (info *httpFileInfo) Mode() os.FileMode {
- if info.dir {
- return os.ModeDir | 0555
- }
- return 0444
-}
-func (info *httpFileInfo) ModTime() time.Time { return time.Time{} }
-func (info *httpFileInfo) IsDir() bool { return info.dir }
-func (info *httpFileInfo) Sys() interface{} { return nil }
diff --git a/go.dev/cmd/internal/site/tmpl.go b/go.dev/cmd/internal/site/tmpl.go
deleted file mode 100644
index 848cdae..0000000
--- a/go.dev/cmd/internal/site/tmpl.go
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright 2021 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 site
-
-import (
- "fmt"
- "reflect"
- "strings"
-
- "golang.org/x/website/internal/backport/html/template"
- "golang.org/x/website/internal/tmplfunc"
- "gopkg.in/yaml.v3"
-)
-
-func (site *Site) initTemplate() error {
- funcs := template.FuncMap{
- "add": func(i, j int) int { return i + j },
- "data": site.data,
- "dict": dict,
- "first": first,
- "markdown": markdown,
- "newest": newest,
- "page": site.pageByPath,
- "pages": site.pagesGlob,
- "replace": replace,
- "rawhtml": rawhtml,
- "trim": strings.Trim,
- "yaml": yamlFn,
- }
-
- site.base = template.New("site").Funcs(funcs)
- if err := tmplfunc.ParseGlob(site.base, site.file("_templates/*.tmpl")); err != nil && !strings.Contains(err.Error(), "pattern matches no files") {
- return err
- }
- return nil
-}
-
-func (site *Site) clone() *template.Template {
- t := template.Must(site.base.Clone())
- if err := tmplfunc.Funcs(t); err != nil {
- panic(err)
- }
- return t
-}
-
-func toString(x interface{}) string {
- switch x := x.(type) {
- case string:
- return x
- case template.HTML:
- return string(x)
- case nil:
- return ""
- default:
- panic(fmt.Sprintf("cannot toString %T", x))
- }
-}
-
-func first(n int, list reflect.Value) reflect.Value {
- if !list.IsValid() {
- return list
- }
- if list.Kind() == reflect.Interface {
- if list.IsNil() {
- return list
- }
- list = list.Elem()
- }
-
- if list.Len() < n {
- return list
- }
- return list.Slice(0, n)
-}
-
-func dict(args ...interface{}) map[string]interface{} {
- m := make(map[string]interface{})
- for i := 0; i < len(args); i += 2 {
- m[args[i].(string)] = args[i+1]
- }
- m["Identifier"] = "IDENT"
- return m
-}
-
-func list(args ...interface{}) []interface{} {
- return args
-}
-
-// markdown is the function provided to templates.
-func markdown(data interface{}) (template.HTML, error) {
- h, err := markdownToHTML(toString(data))
- if err != nil {
- return "", err
- }
- s := strings.TrimSpace(string(h))
- if strings.HasPrefix(s, "<p>") && strings.HasSuffix(s, "</p>") && strings.Count(s, "<p>") == 1 {
- h = template.HTML(strings.TrimSpace(s[len("<p>") : len(s)-len("</p>")]))
- }
- return h, nil
-}
-
-func replace(input, x, y interface{}) string {
- return strings.ReplaceAll(toString(input), toString(x), toString(y))
-}
-
-func rawhtml(s interface{}) template.HTML {
- return template.HTML(toString(s))
-}
-
-func yamlFn(s string) (interface{}, error) {
- var d interface{}
- if err := yaml.Unmarshal([]byte(s), &d); err != nil {
- return nil, err
- }
- return d, nil
-}
diff --git a/go.dev/testdata/golden/solutions/chrome/index.html b/go.dev/testdata/golden/solutions/chrome/index.html
deleted file mode 100644
index 6cc9549..0000000
--- a/go.dev/testdata/golden/solutions/chrome/index.html
+++ /dev/null
@@ -1 +0,0 @@
-<!DOCTYPE html><html><head><title>/solutions/google/chrome</title><link rel="canonical" href="/solutions/google/chrome"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=/solutions/google/chrome" /></head></html>
diff --git a/go.dev/testdata/golden/solutions/coredata/index.html b/go.dev/testdata/golden/solutions/coredata/index.html
deleted file mode 100644
index 5e2d4bd..0000000
--- a/go.dev/testdata/golden/solutions/coredata/index.html
+++ /dev/null
@@ -1 +0,0 @@
-<!DOCTYPE html><html><head><title>/solutions/google/coredata</title><link rel="canonical" href="/solutions/google/coredata"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=/solutions/google/coredata" /></head></html>
diff --git a/go.dev/testdata/golden/solutions/firebase/index.html b/go.dev/testdata/golden/solutions/firebase/index.html
deleted file mode 100644
index 8cd7d16..0000000
--- a/go.dev/testdata/golden/solutions/firebase/index.html
+++ /dev/null
@@ -1 +0,0 @@
-<!DOCTYPE html><html><head><title>/solutions/google/firebase</title><link rel="canonical" href="/solutions/google/firebase"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=/solutions/google/firebase" /></head></html>
diff --git a/go.dev/testdata/golden/solutions/sitereliability/index.html b/go.dev/testdata/golden/solutions/sitereliability/index.html
deleted file mode 100644
index 8c17ec9..0000000
--- a/go.dev/testdata/golden/solutions/sitereliability/index.html
+++ /dev/null
@@ -1 +0,0 @@
-<!DOCTYPE html><html><head><title>/solutions/google/sitereliability</title><link rel="canonical" href="/solutions/google/sitereliability"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=/solutions/google/sitereliability" /></head></html>
diff --git a/internal/codewalk/codewalk.go b/internal/codewalk/codewalk.go
index cefe691..0824347 100644
--- a/internal/codewalk/codewalk.go
+++ b/internal/codewalk/codewalk.go
@@ -81,10 +81,10 @@
}
s.site.ServePage(w, r, web.Page{
- Title: "Codewalk: " + cw.Title,
- TabTitle: cw.Title,
- Template: "codewalk.html",
- Data: cw,
+ "title": "Codewalk: " + cw.Title,
+ "tabTitle": cw.Title,
+ "layout": "codewalk",
+ "codewalk": cw,
})
}
@@ -234,9 +234,9 @@
}
s.site.ServePage(w, r, web.Page{
- Title: "Codewalks",
- Template: "codewalkdir.html",
- Data: v,
+ "title": "Codewalks",
+ "layout": "codewalkdir",
+ "dirs": v,
})
}
diff --git a/internal/dl/server.go b/internal/dl/server.go
index dfb8b03..a663ac8 100644
--- a/internal/dl/server.go
+++ b/internal/dl/server.go
@@ -83,9 +83,9 @@
}
h.site.ServePage(w, r, web.Page{
- Title: "Downloads",
- Template: "dl.html",
- Data: d,
+ "title": "Downloads",
+ "layout": "dl",
+ "dl": d,
})
}
diff --git a/internal/pkgdoc/doc.go b/internal/pkgdoc/doc.go
index 0ad9abc..81fe1fb 100644
--- a/internal/pkgdoc/doc.go
+++ b/internal/pkgdoc/doc.go
@@ -34,16 +34,20 @@
)
type docs struct {
- fs fs.FS
- api api.DB
- site *web.Site
- root *Dir
+ fs fs.FS
+ api api.DB
+ site *web.Site
+ root *Dir
+ forceOld func(*http.Request) bool
}
// NewServer returns an HTTP handler serving package docs
// for packages loaded from fsys (a tree in GOROOT layout),
// styled according to site.
-func NewServer(fsys fs.FS, site *web.Site) (http.Handler, error) {
+// If forceOld is not nil and returns true for a given request,
+// NewServer will serve docs itself instead of redirecting to pkg.go.dev
+// (forcing the ?m=old behavior).
+func NewServer(fsys fs.FS, site *web.Site, forceOld func(*http.Request) bool) (http.Handler, error) {
apiDB, err := api.Load(fsys)
if err != nil {
return nil, err
@@ -57,10 +61,11 @@
Dirs: dirs,
}
docs := &docs{
- fs: fsys,
- api: apiDB,
- site: site,
- root: root,
+ fs: fsys,
+ api: apiDB,
+ site: site,
+ root: root,
+ forceOld: forceOld,
}
return docs, nil
}
@@ -68,8 +73,6 @@
type Page struct {
docs *docs // outer doc collection
- Web *web.Page // filled in by caller
-
OldDocs bool // use ?m=old in doc links
Dirname string // directory containing the package
@@ -90,10 +93,6 @@
DirFlat bool // if set, show directory in a flat (non-indented) manner
}
-func (p *Page) SetWebPage(w *web.Page) {
- p.Web = w
-}
-
type mode uint
const (
@@ -433,7 +432,7 @@
// First, the request can set ?m=old to get the old pages.
// Second, the request can come from China:
// since pkg.go.dev is not available in China, we serve the docs directly.
- if mode&modeOld == 0 && !web.GoogleCN(r) {
+ if mode&modeOld == 0 && (d.forceOld == nil || !d.forceOld(r)) {
if relpath == "" {
relpath = "std"
}
@@ -500,16 +499,16 @@
tabtitle = "Commands"
}
- name := "package.html"
+ layout := "pkg"
if info.Dirname == "src" {
- name = "packageroot.html"
+ layout = "pkgroot"
}
d.site.ServePage(w, r, web.Page{
- Title: title,
- TabTitle: tabtitle,
- Subtitle: subtitle,
- Template: name,
- Data: info,
+ "title": title,
+ "tabTitle": tabtitle,
+ "subtitle": subtitle,
+ "layout": layout,
+ "pkg": info,
})
}
diff --git a/internal/pkgdoc/doc_test.go b/internal/pkgdoc/doc_test.go
index ad1eaa1..d5fa102 100644
--- a/internal/pkgdoc/doc_test.go
+++ b/internal/pkgdoc/doc_test.go
@@ -24,11 +24,8 @@
// ` + packageComment + `
package main`)},
}
- site, err := web.NewSite(fs)
- if err != nil {
- t.Fatal(err)
- }
- h, err := NewServer(fs, site)
+ site := web.NewSite(fs)
+ h, err := NewServer(fs, site, nil)
if err != nil {
t.Fatal(err)
}
@@ -66,11 +63,8 @@
//line foo.go:100`)}, // No newline at end to check corner cases.
}
- site, err := web.NewSite(fs)
- if err != nil {
- t.Fatal(err)
- }
- h, err := NewServer(fs, site)
+ site := web.NewSite(fs)
+ h, err := NewServer(fs, site, nil)
if err != nil {
t.Fatal(err)
}
diff --git a/internal/pkgdoc/html_test.go b/internal/pkgdoc/html_test.go
index 3b02145..af540ed 100644
--- a/internal/pkgdoc/html_test.go
+++ b/internal/pkgdoc/html_test.go
@@ -13,7 +13,6 @@
"testing"
"golang.org/x/website/internal/backport/html/template"
- "golang.org/x/website/internal/web"
)
func TestSrcPosLink(t *testing.T) {
@@ -164,7 +163,6 @@
}
func linkifySource(t *testing.T, src []byte) string {
- site := &web.Site{}
fset := token.NewFileSet()
af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
if err != nil {
@@ -174,11 +172,6 @@
pi := &Page{
fset: fset,
}
- pg := &web.Page{
- Data: pi,
- Site: site,
- }
- pi.SetWebPage(pg)
sep := ""
for _, decl := range af.Decls {
buf.WriteString(sep)
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
index 2584b36..799e11b 100644
--- a/internal/proxy/proxy.go
+++ b/internal/proxy/proxy.go
@@ -16,8 +16,6 @@
"log"
"net/http"
"time"
-
- "golang.org/x/website/internal/web"
)
const playgroundURL = "https://play.golang.org"
@@ -40,9 +38,13 @@
const expires = 7 * 24 * time.Hour // 1 week
var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
-func RegisterHandlers(mux *http.ServeMux) {
+// RegisterHandlers registers handlers
+// for golang.org/compile and golang.org/share on mux.
+// If disallow is non-nil, then the share handler disallows requests
+// for which disallowShare returns true.
+func RegisterHandlers(mux *http.ServeMux, disallowShare func(*http.Request) bool) {
mux.HandleFunc("golang.org/compile", compile)
- mux.HandleFunc("golang.org/share", share)
+ mux.HandleFunc("golang.org/share", share(disallowShare))
}
func compile(w http.ResponseWriter, r *http.Request) {
@@ -122,31 +124,33 @@
return buf.String()
}
-func share(w http.ResponseWriter, r *http.Request) {
- if web.GoogleCN(r) {
- http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
- return
- }
-
- // HACK(cbro): use a simple proxy rather than httputil.ReverseProxy because of Issue #28168.
- // TODO: investigate using ReverseProxy with a Director, unsetting whatever's necessary to make that work.
- req, _ := http.NewRequest("POST", playgroundURL+"/share", r.Body)
- req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
- req = req.WithContext(r.Context())
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- log.Printf("ERROR share error: %v", err)
- http.Error(w, "Internal Server Error", http.StatusInternalServerError)
- return
- }
- copyHeader := func(k string) {
- if v := resp.Header.Get(k); v != "" {
- w.Header().Set(k, v)
+func share(disallow func(*http.Request) bool) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if disallow != nil && disallow(r) {
+ http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+ return
}
+
+ // HACK(cbro): use a simple proxy rather than httputil.ReverseProxy because of Issue #28168.
+ // TODO: investigate using ReverseProxy with a Director, unsetting whatever's necessary to make that work.
+ req, _ := http.NewRequest("POST", playgroundURL+"/share", r.Body)
+ req.Header.Set("Content-Type", r.Header.Get("Content-Type"))
+ req = req.WithContext(r.Context())
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ log.Printf("ERROR share error: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ copyHeader := func(k string) {
+ if v := resp.Header.Get(k); v != "" {
+ w.Header().Set(k, v)
+ }
+ }
+ copyHeader("Content-Type")
+ copyHeader("Content-Length")
+ defer resp.Body.Close()
+ w.WriteHeader(resp.StatusCode)
+ io.Copy(w, resp.Body)
}
- copyHeader("Content-Type")
- copyHeader("Content-Length")
- defer resp.Body.Close()
- w.WriteHeader(resp.StatusCode)
- io.Copy(w, resp.Body)
}
diff --git a/internal/web/docfuncs.go b/internal/web/code.go
similarity index 90%
rename from internal/web/docfuncs.go
rename to internal/web/code.go
index 6494cd7..3da494e 100644
--- a/internal/web/docfuncs.go
+++ b/internal/web/code.go
@@ -8,32 +8,21 @@
"bytes"
"fmt"
"log"
- "path"
"regexp"
"strings"
"golang.org/x/website/internal/backport/html/template"
- "golang.org/x/website/internal/backport/io/fs"
- "golang.org/x/website/internal/history"
"golang.org/x/website/internal/texthtml"
)
-func (s *Site) initDocFuncs() {
- s.docFuncs = template.FuncMap{
- "code": s.code,
- "releases": func() []*history.Major { return history.Majors },
- }
-}
-
-func (s *Site) code(file string, arg ...interface{}) (_ template.HTML, err error) {
+func (s *siteDir) code(file string, arg ...interface{}) (_ template.HTML, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
- file = path.Clean(strings.TrimPrefix(file, "/"))
- btext, err := fs.ReadFile(s.fs, file)
+ btext, err := s.readFile(s.dir, file)
if err != nil {
return "", err
}
diff --git a/internal/web/file.go b/internal/web/file.go
deleted file mode 100644
index 4b83422..0000000
--- a/internal/web/file.go
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright 2009 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 web
-
-import (
- "bytes"
- "encoding/json"
- "log"
- "path"
- "strings"
-
- "golang.org/x/website/internal/backport/io/fs"
-)
-
-type file struct {
- // Copied from document metadata directives
- Title string
- Subtitle string
- Template bool
-
- Path string // URL path
- FilePath string // filesystem path relative to goroot
- Body []byte // content after metadata
-}
-
-type fileJSON struct {
- Title string
- Subtitle string
- Template bool
- Redirect string // if set, redirect to other URL
-}
-
-// open returns the *file for a given relative path or nil if none exists.
-func open(fsys fs.FS, relpath string) *file {
- // Strip trailing .html or .md or /; it all names the same page.
- if strings.HasSuffix(relpath, ".html") {
- relpath = strings.TrimSuffix(relpath, ".html")
- } else if strings.HasSuffix(relpath, ".md") {
- relpath = strings.TrimSuffix(relpath, ".md")
- } else if strings.HasSuffix(relpath, "/") {
- relpath = strings.TrimSuffix(relpath, "/")
- }
-
- // Check md before html to work correctly when x/website is layered atop Go 1.15 goroot during Go 1.15 tests.
- // Want to find x/website's debugging_with_gdb.md not Go 1.15's debuging_with_gdb.html.
- files := []string{relpath + ".md", relpath + ".html", path.Join(relpath, "index.md"), path.Join(relpath, "index.html")}
- var filePath string
- var b []byte
- var err error
- for _, filePath = range files {
- b, err = fs.ReadFile(fsys, filePath)
- if err == nil {
- break
- }
- }
-
- // Special case for memory model and spec, which live
- // in the main Go repo's doc directory and therefore have not
- // been renamed to their serving relpaths.
- // We wait until the ReadFiles above have failed so that the
- // code works if these are ever moved to /ref/spec and /ref/mem.
- if err != nil && relpath == "ref/spec" {
- return open(fsys, "doc/go_spec")
- }
- if err != nil && relpath == "ref/mem" {
- return open(fsys, "doc/go_mem")
- }
-
- if err != nil {
- return nil
- }
-
- // Special case for memory model and spec, continued.
- switch relpath {
- case "doc/go_spec":
- relpath = "ref/spec"
- case "doc/go_mem":
- relpath = "ref/mem"
- }
-
- // If we read an index.md or index.html, the canonical relpath is without the index.md/index.html suffix.
- if name := path.Base(filePath); name == "index.html" || name == "index.md" {
- relpath, _ = path.Split(filePath)
- }
-
- js, body, err := parseFile(b)
- if err != nil {
- log.Printf("extractMetadata %s: %v", relpath, err)
- return nil
- }
-
- f := &file{
- Title: js.Title,
- Subtitle: js.Subtitle,
- Template: js.Template,
- Path: "/" + relpath,
- FilePath: filePath,
- Body: body,
- }
- if js.Redirect != "" {
- // Allow (placeholder) documents to declare a redirect.
- f.Path = js.Redirect
- }
-
- return f
-}
-
-var (
- jsonStart = []byte("<!--{")
- jsonEnd = []byte("}-->")
-)
-
-// parseFile extracts the metaJSON from a byte slice.
-// It returns the metadata and the remaining text.
-// If no metadata is present, it returns an empty metaJSON and the original text.
-func parseFile(b []byte) (meta fileJSON, tail []byte, err error) {
- tail = b
- if !bytes.HasPrefix(b, jsonStart) {
- return
- }
- end := bytes.Index(b, jsonEnd)
- if end < 0 {
- return
- }
- b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
- if err = json.Unmarshal(b, &meta); err != nil {
- return
- }
- tail = tail[end+len(jsonEnd):]
- return
-}
diff --git a/internal/web/markdown.go b/internal/web/markdown.go
deleted file mode 100644
index 2cee319..0000000
--- a/internal/web/markdown.go
+++ /dev/null
@@ -1,72 +0,0 @@
-// 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 web
-
-import (
- "bytes"
- "unicode/utf8"
-
- "github.com/yuin/goldmark"
- "github.com/yuin/goldmark/parser"
- "github.com/yuin/goldmark/renderer/html"
-)
-
-// renderMarkdown converts a limited and opinionated flavor of Markdown (compliant with
-// CommonMark 0.29) to HTML for the purposes of Go websites.
-//
-// The Markdown source may contain raw HTML,
-// but Go templates have already been processed.
-func renderMarkdown(src []byte) ([]byte, error) {
- src = replaceTabs(src)
- // parser.WithHeadingAttribute allows custom ids on headings.
- // html.WithUnsafe allows use of raw HTML, which we need for tables.
- md := goldmark.New(
- goldmark.WithParserOptions(parser.WithHeadingAttribute()),
- goldmark.WithRendererOptions(html.WithUnsafe()))
- var buf bytes.Buffer
- if err := md.Convert(src, &buf); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
-
-// replaceTabs replaces all tabs in text with spaces up to a 4-space tab stop.
-//
-// In Markdown, tabs used for indentation are required to be interpreted as
-// 4-space tab stops. See https://spec.commonmark.org/0.30/#tabs.
-// Go also renders nicely and more compactly on the screen with 4-space
-// tab stops, while browsers often use 8-space.
-// And Goldmark crashes in some inputs that mix spaces and tabs.
-// Fix the crashes and make the Go code consistently compact across browsers,
-// all while staying Markdown-compatible, by expanding to 4-space tab stops.
-//
-// This function does not handle multi-codepoint Unicode sequences correctly.
-func replaceTabs(text []byte) []byte {
- var buf bytes.Buffer
- col := 0
- for len(text) > 0 {
- r, size := utf8.DecodeRune(text)
- text = text[size:]
-
- switch r {
- case '\n':
- buf.WriteByte('\n')
- col = 0
-
- case '\t':
- buf.WriteByte(' ')
- col++
- for col%4 != 0 {
- buf.WriteByte(' ')
- col++
- }
-
- default:
- buf.WriteRune(r)
- col++
- }
- }
- return buf.Bytes()
-}
diff --git a/internal/web/page.go b/internal/web/page.go
new file mode 100644
index 0000000..e6fccd7
--- /dev/null
+++ b/internal/web/page.go
@@ -0,0 +1,179 @@
+// Copyright 2021 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 web
+
+import (
+ "bytes"
+ "encoding/json"
+ "path"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "golang.org/x/website/internal/backport/io/fs"
+ "gopkg.in/yaml.v3"
+)
+
+// A pageFile is a Page loaded from a file.
+// It corresponds to some .md or .html file in the content tree.
+type pageFile struct {
+ file string // .md file for page
+ stat fs.FileInfo // stat for file when page was loaded
+ url string // url excluding site.BaseURL; always begins with slash
+ data []byte // page data (markdown)
+ page Page // parameters passed to templates
+
+ checked int64 // unix nano, atomically updated
+}
+
+// A Page is the data for a web page.
+// See the package doc comment for details.
+type Page map[string]interface{}
+
+func (site *Site) openPage(file string) (*pageFile, error) {
+ // Strip trailing .html or .md or /; it all names the same page.
+ if strings.HasSuffix(file, "/index.md") {
+ file = strings.TrimSuffix(file, "/index.md")
+ } else if strings.HasSuffix(file, "/index.html") {
+ file = strings.TrimSuffix(file, "/index.html")
+ } else if file == "index.md" || file == "index.html" {
+ file = "."
+ } else if strings.HasSuffix(file, "/") {
+ file = strings.TrimSuffix(file, "/")
+ } else if strings.HasSuffix(file, ".html") {
+ file = strings.TrimSuffix(file, ".html")
+ } else {
+ file = strings.TrimSuffix(file, ".md")
+ }
+
+ now := time.Now().UnixNano()
+ if cp, ok := site.cache.Load(file); ok {
+ // Have cache entry; only use if the underlying file hasn't changed.
+ // To avoid continuous stats, only check it has been 3s since the last one.
+ // TODO(rsc): Move caching into a more general layer and cache templates.
+ p := cp.(*pageFile)
+ if now-atomic.LoadInt64(&p.checked) >= 3e9 {
+ info, err := fs.Stat(site.fs, p.file)
+ if err == nil && info.ModTime().Equal(p.stat.ModTime()) && info.Size() == p.stat.Size() {
+ atomic.StoreInt64(&p.checked, now)
+ return p, nil
+ }
+ }
+ }
+
+ // Check md before html to work correctly when x/website is layered atop Go 1.15 goroot during Go 1.15 tests.
+ // Want to find x/website's debugging_with_gdb.md not Go 1.15's debuging_with_gdb.html.
+ files := []string{file + ".md", file + ".html", path.Join(file, "index.md"), path.Join(file, "index.html")}
+ var filePath string
+ var b []byte
+ var err error
+ var stat fs.FileInfo
+ for _, filePath = range files {
+ stat, err = fs.Stat(site.fs, filePath)
+ if err == nil {
+ b, err = site.readFile(".", filePath)
+ if err == nil {
+ break
+ }
+ }
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ // If we read an index.md or index.html, the canonical relpath is without the index.md/index.html suffix.
+ url := path.Join("/", file)
+ if name := path.Base(filePath); name == "index.html" || name == "index.md" {
+ url, _ = path.Split(path.Join("/", filePath))
+ }
+
+ params, body, err := parseMeta(b)
+ if err != nil {
+ return nil, err
+ }
+
+ p := &pageFile{
+ file: filePath,
+ stat: stat,
+ url: url,
+ data: body,
+ page: params,
+ checked: now,
+ }
+
+ // File, FileData, URL
+ p.page["File"] = filePath
+ p.page["FileData"] = string(body)
+ p.page["URL"] = p.url
+
+ // User-specified redirect: overrides url but not URL.
+ if redir, _ := p.page["redirect"].(string); redir != "" {
+ p.url = redir
+ }
+
+ site.cache.Store(file, p)
+
+ return p, nil
+}
+
+var (
+ jsonStart = []byte("<!--{")
+ jsonEnd = []byte("}-->")
+
+ yamlStart = []byte("---\n")
+ yamlEnd = []byte("\n---\n")
+)
+
+// parseMeta extracts top-of-file metadata from the file contents b.
+// If there is no metadata, parseMeta returns Page{}, b, nil.
+// Otherwise, the metdata is extracted, and parseMeta returns
+// the metadata and the remainder of the file.
+// The end of the metadata is overwritten in b to preserve
+// the correct number of newlines so that the line numbers in tail
+// match the line numbers in b.
+//
+// A JSON metadata object is bracketed by <!--{...}-->.
+// A YAML metadata object is bracketed by "---\n" above and below the YAML.
+//
+// JSON is typically used in HTML; YAML is typically used in Markdown.
+func parseMeta(b []byte) (meta Page, tail []byte, err error) {
+ tail = b
+ meta = make(Page)
+ var end int
+ if bytes.HasPrefix(b, jsonStart) {
+ end = bytes.Index(b, jsonEnd)
+ if end < 0 {
+ return
+ }
+ b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
+ if err = json.Unmarshal(b, &meta); err != nil {
+ return
+ }
+ end += len(jsonEnd)
+ for k, v := range meta {
+ delete(meta, k)
+ meta[strings.ToLower(k)] = v
+ }
+ } else if bytes.HasPrefix(b, yamlStart) {
+ end = bytes.Index(b, yamlEnd)
+ if end < 0 {
+ return
+ }
+ b = b[len(yamlStart) : end+1] // drop ---\n but include final \n
+ if err = yaml.Unmarshal(b, &meta); err != nil {
+ return
+ }
+ end += len(yamlEnd)
+ }
+
+ // Put the right number of \n at the start of tail to preserve line numbers.
+ nl := bytes.Count(tail[:end], []byte("\n"))
+ for i := 0; i < nl; i++ {
+ end--
+ tail[end] = '\n'
+ }
+ tail = tail[end:]
+ return
+}
diff --git a/internal/web/pkg.go b/internal/web/pkg.go
new file mode 100644
index 0000000..37709d3
--- /dev/null
+++ b/internal/web/pkg.go
@@ -0,0 +1,82 @@
+// Copyright 2021 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 web
+
+import (
+ "path"
+ "strings"
+ "unicode"
+)
+
+type pkgPath struct{}
+
+func (pkgPath) Base(a string) string { return path.Base(a) }
+func (pkgPath) Clean(a string) string { return path.Clean(a) }
+func (pkgPath) Dir(a string) string { return path.Dir(a) }
+func (pkgPath) Ext(a string) string { return path.Ext(a) }
+func (pkgPath) IsAbs(a string) bool { return path.IsAbs(a) }
+func (pkgPath) Join(a ...string) string { return path.Join(a...) }
+func (pkgPath) Match(a, b string) (bool, error) { return path.Match(a, b) }
+func (pkgPath) Split(a string) (string, string) { return path.Split(a) }
+
+type pkgStrings struct{}
+
+func (pkgStrings) Compare(a, b string) int { return strings.Compare(a, b) }
+func (pkgStrings) Contains(a, b string) bool { return strings.Contains(a, b) }
+func (pkgStrings) ContainsAny(a, b string) bool { return strings.ContainsAny(a, b) }
+func (pkgStrings) ContainsRune(a string, b rune) bool { return strings.ContainsRune(a, b) }
+func (pkgStrings) Count(a, b string) int { return strings.Count(a, b) }
+func (pkgStrings) EqualFold(a, b string) bool { return strings.EqualFold(a, b) }
+func (pkgStrings) Fields(a string) []string { return strings.Fields(a) }
+func (pkgStrings) FieldsFunc(a string, b func(rune) bool) []string { return strings.FieldsFunc(a, b) }
+func (pkgStrings) HasPrefix(a, b string) bool { return strings.HasPrefix(a, b) }
+func (pkgStrings) HasSuffix(a, b string) bool { return strings.HasSuffix(a, b) }
+func (pkgStrings) Index(a, b string) int { return strings.Index(a, b) }
+func (pkgStrings) IndexAny(a, b string) int { return strings.IndexAny(a, b) }
+func (pkgStrings) IndexByte(a string, b byte) int { return strings.IndexByte(a, b) }
+func (pkgStrings) IndexFunc(a string, b func(rune) bool) int { return strings.IndexFunc(a, b) }
+func (pkgStrings) IndexRune(a string, b rune) int { return strings.IndexRune(a, b) }
+func (pkgStrings) Join(a []string, b string) string { return strings.Join(a, b) }
+func (pkgStrings) LastIndex(a, b string) int { return strings.LastIndex(a, b) }
+func (pkgStrings) LastIndexAny(a, b string) int { return strings.LastIndexAny(a, b) }
+func (pkgStrings) LastIndexByte(a string, b byte) int { return strings.LastIndexByte(a, b) }
+func (pkgStrings) LastIndexFunc(a string, b func(rune) bool) int {
+ return strings.LastIndexFunc(a, b)
+}
+func (pkgStrings) Map(a func(rune) rune, b string) string { return strings.Map(a, b) }
+func (pkgStrings) NewReader(a string) *strings.Reader { return strings.NewReader(a) }
+func (pkgStrings) NewReplacer(a ...string) *strings.Replacer { return strings.NewReplacer(a...) }
+func (pkgStrings) Repeat(a string, b int) string { return strings.Repeat(a, b) }
+func (pkgStrings) Replace(a, b, c string, d int) string { return strings.Replace(a, b, c, d) }
+func (pkgStrings) ReplaceAll(a, b, c string) string { return strings.ReplaceAll(a, b, c) }
+func (pkgStrings) Split(a, b string) []string { return strings.Split(a, b) }
+func (pkgStrings) SplitAfter(a, b string) []string { return strings.SplitAfter(a, b) }
+func (pkgStrings) SplitAfterN(a, b string, c int) []string { return strings.SplitAfterN(a, b, c) }
+func (pkgStrings) SplitN(a, b string, c int) []string { return strings.SplitN(a, b, c) }
+func (pkgStrings) Title(a string) string { return strings.Title(a) }
+func (pkgStrings) ToLower(a string) string { return strings.ToLower(a) }
+func (pkgStrings) ToLowerSpecial(a unicode.SpecialCase, b string) string {
+ return strings.ToLowerSpecial(a, b)
+}
+func (pkgStrings) ToTitle(a string) string { return strings.ToTitle(a) }
+func (pkgStrings) ToTitleSpecial(a unicode.SpecialCase, b string) string {
+ return strings.ToTitleSpecial(a, b)
+}
+func (pkgStrings) ToUpper(a string) string { return strings.ToUpper(a) }
+func (pkgStrings) ToUpperSpecial(a unicode.SpecialCase, b string) string {
+ return strings.ToUpperSpecial(a, b)
+}
+func (pkgStrings) ToValidUTF8(a, b string) string { return strings.ToValidUTF8(a, b) }
+func (pkgStrings) Trim(a, b string) string { return strings.Trim(a, b) }
+func (pkgStrings) TrimFunc(a string, b func(rune) bool) string { return strings.TrimFunc(a, b) }
+func (pkgStrings) TrimLeft(a, b string) string { return strings.TrimLeft(a, b) }
+func (pkgStrings) TrimLeftFunc(a string, b func(rune) bool) string { return strings.TrimLeftFunc(a, b) }
+func (pkgStrings) TrimPrefix(a, b string) string { return strings.TrimPrefix(a, b) }
+func (pkgStrings) TrimRight(a, b string) string { return strings.TrimRight(a, b) }
+func (pkgStrings) TrimRightFunc(a string, b func(rune) bool) string {
+ return strings.TrimRightFunc(a, b)
+}
+func (pkgStrings) TrimSpace(a string) string { return strings.TrimSpace(a) }
+func (pkgStrings) TrimSuffix(a, b string) string { return strings.TrimSuffix(a, b) }
diff --git a/internal/web/render.go b/internal/web/render.go
new file mode 100644
index 0000000..fbcc2dd
--- /dev/null
+++ b/internal/web/render.go
@@ -0,0 +1,254 @@
+// 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 web
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "path"
+ "regexp"
+ "strings"
+ "unicode/utf8"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+ "golang.org/x/website/internal/backport/html/template"
+ "golang.org/x/website/internal/backport/io/fs"
+ "golang.org/x/website/internal/tmplfunc"
+)
+
+// renderHTML renders and returns the HTML for the page.
+func (site *Site) renderHTML(p Page, r *http.Request) ([]byte, error) {
+ // Clone p, because we are going to set its Content key-value pair.
+ p2 := make(Page)
+ for k, v := range p {
+ p2[k] = v
+ }
+ p = p2
+
+ url, ok := p["URL"].(string)
+ if !ok {
+ // Set URL - caller did not.
+ p["URL"] = r.URL.Path
+ }
+ file, _ := p["File"].(string)
+ data, _ := p["FileData"].(string)
+
+ // Load base template.
+ base, err := site.readFile(".", "site.tmpl")
+ if err != nil {
+ return nil, err
+ }
+
+ dir := strings.Trim(path.Dir(url), "/")
+ if dir == "" {
+ dir = "."
+ }
+ sd := &siteDir{site, dir}
+
+ t := template.New("site.tmpl").Funcs(template.FuncMap{
+ "add": func(a, b int) int { return a + b },
+ "sub": func(a, b int) int { return a - b },
+ "mul": func(a, b int) int { return a * b },
+ "div": func(a, b int) int { return a / b },
+ "code": sd.code,
+ "data": sd.data,
+ "page": sd.page,
+ "pages": sd.pages,
+ "request": func() *http.Request { return r },
+ "path": func() pkgPath { return pkgPath{} },
+ "strings": func() pkgStrings { return pkgStrings{} },
+ "first": first,
+ "markdown": markdown,
+ "rawhtml": rawhtml,
+ "readfile": sd.readfile,
+ "yaml": yamlFn,
+ })
+ t.Funcs(site.funcs)
+
+ if err := tmplfunc.Parse(t, string(base)); err != nil {
+ return nil, err
+ }
+
+ // Load page-specific layout template.
+ layout, _ := p["layout"].(string)
+ if layout == "" {
+ l, ok := site.findLayout(dir, "default")
+ if ok {
+ layout = l
+ } else {
+ layout = "none"
+ }
+ } else if path.IsAbs(layout) {
+ layout = strings.TrimLeft(path.Clean(layout+".tmpl"), "/")
+ } else if strings.Contains(layout, "/") {
+ layout = path.Join(dir, layout+".tmpl")
+ } else if layout != "none" {
+ l, ok := site.findLayout(dir, layout)
+ if !ok {
+ return nil, fmt.Errorf("cannot find layout %q", layout)
+ }
+ layout = l
+ }
+
+ if layout != "none" {
+ ldata, err := site.readFile(".", layout)
+ if err != nil {
+ return nil, err
+ }
+ if err := tmplfunc.Parse(t.New(layout), string(ldata)); err != nil {
+ return nil, err
+ }
+ }
+
+ var buf bytes.Buffer
+ if _, ok := p["Content"]; !ok && data != "" {
+ // Load actual Markdown content (also a template).
+ tf := t.New(file)
+ if err := tmplfunc.Parse(tf, data); err != nil {
+ return nil, err
+ }
+ if err := tf.Execute(&buf, p); err != nil {
+ return nil, err
+ }
+ if strings.HasSuffix(file, ".md") {
+ html, err := markdownToHTML(buf.String())
+ if err != nil {
+ return nil, err
+ }
+ p["Content"] = html
+ } else {
+ p["Content"] = template.HTML(buf.String())
+ }
+ buf.Reset()
+ }
+
+ if err := t.Execute(&buf, p); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+// findLayout searches the start directory and parent directories for a template with the given base name.
+func (site *Site) findLayout(dir, name string) (string, bool) {
+ name += ".tmpl"
+ for {
+ abs := path.Join(dir, name)
+ if _, err := fs.Stat(site.fs, abs); err == nil {
+ return abs, true
+ }
+ if dir == "." {
+ return "", false
+ }
+ dir = path.Dir(dir)
+ }
+}
+
+// markdownToHTML converts Markdown to HTML.
+// The Markdown source may contain raw HTML,
+// but Go templates have already been processed.
+func markdownToHTML(markdown string) (template.HTML, error) {
+ // parser.WithHeadingAttribute allows custom ids on headings.
+ // html.WithUnsafe allows use of raw HTML, which we need for tables.
+ md := goldmark.New(
+ goldmark.WithParserOptions(
+ parser.WithHeadingAttribute(),
+ parser.WithAutoHeadingID(),
+ parser.WithASTTransformers(util.Prioritized(mdTransformFunc(mdLink), 1)),
+ ),
+ goldmark.WithRendererOptions(html.WithUnsafe()),
+ goldmark.WithExtensions(
+ extension.NewTypographer(),
+ extension.NewLinkify(
+ extension.WithLinkifyAllowedProtocols([][]byte{[]byte("http"), []byte("https")}),
+ extension.WithLinkifyEmailRegexp(regexp.MustCompile(`[^\x00-\x{10FFFF}]`)), // impossible
+ ),
+ extension.DefinitionList,
+ ),
+ )
+ var buf bytes.Buffer
+ if err := md.Convert(replaceTabs([]byte(markdown)), &buf); err != nil {
+ return "", err
+ }
+ return template.HTML(buf.Bytes()), nil
+}
+
+// mdTransformFunc is a func implementing parser.ASTTransformer.
+type mdTransformFunc func(*ast.Document, text.Reader, parser.Context)
+
+func (f mdTransformFunc) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
+ f(node, reader, pc)
+}
+
+// mdLink walks doc, adding rel=noreferrer target=_blank to non-relative links.
+func mdLink(doc *ast.Document, _ text.Reader, _ parser.Context) {
+ mdLinkWalk(doc)
+}
+
+func mdLinkWalk(n ast.Node) {
+ switch n := n.(type) {
+ case *ast.Link:
+ dest := string(n.Destination)
+ if strings.HasPrefix(dest, "https://") || strings.HasPrefix(dest, "http://") {
+ n.SetAttributeString("rel", []byte("noreferrer"))
+ n.SetAttributeString("target", []byte("_blank"))
+ }
+ return
+ case *ast.AutoLink:
+ // All autolinks are non-relative.
+ n.SetAttributeString("rel", []byte("noreferrer"))
+ n.SetAttributeString("target", []byte("_blank"))
+ return
+ }
+
+ for child := n.FirstChild(); child != nil; child = child.NextSibling() {
+ mdLinkWalk(child)
+ }
+}
+
+// replaceTabs replaces all tabs in text with spaces up to a 4-space tab stop.
+//
+// In Markdown, tabs used for indentation are required to be interpreted as
+// 4-space tab stops. See https://spec.commonmark.org/0.30/#tabs.
+// Go also renders nicely and more compactly on the screen with 4-space
+// tab stops, while browsers often use 8-space.
+// And Goldmark crashes in some inputs that mix spaces and tabs.
+// Fix the crashes and make the Go code consistently compact across browsers,
+// all while staying Markdown-compatible, by expanding to 4-space tab stops.
+//
+// This function does not handle multi-codepoint Unicode sequences correctly.
+func replaceTabs(text []byte) []byte {
+ var buf bytes.Buffer
+ col := 0
+ for len(text) > 0 {
+ r, size := utf8.DecodeRune(text)
+ text = text[size:]
+
+ switch r {
+ case '\n':
+ buf.WriteByte('\n')
+ col = 0
+
+ case '\t':
+ buf.WriteByte(' ')
+ col++
+ for col%4 != 0 {
+ buf.WriteByte(' ')
+ col++
+ }
+
+ default:
+ buf.WriteRune(r)
+ col++
+ }
+ }
+ return buf.Bytes()
+}
diff --git a/internal/web/site.go b/internal/web/site.go
index 4d57ab7..e5a814a 100644
--- a/internal/web/site.go
+++ b/internal/web/site.go
@@ -2,20 +2,310 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// Package web implements a basic web site serving framework.
+// The two fundamental types in this package are Site and Page.
+//
+// Sites
+//
+// A Site is an http.Handler that serves requests from a file system.
+// Use NewSite(fsys) to create a new Site.
+//
+// The Site is defined primarily by the content of its file system fsys,
+// which holds files to be served as well as templates for
+// converting Markdown or HTML fragments into full HTML pages.
+//
+// Pages
+//
+// A Page, which is a map[string]interface{}, is the raw data that a Site renders into a web page.
+// Typically a Page is loaded from a *.html or *.md file in the file system fsys, although
+// dynamic pages can be computed and passed to ServePage as well,
+// as described in “Serving Dynamic Pages” below.
+//
+// For a Page loaded from the file system, the key-value pairs in the map
+// are initialized from the YAML or JSON metadata block at the top of a Markdown or HTML file,
+// which looks like (YAML):
+//
+// ---
+// key: value
+// ...
+// ---
+//
+// or (JSON):
+//
+// <!--{
+// "Key": "value",
+// ...
+// }-->
+//
+// By convention, key-value pairs loaded from a metadata block use lower-case keys.
+// For historical reasons, keys in JSON metadata are converted to lower-case when read,
+// so that the two headers above both refer to a key with a lower-case k.
+//
+// A few keys have special meanings:
+//
+// The key-value pair “status: n” sets the HTTP response status to the integer code n.
+//
+// The key-value pair “redirect: url” causes requests for this page redirect to the given
+// relative or absolute URL.
+//
+// The key-value pair “layout: name” selects the page layout template with the given name.
+// See the next section, “Page Rendering”, for details about layout and rendering.
+//
+// In addition to these explicit key-value pairs, pages loaded from the file system
+// have a few implicit key-value pairs added by the page loading process:
+//
+// - File: the path in fsys to the file containing the page
+// - FileData: the file body, with the key-value metadata stripped
+// - URL: this page's URL path (/x/y/z for x/y/z.md, /x/y/ for x/y/index.md)
+//
+// The key “Content” is added during during the rendering process.
+// See “Page Rendering” for details.
+//
+// Page Rendering
+//
+// A Page's content is rendered in two steps: conversion to content, and framing of content.
+//
+// To convert a page to content, the page's file body (its FileData key, a []byte) is parsed
+// and executed as an HTML template, with the page itself passed as the template input data.
+// The template output is then interpreted as Markdown (perhaps with embedded HTML),
+// and converted to HTML. The result is stored in the page under the key “Content”,
+// with type template.HTML.
+//
+// A page's conversion to content can be skipped entirely in dynamically-generated pages
+// by setting the “Content” key before passing the page to ServePage.
+//
+// The second step is framing the content in the overall site HTML, which is done by
+// executing the site template, again using the Page itself as the template input data.
+//
+// The site template is constructed from two files in the file system.
+// The first file is the fsys's “site.tmpl”, which provides the overall HTML frame for the site.
+// The second file is a layout-specific template file, selected by the Page's
+// “layout: name” key-value pair.
+// The renderer searches for “name.tmpl” in the directory containing the page's file,
+// then in the parent of that directory, and so on up to the root.
+// If no such template is found, the rendering fails and reports that error.
+// As a special case, “layout: none” skips the second file entirely.
+//
+// If there is no “layout: name” key-value pair, then the renderer tries using an
+// implicit “layout: default”, but if no such “default.tmpl” template file can be found,
+// the renderer uses an implicit “layout: none” instead.
+//
+// By convention, the site template and the layout-specific template are connected as follows.
+// The site template, at the point where the content should be rendered, executes:
+//
+// {{block "layout" .}}{{.Content}}{{end}}
+//
+// The layout-specific template overrides this block by defining its own template named “layout”.
+// For example:
+//
+// {{define "layout"}}
+// Here's some <blink>great</blink> content: {{.Content}}
+// {{end}}
+//
+// The use of the “block” template construct ensures that
+// if there is no layout-specific template,
+// the content will still be rendered.
+//
+// Page Template Functions
+//
+// In this web server, templates can themselves be invoked as functions.
+// See https://pkg.go.dev/rsc.io/tmplfunc for more details about that feature.
+//
+// During page rendering, both when rendering a page to content and when framing the content,
+// the following template functions are available (in addition to those provided by the
+// template package itself and the per-template functions just mentioned).
+//
+// In all functions taking a file path f, if the path begins with a slash,
+// it is interpreted relative to the fsys root.
+// Otherwise, it is interpreted relative to the directory of the current page's URL.
+//
+// The “{{add x y}}”, “{{sub x y}}”, “{{mul x y}}”, and “{{div x y}}” functions
+// provide basic math on arguments of type int.
+//
+// The “{{code f [start [end]]}}” function returns a template.HTML of a formatted display
+// of code lines from the file f.
+// If both start and end are omitted, then the display shows the entire file.
+// If only the start line is specified, then the display shows that single line.
+// If both start and end are specified, then the display shows a range of lines
+// starting at start up to and including end.
+// The arguments start and end can take two forms: a number indicates a specific line number,
+// and a string is taken to be a regular expresion indicating the earliest matching line
+// in the file (or, for end, the earliest matching line after the start line).
+// Any lines ending in “OMIT” are elided from the display.
+//
+// For example:
+//
+// {{code "hello.go" `^func main` `^}`}}
+//
+// The “{{data f}}” function reads the file f,
+// decodes it as YAML, and then returns the resulting data,
+// typically a map[string]interface{}.
+// It is effectively shorthand for “{{yaml (file f)}}”.
+//
+// The “{{file f}}” function reads the file f and returns its content as a string.
+//
+// The “{{first n slice}}” function returns a slice of the first n elements of slice,
+// or else slice itself when slice has fewer than n elements.
+//
+// The “{{markdown text}}” function interprets text (a string) as Markdown
+// and returns the equivalent HTML as a template.HTML.
+//
+// The “{{page f}}” function returns the page data (a Page)
+// for the static page contained in the file f.
+// The lookup ignores trailing slashes in f as well as the presence or absence
+// of extensions like .md, .html, /index.md, and /index.html,
+// making it possible for f to be a relative or absolute URL path instead of a file path.
+//
+// The “{{pages glob}}” function returns a slice of page data (a []Page)
+// for all pages loaded from files or directories
+// in fsys matching the given glob (a string),
+// according to the usual file path rules (if the glob starts with slash,
+// it is interpreted relative to the fsys root, and otherwise
+// relative to the directory of the page's URL).
+// If the glob pattern matches a directory,
+// the page for the directory's index.md or index.html is used.
+//
+// For example:
+//
+// Here are all the articles:
+// {{range (pages "/articles/*")}}
+// - [{{.title}}]({{.URL}})
+// {{end}}
+//
+// The “{{rawhtml s}}” function converts s (a string) to type template.HTML without any escaping,
+// to allow using s as raw HTML in the final output.
+//
+// The “{{yaml s}}” function decodes s (a string) as YAML and returns the resulting data.
+// It is most useful for defining templates that accept YAML-structured data as a literal argument.
+// For example:
+//
+// {{define "quote info"}}
+// {{with (yaml .info)}}
+// .text
+// — .name{{if .title}}, .title{{end}}
+// {{end}}
+//
+// {{quote `
+// text: If a program is too slow, it must have a loop.
+// name: Ken Thompson
+// `}}
+//
+// The “path” and “strings” functions return package objects with methods for every top-level
+// function in these packages (except path.Split, which has more than one non-error result
+// and would not be invokable). For example, “{{strings.ToUpper "abc"}}”.
+//
+// Serving Requests
+//
+// A Site is an http.Handler that serves requests by consulting the underlying
+// file system and constructing and rendering pages, as well as serving binary
+// and text files.
+//
+// To serve a request for URL path /p, if fsys has a file
+// p/index.md, p/index.html, p.md, or p.html
+// (in that order of preference), then the Site opens that file,
+// parses it into a Page, renders the page as described
+// in the “Page Rendering” section above,
+// and responds to the request with the generated HTML.
+// If the request URL does not match the parsed page's URL,
+// then the Site responds with a redirect to the canonical URL.
+//
+// Otherwise, if fsys has a directory p and the Site
+// can find a template “dir.tmpl” in that directory or a parent,
+// then the Site responds with the rendering of
+//
+// Page{
+// "URL": "/p/",
+// "File": "p",
+// "layout": "dir",
+// "dir": []fs.FileInfo(dir),
+// }
+//
+// where dir is the directory contents.
+//
+// Otherwise, if fsys has a file p containing valid UTF-8 text
+// (at least up to the first kilobyte of the file) and the Site
+// can find a template “text.tmpl” in that file's directory or a parent,
+// and the file is not named robots.txt,
+// and the file does not have a .css, .js, or .svg extension,
+// then the Site responds with the rendering of
+//
+// Page{
+// "URL": "/p",
+// "File": "p",
+// "layout": "texthtml",
+// "texthtml": template.HTML(texthtml),
+// }
+//
+// where texthtml is the text file as rendered by the
+// golang.org/x/website/internal/texthtml package.
+// In the texthtml.Config, GoComments is set to true for
+// file names ending in .go;
+// the h URL query parameter, if present, is passed as Highlight,
+// and the s URL query parameter, if set to lo:hi, is passed as a
+// single-range Selection.
+//
+// If the request has the URL query parameter m=text,
+// then the text file content is not rendered or framed and is instead
+// served directly as a plain text response.
+//
+// Otherwise, if none of those cases apply but the request path p
+// does exist in the file system, then the Site passes the
+// request to an http.FileServer serving from fsys.
+// This last case handles binary static content as well as
+// textual static content excluded from the text file case above.
+//
+// Otherwise, the Site responds with the rendering of
+//
+// Page{
+// "URL": r.URL.Path,
+// "status": 404,
+// "layout": "error",
+// "error": err,
+// }
+//
+// where err is the “not exist” error returned by fs.Stat(fsys, p).
+// (See also the “Serving Errors” section below.)
+//
+// Serving Dynamic Requests
+//
+// Of course, a web site may wish to serve more than static content.
+// To allow dynamically generated web pages to make use of page
+// rendering and site templates, the Site.ServePage method can be
+// called with a dynamically generated Page value, which will then
+// be rendered and served as the result of the request.
+//
+// Serving Errors
+//
+// If an error occurs while serving a request r,
+// the Site responds with the rendering of
+//
+// Page{
+// "URL": r.URL.Path,
+// "status": 500,
+// "layout": "error",
+// "error": err,
+// }
+//
+// If that rendering itself fails, the Site responds with status 500
+// and the cryptic page text “error rendering error”.
+//
+// The Site.ServeError and Site.ServeErrorStatus methods provide a way
+// for dynamic servers to generate similar responses.
+//
package web
import (
"bytes"
+ "errors"
"fmt"
"html"
- "io"
"log"
"net/http"
"path"
"regexp"
- "runtime"
"strconv"
"strings"
+ "sync"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/backport/httpfs"
@@ -24,220 +314,164 @@
"golang.org/x/website/internal/texthtml"
)
-// Site is a website served from a file system.
+// A Site is an http.Handler that serves requests from a file system.
+// See the package doc comment for details.
type Site struct {
- fs fs.FS
-
- mux *http.ServeMux
- fileServer http.Handler
-
- Templates *template.Template
-
- // GoogleAnalytics optionally adds Google Analytics via the provided
- // tracking ID to each page.
- GoogleAnalytics string
-
- docFuncs template.FuncMap
+ fs fs.FS // from NewSite
+ fileServer http.Handler // http.FileServer(http.FS(fs))
+ funcs template.FuncMap // accumulated from s.Funcs
+ cache sync.Map // canonical file path -> *pageFile, for site.openPage
}
-var siteFuncs = template.FuncMap{
- "add": func(a, b int) int { return a + b },
- "sub": func(a, b int) int { return a - b },
- "mul": func(a, b int) int { return a * b },
- "div": func(a, b int) int { return a / b },
-
- "basename": path.Base,
-
- "split": strings.Split,
- "join": strings.Join,
- "hasPrefix": strings.HasPrefix,
- "hasSuffix": strings.HasSuffix,
- "trimPrefix": strings.TrimPrefix,
- "trimSuffix": strings.TrimSuffix,
-}
-
-// NewSite returns a new Presentation from a file system.
-func NewSite(fsys fs.FS) (*Site, error) {
- p := &Site{
+// NewSite returns a new Site for serving pages from the file system fsys.
+func NewSite(fsys fs.FS) *Site {
+ return &Site{
fs: fsys,
- mux: http.NewServeMux(),
fileServer: http.FileServer(httpfs.FS(fsys)),
}
- p.mux.HandleFunc("/", p.serveFile)
- p.initDocFuncs()
+}
- t, err := template.New("").Funcs(siteFuncs).ParseFS(fsys, "lib/godoc/*.html")
- if err != nil {
- return nil, err
+// Funcs adds the functions in m to the set of functions available to templates.
+// Funcs must not be called concurrently with any page rendering.
+func (s *Site) Funcs(m template.FuncMap) {
+ if s.funcs == nil {
+ s.funcs = make(template.FuncMap)
}
- p.Templates = t
-
- return p, nil
-}
-
-// ServeError responds to the request with the given error.
-func (s *Site) ServeError(w http.ResponseWriter, r *http.Request, err error) {
- w.WriteHeader(http.StatusNotFound)
- s.ServePage(w, r, Page{
- Title: r.URL.Path,
- Template: "error.html",
- Data: err,
- })
-}
-
-// ServeHTTP implements http.Handler, dispatching the request appropriately.
-func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- s.mux.ServeHTTP(w, r)
-}
-
-// ServePage responds to the request with the content described by page.
-func (s *Site) ServePage(w http.ResponseWriter, r *http.Request, page Page) {
- page = s.fullPage(r, page)
- if d, ok := page.Data.(interface{ SetWebPage(*Page) }); ok {
- d.SetWebPage(&page)
+ for k, v := range m {
+ s.funcs[k] = v
}
+}
- if page.Template != "" {
- t := s.Templates.Lookup(page.Template)
- var buf bytes.Buffer
- if err := t.Execute(&buf, &page); err != nil {
- log.Printf("%s.Execute: %s", t.Name(), err)
- }
- page.HTML = template.HTML(buf.String())
+// readFile returns the content of the named file in the site's file system.
+// If file begins with a slash, it is interpreted relative to the root of the file system.
+// Otherwise, it is interpreted relative to dir.
+func (site *Site) readFile(dir, file string) ([]byte, error) {
+ if strings.HasPrefix(file, "/") {
+ file = path.Clean(file)
} else {
- page.HTML = page.Data.(template.HTML)
+ file = path.Join(dir, file)
}
-
- applyTemplateToResponseWriter(w, s.Templates.Lookup("site.html"), &page)
+ file = strings.Trim(file, "/")
+ if file == "" {
+ file = "."
+ }
+ return fs.ReadFile(site.fs, file)
}
-// A Page describes the contents of a webpage to be served.
+// ServeError is ServeErrorStatus with HTTP status code 500 (internal server error).
+func (s *Site) ServeError(w http.ResponseWriter, r *http.Request, err error) {
+ s.ServeErrorStatus(w, r, err, http.StatusInternalServerError)
+}
+
+// ServeErrorStatus responds to the request
+// with the given error and HTTP status.
+// It is equivalent to calling ServePage(w, r, p) where p is:
//
-// A Page's Methods are for use by the templates rendering the page.
-type Page struct {
- Title string // <h1>
- TabTitle string // prefix in <title>; defaults to Title
- Subtitle string // subtitle (date for spec, memory model)
- SrcPath string // path to file in /src for text view
-
- // Template and Data describe the data to be
- // rendered into the overall site frame template.
- // If Template is empty, then Data should be a template.HTML
- // holding raw HTML to render into the site frame.
- // Otherwise, Template should be the name of a template file
- // in _content/lib/godoc (for example, "package.html"),
- // and that template will be executed
- // (with the *Page as its data argument) to produce HTML.
- //
- // The overall site template site.html is also invoked with
- // the *Page as its data argument. It is what arranges to call Template.
- Template string // template to apply to data (empty string when Data is raw template.HTML)
- Data interface{} // data to be rendered into page frame
-
- HTML template.HTML
-
- // Filled in automatically by ServePage
- GoogleCN bool // served on golang.google.cn
- GoogleAnalytics string // Google Analytics tag
- Version string
- Site *Site
+// Page{
+// "URL": r.URL.Path,
+// "status": status,
+// "layout": error,
+// "error": err,
+// }
+//
+func (s *Site) ServeErrorStatus(w http.ResponseWriter, r *http.Request, err error, status int) {
+ s.serveErrorStatus(w, r, err, status, false)
}
-// fullPage returns a copy of page with the “automatic” fields filled in.
-func (s *Site) fullPage(r *http.Request, page Page) Page {
- if page.TabTitle == "" {
- page.TabTitle = page.Title
- }
- page.Version = runtime.Version()
- page.GoogleCN = GoogleCN(r)
- page.GoogleAnalytics = s.GoogleAnalytics
- page.Site = s
- return page
-}
+func (s *Site) serveErrorStatus(w http.ResponseWriter, r *http.Request, err error, status int, renderingError bool) {
-type writeErrorSaver struct {
- w io.Writer
- err error
-}
-
-func (w *writeErrorSaver) Write(p []byte) (int, error) {
- n, err := w.w.Write(p)
- if err != nil {
- w.err = err
- }
- return n, err
-}
-
-// applyTemplateToResponseWriter uses an http.ResponseWriter as the io.Writer
-// for the call to template.Execute. It uses an io.Writer wrapper to capture
-// errors from the underlying http.ResponseWriter. Errors are logged only when
-// they come from the template processing and not the Writer; this avoid
-// polluting log files with error messages due to networking issues, such as
-// client disconnects and http HEAD protocol violations.
-func applyTemplateToResponseWriter(rw http.ResponseWriter, t *template.Template, data interface{}) {
- w := &writeErrorSaver{w: rw}
- err := t.Execute(w, data)
- // There are some cases where template.Execute does not return an error when
- // rw returns an error, and some where it does. So check w.err first.
- if w.err == nil && err != nil {
- // Log template errors.
- log.Printf("%s.Execute: %s", t.Name(), err)
- }
-}
-
-func (s *Site) serveFile(w http.ResponseWriter, r *http.Request) {
- if strings.HasSuffix(r.URL.Path, "/index.html") {
- // We'll show index.html for the directory.
- // Use the dir/ version as canonical instead of dir/index.html.
- http.Redirect(w, r, r.URL.Path[0:len(r.URL.Path)-len("index.html")], http.StatusMovedPermanently)
+ if renderingError {
+ log.Printf("error rendering error: %v", err)
+ w.WriteHeader(status)
+ w.Write([]byte("error rendering error"))
return
}
- // Check to see if we need to redirect or serve another file.
+ p := Page{
+ "URL": r.URL.Path,
+ "status": status,
+ "layout": "error",
+ "error": err,
+ }
+ s.servePage(w, r, p, true)
+}
+
+// ServePage renders the page p to HTML and writes that HTML to w.
+// See the package doc comment for details about page rendering.
+//
+// So that all templates can assume the presence of p["URL"],
+// if p["URL"] is unset or does not have type string, then ServePage
+// sets p["URL"] to r.URL.Path in a clone of p before rendering the page.
+func (s *Site) ServePage(w http.ResponseWriter, r *http.Request, p Page) {
+ s.servePage(w, r, p, false)
+}
+
+func (s *Site) servePage(w http.ResponseWriter, r *http.Request, p Page, renderingError bool) {
+ html, err := s.renderHTML(p, r)
+ if err != nil {
+ s.serveErrorStatus(w, r, fmt.Errorf("template execution: %v", err), http.StatusInternalServerError, renderingError)
+ return
+ }
+ if code, ok := p["status"].(int); ok {
+ w.WriteHeader(code)
+ }
+ w.Write(html)
+}
+
+// ServeHTTP implements http.Handler, serving from a file in the site.
+// See the Site type documentation for details about how requests are handled.
+func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) {
abspath := r.URL.Path
relpath := path.Clean(strings.TrimPrefix(abspath, "/"))
- if f := open(s.fs, relpath); f != nil {
- if f.Path != abspath {
+
+ // Is it a page we can generate?
+ if p, err := s.openPage(relpath); err == nil {
+ if p.url != abspath {
// Redirect to canonical path.
- http.Redirect(w, r, f.Path, http.StatusMovedPermanently)
+ status := http.StatusMovedPermanently
+ if i, ok := p.page["status"].(int); ok {
+ status = i
+ }
+ http.Redirect(w, r, p.url, status)
return
}
// Serve from the actual filesystem path.
- s.serveHTML(w, r, f)
+ s.serveHTML(w, r, p)
return
}
- dir, err := fs.Stat(s.fs, relpath)
+ // Is it a directory or file we can serve?
+ info, err := fs.Stat(s.fs, relpath)
if err != nil {
- // Check for spurious trailing slash.
- if strings.HasSuffix(abspath, "/") {
- trimmed := relpath[:len(relpath)-1]
- if _, err := fs.Stat(s.fs, trimmed); err == nil ||
- open(s.fs, trimmed) != nil {
- http.Redirect(w, r, "/"+trimmed, http.StatusMovedPermanently)
- return
+ status := http.StatusInternalServerError
+ if errors.Is(err, fs.ErrNotExist) {
+ status = http.StatusNotFound
+ }
+ s.ServeErrorStatus(w, r, err, status)
+ return
+ }
+
+ // Serve directory.
+ if info != nil && info.IsDir() {
+ if _, ok := s.findLayout(relpath, "dir"); ok {
+ if !maybeRedirect(w, r) {
+ s.serveDir(w, r, relpath)
}
- }
- s.ServeError(w, r, err)
- return
- }
-
- if dir != nil && dir.IsDir() {
- if maybeRedirect(w, r) {
return
}
- s.serveDir(w, r, relpath)
- return
}
+ // Serve text file.
if isTextFile(s.fs, relpath) {
- if maybeRedirectFile(w, r) {
+ if _, ok := s.findLayout(path.Dir(relpath), "text"); ok {
+ if !maybeRedirectFile(w, r) {
+ s.serveText(w, r, relpath)
+ }
return
}
- s.serveText(w, r, relpath)
- return
}
+ // Serve raw bytes.
s.fileServer.ServeHTTP(w, r)
}
@@ -267,63 +501,32 @@
return
}
-var doctype = []byte("<!DOCTYPE ")
-
-func (s *Site) serveHTML(w http.ResponseWriter, r *http.Request, f *file) {
- src := f.Body
- isMarkdown := strings.HasSuffix(f.FilePath, ".md")
+func (s *Site) serveHTML(w http.ResponseWriter, r *http.Request, p *pageFile) {
+ src, _ := p.page["FileData"].(string)
+ filePath, _ := p.page["File"].(string)
+ isMarkdown := strings.HasSuffix(filePath, ".md")
// if it begins with "<!DOCTYPE " assume it is standalone
// html that doesn't need the template wrapping.
- if bytes.HasPrefix(src, doctype) {
- w.Write(src)
+ if strings.HasPrefix(src, "<!DOCTYPE ") {
+ w.Write([]byte(src))
return
}
- page := Page{
- Title: f.Title,
- Subtitle: f.Subtitle,
- }
-
- // evaluate as template if indicated
- if f.Template {
- page = s.fullPage(r, page)
- tmpl, err := template.New("main").Funcs(s.docFuncs).Parse(string(src))
- if err != nil {
- log.Printf("parsing template %s: %v", f.Path, err)
- s.ServeError(w, r, err)
- return
- }
- var buf bytes.Buffer
- if err := tmpl.Execute(&buf, page); err != nil {
- log.Printf("executing template %s: %v", f.Path, err)
- s.ServeError(w, r, err)
- return
- }
- src = buf.Bytes()
- }
-
- // Apply markdown as indicated.
- // (Note template applies before Markdown.)
- if isMarkdown {
- html, err := renderMarkdown(src)
- if err != nil {
- log.Printf("executing markdown %s: %v", f.Path, err)
- s.ServeError(w, r, err)
- return
- }
- src = html
- }
-
// if it's the language spec, add tags to EBNF productions
- if strings.HasSuffix(f.FilePath, "go_spec.html") {
+ if strings.HasSuffix(filePath, "go_spec.html") {
var buf bytes.Buffer
- spec.Linkify(&buf, src)
- src = buf.Bytes()
+ spec.Linkify(&buf, []byte(src))
+ src = buf.String()
}
- page.Data = template.HTML(src)
- s.ServePage(w, r, page)
+ // Template is enabled always in Markdown.
+ // It can only be disabled for HTML files.
+ isTemplate, _ := p.page["template"].(bool)
+ if !isTemplate && !isMarkdown {
+ p.page["Content"] = template.HTML(src)
+ }
+ s.ServePage(w, r, p.page)
}
func (s *Site) serveDir(w http.ResponseWriter, r *http.Request, relpath string) {
@@ -345,13 +548,11 @@
}
}
- dirpath := strings.TrimSuffix(relpath, "/") + "/"
s.ServePage(w, r, Page{
- Title: "Directory",
- SrcPath: dirpath,
- TabTitle: dirpath,
- Template: "dirlist.html",
- Data: info,
+ "URL": r.URL.Path,
+ "File": relpath,
+ "layout": "dir",
+ "dir": info,
})
}
@@ -382,15 +583,11 @@
fmt.Fprintf(&buf, `<p><a href="/%s?m=text">View as plain text</a></p>`, html.EscapeString(relpath))
- title := "Text file"
- if strings.HasSuffix(relpath, ".go") {
- title = "Source file"
- }
s.ServePage(w, r, Page{
- Title: title,
- SrcPath: relpath,
- TabTitle: relpath,
- Data: template.HTML(buf.String()),
+ "URL": r.URL.Path,
+ "File": relpath,
+ "layout": "texthtml",
+ "texthtml": template.HTML(buf.String()),
})
}
diff --git a/internal/web/site_test.go b/internal/web/site_test.go
index 2f3290b..5b962d4 100644
--- a/internal/web/site_test.go
+++ b/internal/web/site_test.go
@@ -18,7 +18,7 @@
t.Helper()
r := &http.Request{URL: &url.URL{Path: path}}
rw := httptest.NewRecorder()
- p.serveFile(rw, r)
+ p.ServeHTTP(rw, r)
if rw.Code != 200 || !strings.Contains(rw.Body.String(), body) {
t.Fatalf("GET %s: expected 200 w/ %q: got %d w/ body:\n%s",
path, body, rw.Code, rw.Body)
@@ -27,13 +27,11 @@
func TestRedirectAndMetadata(t *testing.T) {
fsys := fstest.MapFS{
+ "site.tmpl": {Data: []byte(`{{.Content}}`)},
"doc/x/index.html": {Data: []byte("Hello, x.")},
"lib/godoc/site.html": {Data: []byte(`{{.Data}}`)},
}
- p, err := NewSite(fsys)
- if err != nil {
- t.Fatal(err)
- }
+ site := NewSite(fsys)
// Test that redirect is sent back correctly.
// Used to panic. See golang.org/issue/40665.
@@ -41,25 +39,23 @@
r := &http.Request{URL: &url.URL{Path: dir + "index.html"}}
rw := httptest.NewRecorder()
- p.serveFile(rw, r)
+ site.ServeHTTP(rw, r)
loc := rw.Result().Header.Get("Location")
if rw.Code != 301 || loc != dir {
t.Errorf("GET %s: expected 301 -> %q, got %d -> %q", r.URL.Path, dir, rw.Code, loc)
}
- testServeBody(t, p, dir, "Hello, x")
+ testServeBody(t, site, dir, "Hello, x")
}
func TestMarkdown(t *testing.T) {
- p, err := NewSite(fstest.MapFS{
+ site := NewSite(fstest.MapFS{
+ "site.tmpl": {Data: []byte(`{{.Content}}`)},
"doc/test.md": {Data: []byte("**bold**")},
"doc/test2.md": {Data: []byte(`{{"*template*"}}`)},
"lib/godoc/site.html": {Data: []byte(`{{.Data}}`)},
})
- if err != nil {
- t.Fatal(err)
- }
- testServeBody(t, p, "/doc/test", "<strong>bold</strong>")
- testServeBody(t, p, "/doc/test2", "<em>template</em>")
+ testServeBody(t, site, "/doc/test", "<strong>bold</strong>")
+ testServeBody(t, site, "/doc/test2", "<em>template</em>")
}
diff --git a/internal/web/tmpl.go b/internal/web/tmpl.go
new file mode 100644
index 0000000..2f65f35
--- /dev/null
+++ b/internal/web/tmpl.go
@@ -0,0 +1,161 @@
+// Copyright 2021 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 web
+
+import (
+ "fmt"
+ "path"
+ "reflect"
+ "sort"
+ "strings"
+
+ "golang.org/x/website/internal/backport/html/template"
+ "golang.org/x/website/internal/backport/io/fs"
+ "gopkg.in/yaml.v3"
+)
+
+// A siteDir is a site extended with a known directory for interpreting relative paths.
+type siteDir struct {
+ *Site
+ dir string
+}
+
+func toString(x interface{}) string {
+ switch x := x.(type) {
+ case string:
+ return x
+ case template.HTML:
+ return string(x)
+ case nil:
+ return ""
+ default:
+ panic(fmt.Sprintf("cannot toString %T", x))
+ }
+}
+
+// data parses the named yaml file (relative to dir) and returns its structured data.
+func (site *siteDir) data(name string) (interface{}, error) {
+ data, err := site.readFile(site.dir, name)
+ if err != nil {
+ return nil, err
+ }
+ var d interface{}
+ if err := yaml.Unmarshal(data, &d); err != nil {
+ return nil, err
+ }
+ return d, nil
+}
+
+func first(n int, list reflect.Value) reflect.Value {
+ if !list.IsValid() {
+ return list
+ }
+ if list.Kind() == reflect.Interface {
+ if list.IsNil() {
+ return list
+ }
+ list = list.Elem()
+ }
+
+ if list.Len() < n {
+ return list
+ }
+ return list.Slice(0, n)
+}
+
+// markdown is the function provided to templates.
+func markdown(data interface{}) (template.HTML, error) {
+ h, err := markdownToHTML(toString(data))
+ if err != nil {
+ return "", err
+ }
+ s := strings.TrimSpace(string(h))
+ if strings.HasPrefix(s, "<p>") && strings.HasSuffix(s, "</p>") && strings.Count(s, "<p>") == 1 {
+ h = template.HTML(strings.TrimSpace(s[len("<p>") : len(s)-len("</p>")]))
+ }
+ return h, nil
+}
+
+func (site *siteDir) readfile(name string) (string, error) {
+ data, err := site.readFile(site.dir, name)
+ return string(data), err
+}
+
+// page returns the page params for the page with a given url u.
+// The url may or may not have its leading slash.
+func (site *siteDir) page(u string) (Page, error) {
+ if !path.IsAbs(u) {
+ u = path.Join(site.dir, u)
+ }
+ p, err := site.openPage(strings.Trim(u, "/"))
+ if err != nil {
+ return nil, err
+ }
+ return p.page, nil
+}
+
+// pages returns the page params for pages with urls matching glob.
+func (site *siteDir) pages(glob string) ([]Page, error) {
+ if !path.IsAbs(glob) {
+ glob = path.Join(site.dir, glob)
+ }
+ // TODO(rsc): Add a cache?
+ _, err := path.Match(glob, "")
+ if err != nil {
+ return nil, err
+ }
+ glob = strings.Trim(glob, "/")
+ if glob == "" {
+ glob = "."
+ }
+ matches, err := fs.Glob(site.fs, glob)
+ if err != nil {
+ return nil, err
+ }
+ var out []Page
+ for _, file := range matches {
+ if !strings.HasSuffix(file, ".md") && !strings.HasSuffix(file, ".html") {
+ f := path.Join(file, "index.md")
+ if _, err := fs.Stat(site.fs, f); err != nil {
+ f = path.Join(file, "index.html")
+ if _, err = fs.Stat(site.fs, f); err != nil {
+ continue
+ }
+ }
+ file = f
+ }
+ p, err := site.openPage(file)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, p.page)
+ }
+
+ sort.Slice(out, func(i, j int) bool {
+ return out[i]["URL"].(string) < out[j]["URL"].(string)
+ })
+ return out, nil
+}
+
+// file parses the named file (relative to dir) and returns its content as a string.
+func (site *siteDir) file(name string) (string, error) {
+ data, err := site.readFile(site.dir, name)
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
+
+func rawhtml(s interface{}) template.HTML {
+ return template.HTML(toString(s))
+}
+
+func yamlFn(s string) (interface{}, error) {
+ var d interface{}
+ if err := yaml.Unmarshal([]byte(s), &d); err != nil {
+ return nil, err
+ }
+ return d, nil
+}